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]
12use std::{
13    collections::HashMap,
14    ffi::OsStr,
15    path::{Path, PathBuf},
16    sync::Arc,
17};
18
19use alloy::{
20    network::EthereumWallet,
21    providers::{
22        Identity, ProviderBuilder, RootProvider,
23        fillers::{
24            BlobGasFiller, CachedNonceManager, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
25            WalletFiller,
26        },
27    },
28    rpc::client::ClientBuilder,
29    signers::local::PrivateKeySigner,
30    transports::http::ReqwestTransport,
31};
32use clap::Parser;
33use hopr_chain_api::config::{Addresses as ContractAddresses, EnvironmentType};
34use hopr_crypto_types::keypairs::{ChainKeypair, Keypair};
35use serde::{Deserialize, Serialize};
36use serde_with::{DisplayFromStr, serde_as};
37
38use crate::utils::HelperErrors;
39
40type SharedFillerChain = JoinFill<
41    JoinFill<JoinFill<JoinFill<Identity, ChainIdFiller>, NonceFiller<CachedNonceManager>>, GasFiller>,
42    BlobGasFiller,
43>;
44pub type RpcProvider = FillProvider<JoinFill<SharedFillerChain, WalletFiller<EthereumWallet>>, RootProvider>;
45pub type RpcProviderWithoutSigner = FillProvider<SharedFillerChain, RootProvider>;
46
47// replace NetworkConfig with ProtocolConfig
48#[serde_as]
49#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
50#[serde(deny_unknown_fields)]
51pub struct NetworkDetail {
52    /// block number to start the indexer from
53    pub indexer_start_block_number: u32,
54    /// Type of environment
55    #[serde_as(as = "DisplayFromStr")]
56    pub environment_type: EnvironmentType,
57    /// contract addresses used by the network
58    pub addresses: ContractAddresses,
59}
60
61/// mapping of networks with its details
62#[derive(Default, Debug, Clone, Serialize, Deserialize)]
63pub struct NetworkConfig {
64    // #[serde(flatten)]
65    networks: HashMap<String, NetworkDetail>,
66}
67
68/// Arguments for getting network and ethereum RPC provider.
69///
70/// RPC provider specifies an endpoint that enables an application to communicate with a blockchain network
71/// If not specified, it uses the default value according to the environment config
72/// Network specifies a set of contracts used in HOPR network.
73#[derive(Debug, Clone, Parser)]
74pub struct NetworkProviderArgs {
75    /// Name of the network that the node is running on
76    #[clap(help = "Network name. E.g. monte_rosa", long, short)]
77    network: String,
78
79    /// Path to the root of foundry project (ethereum/contracts), where all the contracts and
80    /// `contracts-addresses.json` are stored Default to "./ethereum/contracts", which is the path to the
81    /// `contracts` folder from the root of monorepo
82    #[clap(
83        env = "HOPLI_CONTRACTS_ROOT",
84        help = "Specify path pointing to the contracts root",
85        long,
86        short,
87        default_value = "./ethereum/contracts"
88    )]
89    contracts_root: Option<String>,
90
91    /// Customized RPC provider endpoint
92    #[clap(help = "Blockchain RPC provider endpoint.", long, short = 'r')]
93    provider_url: String,
94}
95
96impl Default for NetworkProviderArgs {
97    fn default() -> Self {
98        Self {
99            network: "anvil-localhost".into(),
100            contracts_root: Some("./ethereum/contracts".into()),
101            provider_url: "http://127.0.0.1:8545".into(),
102        }
103    }
104}
105
106impl NetworkProviderArgs {
107    /// Get the NetworkDetail (contract addresses, environment type) from network names
108    pub fn get_network_details_from_name(&self) -> Result<NetworkDetail, HelperErrors> {
109        // read `contracts-addresses.json` at make_root_dir_path
110        let contract_root = self.contracts_root.to_owned().unwrap_or(
111            NetworkProviderArgs::default()
112                .contracts_root
113                .ok_or(HelperErrors::UnableToSetFoundryRoot)?,
114        );
115        let contract_environment_config_path =
116            PathBuf::from(OsStr::new(&contract_root)).join("contracts-addresses.json");
117
118        let file_read =
119            std::fs::read_to_string(contract_environment_config_path).map_err(HelperErrors::UnableToReadFromPath)?;
120
121        let network_config = serde_json::from_str::<NetworkConfig>(&file_read).map_err(HelperErrors::SerdeJson)?;
122
123        network_config
124            .networks
125            .get(&self.network)
126            .cloned()
127            .ok_or_else(|| HelperErrors::UnknownNetwork)
128    }
129
130    /// get the provider object
131    pub async fn get_provider_with_signer(&self, chain_key: &ChainKeypair) -> Result<Arc<RpcProvider>, HelperErrors>
132// ) -> Result<Arc<NonceManagerMiddleware<SignerMiddleware<Provider<JsonRpcClient>, Wallet<SigningKey>>>>, HelperErrors>
133    {
134        // Build transport
135        let parsed_url = url::Url::parse(self.provider_url.as_str()).unwrap();
136        let transport_client = ReqwestTransport::new(parsed_url);
137
138        // Build JSON RPC client
139        let rpc_client = ClientBuilder::default().transport(transport_client.clone(), transport_client.guess_local());
140
141        if rpc_client.is_local() {
142            rpc_client.set_poll_interval(std::time::Duration::from_millis(10));
143        };
144
145        // build wallet
146        let wallet = PrivateKeySigner::from_slice(chain_key.secret().as_ref()).expect("failed to construct wallet");
147
148        // Build default JSON RPC provider
149        let provider = ProviderBuilder::new()
150            .disable_recommended_fillers()
151            .filler(ChainIdFiller::default())
152            .filler(NonceFiller::new(CachedNonceManager::default()))
153            .filler(GasFiller)
154            .filler(BlobGasFiller)
155            .wallet(wallet)
156            .connect_client(rpc_client);
157
158        Ok(Arc::new(provider))
159    }
160
161    /// get the provider object without signer
162    pub async fn get_provider_without_signer(&self) -> Result<Arc<RpcProviderWithoutSigner>, HelperErrors> {
163        // Build transport
164        let parsed_url = url::Url::parse(self.provider_url.as_str()).unwrap();
165        let transport_client = ReqwestTransport::new(parsed_url);
166
167        // Build JSON RPC client
168        let rpc_client = ClientBuilder::default().transport(transport_client.clone(), transport_client.guess_local());
169
170        if rpc_client.is_local() {
171            rpc_client.set_poll_interval(std::time::Duration::from_millis(10));
172        };
173
174        // Build default JSON RPC provider
175        let provider = ProviderBuilder::new()
176            .disable_recommended_fillers()
177            // .wallet(wallet)
178            .filler(ChainIdFiller::default())
179            .filler(NonceFiller::new(CachedNonceManager::default()))
180            .filler(GasFiller)
181            .filler(BlobGasFiller)
182            .connect_client(rpc_client);
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 alloy::{
234        node_bindings::{Anvil, AnvilInstance},
235        providers::Provider,
236    };
237    use anyhow::Context;
238
239    use super::*;
240
241    fn create_anvil_at_port(default: bool) -> AnvilInstance {
242        let mut anvil = Anvil::new();
243
244        if !default {
245            let listener =
246                std::net::TcpListener::bind("127.0.0.1:0").unwrap_or_else(|_| panic!("Failed to bind localhost"));
247            let random_port = listener
248                .local_addr()
249                .unwrap_or_else(|_| panic!("Failed to get local address"))
250                .port();
251            anvil = anvil.port(random_port);
252            anvil = anvil.chain_id(random_port.into());
253        } else {
254            anvil = anvil.port(8545u16);
255        }
256        anvil.spawn()
257    }
258
259    #[test]
260    fn read_anvil_localhost_at_right_path() -> anyhow::Result<()> {
261        let correct_dir = &std::env::current_dir()
262            .context("Current dir failed")?
263            .parent()
264            .context("Parent dir failed")?
265            .join("ethereum")
266            .join("contracts");
267        let network = "anvil-localhost";
268        let environment_type = "local";
269        assert!(ensure_environment_and_network_are_set(
270            correct_dir,
271            network,
272            environment_type
273        )?);
274        Ok(())
275    }
276
277    #[test]
278    fn read_anvil_localhost_at_wrong_path() -> anyhow::Result<()> {
279        let wrong_dir = &std::env::current_dir().context("Current dir failed")?;
280        let network = "anvil-localhost";
281        let environment_type = "local";
282        assert!(ensure_environment_and_network_are_set(wrong_dir, network, environment_type).is_err());
283        Ok(())
284    }
285
286    #[test]
287    fn read_non_existing_environment_at_right_path() -> anyhow::Result<()> {
288        let correct_dir = &std::env::current_dir()
289            .context("Current dir failed")?
290            .parent()
291            .context("Parent dir failed")?
292            .join("ethereum")
293            .join("contracts");
294
295        assert!(ensure_environment_and_network_are_set(correct_dir, "non-existing", "development").is_err());
296        Ok(())
297    }
298
299    #[test]
300    fn read_wrong_type_at_right_path() -> anyhow::Result<()> {
301        let correct_dir = &std::env::current_dir()
302            .context("Current dir failed")?
303            .parent()
304            .context("Parent dir failed")?
305            .join("ethereum")
306            .join("contracts");
307        let network = "anvil-localhost";
308        let environment_type = "production";
309        assert!(!ensure_environment_and_network_are_set(
310            correct_dir,
311            network,
312            environment_type
313        )?);
314        Ok(())
315    }
316
317    #[tokio::test]
318    async fn test_network_provider_with_signer() -> anyhow::Result<()> {
319        // create an identity
320        let chain_key = ChainKeypair::random();
321
322        // launch local anvil instance
323        let anvil = create_anvil_at_port(false);
324
325        let network_provider_args = NetworkProviderArgs {
326            network: "anvil-localhost".into(),
327            contracts_root: Some("../ethereum/contracts".into()),
328            provider_url: anvil.endpoint(),
329        };
330
331        let provider = network_provider_args.get_provider_with_signer(&chain_key).await?;
332
333        let chain_id = provider.get_chain_id().await?;
334        assert_eq!(chain_id, anvil.chain_id());
335        Ok(())
336    }
337
338    #[tokio::test]
339    async fn test_default_contracts_root() -> anyhow::Result<()> {
340        // create an identity
341        let chain_key = ChainKeypair::random();
342
343        // launch local anvil instance
344        let anvil = create_anvil_at_port(false);
345
346        let network_provider_args = NetworkProviderArgs {
347            network: "anvil-localhost".into(),
348            contracts_root: None,
349            provider_url: anvil.endpoint(),
350        };
351
352        let provider = network_provider_args.get_provider_with_signer(&chain_key).await?;
353
354        let chain_id = provider.get_chain_id().await?;
355        assert_eq!(chain_id, anvil.chain_id());
356        Ok(())
357    }
358}