// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::{GAS_PRICE_AMOUNT, mainnet}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Not; use url::Url; #[cfg(feature = "env")] use crate::var_names; #[cfg(feature = "env")] use std::env::{VarError, var}; #[cfg(feature = "env")] use std::ffi::OsStr; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ChainDetails { pub bech32_account_prefix: String, pub mix_denom: DenomDetailsOwned, pub stake_denom: DenomDetailsOwned, } impl ChainDetails { pub fn mainnet() -> Self { ChainDetails { bech32_account_prefix: mainnet::BECH32_PREFIX.into(), mix_denom: mainnet::MIX_DENOM.into(), stake_denom: mainnet::STAKE_DENOM.into(), } } } #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, #[serde(default)] pub performance_contract_address: Option, pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, } // I wanted to use the simpler `NetworkDetails` name, but there's a clash // with `NetworkDetails` defined in all.rs... #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct NymNetworkDetails { pub network_name: String, pub chain_details: ChainDetails, pub endpoints: Vec, pub contracts: NymContracts, pub nym_vpn_api_url: Option, pub nym_api_urls: Option>, pub nym_vpn_api_urls: Option>, } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ApiUrl { /// Expects a string formatted Url /// /// see https://docs.rs/url/latest/url/struct.Url.html pub url: String, /// Optional alternative equivalent hostnames. Each entry must parse as valid Host /// /// see https://docs.rs/url/latest/url/enum.Host.html pub front_hosts: Option>, } #[derive(Copy, Clone, Debug, Serialize)] pub struct ApiUrlConst<'a> { pub url: &'a str, pub front_hosts: Option<&'a [&'a str]>, } impl From> for ApiUrl { fn from(value: ApiUrlConst) -> Self { ApiUrl { url: value.url.to_string(), front_hosts: value .front_hosts .map(|slice| slice.iter().map(|s| s.to_string()).collect()), } } } // by default we assume the same defaults as mainnet, i.e. same prefixes and denoms impl Default for NymNetworkDetails { fn default() -> Self { NymNetworkDetails::new_mainnet() } } impl NymNetworkDetails { pub fn new_empty() -> Self { NymNetworkDetails { network_name: Default::default(), chain_details: ChainDetails { bech32_account_prefix: Default::default(), mix_denom: DenomDetailsOwned { base: Default::default(), display: Default::default(), display_exponent: Default::default(), }, stake_denom: DenomDetailsOwned { base: Default::default(), display: Default::default(), display_exponent: Default::default(), }, }, endpoints: Default::default(), contracts: Default::default(), nym_vpn_api_url: Default::default(), nym_api_urls: Default::default(), nym_vpn_api_urls: Default::default(), } } #[cfg(feature = "env")] pub fn new_from_env() -> Self { fn get_optional_env>(env: K) -> Option { match var(env) { Ok(var) => { if var.is_empty() { None } else { Some(var) } } Err(VarError::NotPresent) => None, err => panic!("Unable to set: {err:?}"), } } let nym_api = var(var_names::NYM_API).expect("nym api not set"); let nym_api_urls = try_parse_api_urls(var_names::NYM_APIS).unwrap_or(vec![ApiUrl { url: nym_api.clone(), front_hosts: None, }]); let nym_vpn_api_urls = try_parse_api_urls(var_names::NYM_VPN_APIS); NymNetworkDetails::new_empty() .with_network_name(var(var_names::NETWORK_NAME).expect("network name not set")) .with_bech32_account_prefix( var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"), ) .with_mix_denom(DenomDetailsOwned { base: var(var_names::MIX_DENOM).expect("mix denomination base not set"), display: var(var_names::MIX_DENOM_DISPLAY) .expect("mix denomination display not set"), display_exponent: var(var_names::DENOMS_EXPONENT) .expect("denomination exponent not set") .parse() .expect("denomination exponent is not u32"), }) .with_stake_denom(DenomDetailsOwned { base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"), display: var(var_names::STAKE_DENOM_DISPLAY) .expect("stake denomination display not set"), display_exponent: var(var_names::DENOMS_EXPONENT) .expect("denomination exponent not set") .parse() .expect("denomination exponent is not u32"), }) .with_additional_validator_endpoint(ValidatorDetails::new( var(var_names::NYXD).expect("nyxd validator not set"), Some(nym_api.clone()), get_optional_env(var_names::NYXD_WEBSOCKET), )) .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS)) .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS)) .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS)) .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API)) .with_nym_vpn_api_urls(nym_vpn_api_urls) .with_nym_api_urls(nym_api_urls) } pub fn new_mainnet() -> Self { fn parse_optional_str(raw: &str) -> Option { raw.is_empty().not().then(|| raw.into()) } // Consider caching this process (lazy static) NymNetworkDetails { network_name: mainnet::NETWORK_NAME.into(), chain_details: ChainDetails::mainnet(), endpoints: mainnet::validators(), contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), performance_contract_address: parse_optional_str( mainnet::PERFORMANCE_CONTRACT_ADDRESS, ), ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str( mainnet::COCONUT_DKG_CONTRACT_ADDRESS, ), }, nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()), nym_vpn_api_urls: Some( mainnet::NYM_VPN_APIS .iter() .copied() .map(Into::into) .collect(), ), } } #[rustfmt::skip] #[cfg(feature = "env")] pub fn export_to_env(self) { use crate::var_names; use std::env::set_var; fn set_optional_var(var_name: &str, value: Option) { if let Some(value) = value { unsafe {set_var(var_name, value)} } } unsafe { let nym_api_urls = self.nym_api_urls(); let nym_vpn_api_urls = self.nym_vpn_api_urls(); set_var(var_names::NETWORK_NAME, self.network_name); set_var(var_names::BECH32_PREFIX, self.chain_details.bech32_account_prefix); set_var(var_names::MIX_DENOM, self.chain_details.mix_denom.base); set_var(var_names::MIX_DENOM_DISPLAY, self.chain_details.mix_denom.display); set_var(var_names::STAKE_DENOM, self.chain_details.stake_denom.base); set_var(var_names::STAKE_DENOM_DISPLAY, self.chain_details.stake_denom.display); set_var(var_names::DENOMS_EXPONENT, self.chain_details.mix_denom.display_exponent.to_string()); if let Some(e) = self.endpoints.first() { set_var(var_names::NYXD, e.nyxd_url.clone()); set_optional_var(var_names::NYM_API, e.api_url.clone()); set_optional_var(var_names::NYXD_WEBSOCKET, e.websocket_url.clone()); } set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address); set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address); set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address); set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address); set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address); set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address); set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url); set_optional_var(var_names::NYM_VPN_APIS, nym_vpn_api_urls); set_optional_var(var_names::NYM_APIS, nym_api_urls); } } pub fn default_gas_price_amount(&self) -> f64 { GAS_PRICE_AMOUNT } #[must_use] pub fn with_network_name(mut self, network_name: String) -> Self { self.network_name = network_name; self } #[must_use] pub fn with_chain_details(mut self, chain_details: ChainDetails) -> Self { self.chain_details = chain_details; self } #[must_use] pub fn with_bech32_account_prefix>(mut self, prefix: S) -> Self { self.chain_details.bech32_account_prefix = prefix.into(); self } #[must_use] pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self { self.chain_details.mix_denom = mix_denom; self } #[must_use] pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self { self.chain_details.stake_denom = stake_denom; self } #[must_use] pub fn with_base_mix_denom>(mut self, base_mix_denom: S) -> Self { self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into()); self } #[must_use] pub fn with_base_stake_denom>(mut self, base_stake_denom: S) -> Self { self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into()); self } #[must_use] pub fn with_additional_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { self.endpoints.push(endpoint); self } #[must_use] pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { self.endpoints = vec![endpoint]; self } #[must_use] pub fn with_contracts(mut self, contracts: NymContracts) -> Self { self.contracts = contracts; self } #[must_use] pub fn with_mixnet_contract>(mut self, contract: Option) -> Self { self.contracts.mixnet_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_vesting_contract>(mut self, contract: Option) -> Self { self.contracts.vesting_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_ecash_contract>(mut self, contract: Option) -> Self { self.contracts.ecash_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_group_contract>(mut self, contract: Option) -> Self { self.contracts.group_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_multisig_contract>(mut self, contract: Option) -> Self { self.contracts.multisig_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_coconut_dkg_contract>(mut self, contract: Option) -> Self { self.contracts.coconut_dkg_contract_address = contract.map(Into::into); self } #[must_use] pub fn with_nym_vpn_api_url>(mut self, endpoint: Option) -> Self { self.nym_vpn_api_url = endpoint.map(Into::into); self } #[must_use] pub fn with_nym_api_urls(mut self, urls: Vec) -> Self { self.nym_api_urls = Some(urls); self } #[must_use] pub fn with_nym_vpn_api_urls(mut self, urls: Option>) -> Self { self.nym_vpn_api_urls = urls; self } pub fn nym_vpn_api_url(&self) -> Option { self.nym_vpn_api_url.as_ref().map(|url| { url.parse() .expect("the provided nym-vpn api url is invalid!") }) } #[cfg(feature = "env")] fn nym_api_urls(&self) -> Option { serde_json::to_string(self.nym_api_urls.as_deref()?) .inspect_err(|e| tracing::warn!("failed to serialize nym_api_urls for env: {e}")) .ok() } #[cfg(feature = "env")] fn nym_vpn_api_urls(&self) -> Option { serde_json::to_string(self.nym_vpn_api_urls.as_deref()?) .inspect_err(|e| tracing::warn!("failed to serialize nym_vpn_api_urls for env: {e}")) .ok() } } #[cfg(feature = "env")] fn try_parse_api_urls(k: impl AsRef) -> Option> { let raw = var(k).ok()?; serde_json::from_str(&raw) .inspect_err(|e| tracing::warn!("failed to parse api urls from env \"{raw:?}\": {e}")) .ok() } #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct DenomDetails { pub base: &'static str, pub display: &'static str, // i.e. display_amount * 10^display_exponent = base_amount pub display_exponent: u32, } impl DenomDetails { pub const fn new(base: &'static str, display: &'static str, display_exponent: u32) -> Self { DenomDetails { base, display, display_exponent, } } } #[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct DenomDetailsOwned { pub base: String, pub display: String, // i.e. display_amount * 10^display_exponent = base_amount pub display_exponent: u32, } impl From for DenomDetailsOwned { fn from(details: DenomDetails) -> Self { DenomDetailsOwned { base: details.base.to_owned(), display: details.display.to_owned(), display_exponent: details.display_exponent, } } } impl DenomDetailsOwned { pub fn base_only(base: String) -> Self { DenomDetailsOwned { base: base.clone(), display: base, display_exponent: 0, } } } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct ValidatorDetails { // it is assumed those values are always valid since they're being provided in our defaults file pub nyxd_url: String, // pub websocket_url: Option, // Right now api_url is optional as we are not running the api reliably on all validators // however, later on it should be a mandatory field pub api_url: Option, // TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting } impl ValidatorDetails { pub fn new>(nyxd_url: S, api_url: Option, websocket_url: Option) -> Self { ValidatorDetails { nyxd_url: nyxd_url.into(), websocket_url: websocket_url.map(Into::into), api_url: api_url.map(Into::into), } } pub fn new_nyxd_only>(nyxd_url: S) -> Self { ValidatorDetails { nyxd_url: nyxd_url.into(), websocket_url: None, api_url: None, } } pub fn nyxd_url(&self) -> Url { self.nyxd_url .parse() .expect("the provided nyxd url is invalid!") } pub fn api_url(&self) -> Option { self.api_url .as_ref() .map(|url| url.parse().expect("the provided api url is invalid!")) } pub fn websocket_url(&self) -> Option { self.websocket_url .as_ref() .map(|url| url.parse().expect("the provided websocket url is invalid!")) } }