Skip to content

feat: CLI configuration #447

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

Merged
merged 8 commits into from
Jul 15, 2025
Merged

feat: CLI configuration #447

merged 8 commits into from
Jul 15, 2025

Conversation

Dodecahedr0x
Copy link
Contributor

@Dodecahedr0x Dodecahedr0x commented Jul 10, 2025

This PR is another attempt to allow layered configuration (default -> file -> args -> env) while providing users with information on what parameters they can adjust.

It overrides #346 with the following advantages:

  • Automatically derives CLI and env var names to avoid repetitions and reduce risk of typos
  • Single definition of the configuration

The weakness of this approach is that it utilizes small procedural macros to avoid repetition, which can compromise readability and make debugging more challenging.

It introduces a breaking change in the format of the remote because Clap does not accept an enum variant with complex values.

Greptile Summary

Major refactoring of configuration management using Clap for CLI support, introducing layered configuration (default -> file -> args -> env) with automatic CLI and environment variable derivation.

  • Added magicblock-config-macro crate with procedural macros clap_prefix and clap_from_serde for automatic CLI argument generation
  • Changed remote configuration format from remote = "devnet" to remote.cluster = "devnet" across all TOML files (breaking change)
  • Consolidated configuration types from individual crates into centralized magicblock-config crate
  • Fixed FQDN typo in validator config (renamed from fdqn to fqdn)
  • Added merge functionality to all config structs for layered configuration support

@Dodecahedr0x Dodecahedr0x requested review from thlorenz, bmuddha and GabrielePicco and removed request for thlorenz and bmuddha July 10, 2025 22:29
@Dodecahedr0x Dodecahedr0x marked this pull request as ready for review July 11, 2025 06:59
@Dodecahedr0x Dodecahedr0x mentioned this pull request Jul 11, 2025
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

58 files reviewed, 14 comments
Edit PR Review Bot Settings | Greptile

@@ -0,0 +1,74 @@
# MagicBlock Config Macro

A set a macro helpers to keep the config DRY (Don't Repeat Yourself). It contains two attributes meant to be used on struct that need to derive `serde::Deserialize` and `clap::Args`.
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Grammar error in 'A set a macro helpers' - should be 'A set of macro helpers'

Suggested change
A set a macro helpers to keep the config DRY (Don't Repeat Yourself). It contains two attributes meant to be used on struct that need to derive `serde::Deserialize` and `clap::Args`.
A set of macro helpers to keep the config DRY (Don't Repeat Yourself). It contains two attributes meant to be used on struct that need to derive `serde::Deserialize` and `clap::Args`.

#[serde(default = "helpers::serde_defaults::bool_true")]
enabled: bool,
#[clap_from_serde_skip]
#[serde(default = "helpers::serde_default::url_none")]
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Typo in attribute path: 'serde_default' should be 'serde_defaults' to match the example above

Suggested change
#[serde(default = "helpers::serde_default::url_none")]
#[serde(default = "helpers::serde_defaults::url_none")]

Comment on lines 17 to 20
#[derive_env_var]
#[clap_from_serde_skip]
#[arg(help = "Whether to reset the ledger before starting the validator.")]
#[serde(default = "bool_true")]
pub reset: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

style: reset field has both derive_env_var and clap_from_serde_skip. Inconsistent handling could lead to confusion about whether this is configurable via env vars.

pub snapshot_frequency: u64,
}

pub const TEST_SNAPSHOT_FREQUENCY: u64 = 50;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: TEST_SNAPSHOT_FREQUENCY used as production default. Should separate test and production constants

Comment on lines 165 to 171
env::set_var("REMOTE_WS_URL", base_cluster_ws);
let config = MagicBlockConfig::try_parse_config_from_arg(&vec![
"--config-file".to_string(),
config_file_dir.to_str().unwrap().to_string(),
])
.unwrap()
.config;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Duplicating config loading logic - consider extracting to a helper function

Comment on lines +12 to 19
#[derive_env_var]
#[arg(help = "The address the RPC will listen on.")]
#[serde(
default = "default_addr",
deserialize_with = "deserialize_addr",
serialize_with = "serialize_addr"
)]
pub addr: IpAddr,
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: max_ws_connections is missing #[derive_env_var] attribute, breaking pattern with other fields and preventing env var configuration

Suggested change
#[derive_env_var]
#[arg(help = "The address the RPC will listen on.")]
#[serde(
default = "default_addr",
deserialize_with = "deserialize_addr",
serialize_with = "serialize_addr"
)]
pub addr: IpAddr,
#[derive_env_var]
#[arg(help = "The address the RPC will listen on.")]
#[serde(
default = "default_addr",
deserialize_with = "deserialize_addr",
serialize_with = "serialize_addr"
)]
pub addr: IpAddr,
#[derive_env_var]
#[arg(help = "The port the RPC will listen on.")]
#[serde(default = "default_port")]
pub port: u16,
#[derive_env_var]
#[arg(help = "The max number of WebSocket connections to accept.")]
#[serde(default = "default_max_ws_connections")]
pub max_ws_connections: usize,

Comment on lines +61 to +64
fn clap_deserialize_addr(s: &str) -> Result<IpAddr, String> {
s.parse().map_err(|err| format!("Invalid address: {err}"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: clap_deserialize_addr appears unused - should be referenced by a #[arg(value_parser)] attribute on addr field if intended for CLI parsing

Comment on lines +128 to +131
s.split(':').map(|part| part.to_string()).collect();
let [id, path] = parts.as_slice() else {
return Err(format!("Invalid program config: {s}"));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Program config parser assumes exactly 2 parts - doesn't handle paths with colons properly

Suggested change
s.split(':').map(|part| part.to_string()).collect();
let [id, path] = parts.as_slice() else {
return Err(format!("Invalid program config: {s}"));
};
let parts: Vec<String> =
s.splitn(2, ':').map(|part| part.to_string()).collect();
let [id, path] = parts.as_slice() else {
return Err(format!("Invalid program config: {s}"));
};

Comment on lines 41 to 44
// Remove the attribute
attrs.remove(i);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Potential index out of bounds when removing attribute - use drain_filter() or iterate in reverse to safely remove items

Comment on lines +78 to +82
if nv.path.is_ident("default") {
self.meta_attrs
.push(quote! { default_value_t = #value_token () });
} else if nv.path.is_ident("deserialize_with") {
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Default value has unexpected extra parentheses () which could cause compilation errors

Suggested change
if nv.path.is_ident("default") {
self.meta_attrs
.push(quote! { default_value_t = #value_token () });
} else if nv.path.is_ident("deserialize_with") {
if nv.path.is_ident("default") {
self.meta_attrs
.push(quote! { default_value_t = #value_token });
} else if nv.path.is_ident("deserialize_with") {

Copy link
Contributor

@bmuddha bmuddha left a comment

Choose a reason for hiding this comment

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

Looks legit, cleaner than it was, breaking changes fine for now. Please address the failing integration test, and good to merge.

Copy link
Contributor

@GabrielePicco GabrielePicco left a comment

Choose a reason for hiding this comment

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

LGTM!

@Dodecahedr0x Dodecahedr0x changed the base branch from master to dev July 15, 2025 08:33
@Dodecahedr0x Dodecahedr0x merged commit cc63fae into dev Jul 15, 2025
4 checks passed
@Dodecahedr0x Dodecahedr0x deleted the feat/clap-config branch July 15, 2025 19:56
thlorenz added a commit that referenced this pull request Jul 17, 2025
* dev:
  feat: CLI configuration (#447)
  Track delegation status (#452)
  Change account layout (#451)
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.

3 participants