1use 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#[serde_as]
45#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
46#[serde(deny_unknown_fields)]
47pub struct NetworkDetail {
48 pub indexer_start_block_number: u32,
50 #[serde_as(as = "DisplayFromStr")]
52 pub environment_type: EnvironmentType,
53 pub addresses: ContractAddresses,
55}
56
57#[derive(Default, Debug, Clone, Serialize, Deserialize)]
59pub struct NetworkConfig {
60 networks: HashMap<String, NetworkDetail>,
62}
63
64#[derive(Debug, Clone, Parser)]
70pub struct NetworkProviderArgs {
71 #[clap(help = "Network name. E.g. monte_rosa", long, short)]
73 network: String,
74
75 #[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 #[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 pub fn get_network_details_from_name(&self) -> Result<NetworkDetail, HelperErrors> {
104 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 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 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 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(ðers::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 pub async fn get_provider_without_signer(&self) -> Result<Arc<Provider<JsonRpcClient>>, HelperErrors> {
163 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 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(ðers::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
188pub 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
204pub 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
214pub fn get_network_details_from_name(make_root_dir_path: &Path, network: &str) -> Result<NetworkDetail, HelperErrors> {
216 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 let chain_key = ChainKeypair::random();
314
315 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 let chain_key = ChainKeypair::random();
335
336 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}