Browse Source

chore(fortuna) Config API (#2895)

* chore(fortuna) Config API

* update

* push

* requested comments

* fmt changes

* fix

* requested changes

* removed state in tests

* arc -> &

---------

Co-authored-by: Tejas Badadare <tejasbadadare@gmail.com>
Aditya Arora 3 months ago
parent
commit
394d010d93

+ 189 - 2
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<ApiMetrics>,
 
     pub explorer_metrics: Arc<ExplorerMetrics>,
+
+    pub config: Config,
 }
 
 impl ApiState {
@@ -80,6 +86,7 @@ impl ApiState {
         chains: Arc<RwLock<HashMap<ChainId, ApiBlockChainState>>>,
         metrics_registry: Arc<RwLock<Registry>>,
         history: Arc<History>,
+        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<ChainConfigSummary> = 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<ChainConfigSummary> = 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);
+    }
 }

+ 30 - 0
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<ApiState>,
+) -> Result<Json<Vec<ChainConfigSummary>>, 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))
+}

+ 4 - 2
apps/fortuna/src/command/run.rs

@@ -28,6 +28,7 @@ pub async fn run_api(
     chains: Arc<RwLock<HashMap<String, ApiBlockChainState>>>,
     metrics_registry: Arc<RwLock<Registry>>,
     history: Arc<History>,
+    config: &Config,
     mut rx_exit: watch::Receiver<bool>,
 ) -> 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<State>` 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?;

+ 5 - 3
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"
+checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"