1use 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#[serde_as]
49#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
50#[serde(deny_unknown_fields)]
51pub struct NetworkDetail {
52 pub indexer_start_block_number: u32,
54 #[serde_as(as = "DisplayFromStr")]
56 pub environment_type: EnvironmentType,
57 pub addresses: ContractAddresses,
59}
60
61#[derive(Default, Debug, Clone, Serialize, Deserialize)]
63pub struct NetworkConfig {
64 networks: HashMap<String, NetworkDetail>,
66}
67
68#[derive(Debug, Clone, Parser)]
74pub struct NetworkProviderArgs {
75 #[clap(help = "Network name. E.g. monte_rosa", long, short)]
77 network: String,
78
79 #[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 #[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 pub fn get_network_details_from_name(&self) -> Result<NetworkDetail, HelperErrors> {
109 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 pub async fn get_provider_with_signer(&self, chain_key: &ChainKeypair) -> Result<Arc<RpcProvider>, HelperErrors>
132{
134 let parsed_url = url::Url::parse(self.provider_url.as_str()).unwrap();
136 let transport_client = ReqwestTransport::new(parsed_url);
137
138 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 let wallet = PrivateKeySigner::from_slice(chain_key.secret().as_ref()).expect("failed to construct wallet");
147
148 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 pub async fn get_provider_without_signer(&self) -> Result<Arc<RpcProviderWithoutSigner>, HelperErrors> {
163 let parsed_url = url::Url::parse(self.provider_url.as_str()).unwrap();
165 let transport_client = ReqwestTransport::new(parsed_url);
166
167 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 let provider = ProviderBuilder::new()
176 .disable_recommended_fillers()
177 .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
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 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 let chain_key = ChainKeypair::random();
321
322 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 let chain_key = ChainKeypair::random();
342
343 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}