hopli/
environment_config.rs

1//! This module contains definiation of arguments that specify the environment
2//! and networks that a HOPR node runs in.
3//!
4//! [EnvironmentType] defines the environment type. EnvironmentType of a network is defined in
5//! `contracts-addresses.json` under the foundry contract root. Different environment type uses
6//! a different foundry profile.
7//!
8//! Network is a collection of several major/minor releases.
9//!
10//! [NetworkDetail] specifies the environment type of the network, the starting block number, and
11//! the deployed contract addresses in [ContractAddresses]
12
13use clap::Parser;
14use ethers::{
15    core::k256::ecdsa::SigningKey,
16    middleware::{MiddlewareBuilder, NonceManagerMiddleware, SignerMiddleware},
17    providers::{Middleware, Provider},
18    signers::{LocalWallet, Signer, Wallet},
19};
20use serde::{Deserialize, Serialize};
21use serde_with::{serde_as, DisplayFromStr};
22use std::{
23    collections::HashMap,
24    ffi::OsStr,
25    path::{Path, PathBuf},
26    sync::Arc,
27};
28
29use hopr_chain_api::config::{Addresses as ContractAddresses, EnvironmentType};
30use hopr_chain_rpc::{
31    client::{surf_client::SurfRequestor as DefaultHttpPostRequestor, SimpleJsonRpcRetryPolicy},
32    errors::RpcError,
33    rpc::RpcOperationsConfig,
34};
35use hopr_crypto_types::keypairs::ChainKeypair;
36use hopr_crypto_types::keypairs::Keypair;
37
38use crate::utils::HelperErrors;
39
40pub type JsonRpcClient =
41    hopr_chain_rpc::client::JsonRpcProviderClient<DefaultHttpPostRequestor, SimpleJsonRpcRetryPolicy>;
42
43// replace NetworkConfig with ProtocolConfig
44#[serde_as]
45#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
46#[serde(deny_unknown_fields)]
47pub struct NetworkDetail {
48    /// block number to start the indexer from
49    pub indexer_start_block_number: u32,
50    /// Type of environment
51    #[serde_as(as = "DisplayFromStr")]
52    pub environment_type: EnvironmentType,
53    /// contract addresses used by the network
54    pub addresses: ContractAddresses,
55}
56
57/// mapping of networks with its details
58#[derive(Default, Debug, Clone, Serialize, Deserialize)]
59pub struct NetworkConfig {
60    // #[serde(flatten)]
61    networks: HashMap<String, NetworkDetail>,
62}
63
64/// Arguments for getting network and ethereum RPC provider.
65///
66/// RPC provider specifies an endpoint that enables an application to communicate with a blockchain network
67/// If not specified, it uses the default value according to the environment config
68/// Network specifies a set of contracts used in HOPR network.
69#[derive(Debug, Clone, Parser)]
70pub struct NetworkProviderArgs {
71    /// Name of the network that the node is running on
72    #[clap(help = "Network name. E.g. monte_rosa", long, short)]
73    network: String,
74
75    /// Path to the root of foundry project (ethereum/contracts), where all the contracts and `contracts-addresses.json` are stored
76    /// Default to "./ethereum/contracts", which is the path to the `contracts` folder from the root of monorepo
77    #[clap(
78        env = "HOPLI_CONTRACTS_ROOT",
79        help = "Specify path pointing to the contracts root",
80        long,
81        short,
82        default_value = "./ethereum/contracts"
83    )]
84    contracts_root: Option<String>,
85
86    /// Customized RPC provider endpoint
87    #[clap(help = "Blockchain RPC provider endpoint.", long, short = 'r')]
88    provider_url: String,
89}
90
91impl Default for NetworkProviderArgs {
92    fn default() -> Self {
93        Self {
94            network: "anvil-localhost".into(),
95            contracts_root: Some("./ethereum/contracts".into()),
96            provider_url: "http://127.0.0.1:8545".into(),
97        }
98    }
99}
100
101impl NetworkProviderArgs {
102    /// Get the NetworkDetail (contract addresses, environment type) from network names
103    pub fn get_network_details_from_name(&self) -> Result<NetworkDetail, HelperErrors> {
104        // read `contracts-addresses.json` at make_root_dir_path
105        let contract_root = self.contracts_root.to_owned().unwrap_or(
106            NetworkProviderArgs::default()
107                .contracts_root
108                .ok_or(HelperErrors::UnableToSetFoundryRoot)?,
109        );
110        let contract_environment_config_path =
111            PathBuf::from(OsStr::new(&contract_root)).join("contracts-addresses.json");
112
113        let file_read =
114            std::fs::read_to_string(contract_environment_config_path).map_err(HelperErrors::UnableToReadFromPath)?;
115
116        let network_config = serde_json::from_str::<NetworkConfig>(&file_read).map_err(HelperErrors::SerdeJson)?;
117
118        network_config
119            .networks
120            .get(&self.network)
121            .cloned()
122            .ok_or_else(|| HelperErrors::UnknownNetwork)
123    }
124
125    /// get the provider object
126    pub async fn get_provider_with_signer(
127        &self,
128        chain_key: &ChainKeypair,
129    ) -> Result<Arc<NonceManagerMiddleware<SignerMiddleware<Provider<JsonRpcClient>, Wallet<SigningKey>>>>, HelperErrors>
130    {
131        // Build JSON RPC client
132        let rpc_client = JsonRpcClient::new(
133            self.provider_url.as_str(),
134            DefaultHttpPostRequestor::new(hopr_chain_rpc::HttpPostRequestorConfig {
135                max_requests_per_sec: None,
136                ..Default::default()
137            }),
138            SimpleJsonRpcRetryPolicy::default(),
139        );
140
141        // Build default JSON RPC provider
142        let mut provider = Provider::new(rpc_client);
143
144        let chain_id = provider.get_chainid().await.map_err(RpcError::ProviderError)?;
145        let default_tx_polling_interval = if chain_id.eq(&ethers::types::U256::from(31337u32)) {
146            std::time::Duration::from_millis(10)
147        } else {
148            RpcOperationsConfig::default().tx_polling_interval
149        };
150        provider.set_interval(default_tx_polling_interval);
151
152        let wallet = LocalWallet::from_bytes(chain_key.secret().as_ref())?.with_chain_id(chain_id.as_u64());
153
154        Ok(Arc::new(
155            provider
156                .with_signer(wallet)
157                .nonce_manager(chain_key.public().to_address().into()),
158        ))
159    }
160
161    /// get the provider object without signer
162    pub async fn get_provider_without_signer(&self) -> Result<Arc<Provider<JsonRpcClient>>, HelperErrors> {
163        // Build JSON RPC client
164        let rpc_client = JsonRpcClient::new(
165            self.provider_url.as_str(),
166            DefaultHttpPostRequestor::new(hopr_chain_rpc::HttpPostRequestorConfig {
167                max_requests_per_sec: None,
168                ..Default::default()
169            }),
170            SimpleJsonRpcRetryPolicy::default(),
171        );
172
173        // Build default JSON RPC provider
174        let mut provider = Provider::new(rpc_client);
175
176        let chain_id = provider.get_chainid().await.map_err(RpcError::ProviderError)?;
177        let default_tx_polling_interval = if chain_id.eq(&ethers::types::U256::from(31337u32)) {
178            std::time::Duration::from_millis(10)
179        } else {
180            RpcOperationsConfig::default().tx_polling_interval
181        };
182        provider.set_interval(default_tx_polling_interval);
183
184        Ok(Arc::new(provider))
185    }
186}
187
188/// ensures that the network and environment_type exist
189/// in `contracts-addresses.json` and are matched
190pub fn ensure_environment_and_network_are_set(
191    make_root_dir_path: &Path,
192    network: &str,
193    environment_type: &str,
194) -> Result<bool, HelperErrors> {
195    let network_detail = get_network_details_from_name(make_root_dir_path, network)?;
196
197    if network_detail.environment_type.to_string() == environment_type {
198        Ok(true)
199    } else {
200        Ok(false)
201    }
202}
203
204/// Returns the environment type from the network name
205/// according to `contracts-addresses.json`
206pub fn get_environment_type_from_name(
207    make_root_dir_path: &Path,
208    network: &str,
209) -> Result<EnvironmentType, HelperErrors> {
210    let network_detail = get_network_details_from_name(make_root_dir_path, network)?;
211    Ok(network_detail.environment_type)
212}
213
214/// Get the NetworkDetail (contract addresses, environment type) from network names
215pub fn get_network_details_from_name(make_root_dir_path: &Path, network: &str) -> Result<NetworkDetail, HelperErrors> {
216    // read `contracts-addresses.json` at make_root_dir_path
217    let contract_environment_config_path = make_root_dir_path.join("contracts-addresses.json");
218
219    let file_read =
220        std::fs::read_to_string(contract_environment_config_path).map_err(HelperErrors::UnableToReadFromPath)?;
221
222    let network_config = serde_json::from_str::<NetworkConfig>(&file_read).map_err(HelperErrors::SerdeJson)?;
223
224    network_config
225        .networks
226        .get(network)
227        .cloned()
228        .ok_or_else(|| HelperErrors::UnknownNetwork)
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use anyhow::Context;
235
236    fn create_anvil_at_port(default: bool) -> ethers::utils::AnvilInstance {
237        let mut anvil = ethers::utils::Anvil::new();
238
239        if !default {
240            let listener =
241                std::net::TcpListener::bind("127.0.0.1:0").unwrap_or_else(|_| panic!("Failed to bind localhost"));
242            let random_port = listener
243                .local_addr()
244                .unwrap_or_else(|_| panic!("Failed to get local address"))
245                .port();
246            anvil = anvil.port(random_port);
247            anvil = anvil.chain_id(random_port);
248        } else {
249            anvil = anvil.port(8545u16);
250        }
251        anvil.spawn()
252    }
253
254    #[test]
255    fn read_anvil_localhost_at_right_path() -> anyhow::Result<()> {
256        let correct_dir = &std::env::current_dir()
257            .context("Current dir failed")?
258            .parent()
259            .context("Parent dir failed")?
260            .join("ethereum")
261            .join("contracts");
262        let network = "anvil-localhost";
263        let environment_type = "local";
264        match ensure_environment_and_network_are_set(correct_dir, network, environment_type) {
265            Ok(result) => assert!(result),
266            _ => assert!(false),
267        }
268        Ok(())
269    }
270
271    #[test]
272    fn read_anvil_localhost_at_wrong_path() -> anyhow::Result<()> {
273        let wrong_dir = &std::env::current_dir().context("Current dir failed")?;
274        let network = "anvil-localhost";
275        let environment_type = "local";
276        assert!(ensure_environment_and_network_are_set(wrong_dir, network, environment_type).is_err());
277        Ok(())
278    }
279
280    #[test]
281    fn read_non_existing_environment_at_right_path() -> anyhow::Result<()> {
282        let correct_dir = &std::env::current_dir()
283            .context("Current dir failed")?
284            .parent()
285            .context("Parent dir failed")?
286            .join("ethereum")
287            .join("contracts");
288
289        assert!(ensure_environment_and_network_are_set(correct_dir, "non-existing", "development").is_err());
290        Ok(())
291    }
292
293    #[test]
294    fn read_wrong_type_at_right_path() -> anyhow::Result<()> {
295        let correct_dir = &std::env::current_dir()
296            .context("Current dir failed")?
297            .parent()
298            .context("Parent dir failed")?
299            .join("ethereum")
300            .join("contracts");
301        let network = "anvil-localhost";
302        let environment_type = "production";
303        match ensure_environment_and_network_are_set(correct_dir, network, environment_type) {
304            Ok(result) => assert!(!result),
305            _ => assert!(false),
306        }
307        Ok(())
308    }
309
310    #[async_std::test]
311    async fn test_network_provider_with_signer() -> anyhow::Result<()> {
312        // create an identity
313        let chain_key = ChainKeypair::random();
314
315        // launch local anvil instance
316        let anvil = create_anvil_at_port(false);
317
318        let network_provider_args = NetworkProviderArgs {
319            network: "anvil-localhost".into(),
320            contracts_root: Some("../ethereum/contracts".into()),
321            provider_url: anvil.endpoint().into(),
322        };
323
324        let provider = network_provider_args.get_provider_with_signer(&chain_key).await?;
325
326        let chain_id = provider.get_chainid().await?;
327        assert_eq!(chain_id, anvil.chain_id().into());
328        Ok(())
329    }
330
331    #[async_std::test]
332    async fn test_default_contracts_root() -> anyhow::Result<()> {
333        // create an identity
334        let chain_key = ChainKeypair::random();
335
336        // launch local anvil instance
337        let anvil = create_anvil_at_port(false);
338
339        let network_provider_args = NetworkProviderArgs {
340            network: "anvil-localhost".into(),
341            contracts_root: None,
342            provider_url: anvil.endpoint().into(),
343        };
344
345        let provider = network_provider_args.get_provider_with_signer(&chain_key).await?;
346
347        let chain_id = provider.get_chainid().await?;
348        assert_eq!(chain_id, anvil.chain_id().into());
349        Ok(())
350    }
351}