diff --git a/Cargo.lock b/Cargo.lock index 0be539d9..03ac06c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2576,11 +2576,13 @@ dependencies = [ "ethers", "eyre", "js-sys", + "nomad-test", "nomad-types", "once_cell", "serde 1.0.136", "serde_json", "serde_yaml", + "serial_test", "wasm-bindgen", "wee_alloc", ] diff --git a/Cargo.toml b/Cargo.toml index 9c705f91..54350ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ cargo-features = ["edition2021"] [workspace] +resolver = "2" members = [ "accumulator", diff --git a/agents/kathy/src/settings.rs b/agents/kathy/src/settings.rs index 3892f6d3..f4e7f23d 100644 --- a/agents/kathy/src/settings.rs +++ b/agents/kathy/src/settings.rs @@ -10,7 +10,123 @@ mod test { use super::*; use nomad_base::{get_remotes_from_env, NomadAgent}; use nomad_test::test_utils; - use nomad_xyz_configuration::AgentSecrets; + use nomad_xyz_configuration::{ + agent::SignerConf, ethereum::Connection, AgentSecrets, ChainConf, + }; + + #[tokio::test] + #[serial_test::serial] + async fn it_builds_settings_from_env_mixed() { + test_utils::run_test_with_env("../../fixtures/env.test-signer-mixed", || async move { + let run_env = dotenv::var("RUN_ENV").unwrap(); + let agent_home = dotenv::var("AGENT_HOME_NAME").unwrap(); + + let settings = KathySettings::new().unwrap(); + + let config = nomad_xyz_configuration::get_builtin(&run_env).unwrap(); + + let remotes = get_remotes_from_env!(agent_home, config); + let mut networks = remotes.clone(); + networks.insert(agent_home.clone()); + + let secrets = AgentSecrets::from_env(&networks).unwrap(); + + settings + .base + .validate_against_config_and_secrets( + crate::Kathy::AGENT_NAME, + &agent_home, + &remotes, + config, + &secrets, + ) + .unwrap(); + + assert_eq!( + *settings.base.signers.get("moonbeam").unwrap(), + SignerConf::Aws { + id: "moonbeam_id".into(), + region: "moonbeam_region".into(), + } + ); + assert_eq!( + *settings.base.signers.get("ethereum").unwrap(), + SignerConf::HexKey( + "0x1111111111111111111111111111111111111111111111111111111111111111" + .parse() + .unwrap() + ) + ); + assert_eq!( + *settings.base.signers.get("evmos").unwrap(), + SignerConf::Aws { + id: "default_id".into(), + region: "default_region".into(), + } + ); + assert_eq!( + settings.base.home.chain, + ChainConf::Ethereum(Connection::Http( + "https://main-light.eth.linkpool.io/".into() + )) + ); + assert_eq!( + settings.base.replicas.get("moonbeam").unwrap().chain, + ChainConf::Ethereum(Connection::Http("https://rpc.api.moonbeam.network".into())) + ); + assert_eq!( + settings.base.replicas.get("evmos").unwrap().chain, + ChainConf::Ethereum(Connection::Http("https://eth.bd.evmos.org:8545".into())) + ); + }) + .await + } + + #[tokio::test] + #[serial_test::serial] + async fn it_builds_settings_from_env_default() { + test_utils::run_test_with_env("../../fixtures/env.test-signer-default", || async move { + let run_env = dotenv::var("RUN_ENV").unwrap(); + let agent_home = dotenv::var("AGENT_HOME_NAME").unwrap(); + + let settings = KathySettings::new().unwrap(); + + let config = nomad_xyz_configuration::get_builtin(&run_env).unwrap(); + + let remotes = get_remotes_from_env!(agent_home, config); + let mut networks = remotes.clone(); + networks.insert(agent_home.clone()); + + let secrets = AgentSecrets::from_env(&networks).unwrap(); + + settings + .base + .validate_against_config_and_secrets( + crate::Kathy::AGENT_NAME, + &agent_home, + &remotes, + config, + &secrets, + ) + .unwrap(); + + let default_config = SignerConf::Aws { + id: "default_id".into(), + region: "default_region".into(), + }; + for (_, config) in &settings.base.signers { + assert_eq!(*config, default_config); + } + assert!(matches!( + settings.base.home.chain, + ChainConf::Ethereum { .. } + )); + for (_, config) in &settings.base.replicas { + assert!(matches!(config.chain, ChainConf::Ethereum { .. })); + } + }) + .await + } #[tokio::test] #[serial_test::serial] diff --git a/configuration/CHANGELOG.md b/configuration/CHANGELOG.md index 2d6325c5..18ac42dc 100644 --- a/configuration/CHANGELOG.md +++ b/configuration/CHANGELOG.md @@ -2,6 +2,9 @@ ### Unreleased +- add handling for default keys `TRANSACTIONSIGNERS_DEFAULT_{KEY,ID,REGION}` and `RPCS_DEFAULT_RPCSTYLE` +- add tests for new default config keys + ### v0.1.0-rc.23 - fix typo in staging goerli rpc url diff --git a/configuration/Cargo.toml b/configuration/Cargo.toml index 8f54ba08..3c811aa0 100644 --- a/configuration/Cargo.toml +++ b/configuration/Cargo.toml @@ -35,4 +35,8 @@ js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["serde-serialize"] } [dev-dependencies] -dotenv = "0.15.0" \ No newline at end of file +dotenv = "0.15.0" +serial_test = "0.6.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +nomad-test = { path = "../nomad-test" } diff --git a/configuration/src/agent/signer.rs b/configuration/src/agent/signer.rs index 36aa1728..83ae3d5b 100644 --- a/configuration/src/agent/signer.rs +++ b/configuration/src/agent/signer.rs @@ -28,7 +28,7 @@ impl Default for SignerConf { } impl FromEnv for SignerConf { - fn from_env(prefix: &str) -> Option { + fn from_env(prefix: &str, default_prefix: Option<&str>) -> Option { // ordering this first preferentially uses AWS if both are specified if let Ok(id) = std::env::var(&format!("{}_ID", prefix)) { if let Ok(region) = std::env::var(&format!("{}_REGION", prefix)) { @@ -40,6 +40,10 @@ impl FromEnv for SignerConf { return Some(SignerConf::HexKey(HexString::from_str(&signer_key).ok()?)); } + if let Some(prefix) = default_prefix { + return SignerConf::from_env(prefix, None); + } + None } } diff --git a/configuration/src/chains/mod.rs b/configuration/src/chains/mod.rs index e6175b81..5801bcaa 100644 --- a/configuration/src/chains/mod.rs +++ b/configuration/src/chains/mod.rs @@ -23,12 +23,17 @@ impl Default for ChainConf { } impl FromEnv for ChainConf { - fn from_env(prefix: &str) -> Option { - let rpc_style = std::env::var(&format!("{}_RPCSTYLE", prefix)).ok()?; + fn from_env(prefix: &str, default_prefix: Option<&str>) -> Option { + let mut rpc_style = std::env::var(&format!("{}_RPCSTYLE", prefix)).ok(); + + if let (None, Some(prefix)) = (&rpc_style, default_prefix) { + rpc_style = std::env::var(&format!("{}_RPCSTYLE", prefix)).ok(); + } + let rpc_url = std::env::var(&format!("{}_CONNECTION_URL", prefix)).ok()?; let json = json!({ - "rpcStyle": rpc_style, + "rpcStyle": rpc_style?, "connection": rpc_url, }); diff --git a/configuration/src/secrets.rs b/configuration/src/secrets.rs index 7c515b43..7551a3c1 100644 --- a/configuration/src/secrets.rs +++ b/configuration/src/secrets.rs @@ -36,9 +36,14 @@ impl AgentSecrets { for network in networks.iter() { let network_upper = network.to_uppercase(); - let chain_conf = ChainConf::from_env(&format!("RPCS_{}", network_upper))?; - let transaction_signer = - SignerConf::from_env(&format!("TRANSACTIONSIGNERS_{}", network_upper))?; + + let chain_conf = + ChainConf::from_env(&format!("RPCS_{}", network_upper), Some("RPCS_DEFAULT"))?; + + let transaction_signer = SignerConf::from_env( + &format!("TRANSACTIONSIGNERS_{}", network_upper), + Some("TRANSACTIONSIGNERS_DEFAULT"), + )?; secrets.rpcs.insert(network.to_owned(), chain_conf); secrets @@ -46,7 +51,7 @@ impl AgentSecrets { .insert(network.to_owned(), transaction_signer); } - let attestation_signer = SignerConf::from_env("ATTESTATION_SIGNER"); + let attestation_signer = SignerConf::from_env("ATTESTATION_SIGNER", None); secrets.attestation_signer = attestation_signer; Some(secrets) @@ -110,18 +115,89 @@ impl AgentSecrets { #[cfg(test)] mod test { use super::*; - const SECRETS_JSON_PATH: &str = "../fixtures/test_secrets.json"; - const SECRETS_ENV_PATH: &str = "../fixtures/env.test"; + use crate::ethereum::Connection; + use nomad_test::test_utils; + + #[test] + #[serial_test::serial] + fn it_builds_from_env_mixed() { + test_utils::run_test_with_env_sync("../fixtures/env.test-signer-mixed", move || { + let networks = &crate::get_builtin("test").unwrap().networks; + let secrets = + AgentSecrets::from_env(networks).expect("Failed to load secrets from env"); + + assert_eq!( + *secrets.transaction_signers.get("moonbeam").unwrap(), + SignerConf::Aws { + id: "moonbeam_id".into(), + region: "moonbeam_region".into(), + } + ); + assert_eq!( + *secrets.transaction_signers.get("ethereum").unwrap(), + SignerConf::HexKey( + "0x1111111111111111111111111111111111111111111111111111111111111111" + .parse() + .unwrap() + ) + ); + assert_eq!( + *secrets.transaction_signers.get("evmos").unwrap(), + SignerConf::Aws { + id: "default_id".into(), + region: "default_region".into(), + } + ); + assert_eq!( + *secrets.rpcs.get("moonbeam").unwrap(), + ChainConf::Ethereum(Connection::Http("https://rpc.api.moonbeam.network".into())) + ); + assert_eq!( + *secrets.rpcs.get("ethereum").unwrap(), + ChainConf::Ethereum(Connection::Http( + "https://main-light.eth.linkpool.io/".into() + )) + ); + assert_eq!( + *secrets.rpcs.get("evmos").unwrap(), + ChainConf::Ethereum(Connection::Http("https://eth.bd.evmos.org:8545".into())) + ); + }); + } + + #[test] + #[serial_test::serial] + fn it_builds_from_env_default() { + test_utils::run_test_with_env_sync("../fixtures/env.test-signer-default", move || { + let networks = &crate::get_builtin("test").unwrap().networks; + let secrets = + AgentSecrets::from_env(networks).expect("Failed to load secrets from env"); + + let default_config = SignerConf::Aws { + id: "default_id".into(), + region: "default_region".into(), + }; + for (_, config) in &secrets.transaction_signers { + assert_eq!(*config, default_config); + } + for (_, config) in &secrets.rpcs { + assert!(matches!(*config, ChainConf::Ethereum { .. })); + } + }); + } #[test] + #[serial_test::serial] fn it_builds_from_env() { - let networks = &crate::get_builtin("test").unwrap().networks; - dotenv::from_filename(SECRETS_ENV_PATH).unwrap(); - AgentSecrets::from_env(networks).expect("Failed to load secrets from env"); + test_utils::run_test_with_env_sync("../fixtures/env.test", move || { + let networks = &crate::get_builtin("test").unwrap().networks; + AgentSecrets::from_env(networks).expect("Failed to load secrets from env"); + }); } #[test] fn it_builds_from_file() { - AgentSecrets::from_file(SECRETS_JSON_PATH).expect("Failed to load secrets from file"); + AgentSecrets::from_file("../fixtures/test_secrets.json") + .expect("Failed to load secrets from file"); } } diff --git a/configuration/src/traits/env.rs b/configuration/src/traits/env.rs index 5c29f7a3..d165effd 100644 --- a/configuration/src/traits/env.rs +++ b/configuration/src/traits/env.rs @@ -7,9 +7,11 @@ pub trait EnvOverridable { /// Implemented by structs that are built from environment variables (signers, /// connections, etc) pub trait FromEnv { - /// Optionally load self from env vars. Return None if any necessary env var - /// is missing. - fn from_env(prefix: &str) -> Option + /// Optionally load self from env vars. + /// Accepts a `default_prefix` which will be looked for if `prefix` isn't found. + /// If both are present, `prefix` has precedence over `default_prefix`. + /// Return None if *any* necessary env var is missing. + fn from_env(prefix: &str, default_prefix: Option<&str>) -> Option where Self: Sized; } diff --git a/fixtures/env.test-signer-default b/fixtures/env.test-signer-default new file mode 100644 index 00000000..b7fbe1d9 --- /dev/null +++ b/fixtures/env.test-signer-default @@ -0,0 +1,17 @@ +# Matches configuration/configs/test.json + +RUN_ENV=test +AGENT_HOME_NAME=ethereum +AGENT_REPLICAS_ALL=true + +RPCS_DEFAULT_RPCSTYLE=ethereum + +RPCS_ETHEREUM_CONNECTION_URL=https://main-light.eth.linkpool.io/ +RPCS_MOONBEAM_CONNECTION_URL=https://rpc.api.moonbeam.network +RPCS_EVMOS_CONNECTION_URL=https://eth.bd.evmos.org:8545 + +TRANSACTIONSIGNERS_DEFAULT_REGION=default_region +TRANSACTIONSIGNERS_DEFAULT_ID=default_id + +ATTESTATION_SIGNER_ID=dummy_id +ATTESTATION_SIGNER_REGION=dummy_region diff --git a/fixtures/env.test-signer-mixed b/fixtures/env.test-signer-mixed new file mode 100644 index 00000000..0141c1a4 --- /dev/null +++ b/fixtures/env.test-signer-mixed @@ -0,0 +1,23 @@ +# Matches configuration/configs/test.json + +RUN_ENV=test +AGENT_HOME_NAME=ethereum +AGENT_REPLICAS_ALL=true + +RPCS_DEFAULT_RPCSTYLE=ethereum +RPCS_EVMOS_RPCSTYLE=ethereum + +RPCS_ETHEREUM_CONNECTION_URL=https://main-light.eth.linkpool.io/ +RPCS_MOONBEAM_CONNECTION_URL=https://rpc.api.moonbeam.network +RPCS_EVMOS_CONNECTION_URL=https://eth.bd.evmos.org:8545 + +TRANSACTIONSIGNERS_ETHEREUM_KEY=0x1111111111111111111111111111111111111111111111111111111111111111 + +TRANSACTIONSIGNERS_MOONBEAM_ID=moonbeam_id +TRANSACTIONSIGNERS_MOONBEAM_REGION=moonbeam_region + +TRANSACTIONSIGNERS_DEFAULT_REGION=default_region +TRANSACTIONSIGNERS_DEFAULT_ID=default_id + +ATTESTATION_SIGNER_ID=dummy_id +ATTESTATION_SIGNER_REGION=dummy_region diff --git a/nomad-test/src/test_utils.rs b/nomad-test/src/test_utils.rs index 9225d512..f4d575d3 100644 --- a/nomad-test/src/test_utils.rs +++ b/nomad-test/src/test_utils.rs @@ -55,6 +55,19 @@ where assert!(result.is_ok()) } +pub fn run_test_with_env_sync(path: impl AsRef, test: T) +where + T: FnOnce() + panic::UnwindSafe, +{ + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + dotenv::from_filename(path).unwrap(); + test() + })); + + clear_env_vars(); + assert!(result.is_ok()) +} + pub fn clear_env_vars() { let env_vars = env::vars(); for (key, _) in env_vars.into_iter() {