diff --git a/Cargo.lock b/Cargo.lock index f41a51eba4..da57b15857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3071,7 +3071,7 @@ dependencies = [ [[package]] name = "fortuna" -version = "8.2.2" +version = "8.2.3" dependencies = [ "anyhow", "axum 0.6.20", diff --git a/apps/fortuna/Cargo.toml b/apps/fortuna/Cargo.toml index f21ee9918d..4451a05c2b 100644 --- a/apps/fortuna/Cargo.toml +++ b/apps/fortuna/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fortuna" -version = "8.2.2" +version = "8.2.3" edition = "2021" [lib] diff --git a/apps/fortuna/src/api.rs b/apps/fortuna/src/api.rs index 5f7ab2cf24..8e80f3cd4f 100644 --- a/apps/fortuna/src/api.rs +++ b/apps/fortuna/src/api.rs @@ -1,6 +1,7 @@ use { crate::{ chain::reader::{BlockNumber, BlockStatus, EntropyReader}, + config::Config, history::History, state::MonitoredHashChainState, }, @@ -22,9 +23,12 @@ use { tokio::sync::RwLock, url::Url, }; -pub use {chain_ids::*, explorer::*, index::*, live::*, metrics::*, ready::*, revelation::*}; +pub use { + chain_ids::*, config::*, explorer::*, index::*, live::*, metrics::*, ready::*, revelation::*, +}; mod chain_ids; +mod config; mod explorer; mod index; mod live; @@ -73,6 +77,8 @@ pub struct ApiState { pub metrics: Arc, pub explorer_metrics: Arc, + + pub config: Config, } impl ApiState { @@ -80,6 +86,7 @@ impl ApiState { chains: Arc>>, metrics_registry: Arc>, history: Arc, + config: &Config, ) -> ApiState { let metrics = ApiMetrics { http_requests: Family::default(), @@ -100,6 +107,7 @@ impl ApiState { explorer_metrics, history, metrics_registry, + config: config.clone(), } } } @@ -211,6 +219,7 @@ pub fn routes(state: ApiState) -> Router<(), Body> { "/v1/chains/:chain_id/revelations/:sequence", get(revelation), ) + .route("/v1/chains/configs", get(get_chain_configs)) .with_state(state) } @@ -230,9 +239,10 @@ mod test { crate::{ api::{ self, ApiBlockChainState, ApiState, BinaryEncoding, Blob, BlockchainState, - GetRandomValueResponse, + ChainConfigSummary, GetRandomValueResponse, }, chain::reader::{mock::MockEntropyReader, BlockStatus}, + config::Config, history::History, state::{HashChainState, MonitoredHashChainState, PebbleHashChain}, }, @@ -311,10 +321,40 @@ mod test { ApiBlockChainState::Initialized(avax_state), ); + // Create a minimal config for testing + let config = Config { + chains: HashMap::new(), + provider: crate::config::ProviderConfig { + uri: "http://localhost:8080/".to_string(), + address: PROVIDER, + private_key: crate::config::SecretString { + value: Some("0xabcd".to_string()), + file: None, + }, + secret: crate::config::SecretString { + value: Some("abcd".to_string()), + file: None, + }, + chain_length: 100000, + chain_sample_interval: 10, + fee_manager: None, + }, + keeper: crate::config::KeeperConfig { + private_key: crate::config::SecretString { + value: Some("0xabcd".to_string()), + file: None, + }, + fee_manager_private_key: None, + other_keeper_addresses: vec![], + replica_config: None, + }, + }; + let api_state = ApiState::new( Arc::new(RwLock::new(chains)), metrics_registry, Arc::new(History::new().await.unwrap()), + &config, ) .await; @@ -534,4 +574,151 @@ mod test { ) .await; } + + #[tokio::test] + async fn test_chain_configs() { + let (server, _, _) = test_server().await; + + // Test the chain configs endpoint + let response = server.get("/v1/chains/configs").await; + response.assert_status(StatusCode::OK); + + // Parse the response as JSON + let configs: Vec = response.json(); + + // Verify the response structure - should be empty for test server + assert_eq!( + configs.len(), + 0, + "Should return empty configs for test server" + ); + } + + #[tokio::test] + async fn test_chain_configs_with_data() { + // Create a config with actual chain data + let mut config_chains = HashMap::new(); + config_chains.insert( + "ethereum".to_string(), + crate::config::EthereumConfig { + geth_rpc_addr: "http://localhost:8545".to_string(), + contract_addr: Address::from_low_u64_be(0x1234), + reveal_delay_blocks: 1, + confirmed_block_status: BlockStatus::Latest, + backlog_range: 1000, + legacy_tx: false, + gas_limit: 500000, + priority_fee_multiplier_pct: 100, + escalation_policy: crate::config::EscalationPolicyConfig::default(), + min_profit_pct: 0, + target_profit_pct: 20, + max_profit_pct: 100, + min_keeper_balance: 100000000000000000, + fee: 1500000000000000, + sync_fee_only_on_register: true, + commitments: None, + max_num_hashes: None, + block_delays: vec![5], + }, + ); + config_chains.insert( + "avalanche".to_string(), + crate::config::EthereumConfig { + geth_rpc_addr: "http://localhost:9650".to_string(), + contract_addr: Address::from_low_u64_be(0x5678), + reveal_delay_blocks: 2, + confirmed_block_status: BlockStatus::Latest, + backlog_range: 1000, + legacy_tx: false, + gas_limit: 600000, + priority_fee_multiplier_pct: 100, + escalation_policy: crate::config::EscalationPolicyConfig::default(), + min_profit_pct: 0, + target_profit_pct: 20, + max_profit_pct: 100, + min_keeper_balance: 100000000000000000, + fee: 2000000000000000, + sync_fee_only_on_register: true, + commitments: None, + max_num_hashes: None, + block_delays: vec![5], + }, + ); + + let config = Config { + chains: config_chains, + provider: crate::config::ProviderConfig { + uri: "http://localhost:8080/".to_string(), + address: PROVIDER, + private_key: crate::config::SecretString { + value: Some("0xabcd".to_string()), + file: None, + }, + secret: crate::config::SecretString { + value: Some("abcd".to_string()), + file: None, + }, + chain_length: 100000, + chain_sample_interval: 10, + fee_manager: None, + }, + keeper: crate::config::KeeperConfig { + private_key: crate::config::SecretString { + value: Some("0xabcd".to_string()), + file: None, + }, + fee_manager_private_key: None, + other_keeper_addresses: vec![], + replica_config: None, + }, + }; + + let metrics_registry = Arc::new(RwLock::new(Registry::default())); + let api_state = ApiState::new( + Arc::new(RwLock::new(HashMap::new())), + metrics_registry, + Arc::new(History::new().await.unwrap()), + &config, + ) + .await; + + let app = api::routes(api_state); + let server = TestServer::new(app).unwrap(); + + // Test the chain configs endpoint + let response = server.get("/v1/chains/configs").await; + response.assert_status(StatusCode::OK); + + // Parse the response as JSON + let configs: Vec = response.json(); + + // Verify we have 2 chains + assert_eq!(configs.len(), 2, "Should return 2 chain configs"); + + // Find ethereum config + let eth_config = configs + .iter() + .find(|c| c.name == "ethereum") + .expect("Ethereum config not found"); + assert_eq!( + eth_config.contract_addr, + "0x0000000000000000000000000000000000001234" + ); + assert_eq!(eth_config.reveal_delay_blocks, 1); + assert_eq!(eth_config.gas_limit, 500000); + assert_eq!(eth_config.fee, 1500000000000000); + + // Find avalanche config + let avax_config = configs + .iter() + .find(|c| c.name == "avalanche") + .expect("Avalanche config not found"); + assert_eq!( + avax_config.contract_addr, + "0x0000000000000000000000000000000000005678" + ); + assert_eq!(avax_config.reveal_delay_blocks, 2); + assert_eq!(avax_config.gas_limit, 600000); + assert_eq!(avax_config.fee, 2000000000000000); + } } diff --git a/apps/fortuna/src/api/config.rs b/apps/fortuna/src/api/config.rs new file mode 100644 index 0000000000..7fc00e7104 --- /dev/null +++ b/apps/fortuna/src/api/config.rs @@ -0,0 +1,30 @@ +use { + crate::api::{ApiState, RestError}, + axum::{extract::State, Json}, + serde::Serialize, +}; + +#[derive(Serialize, serde::Deserialize)] +pub struct ChainConfigSummary { + pub name: String, + pub contract_addr: String, + pub reveal_delay_blocks: u64, + pub gas_limit: u32, + pub fee: u128, +} + +pub async fn get_chain_configs( + State(state): State, +) -> Result>, RestError> { + let mut configs = Vec::new(); + for (name, chain) in state.config.chains.iter() { + configs.push(ChainConfigSummary { + name: name.clone(), + contract_addr: format!("0x{:x}", chain.contract_addr), + reveal_delay_blocks: chain.reveal_delay_blocks, + gas_limit: chain.gas_limit, + fee: chain.fee, + }); + } + Ok(Json(configs)) +} diff --git a/apps/fortuna/src/command/run.rs b/apps/fortuna/src/command/run.rs index a06909de78..c9f630d6b7 100644 --- a/apps/fortuna/src/command/run.rs +++ b/apps/fortuna/src/command/run.rs @@ -28,6 +28,7 @@ pub async fn run_api( chains: Arc>>, metrics_registry: Arc>, history: Arc, + config: &Config, mut rx_exit: watch::Receiver, ) -> Result<()> { #[derive(OpenApi)] @@ -54,7 +55,7 @@ pub async fn run_api( )] struct ApiDoc; - let api_state = api::ApiState::new(chains, metrics_registry, history).await; + let api_state = api::ApiState::new(chains, metrics_registry, history, config).await; // Initialize Axum Router. Note the type here is a `Router` due to the use of the // `with_state` method which replaces `Body` with `State` in the type signature. @@ -85,7 +86,7 @@ pub async fn run_api( pub async fn run(opts: &RunOptions) -> Result<()> { // Load environment variables from a .env file if present - let _ = dotenv::dotenv()?; + let _ = dotenv::dotenv().map_err(|e| anyhow!("Failed to load .env file: {}", e))?; let config = Config::load(&opts.config.config)?; let secret = config.provider.secret.load()?.ok_or(anyhow!( "Please specify a provider secret in the config file." @@ -170,6 +171,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> { chains.clone(), metrics_registry.clone(), history, + &config, rx_exit, ) .await?; diff --git a/target_chains/cosmwasm/examples/cw-contract/Cargo.lock b/target_chains/cosmwasm/examples/cw-contract/Cargo.lock index c255e5b155..b482f3fc0f 100644 --- a/target_chains/cosmwasm/examples/cw-contract/Cargo.lock +++ b/target_chains/cosmwasm/examples/cw-contract/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -485,7 +485,9 @@ dependencies = [ [[package]] name = "pyth-sdk-cw" -version = "0.1.0" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c04e9f2961bce1ef13b09afcdb5aee7d4ddde83669e5f9d2824ba422cb00de48" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -747,4 +749,4 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" name = "zeroize" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" \ No newline at end of file +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"