nym_network_defaults/
network.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{GAS_PRICE_AMOUNT, mainnet};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::ops::Not;
8use url::Url;
9
10#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
11#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
12pub struct ChainDetails {
13    pub bech32_account_prefix: String,
14    pub mix_denom: DenomDetailsOwned,
15    pub stake_denom: DenomDetailsOwned,
16}
17
18#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
19#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
20pub struct NymContracts {
21    pub mixnet_contract_address: Option<String>,
22    pub vesting_contract_address: Option<String>,
23    #[serde(default)]
24    pub performance_contract_address: Option<String>,
25    pub ecash_contract_address: Option<String>,
26    pub group_contract_address: Option<String>,
27    pub multisig_contract_address: Option<String>,
28    pub coconut_dkg_contract_address: Option<String>,
29}
30
31// I wanted to use the simpler `NetworkDetails` name, but there's a clash
32// with `NetworkDetails` defined in all.rs...
33#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
34#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
35pub struct NymNetworkDetails {
36    pub network_name: String,
37    pub chain_details: ChainDetails,
38    pub endpoints: Vec<ValidatorDetails>,
39    pub contracts: NymContracts,
40    pub nym_vpn_api_url: Option<String>,
41    pub nym_api_urls: Option<Vec<ApiUrl>>,
42    pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
43}
44
45#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
46#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
47pub struct ApiUrl {
48    /// Expects a string formatted Url
49    ///
50    /// see https://docs.rs/url/latest/url/struct.Url.html
51    pub url: String,
52    /// Optional alternative equivalent hostnames. Each entry must parse as valid Host
53    ///
54    /// see https://docs.rs/url/latest/url/enum.Host.html
55    pub front_hosts: Option<Vec<String>>,
56}
57
58#[derive(Copy, Clone)]
59pub struct ApiUrlConst<'a> {
60    pub url: &'a str,
61    pub front_hosts: Option<&'a [&'a str]>,
62}
63
64impl From<ApiUrlConst<'_>> for ApiUrl {
65    fn from(value: ApiUrlConst) -> Self {
66        ApiUrl {
67            url: value.url.to_string(),
68            front_hosts: value
69                .front_hosts
70                .map(|slice| slice.iter().map(|s| s.to_string()).collect()),
71        }
72    }
73}
74
75// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
76impl Default for NymNetworkDetails {
77    fn default() -> Self {
78        NymNetworkDetails::new_mainnet()
79    }
80}
81
82impl NymNetworkDetails {
83    pub fn new_empty() -> Self {
84        NymNetworkDetails {
85            network_name: Default::default(),
86            chain_details: ChainDetails {
87                bech32_account_prefix: Default::default(),
88                mix_denom: DenomDetailsOwned {
89                    base: Default::default(),
90                    display: Default::default(),
91                    display_exponent: Default::default(),
92                },
93                stake_denom: DenomDetailsOwned {
94                    base: Default::default(),
95                    display: Default::default(),
96                    display_exponent: Default::default(),
97                },
98            },
99            endpoints: Default::default(),
100            contracts: Default::default(),
101            nym_vpn_api_url: Default::default(),
102            nym_api_urls: Default::default(),
103            nym_vpn_api_urls: Default::default(),
104        }
105    }
106
107    #[cfg(feature = "env")]
108    pub fn new_from_env() -> Self {
109        use crate::var_names;
110        use std::env::{VarError, var};
111        use std::ffi::OsStr;
112
113        fn get_optional_env<K: AsRef<OsStr>>(env: K) -> Option<String> {
114            match var(env) {
115                Ok(var) => {
116                    if var.is_empty() {
117                        None
118                    } else {
119                        Some(var)
120                    }
121                }
122                Err(VarError::NotPresent) => None,
123                err => panic!("Unable to set: {err:?}"),
124            }
125        }
126
127        let nym_api = var(var_names::NYM_API).expect("nym api not set");
128
129        NymNetworkDetails::new_empty()
130            .with_network_name(var(var_names::NETWORK_NAME).expect("network name not set"))
131            .with_bech32_account_prefix(
132                var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"),
133            )
134            .with_mix_denom(DenomDetailsOwned {
135                base: var(var_names::MIX_DENOM).expect("mix denomination base not set"),
136                display: var(var_names::MIX_DENOM_DISPLAY)
137                    .expect("mix denomination display not set"),
138                display_exponent: var(var_names::DENOMS_EXPONENT)
139                    .expect("denomination exponent not set")
140                    .parse()
141                    .expect("denomination exponent is not u32"),
142            })
143            .with_stake_denom(DenomDetailsOwned {
144                base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"),
145                display: var(var_names::STAKE_DENOM_DISPLAY)
146                    .expect("stake denomination display not set"),
147                display_exponent: var(var_names::DENOMS_EXPONENT)
148                    .expect("denomination exponent not set")
149                    .parse()
150                    .expect("denomination exponent is not u32"),
151            })
152            .with_additional_validator_endpoint(ValidatorDetails::new(
153                var(var_names::NYXD).expect("nyxd validator not set"),
154                Some(nym_api.clone()),
155                get_optional_env(var_names::NYXD_WEBSOCKET),
156            ))
157            .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
158            .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
159            .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
160            .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
161            .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
162            .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
163            .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
164            .with_nym_api_urls(Some(vec![ApiUrl {
165                url: nym_api,
166                front_hosts: None,
167            }]))
168    }
169
170    pub fn new_mainnet() -> Self {
171        fn parse_optional_str(raw: &str) -> Option<String> {
172            raw.is_empty().not().then(|| raw.into())
173        }
174
175        // Consider caching this process (lazy static)
176        NymNetworkDetails {
177            network_name: mainnet::NETWORK_NAME.into(),
178            chain_details: ChainDetails {
179                bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
180                mix_denom: mainnet::MIX_DENOM.into(),
181                stake_denom: mainnet::STAKE_DENOM.into(),
182            },
183            endpoints: mainnet::validators(),
184            contracts: NymContracts {
185                mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
186                vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
187                performance_contract_address: parse_optional_str(
188                    mainnet::PERFORMANCE_CONTRACT_ADDRESS,
189                ),
190                ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
191                group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
192                multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
193                coconut_dkg_contract_address: parse_optional_str(
194                    mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
195                ),
196            },
197            nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
198            nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()),
199            nym_vpn_api_urls: Some(
200                mainnet::NYM_VPN_APIS
201                    .iter()
202                    .copied()
203                    .map(Into::into)
204                    .collect(),
205            ),
206        }
207    }
208
209    #[rustfmt::skip]
210    #[cfg(feature = "env")]
211    pub fn export_to_env(self) {
212        use crate::var_names;
213        use std::env::set_var;
214
215        fn set_optional_var(var_name: &str, value: Option<String>) {
216            if let Some(value) = value {
217                unsafe {set_var(var_name, value)}
218            }
219        }
220        unsafe {
221            set_var(var_names::NETWORK_NAME, self.network_name);
222            set_var(var_names::BECH32_PREFIX, self.chain_details.bech32_account_prefix);
223
224            set_var(var_names::MIX_DENOM, self.chain_details.mix_denom.base);
225            set_var(var_names::MIX_DENOM_DISPLAY, self.chain_details.mix_denom.display);
226
227            set_var(var_names::STAKE_DENOM, self.chain_details.stake_denom.base);
228            set_var(var_names::STAKE_DENOM_DISPLAY, self.chain_details.stake_denom.display);
229
230            set_var(var_names::DENOMS_EXPONENT, self.chain_details.mix_denom.display_exponent.to_string());
231
232            if let Some(e) = self.endpoints.first() {
233                set_var(var_names::NYXD, e.nyxd_url.clone());
234                set_optional_var(var_names::NYM_API, e.api_url.clone());
235                set_optional_var(var_names::NYXD_WEBSOCKET, e.websocket_url.clone());
236            }
237
238            set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
239            set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
240            set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
241            set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
242            set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
243            set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
244
245            set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
246        }
247
248
249    }
250
251    pub fn default_gas_price_amount(&self) -> f64 {
252        GAS_PRICE_AMOUNT
253    }
254
255    #[must_use]
256    pub fn with_network_name(mut self, network_name: String) -> Self {
257        self.network_name = network_name;
258        self
259    }
260
261    #[must_use]
262    pub fn with_chain_details(mut self, chain_details: ChainDetails) -> Self {
263        self.chain_details = chain_details;
264        self
265    }
266
267    #[must_use]
268    pub fn with_bech32_account_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
269        self.chain_details.bech32_account_prefix = prefix.into();
270        self
271    }
272
273    #[must_use]
274    pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self {
275        self.chain_details.mix_denom = mix_denom;
276        self
277    }
278
279    #[must_use]
280    pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self {
281        self.chain_details.stake_denom = stake_denom;
282        self
283    }
284
285    #[must_use]
286    pub fn with_base_mix_denom<S: Into<String>>(mut self, base_mix_denom: S) -> Self {
287        self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into());
288        self
289    }
290
291    #[must_use]
292    pub fn with_base_stake_denom<S: Into<String>>(mut self, base_stake_denom: S) -> Self {
293        self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into());
294        self
295    }
296
297    #[must_use]
298    pub fn with_additional_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self {
299        self.endpoints.push(endpoint);
300        self
301    }
302
303    #[must_use]
304    pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self {
305        self.endpoints = vec![endpoint];
306        self
307    }
308
309    #[must_use]
310    pub fn with_contracts(mut self, contracts: NymContracts) -> Self {
311        self.contracts = contracts;
312        self
313    }
314
315    #[must_use]
316    pub fn with_mixnet_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
317        self.contracts.mixnet_contract_address = contract.map(Into::into);
318        self
319    }
320
321    #[must_use]
322    pub fn with_vesting_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
323        self.contracts.vesting_contract_address = contract.map(Into::into);
324        self
325    }
326
327    #[must_use]
328    pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
329        self.contracts.ecash_contract_address = contract.map(Into::into);
330        self
331    }
332
333    #[must_use]
334    pub fn with_group_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
335        self.contracts.group_contract_address = contract.map(Into::into);
336        self
337    }
338
339    #[must_use]
340    pub fn with_multisig_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
341        self.contracts.multisig_contract_address = contract.map(Into::into);
342        self
343    }
344
345    #[must_use]
346    pub fn with_coconut_dkg_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
347        self.contracts.coconut_dkg_contract_address = contract.map(Into::into);
348        self
349    }
350
351    #[must_use]
352    pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
353        self.nym_vpn_api_url = endpoint.map(Into::into);
354        self
355    }
356
357    #[must_use]
358    pub fn with_nym_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
359        self.nym_api_urls = urls;
360        self
361    }
362
363    pub fn nym_vpn_api_url(&self) -> Option<Url> {
364        self.nym_vpn_api_url.as_ref().map(|url| {
365            url.parse()
366                .expect("the provided nym-vpn api url is invalid!")
367        })
368    }
369}
370
371#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
372pub struct DenomDetails {
373    pub base: &'static str,
374    pub display: &'static str,
375    // i.e. display_amount * 10^display_exponent = base_amount
376    pub display_exponent: u32,
377}
378
379impl DenomDetails {
380    pub const fn new(base: &'static str, display: &'static str, display_exponent: u32) -> Self {
381        DenomDetails {
382            base,
383            display,
384            display_exponent,
385        }
386    }
387}
388
389#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)]
390#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
391pub struct DenomDetailsOwned {
392    pub base: String,
393    pub display: String,
394    // i.e. display_amount * 10^display_exponent = base_amount
395    pub display_exponent: u32,
396}
397
398impl From<DenomDetails> for DenomDetailsOwned {
399    fn from(details: DenomDetails) -> Self {
400        DenomDetailsOwned {
401            base: details.base.to_owned(),
402            display: details.display.to_owned(),
403            display_exponent: details.display_exponent,
404        }
405    }
406}
407
408impl DenomDetailsOwned {
409    pub fn base_only(base: String) -> Self {
410        DenomDetailsOwned {
411            base: base.clone(),
412            display: base,
413            display_exponent: 0,
414        }
415    }
416}
417
418#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
419#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
420pub struct ValidatorDetails {
421    // it is assumed those values are always valid since they're being provided in our defaults file
422    pub nyxd_url: String,
423    //
424    pub websocket_url: Option<String>,
425
426    // Right now api_url is optional as we are not running the api reliably on all validators
427    // however, later on it should be a mandatory field
428    pub api_url: Option<String>,
429    // TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting
430}
431
432impl ValidatorDetails {
433    pub fn new<S: Into<String>>(nyxd_url: S, api_url: Option<S>, websocket_url: Option<S>) -> Self {
434        ValidatorDetails {
435            nyxd_url: nyxd_url.into(),
436            websocket_url: websocket_url.map(Into::into),
437            api_url: api_url.map(Into::into),
438        }
439    }
440
441    pub fn new_nyxd_only<S: Into<String>>(nyxd_url: S) -> Self {
442        ValidatorDetails {
443            nyxd_url: nyxd_url.into(),
444            websocket_url: None,
445            api_url: None,
446        }
447    }
448
449    pub fn nyxd_url(&self) -> Url {
450        self.nyxd_url
451            .parse()
452            .expect("the provided nyxd url is invalid!")
453    }
454
455    pub fn api_url(&self) -> Option<Url> {
456        self.api_url
457            .as_ref()
458            .map(|url| url.parse().expect("the provided api url is invalid!"))
459    }
460
461    pub fn websocket_url(&self) -> Option<Url> {
462        self.websocket_url
463            .as_ref()
464            .map(|url| url.parse().expect("the provided websocket url is invalid!"))
465    }
466}