feat(wallet): select validator (#3375)

This commit is contained in:
Pierre Dommerc
2023-05-12 10:37:08 +02:00
committed by GitHub
parent c46d04d3c4
commit edbf35cb34
13 changed files with 452 additions and 56 deletions
@@ -1,6 +1,6 @@
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Config as ClientConfig, NyxdClient, QueryNyxdClient};
use crate::NymApiClient;
use crate::{NymApiClient, ValidatorClientError};
use crate::nyxd::traits::MixnetQueryClient;
use colored::Colorize;
@@ -45,6 +45,23 @@ pub async fn run_validator_connection_test<H: BuildHasher + 'static>(
)
}
pub async fn test_nyxd_url_connection(
network: NymNetworkDetails,
nyxd_url: Url,
address: cosmrs::AccountId,
) -> Result<bool, ValidatorClientError> {
let config = ClientConfig::try_from_nym_network_details(&network)
.expect("failed to create valid nyxd client config");
let mut nyxd_client = NyxdClient::<QueryNyxdClient>::connect(config, nyxd_url.as_str())?;
// possibly redundant, but lets just leave it here
nyxd_client.set_mixnet_contract_address(address);
match test_nyxd_connection(network, &nyxd_url, &nyxd_client).await {
ConnectionResult::Nyxd(_, _, res) => Ok(res),
_ => Ok(false), // ✶ not possible to happens
}
}
fn setup_connection_tests<H: BuildHasher + 'static>(
nyxd_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
api_urls: impl Iterator<Item = (NymNetworkDetails, Url)>,
@@ -105,7 +122,7 @@ async fn test_nyxd_connection(
{
Ok(Err(NyxdError::TendermintError(e))) => {
// If we get a tendermint-rpc error, we classify the node as not contactable
log::debug!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
false
}
Ok(Err(NyxdError::AbciError { code, log, .. })) => {
@@ -118,12 +135,12 @@ async fn test_nyxd_connection(
code == 18
}
Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => {
log::debug!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
false
}
Ok(Err(e)) => {
// For any other error, we're optimistic and just try anyway.
log::debug!(
log::warn!(
"Checking: nyxd_url: {url}: {}, but with error: {e}",
"success".green()
);
@@ -134,7 +151,7 @@ async fn test_nyxd_connection(
true
}
Err(e) => {
log::debug!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
false
}
};
+57 -4
View File
@@ -57,6 +57,10 @@ pub struct NetworkConfig {
// User selected urls
selected_nyxd_url: Option<Url>,
// Default nyxd URL assigned during login, can be used when the user wants
// to revert back its selected validator URL
default_nyxd_url: Option<Url>,
selected_api_url: Option<Url>,
// Additional user provided validators.
@@ -85,6 +89,7 @@ impl Default for NetworkConfig {
fn default() -> Self {
Self {
version: Some(CURRENT_NETWORK_CONFIG_VERSION),
default_nyxd_url: None,
selected_nyxd_url: None,
selected_api_url: None,
nyxd_urls: None,
@@ -241,6 +246,30 @@ impl Config {
.expect("Wrong format for bandwidth claim contract address")
}
pub fn set_default_nyxd_urls(&mut self, urls: &HashMap<WalletNetwork, Url>) {
for (network, url) in urls {
self.set_default_nyxd_url(url.to_owned(), network);
}
}
pub fn set_default_nyxd_url(&mut self, nyxd_url: Url, network: &WalletNetwork) {
log::debug!(
"set default nyxd URL for {network} {}",
nyxd_url.to_string()
);
if let Some(net) = self.networks.get_mut(&network.as_key()) {
net.default_nyxd_url = Some(nyxd_url);
} else {
self.networks.insert(
network.as_key(),
NetworkConfig {
default_nyxd_url: Some(nyxd_url),
..NetworkConfig::default()
},
);
}
}
pub fn select_nyxd_url(&mut self, nyxd_url: Url, network: WalletNetwork) {
if let Some(net) = self.networks.get_mut(&network.as_key()) {
net.selected_nyxd_url = Some(nyxd_url);
@@ -255,6 +284,13 @@ impl Config {
}
}
pub fn reset_nyxd_url(&mut self, network: WalletNetwork) {
match self.networks.get_mut(&network.as_key()) {
Some(net) => net.selected_nyxd_url = None,
None => log::warn!("reset_nyxd_url: {network} network not found, ignoring"),
}
}
pub fn select_nym_api_url(&mut self, api_url: Url, network: WalletNetwork) {
if let Some(net) = self.networks.get_mut(&network.as_key()) {
net.selected_api_url = Some(api_url);
@@ -262,7 +298,7 @@ impl Config {
self.networks.insert(
network.as_key(),
NetworkConfig {
selected_nyxd_url: Some(api_url),
selected_api_url: Some(api_url),
..NetworkConfig::default()
},
);
@@ -270,9 +306,25 @@ impl Config {
}
pub fn get_selected_validator_nyxd_url(&self, network: WalletNetwork) -> Option<Url> {
self.networks
.get(&network.as_key())
.and_then(|config| config.selected_nyxd_url.clone())
self.networks.get(&network.as_key()).and_then(|config| {
log::debug!(
"get selected nyxd url for {} {:?}",
network.to_string(),
config.selected_nyxd_url,
);
config.selected_nyxd_url.clone()
})
}
pub fn get_default_nyxd_url(&self, network: WalletNetwork) -> Option<Url> {
self.networks.get(&network.as_key()).and_then(|config| {
log::debug!(
"get default nyxd url for {} {:?}",
network.to_string(),
config.default_nyxd_url,
);
config.default_nyxd_url.clone()
})
}
pub fn get_selected_nym_api_url(&self, network: &WalletNetwork) -> Option<Url> {
@@ -573,6 +625,7 @@ api_url = 'https://baz/api'
r#"{
"version": 1,
"selected_nyxd_url": null,
"default_nyxd_url": null,
"selected_api_url": "https://my_api_url.com/",
"nyxd_urls": [
{
+4
View File
@@ -130,6 +130,10 @@ pub enum BackendError {
NewWindowError,
#[error("Failed to check for application update")]
CheckAppVersionError,
#[error("Failed to connect to the provided validator URL")]
WalletValidatorConnectionFailed,
#[error("No defined default validator URL")]
WalletNoDefaultValidator,
#[error(transparent)]
WalletError {
+3
View File
@@ -94,6 +94,9 @@ fn main() {
network_config::remove_validator,
network_config::select_nym_api_url,
network_config::select_nyxd_url,
network_config::reset_nyxd_url,
network_config::get_default_nyxd_url,
network_config::get_selected_nyxd_url,
network_config::update_nyxd_urls,
state::load_config_from_files,
state::save_config_to_files,
+35 -1
View File
@@ -26,6 +26,28 @@ pub async fn get_nym_api_urls(
Ok(ValidatorUrls { urls })
}
#[tauri::command]
pub async fn get_selected_nyxd_url(
network: WalletNetwork,
state: tauri::State<'_, WalletState>,
) -> Result<Option<String>, BackendError> {
let state = state.read().await;
let url = state.get_selected_nyxd_url(&network).map(String::from);
log::info!("Selected nyxd url for {network}: {:?}", url);
Ok(url)
}
#[tauri::command]
pub async fn get_default_nyxd_url(
network: WalletNetwork,
state: tauri::State<'_, WalletState>,
) -> Result<String, BackendError> {
let state = state.read().await;
let url = state.get_default_nyxd_url(&network).map(String::from);
log::info!("Default nyxd url for {network}: {:?}", url);
url.ok_or_else(|| BackendError::WalletNoDefaultValidator)
}
#[tauri::command]
pub async fn select_nyxd_url(
url: &str,
@@ -33,7 +55,19 @@ pub async fn select_nyxd_url(
state: tauri::State<'_, WalletState>,
) -> Result<(), BackendError> {
log::debug!("Selecting new nyxd url for {network}: {url}");
state.write().await.select_nyxd_url(url, network)?;
state.write().await.select_nyxd_url(url, network).await?;
state.read().await.save_config_files()?;
Ok(())
}
#[tauri::command]
pub async fn reset_nyxd_url(
network: WalletNetwork,
state: tauri::State<'_, WalletState>,
) -> Result<(), BackendError> {
log::debug!("Resetting nyxd url for {network} to default");
state.write().await.reset_nyxd_url(network)?;
state.read().await.save_config_files()?;
Ok(())
}
@@ -3,8 +3,8 @@ use crate::error::BackendError;
use crate::network_config;
use crate::state::{WalletAccountIds, WalletState};
use crate::wallet_storage::{self, UserPassword, DEFAULT_LOGIN_ID};
use bip39::rand::{self, seq::SliceRandom};
use bip39::{Language, Mnemonic};
use bip39::rand::seq::SliceRandom;
use bip39::{rand, Language, Mnemonic};
use cosmrs::bip32::DerivationPath;
use itertools::Itertools;
use nym_config::defaults::{NymNetworkDetails, COSMOS_DERIVATION_PATH};
@@ -118,6 +118,14 @@ async fn _connect_with_mnemonic(
let state = state.read().await;
(state.get_all_nyxd_urls(), state.get_all_api_urls())
};
let (nyxd_urls, api_urls) = run_connection_test(
untested_nyxd_urls.clone(),
untested_api_urls.clone(),
&config,
)
.await;
let default_nyxd_urls: HashMap<WalletNetwork, Url> = untested_nyxd_urls
.iter()
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
@@ -127,19 +135,17 @@ async fn _connect_with_mnemonic(
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
.collect();
// Run connection tests on all nyxd and nym-api endpoints
let (nyxd_urls, api_urls) =
run_connection_test(untested_nyxd_urls, untested_api_urls, &config).await;
let nyxd_urls = pick_good_nyxd_urls(&default_nyxd_urls, &nyxd_urls).await?;
let api_urls = pick_good_api_urls(&default_api_urls, &api_urls).await?;
{
let mut w_state = state.write().await;
// Save the checked nyxd URLs
w_state.set_default_nyxd_urls(&nyxd_urls);
}
// Create clients for all networks
let clients = create_clients(
&nyxd_urls,
&api_urls,
&default_nyxd_urls,
&default_api_urls,
&config,
&mnemonic,
)?;
let clients = create_clients(&nyxd_urls, &api_urls, &config, &mnemonic)?;
// Set the default account
let default_network = WalletNetwork::MAINNET;
@@ -196,9 +202,51 @@ async fn run_connection_test(
.await
}
fn create_clients(
async fn pick_good_nyxd_urls(
default_nyxd_urls: &HashMap<WalletNetwork, Url>,
nyxd_urls: &HashMap<NymNetworkDetails, Vec<(Url, bool)>>,
) -> Result<HashMap<WalletNetwork, Url>, BackendError> {
let nyxd_urls: HashMap<WalletNetwork, Url> = WalletNetwork::iter()
.map(|network| {
let default_nyxd_url = default_nyxd_urls
.get(&network)
.expect("Expected at least one nyxd_url");
let url = select_random_responding_url(nyxd_urls, network).unwrap_or_else(|| {
log::warn!(
"No successful nyxd_urls for {network}: using default: {default_nyxd_url}"
);
default_nyxd_url.clone()
});
log::info!("Set default nyxd_url for {network}: {url}");
(network, url)
})
.collect();
Ok(nyxd_urls)
}
async fn pick_good_api_urls(
default_api_urls: &HashMap<WalletNetwork, Url>,
api_urls: &HashMap<NymNetworkDetails, Vec<(Url, bool)>>,
) -> Result<HashMap<WalletNetwork, Url>, BackendError> {
let api_urls: HashMap<WalletNetwork, Url> = WalletNetwork::iter()
.map(|network| {
let default_api_url = default_api_urls
.get(&network)
.expect("Expected at least one api_url");
let url = select_first_responding_url(api_urls, network).unwrap_or_else(|| {
log::warn!("No passing api_urls for {network}: using default: {default_api_url}");
default_api_url.clone()
});
log::info!("Set default api_url for {network}: {url}");
(network, url)
})
.collect();
Ok(api_urls)
}
fn create_clients(
default_nyxd_urls: &HashMap<WalletNetwork, Url>,
default_api_urls: &HashMap<WalletNetwork, Url>,
config: &Config,
@@ -210,28 +258,22 @@ fn create_clients(
log::debug!("Using selected nyxd_url for {network}: {url}");
url.clone()
} else {
let default_nyxd_url = default_nyxd_urls
let url = default_nyxd_urls
.get(&network)
.expect("Expected at least one nyxd_url");
select_random_responding_url(nyxd_urls, network).unwrap_or_else(|| {
log::debug!(
"No successful nyxd_urls for {network}: using default: {default_nyxd_url}"
);
default_nyxd_url.clone()
})
log::debug!("Using default nyxd_url for {network}: {url}");
url.to_owned()
};
let api_url = if let Some(url) = config.get_selected_nym_api_url(&network) {
log::debug!("Using selected api_url for {network}: {url}");
url.clone()
} else {
let default_api_url = default_api_urls
let url = default_api_urls
.get(&network)
.expect("Expected at least one api url");
select_first_responding_url(api_urls, network).unwrap_or_else(|| {
log::debug!("No passing api_urls for {network}: using default: {default_api_url}");
default_api_url.clone()
})
log::debug!("Using default api_url for {network}: {url}");
url.to_owned()
};
log::info!("Connecting to: nyxd_url: {nyxd_url} for {network}");
+37 -1
View File
@@ -437,7 +437,20 @@ impl WalletStateInner {
}
}
pub fn select_nyxd_url(&mut self, url: &str, network: Network) -> Result<(), BackendError> {
pub async fn select_nyxd_url(
&mut self,
url: &str,
network: Network,
) -> Result<(), BackendError> {
if !nym_validator_client::connection_tester::test_nyxd_url_connection(
network.into(),
url.parse()?,
self.config.get_mixnet_contract_address(network),
)
.await?
{
return Err(BackendError::WalletValidatorConnectionFailed);
}
self.config.select_nyxd_url(url.parse()?, network);
if let Ok(client) = self.client_mut(network) {
client.change_nyxd(url.parse()?)?;
@@ -445,6 +458,29 @@ impl WalletStateInner {
Ok(())
}
pub fn reset_nyxd_url(&mut self, network: Network) -> Result<(), BackendError> {
self.config.reset_nyxd_url(network);
let default_nyxd = self.config.get_default_nyxd_url(network);
if let Ok(client) = self.client_mut(network) {
if let Some(url) = default_nyxd {
client.change_nyxd(url)?;
}
}
Ok(())
}
pub fn set_default_nyxd_urls(&mut self, urls: &HashMap<Network, Url>) {
self.config.set_default_nyxd_urls(urls);
}
pub fn get_selected_nyxd_url(&self, network: &Network) -> Option<Url> {
self.config.get_selected_validator_nyxd_url(*network)
}
pub fn get_default_nyxd_url(&self, network: &Network) -> Option<Url> {
self.config.get_default_nyxd_url(*network)
}
pub fn select_nym_api_url(&mut self, url: &str, network: Network) -> Result<(), BackendError> {
self.config.select_nym_api_url(url.parse()?, network);
if let Ok(client) = self.client_mut(network) {
@@ -0,0 +1,185 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, FormControl, Grid, Stack, Switch, TextField, Typography } from '@mui/material';
import { useSnackbar } from 'notistack';
import {
checkMixnodeOwnership,
getDefaultValidatorUrl,
getSelectedValidatorUrl,
resetValidatorUrl,
setSelectedValidatorUrl as setSelectedValidatorUrlReq,
} from '../../requests';
import { AppContext } from '../../context';
import { Console } from '../../utils/console';
import { Network } from '../../types';
const SelectValidator = () => {
const [customValidatorEnabled, setCustomValidatorEnabled] = useState<boolean>(false);
const [selectedValidatorUrl, setSelectedValidatorUrl] = useState<string | null>();
const [defaultValidatorUrl, setDefaultValidatorUrl] = useState<string | null>();
const [validatorUrlInput, setValidatorUrlInput] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const { network } = useContext(AppContext);
const { enqueueSnackbar } = useSnackbar();
const getDefaultValidator = async (net: Network) => {
if (!network) {
return;
}
try {
const defaultValidator = await getDefaultValidatorUrl(net);
setDefaultValidatorUrl(defaultValidator);
} catch (e) {
Console.error(`an error occurred while requesting the default validator URL: ${e}`);
}
};
const getSelectedValidator = async (net: Network) => {
if (!network) {
return null;
}
try {
const selectedValidator = await getSelectedValidatorUrl(net);
setSelectedValidatorUrl(selectedValidator);
} catch (e) {
Console.error(`an error occurred while requesting the selected validator URL: ${e}`);
}
return null;
};
useEffect(() => {
if (network) {
getDefaultValidator(network);
getSelectedValidator(network);
}
}, [network, customValidatorEnabled]);
useEffect(() => {
// on network change, turn off the custom val switch if there is no selected val
// for this network
if (!selectedValidatorUrl) {
setCustomValidatorEnabled(false);
setValidatorUrlInput('');
}
}, [network, selectedValidatorUrl]);
useEffect(() => {
if (selectedValidatorUrl && selectedValidatorUrl !== defaultValidatorUrl) {
setCustomValidatorEnabled(true);
}
if (selectedValidatorUrl) {
setValidatorUrlInput(selectedValidatorUrl);
}
}, [selectedValidatorUrl, defaultValidatorUrl, network]);
const onToggle = async () => {
if (!customValidatorEnabled) {
setCustomValidatorEnabled(true);
return;
}
setIsLoading(true);
try {
await resetValidatorUrl(network as Network);
setValidatorUrlInput('');
setSelectedValidatorUrl(null);
setCustomValidatorEnabled(false);
} catch (e) {
Console.error(e);
} finally {
setIsLoading(false);
}
};
const saveValidator = async () => {
if (!network || !validatorUrlInput || validatorUrlInput === defaultValidatorUrl) {
return;
}
setIsLoading(true);
try {
// this tauri request also does a basic connection check
await setSelectedValidatorUrlReq({ network, url: validatorUrlInput });
} catch (e) {
Console.error(e);
enqueueSnackbar(`Invalid validator URL: ${e}`, { variant: 'error' });
setIsLoading(false);
return;
}
// to enforce the validator URL is valid, try to query the node ownership
// if it fails, that means the endpoint is wrong
// TODO this check logic should be handled directly in the rust side, `select_nyxd_url` command
try {
await checkMixnodeOwnership();
enqueueSnackbar('Validator URL saved', { variant: 'success' });
} catch (e) {
Console.error(e);
enqueueSnackbar('The given validator URL is not valid for the currently selected network', { variant: 'error' });
await resetValidatorUrl(network as Network);
setSelectedValidatorUrl(null);
} finally {
setIsLoading(false);
}
};
return (
<Grid container spacing={2} padding={3}>
<Grid item sm={12} md={7} lg={8}>
<Stack direction="column" gap={1}>
<Typography variant="h6">Change validator</Typography>
<Typography variant="caption" sx={{ color: 'nym.text.muted' }}>
You can use the validator of your choice by providing its RPC URL address
</Typography>
<Stack direction="row" spacing={3} mt={2} alignItems="center">
<Typography>Turn Off</Typography>
<Switch checked={customValidatorEnabled} onChange={onToggle} inputProps={{ 'aria-label': 'controlled' }} />
<Typography>Turn On</Typography>
</Stack>
</Stack>
</Grid>
<Grid item sm={12} md={5} lg={4}>
<Stack spacing={3} alignItems="flex-end">
{customValidatorEnabled ? (
<FormControl fullWidth>
<Stack spacing={3} mt={2}>
<TextField
name="validatorUrl"
label="Validator URL"
value={validatorUrlInput}
onChange={(e) => setValidatorUrlInput(e.target.value)}
error={false}
InputLabelProps={{ shrink: true }}
fullWidth
disabled={!customValidatorEnabled}
autoFocus
/>
<Button
size="large"
variant="contained"
disabled={
!validatorUrlInput ||
validatorUrlInput.length === 0 ||
validatorUrlInput === defaultValidatorUrl ||
validatorUrlInput === selectedValidatorUrl ||
isLoading ||
!customValidatorEnabled
}
onClick={saveValidator}
>
Use this validator
</Button>
</Stack>
</FormControl>
) : (
<Stack spacing={2} alignItems="end" mt={3} mr={1}>
<Typography variant="body2">Default validator address</Typography>
<Typography>{defaultValidatorUrl}</Typography>
</Stack>
)}
</Stack>
</Grid>
</Grid>
);
};
export default SelectValidator;
+2 -19
View File
@@ -57,6 +57,8 @@ import {
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
import {
fireRequests,
TauriReq,
attachDefaultOperatingCost,
decCoinToDisplay,
toPercentFloatString,
@@ -162,12 +164,6 @@ export const BondingContext = createContext<TBondingContext>({
isVestingAccount: false,
});
type TauriReq<Req extends Function & ((a: any, b?: any) => Promise<any>)> = {
name: Req['name'];
request: () => ReturnType<Req>;
onFulfilled: (value: Awaited<ReturnType<Req>>) => void;
};
export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
@@ -192,19 +188,6 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
setBondedNode(undefined);
};
const fireRequests = async (reqs: TauriReq<any>[]) => {
const promises = await Promise.allSettled(reqs.map((r) => r.request()));
promises.forEach((res, index) => {
if (res.status === 'rejected') {
Console.warn(`${reqs[index].name} request fails`, res.reason);
}
if (res.status === 'fulfilled') {
reqs[index].onFulfilled(res.value as any);
}
});
};
/**
* Fetch mixnode **optional** data.
* ⚠ The underlying queries are allowed to fail.
@@ -3,6 +3,7 @@ import { Box, Button, Divider, Stack, Typography } from '@mui/material';
import { helpLogToggleWindow } from '../../requests';
import { AppContext } from '../../context';
import { config } from '../../config';
import SelectValidator from '../../components/Settings/SelectValidator';
const AdvancedSettings = () => {
const { handleShowTerminal, appEnv } = useContext(AppContext);
@@ -40,6 +41,8 @@ const AdvancedSettings = () => {
</Button>
</Box>
</Stack>
<Divider />
<SelectValidator />
</Box>
);
};
+11
View File
@@ -3,3 +3,14 @@ import { Network } from 'src/types';
import { invokeWrapper } from './wrapper';
export const selectNetwork = async (network: Network) => invokeWrapper<Account>('switch_network', { network });
export const getSelectedValidatorUrl = async (network: Network) =>
invokeWrapper<string | null>('get_selected_nyxd_url', { network });
export const getDefaultValidatorUrl = async (network: Network) =>
invokeWrapper<string | null>('get_default_nyxd_url', { network });
export const setSelectedValidatorUrl = async (args: { network: Network; url: string }) =>
invokeWrapper<void>('select_nyxd_url', args);
export const resetValidatorUrl = async (network: Network) => invokeWrapper<void>('reset_nyxd_url', { network });
+22
View File
@@ -0,0 +1,22 @@
import { Console } from 'src/utils/console';
export type TauriReq<Req extends Function & ((a: any, b?: any) => Promise<any>)> = {
name: Req['name'];
request: () => ReturnType<Req>;
onFulfilled: (value: Awaited<ReturnType<Req>>) => void;
};
async function fireRequests(requests: TauriReq<any>[]) {
const promises = await Promise.allSettled(requests.map((r) => r.request()));
promises.forEach((res, index) => {
if (res.status === 'rejected') {
Console.warn(`${requests[index].name} request fails`, res.reason);
}
if (res.status === 'fulfilled') {
requests[index].onFulfilled(res.value as any);
}
});
}
export default fireRequests;
+3
View File
@@ -228,3 +228,6 @@ export const getIntervalAsDate = async () => {
return { nextEpoch, nextInterval };
};
export * from './fireRequests';
export { default as fireRequests } from './fireRequests';