hopr_chain_config/
lib.rs

1use std::{collections::BTreeMap, str::FromStr};
2
3use hopr_chain_types::ContractAddresses;
4use hopr_primitive_types::primitives::Address;
5use semver::{Version, VersionReq};
6use serde::{Deserialize, Serialize};
7use serde_with::{DisplayFromStr, serde_as};
8use validator::Validate;
9
10pub mod errors;
11pub use errors::{HoprConfigError, Result};
12
13/// Holds all information we need about the blockchain network
14/// the client is going to use
15#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
16#[serde(deny_unknown_fields)]
17pub struct ChainOptions {
18    pub description: String,
19    /// >= 0
20    pub chain_id: u32,
21    pub live: bool,
22    /// a valid HTTP url pointing at a RPC endpoint
23    pub default_provider: String,
24    /// a valid HTTP url pointing at a RPC endpoint
25    pub etherscan_api_url: Option<String>,
26    /// The absolute maximum you are willing to pay per unit of gas to get your transaction included in a block, e.g.
27    /// '10 gwei'
28    pub max_fee_per_gas: String,
29    /// Tips paid directly to miners, e.g. '2 gwei'
30    pub max_priority_fee_per_gas: String,
31    pub native_token_name: String,
32    pub hopr_token_name: String,
33    /// expected block time on the chain in milliseconds
34    pub block_time: u64,
35    /// optional maximum number of RPC requests per second for this chain provider
36    pub max_rpc_requests_per_sec: Option<u32>,
37    pub tags: Option<Vec<String>>,
38}
39
40/// Types of HOPR network environments.
41#[derive(Debug, Copy, Clone, Deserialize, Serialize, Eq, PartialEq, strum::Display, strum::EnumString)]
42#[serde(rename_all(deserialize = "lowercase"))]
43#[strum(serialize_all = "lowercase")]
44pub enum EnvironmentType {
45    Production,
46    Staging,
47    Development,
48    Local,
49}
50
51/// Holds all information about the protocol network
52/// to be used by the client
53#[serde_as]
54#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
55#[serde(deny_unknown_fields)]
56pub struct Network {
57    /// must match one of the Network.id
58    pub chain: String,
59    #[serde_as(as = "DisplayFromStr")]
60    pub environment_type: EnvironmentType,
61    /// Node.js-fashioned semver string
62    pub version_range: String,
63    /// block number to start the indexer from
64    pub indexer_start_block_number: u32,
65    pub tags: Vec<String>,
66    /// contract addresses used by the network
67    pub addresses: Addresses,
68    /// number of follow-on blocks required until a block is considered confirmed on-chain
69    pub confirmations: u32,
70    /// milliseconds between polling the RPC for new transactions
71    pub tx_polling_interval: u64,
72    /// maximum block range to fetch while indexing logs
73    pub max_block_range: u64,
74}
75
76#[serde_as]
77#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
78#[serde(deny_unknown_fields)]
79pub struct Addresses {
80    /// address of contract that manages authorization to access the Hopr network
81    #[serde_as(as = "DisplayFromStr")]
82    pub network_registry: Address,
83    /// address of contract that maps to the requirements that need to be fulfilled
84    /// in order to access the network, upgradeable
85    #[serde_as(as = "DisplayFromStr")]
86    pub network_registry_proxy: Address,
87    /// HoprChannels contract address, implementation of mixnet incentives
88    #[serde_as(as = "DisplayFromStr")]
89    pub channels: Address,
90    /// Hopr token contract address
91    #[serde_as(as = "DisplayFromStr")]
92    pub token: Address,
93    /// contract address of Safe capability module implementation
94    #[serde_as(as = "DisplayFromStr")]
95    pub module_implementation: Address,
96    /// address of contract that maps between Safe instances and node addresses
97    #[serde_as(as = "DisplayFromStr")]
98    pub node_safe_registry: Address,
99    /// address of contract that allows Hopr Association to dictate price per packet in Hopr
100    #[serde_as(as = "DisplayFromStr")]
101    pub ticket_price_oracle: Address,
102    /// address of contract that allows Hopr Association to dictate the minimum ticket winning probability in Hopr
103    #[serde_as(as = "DisplayFromStr")]
104    pub winning_probability_oracle: Address,
105    /// address of contract that manages transport announcements in the hopr network
106    #[serde_as(as = "DisplayFromStr")]
107    pub announcements: Address,
108    /// factory contract to produce Safe instances
109    #[serde_as(as = "DisplayFromStr")]
110    pub node_stake_v2_factory: Address,
111}
112
113#[serde_as]
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ChainNetworkConfig {
116    /// the network identifier, e.g. monte_rosa
117    pub id: String,
118    pub chain: ChainOptions,
119    pub environment_type: EnvironmentType,
120    pub channel_contract_deploy_block: u32,
121    /// address of contract that manages authorization to access the Hopr network
122    #[serde_as(as = "DisplayFromStr")]
123    pub network_registry: Address,
124    /// address of contract that maps to the requirements that need to be fulfilled
125    /// in order to access the network, upgradeable
126    #[serde_as(as = "DisplayFromStr")]
127    pub network_registry_proxy: Address,
128    /// HoprChannels contract address, implementation of mixnet incentives
129    #[serde_as(as = "DisplayFromStr")]
130    pub channels: Address,
131    /// Hopr token contract address
132    #[serde_as(as = "DisplayFromStr")]
133    pub token: Address,
134    /// contract address of Safe capability module implementation
135    #[serde_as(as = "DisplayFromStr")]
136    pub module_implementation: Address,
137    /// address of contract that maps between Safe instances and node addresses
138    #[serde_as(as = "DisplayFromStr")]
139    pub node_safe_registry: Address,
140    /// address of contract that allows Hopr Association to dictate price per packet in Hopr
141    #[serde_as(as = "DisplayFromStr")]
142    pub ticket_price_oracle: Address,
143    /// address of contract that allows Hopr Association to dictate the minimum ticket winning probability in Hopr
144    #[serde_as(as = "DisplayFromStr")]
145    pub winning_probability_oracle: Address,
146    /// address of contract that manages transport announcements in the hopr network
147    #[serde_as(as = "DisplayFromStr")]
148    pub announcements: Address,
149    /// factory contract to produce Safe instances
150    #[serde_as(as = "DisplayFromStr")]
151    pub node_stake_v2_factory: Address,
152    /// number of follow-on blocks required until a block is considered confirmed on-chain
153    pub confirmations: u32,
154    /// milliseconds between polling the RPC for new transactions
155    pub tx_polling_interval: u64,
156    /// maximum block range to fetch when indexing logs
157    pub max_block_range: u64,
158    /// maximum number of RPC requests per second
159    pub max_requests_per_sec: Option<u32>,
160}
161
162/// Check whether the version is allowed
163fn satisfies(version: &str, allowed_versions: &str) -> Result<bool> {
164    let allowed_versions = VersionReq::parse(allowed_versions)
165        .map_err(|e| HoprConfigError::Configuration(format!("failed to deserialize allowed version string: {e}")))?;
166
167    let version = Version::from_str(version).map_err(|e| {
168        HoprConfigError::Configuration(format!("failed to deserialize current lib version string: {e}"))
169    })?;
170
171    Ok(allowed_versions.matches(&version))
172}
173
174impl ChainNetworkConfig {
175    /// Returns the network details, returns an error if network is not supported
176    /// Performs a version check as part of the validation.
177    pub fn new(
178        id: &str,
179        version: &str,
180        maybe_custom_provider: Option<&str>,
181        max_rpc_requests_per_sec: Option<u32>,
182        protocol_config: &mut ProtocolsConfig,
183    ) -> std::result::Result<Self, String> {
184        let network = protocol_config
185            .networks
186            .get_mut(id)
187            .ok_or(format!("Could not find network {id} in protocol config"))?;
188
189        let chain = protocol_config
190            .chains
191            .get_mut(&network.chain)
192            .ok_or(format!("Invalid chain {} for network {id}", network.chain))?;
193
194        if let Some(custom_provider) = maybe_custom_provider {
195            chain.default_provider = custom_provider.into();
196        }
197
198        match satisfies(version, network.version_range.as_str()) {
199            Ok(true) => Ok(ChainNetworkConfig {
200                announcements: network.addresses.announcements.to_owned(),
201                chain: chain.to_owned(),
202                channel_contract_deploy_block: network.indexer_start_block_number,
203                channels: network.addresses.channels.to_owned(),
204                confirmations: network.confirmations,
205                environment_type: network.environment_type,
206                id: network.chain.to_owned(),
207                module_implementation: network.addresses.module_implementation.to_owned(),
208                network_registry: network.addresses.network_registry.to_owned(),
209                network_registry_proxy: network.addresses.network_registry_proxy.to_owned(),
210                node_safe_registry: network.addresses.node_safe_registry.to_owned(),
211                node_stake_v2_factory: network.addresses.node_stake_v2_factory.to_owned(),
212                ticket_price_oracle: network.addresses.ticket_price_oracle.to_owned(),
213                winning_probability_oracle: network.addresses.winning_probability_oracle.to_owned(),
214                token: network.addresses.token.to_owned(),
215                tx_polling_interval: network.tx_polling_interval,
216                max_block_range: network.max_block_range,
217                max_requests_per_sec: max_rpc_requests_per_sec.or(chain.max_rpc_requests_per_sec),
218            }),
219            Ok(false) => Err(HoprConfigError::UnsupportedNetwork(id.into()).to_string()),
220            Err(e) => Err(e.to_string()),
221        }
222    }
223
224    /// Returns the network details, returns an error if network is not supported
225    pub fn new_no_version_check(
226        id: &str,
227        maybe_custom_provider: Option<&str>,
228        max_rpc_requests_per_sec: Option<u32>,
229        protocol_config: &mut ProtocolsConfig,
230    ) -> std::result::Result<Self, String> {
231        let network = protocol_config
232            .networks
233            .get_mut(id)
234            .ok_or(format!("Could not find network {id} in protocol config"))?;
235
236        let chain = protocol_config
237            .chains
238            .get_mut(&network.chain)
239            .ok_or(format!("Invalid chain {} for network {id}", network.chain))?;
240
241        if let Some(custom_provider) = maybe_custom_provider {
242            chain.default_provider = custom_provider.into();
243        }
244
245        Ok(ChainNetworkConfig {
246            announcements: network.addresses.announcements.to_owned(),
247            chain: chain.to_owned(),
248            channel_contract_deploy_block: network.indexer_start_block_number,
249            channels: network.addresses.channels.to_owned(),
250            confirmations: network.confirmations,
251            environment_type: network.environment_type,
252            id: network.chain.to_owned(),
253            module_implementation: network.addresses.module_implementation.to_owned(),
254            network_registry: network.addresses.network_registry.to_owned(),
255            network_registry_proxy: network.addresses.network_registry_proxy.to_owned(),
256            node_safe_registry: network.addresses.node_safe_registry.to_owned(),
257            node_stake_v2_factory: network.addresses.node_stake_v2_factory.to_owned(),
258            ticket_price_oracle: network.addresses.ticket_price_oracle.to_owned(),
259            winning_probability_oracle: network.addresses.winning_probability_oracle.to_owned(),
260            token: network.addresses.token.to_owned(),
261            tx_polling_interval: network.tx_polling_interval,
262            max_block_range: network.max_block_range,
263            max_requests_per_sec: max_rpc_requests_per_sec.or(chain.max_rpc_requests_per_sec),
264        })
265    }
266}
267
268impl From<&ChainNetworkConfig> for ContractAddresses {
269    fn from(network: &ChainNetworkConfig) -> Self {
270        Self {
271            token: network.token,
272            channels: network.channels,
273            announcements: network.announcements,
274            network_registry: network.network_registry,
275            network_registry_proxy: network.network_registry_proxy,
276            safe_registry: network.node_safe_registry,
277            price_oracle: network.ticket_price_oracle,
278            win_prob_oracle: network.winning_probability_oracle,
279            stake_factory: network.node_stake_v2_factory,
280            module_implementation: network.module_implementation,
281        }
282    }
283}
284
285/// The entire on-chain protocol configuration containing the information about
286/// usable networks and chains.
287#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
288#[serde(deny_unknown_fields)]
289pub struct ProtocolsConfig {
290    pub networks: BTreeMap<String, Network>,
291    pub chains: BTreeMap<String, ChainOptions>,
292}
293
294impl Default for ProtocolsConfig {
295    fn default() -> Self {
296        Self::from_str(include_str!(concat!(env!("OUT_DIR"), "/protocol-config.json")))
297            .expect("bundled protocol config should be always valid")
298    }
299}
300
301impl FromStr for ProtocolsConfig {
302    type Err = HoprConfigError;
303
304    /// Reads the protocol config JSON file and returns it
305    fn from_str(data: &str) -> std::result::Result<Self, Self::Err> {
306        serde_json::from_str::<ProtocolsConfig>(data).map_err(|e| HoprConfigError::Configuration(e.to_string()))
307    }
308}
309
310impl std::cmp::PartialEq for ProtocolsConfig {
311    fn eq(&self, other: &Self) -> bool {
312        Vec::from_iter(self.networks.clone()) == Vec::from_iter(other.networks.clone())
313            && Vec::from_iter(self.chains.clone()) == Vec::from_iter(other.chains.clone())
314    }
315}
316
317impl ProtocolsConfig {
318    /// Returns a list of environments which the node is able to work with
319    /// TODO: crate::constants::APP_VERSION_COERCED
320    pub fn supported_networks(&self, version: &str) -> Vec<String> {
321        let mut allowed = vec![];
322
323        for (name, env) in self.networks.iter() {
324            let range = env.version_range.to_owned();
325
326            if let Ok(true) = satisfies(version, range.as_str()) {
327                allowed.push(name.clone())
328            }
329        }
330
331        allowed
332    }
333}
334#[cfg(test)]
335mod test {
336    use super::*;
337
338    #[test]
339    fn test_default_protocol_config_can_be_deserialized() {
340        let _ = ProtocolsConfig::default();
341    }
342
343    #[test]
344    fn test_version_is_satisfied_should_work_on_ranges() {
345        let actual = satisfies("1.90.0", ">=1.89, <1.93");
346        assert!(actual.is_ok());
347        assert!(actual.expect("should be contained"))
348    }
349
350    #[test]
351    fn test_version_is_satisfied_should_work_for_glob() {
352        let actual = satisfies("1.2.3", "*");
353        assert!(actual.is_ok());
354        assert!(actual.expect("should be contained"))
355    }
356}