merging develop
@@ -1599,6 +1599,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
@@ -38,6 +38,7 @@ const DEFAULT_RECONNECTION_BACKOFF: Duration = Duration::from_secs(5);
|
||||
|
||||
pub struct GatewayClient {
|
||||
authenticated: bool,
|
||||
#[cfg(feature = "coconut")]
|
||||
bandwidth_remaining: i64,
|
||||
gateway_address: String,
|
||||
gateway_identity: identity::PublicKey,
|
||||
@@ -71,8 +72,11 @@ impl GatewayClient {
|
||||
) -> Self {
|
||||
GatewayClient {
|
||||
authenticated: false,
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
bandwidth_remaining: 0,
|
||||
gateway_address,
|
||||
|
||||
gateway_identity,
|
||||
local_identity,
|
||||
shared_key,
|
||||
@@ -114,7 +118,10 @@ impl GatewayClient {
|
||||
|
||||
GatewayClient {
|
||||
authenticated: false,
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
bandwidth_remaining: 0,
|
||||
|
||||
gateway_address,
|
||||
gateway_identity,
|
||||
local_identity,
|
||||
@@ -495,6 +502,7 @@ impl GatewayClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 {
|
||||
packets
|
||||
.iter()
|
||||
@@ -509,6 +517,7 @@ impl GatewayClient {
|
||||
if !self.authenticated {
|
||||
return Err(GatewayClientError::NotAuthenticated);
|
||||
}
|
||||
#[cfg(feature = "coconut")]
|
||||
if self.estimate_required_bandwidth(&packets) < self.bandwidth_remaining {
|
||||
return Err(GatewayClientError::NotEnoughBandwidth);
|
||||
}
|
||||
@@ -576,6 +585,7 @@ impl GatewayClient {
|
||||
if !self.authenticated {
|
||||
return Err(GatewayClientError::NotAuthenticated);
|
||||
}
|
||||
#[cfg(feature = "coconut")]
|
||||
if (mix_packet.sphinx_packet().len() as i64) > self.bandwidth_remaining {
|
||||
return Err(GatewayClientError::NotEnoughBandwidth);
|
||||
}
|
||||
@@ -614,6 +624,7 @@ impl GatewayClient {
|
||||
if !self.authenticated {
|
||||
return Err(GatewayClientError::NotAuthenticated);
|
||||
}
|
||||
#[cfg(feature = "coconut")]
|
||||
if self.bandwidth_remaining <= 0 {
|
||||
return Err(GatewayClientError::NotEnoughBandwidth);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ okapi = { version = "0.6.0-alpha-1", features = ["derive_json_schema"] }
|
||||
rocket_okapi = "0.7.0-alpha-1"
|
||||
log = "0.4.0"
|
||||
pretty_env_logger = "0.4.0"
|
||||
thiserror = "1.0.29"
|
||||
|
||||
mixnet-contract = { path = "../common/mixnet-contract" }
|
||||
network-defaults = { path = "../common/network-defaults" }
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
Network Explorer API
|
||||
====================
|
||||
|
||||
An API that can:
|
||||
An API that provides data for the [Network Explorer](../explorer).
|
||||
|
||||
* calculate how many nodes are in which country, by checking the IPs of all nodes against an external service
|
||||
* serve "hello world" via HTTP
|
||||
Features:
|
||||
|
||||
- geolocates mixnodes using https://freegeoip.app/
|
||||
- calculates how many nodes are in each country
|
||||
- proxies mixnode API requests to add HTTPS
|
||||
|
||||
## Running
|
||||
|
||||
TODO:
|
||||
Supply the environment variable `GEO_IP_SERVICE_API_KEY` with a key from https://freegeoip.app/.
|
||||
|
||||
Run as a service and reverse proxy with `nginx` to add `https` with Lets Encrypt.
|
||||
|
||||
# TODO / Known Issues
|
||||
|
||||
## TODO
|
||||
|
||||
* record the number of mixnodes on a given date and write to a file for later retrieval
|
||||
* store the nodes per country state in a variable
|
||||
* grab mixnode description info via reqwest and serve it (avoid mixed-content errors)
|
||||
* serve it all over http
|
||||
* dependency injection
|
||||
* tests
|
||||
* tests
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use log::info;
|
||||
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub(crate) struct CountryStatisticsDistributionTask {
|
||||
state: ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl CountryStatisticsDistributionTask {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
|
||||
CountryStatisticsDistributionTask { state }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning mix node country distribution task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 15)); // every 15 mins
|
||||
loop {
|
||||
// wait for the next interval tick
|
||||
interval_timer.tick().await;
|
||||
self.calculate_nodes_per_country().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
||||
async fn calculate_nodes_per_country(&mut self) {
|
||||
let cache = self.state.inner.mix_nodes.get_location_cache().await;
|
||||
|
||||
let three_letter_iso_country_codes: Vec<String> = cache
|
||||
.values()
|
||||
.flat_map(|i| {
|
||||
i.location
|
||||
.as_ref()
|
||||
.map(|j| j.three_letter_iso_country_code.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut distribution = CountryNodesDistribution::new();
|
||||
|
||||
info!("Calculating country distribution from located mixnodes...");
|
||||
for three_letter_iso_country_code in three_letter_iso_country_codes {
|
||||
*(distribution.entry(three_letter_iso_country_code)).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// replace the shared distribution to be the new distribution
|
||||
self.state
|
||||
.inner
|
||||
.country_node_distribution
|
||||
.set_all(distribution)
|
||||
.await;
|
||||
|
||||
info!(
|
||||
"Mixnode country distribution done: {:?}",
|
||||
self.state.inner.country_node_distribution.get_all().await
|
||||
);
|
||||
|
||||
// keep state on disk, so that when this process dies it can start up again and users get some data
|
||||
self.state.write_to_file().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use log::{info, warn};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::mix_nodes::{GeoLocation, Location};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub(crate) struct GeoLocateTask {
|
||||
state: ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl GeoLocateTask {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
|
||||
GeoLocateTask { state }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning mix node locator task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50));
|
||||
loop {
|
||||
// wait for the next interval tick
|
||||
interval_timer.tick().await;
|
||||
self.locate_mix_nodes().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn locate_mix_nodes(&mut self) {
|
||||
let mixnode_bonds = self.state.inner.mix_nodes.get().await.value;
|
||||
|
||||
for (i, cache_item) in mixnode_bonds.values().enumerate() {
|
||||
if self
|
||||
.state
|
||||
.inner
|
||||
.mix_nodes
|
||||
.is_location_valid(&cache_item.mix_node.identity_key)
|
||||
.await
|
||||
{
|
||||
// when the cached location is valid, don't locate and continue to next mix node
|
||||
continue;
|
||||
}
|
||||
|
||||
// the mix node has not been located or is the cache time has expired
|
||||
match locate(&cache_item.mix_node.host).await {
|
||||
Ok(geo_location) => {
|
||||
let location = Location::new(geo_location);
|
||||
|
||||
trace!(
|
||||
"{} mix nodes already located. Ip {} is located in {:#?}",
|
||||
i,
|
||||
cache_item.mix_node.host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
if i > 0 && (i % 100) == 0 {
|
||||
info!(
|
||||
"Located {} mixnodes...",
|
||||
i + 1,
|
||||
);
|
||||
}
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.mix_nodes
|
||||
.set_location(&cache_item.mix_node.identity_key, Some(location))
|
||||
.await;
|
||||
|
||||
// one node has been located, so return out of the loop
|
||||
return;
|
||||
}
|
||||
Err(e) => match e {
|
||||
LocateError::ReqwestError(e) => warn!(
|
||||
"❌ Oh no! Location for {} failed {}",
|
||||
cache_item.mix_node.host, e
|
||||
),
|
||||
LocateError::NotFound(e) => {
|
||||
warn!(
|
||||
"❌ Location for {} not found. Response body: {}",
|
||||
cache_item.mix_node.host, e
|
||||
);
|
||||
self.state
|
||||
.inner
|
||||
.mix_nodes
|
||||
.set_location(&cache_item.mix_node.identity_key, None)
|
||||
.await;
|
||||
},
|
||||
LocateError::RateLimited(e) => warn!(
|
||||
"❌ Oh no, we've been rate limited! Location for {} failed. Response body: {}",
|
||||
cache_item.mix_node.host, e
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
trace!("All mix nodes located");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum LocateError {
|
||||
#[error("Oops, we have made too many requests and are being rate limited. Request body: {0}")]
|
||||
RateLimited(String),
|
||||
|
||||
#[error("Geolocation not found. Request body: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error(transparent)]
|
||||
ReqwestError(#[from] ReqwestError),
|
||||
}
|
||||
|
||||
async fn locate(ip: &str) -> Result<GeoLocation, LocateError> {
|
||||
let api_key = ::std::env::var("GEO_IP_SERVICE_API_KEY")
|
||||
.expect("Env var GEO_IP_SERVICE_API_KEY is not set");
|
||||
let uri = format!("{}/{}?apikey={}", crate::GEO_IP_SERVICE, ip, api_key);
|
||||
match reqwest::get(uri.clone()).await {
|
||||
Ok(response) => {
|
||||
if response.status() == 429 {
|
||||
return Err(LocateError::RateLimited(
|
||||
response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "(the response body is empty)".to_string()),
|
||||
));
|
||||
}
|
||||
if response.status() == 404 {
|
||||
return Err(LocateError::NotFound(
|
||||
response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "(the response body is empty)".to_string()),
|
||||
));
|
||||
}
|
||||
let location = response.json::<GeoLocation>().await?;
|
||||
Ok(location)
|
||||
}
|
||||
Err(e) => Err(LocateError::ReqwestError(e)),
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,4 @@
|
||||
use log::{info, trace, warn};
|
||||
use reqwest::Error as ReqwestError;
|
||||
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
use crate::mix_nodes::{GeoLocation, Location};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub mod country_nodes_distribution;
|
||||
pub mod distribution;
|
||||
pub mod geolocate;
|
||||
pub mod http;
|
||||
|
||||
pub(crate) struct CountryStatistics {
|
||||
state: ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl CountryStatistics {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
|
||||
CountryStatistics { state }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
|
||||
loop {
|
||||
// wait for the next interval tick
|
||||
interval_timer.tick().await;
|
||||
|
||||
info!("Running task...");
|
||||
self.calculate_nodes_per_country().await;
|
||||
info!("Done");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
||||
async fn calculate_nodes_per_country(&mut self) {
|
||||
// force the mixnode cache to invalidate
|
||||
let mixnode_bonds = self.state.inner.mix_nodes.refresh_and_get().await.value;
|
||||
|
||||
let mut distribution = CountryNodesDistribution::new();
|
||||
|
||||
info!("Locating mixnodes...");
|
||||
for (i, cache_item) in mixnode_bonds.values().enumerate() {
|
||||
match locate(&cache_item.bond.mix_node.host).await {
|
||||
Ok(geo_location) => {
|
||||
let location = Location::new(geo_location);
|
||||
|
||||
*(distribution.entry(location.three_letter_iso_country_code.to_string()))
|
||||
.or_insert(0) += 1;
|
||||
|
||||
trace!(
|
||||
"Ip {} is located in {:#?}",
|
||||
cache_item.bond.mix_node.host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.mix_nodes
|
||||
.set_location(&cache_item.bond.mix_node.identity_key, location)
|
||||
.await;
|
||||
|
||||
if (i % 100) == 0 {
|
||||
info!(
|
||||
"Located {} mixnodes in {} countries",
|
||||
i + 1,
|
||||
distribution.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("❌ Oh no! Location failed {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// replace the shared distribution to be the new distribution
|
||||
self.state
|
||||
.inner
|
||||
.country_node_distribution
|
||||
.set_all(distribution)
|
||||
.await;
|
||||
|
||||
info!(
|
||||
"Locating mixnodes done: {:?}",
|
||||
self.state.inner.country_node_distribution.get_all().await
|
||||
);
|
||||
|
||||
// keep state on disk, so that when this process dies it can start up again and users get some data
|
||||
self.state.write_to_file().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn locate(ip: &str) -> Result<GeoLocation, ReqwestError> {
|
||||
let response = reqwest::get(format!("{}{}", crate::GEO_IP_SERVICE, ip)).await?;
|
||||
let location = response.json::<GeoLocation>().await?;
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ mod mix_nodes;
|
||||
mod ping;
|
||||
mod state;
|
||||
|
||||
const GEO_IP_SERVICE: &str = "https://freegeoip.app/json/";
|
||||
const GEO_IP_SERVICE: &str = "https://api.freegeoip.app/json";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -36,7 +36,12 @@ impl ExplorerApi {
|
||||
info!("Explorer API starting up...");
|
||||
|
||||
// spawn concurrent tasks
|
||||
country_statistics::CountryStatistics::new(self.state.clone()).start();
|
||||
mix_nodes::tasks::MixNodesTasks::new(self.state.clone()).start();
|
||||
country_statistics::distribution::CountryStatisticsDistributionTask::new(
|
||||
self.state.clone(),
|
||||
)
|
||||
.start();
|
||||
country_statistics::geolocate::GeoLocateTask::new(self.state.clone()).start();
|
||||
http::start(self.state.clone());
|
||||
|
||||
// wait for user to press ctrl+C
|
||||
|
||||
@@ -34,27 +34,7 @@ pub(crate) struct PrettyMixNodeBondWithLocation {
|
||||
pub(crate) async fn list(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyMixNodeBondWithLocation>> {
|
||||
Json(
|
||||
state
|
||||
.inner
|
||||
.mix_nodes
|
||||
.get()
|
||||
.await
|
||||
.value
|
||||
.values()
|
||||
.map(|i| {
|
||||
let mix_node = i.bond.clone();
|
||||
PrettyMixNodeBondWithLocation {
|
||||
location: i.location.clone(),
|
||||
bond_amount: mix_node.bond_amount,
|
||||
total_delegation: mix_node.total_delegation,
|
||||
owner: mix_node.owner,
|
||||
layer: mix_node.layer,
|
||||
mix_node: mix_node.mix_node,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<PrettyMixNodeBondWithLocation>>(),
|
||||
)
|
||||
Json(state.inner.mix_nodes.get_mixnodes_with_location().await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub(crate) mod tasks;
|
||||
mod utils;
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -7,6 +8,7 @@ use std::time::{Duration, SystemTime};
|
||||
use rocket::tokio::sync::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::mix_node::http::PrettyMixNodeBondWithLocation;
|
||||
use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code;
|
||||
use mixnet_contract::{Delegation, MixNodeBond, RawDelegationData, UnpackedDelegation};
|
||||
use network_defaults::{
|
||||
@@ -14,8 +16,9 @@ use network_defaults::{
|
||||
};
|
||||
use validator_client::nymd::QueryNymdClient;
|
||||
|
||||
pub(crate) type LocationCache = HashMap<String, Location>;
|
||||
pub(crate) type LocationCache = HashMap<String, LocationCacheItem>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct GeoLocation {
|
||||
pub(crate) ip: String,
|
||||
@@ -31,6 +34,12 @@ pub(crate) struct GeoLocation {
|
||||
pub(crate) metro_code: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct LocationCacheItem {
|
||||
pub(crate) location: Option<Location>,
|
||||
pub(crate) valid_until: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)]
|
||||
pub(crate) struct Location {
|
||||
pub(crate) two_letter_iso_country_code: String,
|
||||
@@ -53,16 +62,19 @@ impl Location {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct MixNodeBondWithLocation {
|
||||
pub(crate) location: Option<Location>,
|
||||
pub(crate) bond: MixNodeBond,
|
||||
impl LocationCacheItem {
|
||||
pub(crate) fn new_from_location(location: Option<Location>) -> Self {
|
||||
LocationCacheItem {
|
||||
location,
|
||||
valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct MixNodesResult {
|
||||
pub(crate) valid_until: SystemTime,
|
||||
pub(crate) value: HashMap<String, MixNodeBondWithLocation>,
|
||||
pub(crate) value: HashMap<String, MixNodeBond>,
|
||||
location_cache: LocationCache,
|
||||
}
|
||||
|
||||
@@ -92,23 +104,28 @@ impl ThreadsafeMixNodesResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.location_cache
|
||||
.get(identity_key)
|
||||
.map(|cache_item| cache_item.valid_until > SystemTime::now())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_location_cache(&self) -> LocationCache {
|
||||
self.inner.read().await.location_cache.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_location(&self, identity_key: &str, location: Location) {
|
||||
pub(crate) async fn set_location(&self, identity_key: &str, location: Option<Location>) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
// cache the location for this mix node so that it can be used when the mix node list is refreshed
|
||||
guard
|
||||
.location_cache
|
||||
.insert(identity_key.to_string(), location.clone());
|
||||
|
||||
// add the location to the mix node
|
||||
guard
|
||||
.value
|
||||
.entry(identity_key.to_string())
|
||||
.and_modify(|item| item.location = Some(location));
|
||||
guard.location_cache.insert(
|
||||
identity_key.to_string(),
|
||||
LocationCacheItem::new_from_location(location),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn get(&self) -> MixNodesResult {
|
||||
@@ -124,25 +141,34 @@ impl ThreadsafeMixNodesResult {
|
||||
self.inner.read().await.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_and_get(&self) -> MixNodesResult {
|
||||
self.refresh().await;
|
||||
self.inner.read().await.clone()
|
||||
pub(crate) async fn get_mixnodes_with_location(&self) -> Vec<PrettyMixNodeBondWithLocation> {
|
||||
let guard = self.inner.read().await;
|
||||
guard
|
||||
.value
|
||||
.values()
|
||||
.map(|bond| {
|
||||
let location = guard.location_cache.get(&bond.mix_node.identity_key);
|
||||
let copy = bond.clone();
|
||||
PrettyMixNodeBondWithLocation {
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
bond_amount: copy.bond_amount,
|
||||
total_delegation: copy.total_delegation,
|
||||
owner: copy.owner,
|
||||
layer: copy.layer,
|
||||
mix_node: copy.mix_node,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn refresh(&self) {
|
||||
pub(crate) async fn refresh(&self) {
|
||||
// get mixnodes and cache the new value
|
||||
let value = retrieve_mixnodes().await;
|
||||
let location_cache = self.inner.read().await.location_cache.clone();
|
||||
*self.inner.write().await = MixNodesResult {
|
||||
value: value
|
||||
.into_iter()
|
||||
.map(|bond| {
|
||||
let location = location_cache.get(&bond.mix_node.identity_key).cloned(); // add the location, if we've located this mix node before
|
||||
(
|
||||
bond.mix_node.identity_key.to_string(),
|
||||
MixNodeBondWithLocation { location, bond },
|
||||
)
|
||||
})
|
||||
.map(|bond| (bond.mix_node.identity_key.to_string(), bond))
|
||||
.collect(),
|
||||
valid_until: SystemTime::now() + Duration::from_secs(60 * 10), // valid for 10 minutes
|
||||
location_cache,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub(crate) struct MixNodesTasks {
|
||||
state: ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl MixNodesTasks {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
|
||||
MixNodesTasks { state }
|
||||
}
|
||||
|
||||
pub(crate) fn start(self) {
|
||||
info!("Spawning mix nodes task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 60)); // every hour
|
||||
loop {
|
||||
// wait for the next interval tick
|
||||
interval_timer.tick().await;
|
||||
|
||||
info!("Updating mix node cache...");
|
||||
self.state.inner.mix_nodes.refresh().await;
|
||||
info!("Done");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,7 @@ pub struct ExplorerApiState {
|
||||
|
||||
impl ExplorerApiState {
|
||||
pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option<MixNodeBond> {
|
||||
self.mix_nodes
|
||||
.get()
|
||||
.await
|
||||
.value
|
||||
.get(pubkey)
|
||||
.map(|cache_item| cache_item.bond.clone())
|
||||
self.mix_nodes.get().await.value.get(pubkey).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +41,12 @@ pub struct ExplorerApiStateOnDisk {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ExplorerApiStateContext {
|
||||
pub(crate) inner: ExplorerApiState,
|
||||
state_file: String,
|
||||
}
|
||||
|
||||
impl ExplorerApiStateContext {
|
||||
pub(crate) fn new() -> Self {
|
||||
ExplorerApiStateContext {
|
||||
inner: ExplorerApiStateContext::read_from_file(),
|
||||
state_file: std::env::var("API_STATE_FILE").unwrap_or_else(|_| STATE_FILE.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 47 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "tauri-wallet",
|
||||
"productName": "nym-wallet",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"build": {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const Confirmation = ({
|
||||
{error === null ? (
|
||||
SuccessMessage
|
||||
) : (
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="errorMessage">
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
<strong>{failureMessage}</strong> - {error.message}
|
||||
</Alert>
|
||||
|
||||
@@ -40,6 +40,7 @@ export const CopyToClipboard = ({ text }: { text: string }) => {
|
||||
size="small"
|
||||
variant={copied ? 'text' : 'outlined'}
|
||||
aria-label="save"
|
||||
data-testid="copyButton"
|
||||
onClick={() => handleCopy({ text, cb: updateCopyStatus })}
|
||||
endIcon={copied && <Check />}
|
||||
style={copied ? { background: green[500], color: 'white' } : {}}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Button } from '@material-ui/core'
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="errorMessage">
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
{error.message}
|
||||
</Alert>
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="stackTrace">
|
||||
<AlertTitle>Stack trace</AlertTitle>
|
||||
{error.stack}
|
||||
</Alert>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const Nav = () => {
|
||||
)}
|
||||
|
||||
<ListItem button onClick={logOut}>
|
||||
<ListItemIcon className={classes.navItem}>
|
||||
<ListItemIcon data-testid="logOut" className={classes.navItem}>
|
||||
<ExitToApp />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
|
||||
@@ -28,7 +28,7 @@ export const BalanceCard = () => {
|
||||
noPadding
|
||||
Action={
|
||||
<Tooltip title="Refresh balance">
|
||||
<IconButton onClick={getBalance.fetchBalance} size="small">
|
||||
<IconButton data-testid="refreshBalance" onClick={getBalance.fetchBalance} size="small">
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -71,6 +71,7 @@ export const AddressCard = () => {
|
||||
title="Address"
|
||||
subheader="Wallet payments address"
|
||||
noPadding
|
||||
data-testid="walletAddressHeader"
|
||||
Action={
|
||||
<Tooltip title={!copyState ? 'Copy address' : 'Copied'}>
|
||||
<span>
|
||||
@@ -106,7 +107,7 @@ export const AddressCard = () => {
|
||||
}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
<Typography data-testid="walletAddress"
|
||||
style={{ fontWeight: theme.typography.fontWeightRegular }}
|
||||
>
|
||||
{truncate(clientDetails?.client_address!, 35)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Alert, AlertTitle } from '@material-ui/lab'
|
||||
export const NoClientError = () => {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>No client detected</AlertTitle>
|
||||
<AlertTitle data-testid="clientError">No client detected</AlertTitle>
|
||||
Have you signed in? Try to go back to{' '}
|
||||
<Link to="/signin">the main page</Link> and try again
|
||||
</Alert>
|
||||
|
||||
@@ -34,11 +34,13 @@ export const NodeTypeSelector = ({
|
||||
value={EnumNodeType.mixnode}
|
||||
control={<Radio />}
|
||||
label="Mixnode"
|
||||
data-testid="mixNode"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={EnumNodeType.gateway}
|
||||
control={<Radio />}
|
||||
data-testid="gateWay"
|
||||
label="Gateway"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -31,12 +31,13 @@ export const Balance = () => {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NymCard title="Check Balance">
|
||||
<NymCard title="Check Balance" data-testid="checkBalance">
|
||||
<Grid container direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
data-testid="errorRefresh"
|
||||
action={<RefreshAction />}
|
||||
style={{ padding: theme.spacing(2) }}
|
||||
>
|
||||
@@ -46,6 +47,7 @@ export const Balance = () => {
|
||||
{!error && (
|
||||
<Alert
|
||||
severity="success"
|
||||
data-testid="refreshSuccess"
|
||||
style={{ padding: theme.spacing(2, 3) }}
|
||||
action={<RefreshAction />}
|
||||
>
|
||||
|
||||
@@ -142,7 +142,7 @@ export const BondForm = ({
|
||||
</Grid>
|
||||
{fees && (
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
<Alert severity="info" data-testid="feeAmount">
|
||||
{`A fee of ${
|
||||
watchNodeType === EnumNodeType.mixnode
|
||||
? fees.mixnode.amount
|
||||
@@ -369,6 +369,7 @@ export const BondForm = ({
|
||||
color="primary"
|
||||
type="submit"
|
||||
size="large"
|
||||
data-testid="submitButton"
|
||||
disableElevation
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Bond = () => {
|
||||
<NymCard title="Bond" subheader="Bond a node or gateway" noPadding>
|
||||
{ownership?.hasOwnership && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
severity="warning"
|
||||
action={
|
||||
<Button
|
||||
disabled={status === EnumRequestStatus.loading}
|
||||
@@ -53,6 +53,7 @@ export const Bond = () => {
|
||||
getBalance.fetchBalance()
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
}}
|
||||
data-testid="unBond"
|
||||
>
|
||||
Unbond
|
||||
</Button>
|
||||
@@ -93,10 +94,10 @@ export const Bond = () => {
|
||||
<RequestStatus
|
||||
status={status}
|
||||
Success={
|
||||
<Alert severity="success">Successfully bonded node</Alert>
|
||||
<Alert severity="success" data-testid="bondSuccess">Successfully bonded node</Alert>
|
||||
}
|
||||
Error={
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="bondError">
|
||||
An error occurred with the request: {message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const DelegateForm = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
<Alert severity="info" data-testid="feeAmount">
|
||||
{`A fee of ${
|
||||
watchNodeType === EnumNodeType.mixnode
|
||||
? fees.mixnode.amount
|
||||
@@ -153,6 +153,7 @@ export const DelegateForm = ({
|
||||
<Button
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting}
|
||||
data-testid="delegateButton"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
|
||||
@@ -41,6 +41,7 @@ export const Delegate = () => {
|
||||
title="Delegate"
|
||||
subheader="Delegate to mixnode or gateway"
|
||||
noPadding
|
||||
data-testid="delegateCard"
|
||||
>
|
||||
{isLoading && (
|
||||
<Box
|
||||
@@ -72,13 +73,13 @@ export const Delegate = () => {
|
||||
<RequestStatus
|
||||
status={status}
|
||||
Error={
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="delegateError">
|
||||
An error occurred with the request:
|
||||
<Box style={{ wordBreak: 'break-word' }}>{message}</Box>
|
||||
</Alert>
|
||||
}
|
||||
Success={
|
||||
<Alert severity="success">
|
||||
<Alert severity="success" data-testid="delegateSuccess">
|
||||
<AlertTitle>Delegation complete</AlertTitle>
|
||||
{message}
|
||||
</Alert>
|
||||
@@ -95,6 +96,7 @@ export const Delegate = () => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="finishButton"
|
||||
onClick={() => {
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Receive = () => {
|
||||
<NymCard title="Receive Nym">
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
<Alert severity="info" data-testid="receiveNym">
|
||||
You can receive tokens by providing this address to the sender
|
||||
</Alert>
|
||||
</Grid>
|
||||
@@ -40,6 +40,7 @@ export const Receive = () => {
|
||||
>
|
||||
<Grid item>
|
||||
<Typography
|
||||
data-testid="clientAddress"
|
||||
variant={matches ? 'h5' : 'subtitle1'}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
@@ -61,7 +62,7 @@ export const Receive = () => {
|
||||
component="div"
|
||||
>
|
||||
{clientDetails && (
|
||||
<QRCode value={clientDetails.client_address} />
|
||||
<QRCode data-testid="qrCode" value={clientDetails.client_address} />
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const SendConfirmation = ({
|
||||
marginBottom: theme.spacing(1),
|
||||
}}
|
||||
/>
|
||||
<Typography>Transaction complete</Typography>
|
||||
<Typography data-testid="transactionComplete">Transaction complete</Typography>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
@@ -60,7 +60,7 @@ export const SendConfirmation = ({
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ wordBreak: 'break-all' }}>
|
||||
<Typography>{data.to_address}</Typography>
|
||||
<Typography data-testid="toAddress">{data.to_address}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
@@ -70,7 +70,7 @@ export const SendConfirmation = ({
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography>{data.amount.amount + ' punks'}</Typography>
|
||||
<Typography data-testid="sendAmount">{data.amount.amount + ' punks'}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const SendError = ({ message }: { message?: string }) => {
|
||||
variant="outlined"
|
||||
style={{ width: '100%', padding: theme.spacing(2) }}
|
||||
>
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="transactioError">
|
||||
An error occured during the request {message}
|
||||
</Alert>
|
||||
</Card>
|
||||
|
||||
@@ -44,19 +44,19 @@ export const SendReview = () => {
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<SendReviewField title="From" subtitle={values.from} />
|
||||
<SendReviewField title="From" subtitle={values.from} data-testid="fromAddress" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Divider light />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SendReviewField title="To" subtitle={values.to} />
|
||||
<SendReviewField title="To" subtitle={values.to} data-testid="toAddress"/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Divider light />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SendReviewField title="Amount" subtitle={values.amount} />
|
||||
<SendReviewField title="Amount" subtitle={values.amount} data-testid="transferAmount"/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Divider light />
|
||||
@@ -65,6 +65,7 @@ export const SendReview = () => {
|
||||
<SendReviewField
|
||||
title="Transfer fee"
|
||||
subtitle={transferFee + ' PUNK'}
|
||||
data-testid="transferFee"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -148,6 +148,7 @@ export const SendWizard = () => {
|
||||
disableElevation
|
||||
style={{ marginRight: theme.spacing(1) }}
|
||||
onClick={handlePreviousStep}
|
||||
data-testid="backButton"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
@@ -156,6 +157,7 @@ export const SendWizard = () => {
|
||||
variant={activeStep > 0 ? 'contained' : 'text'}
|
||||
color={activeStep > 0 ? 'primary' : 'default'}
|
||||
disableElevation
|
||||
data-testid="button"
|
||||
onClick={
|
||||
activeStep === 0
|
||||
? handleNextStep
|
||||
|
||||
@@ -239,7 +239,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
/>
|
||||
<Typography>Wallet setup complete</Typography>
|
||||
</div>
|
||||
<Alert severity="info" style={{ marginBottom: theme.spacing(2) }}>
|
||||
<Alert severity="info" style={{ marginBottom: theme.spacing(2) }} data-testid="mnemonicWarning">
|
||||
Please store your <strong>mnemonic</strong> in a safe place.
|
||||
You'll need it to access your wallet
|
||||
</Alert>
|
||||
@@ -257,7 +257,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography>{accountDetails.mnemonic}</Typography>
|
||||
<Typography data-testid="mnemonicPhrase">{accountDetails.mnemonic}</Typography>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||
>
|
||||
@@ -273,7 +273,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography>{accountDetails.client_address}</Typography>
|
||||
<Typography data-testid="walletAdress">{accountDetails.client_address}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
@@ -282,7 +282,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
</Grid>
|
||||
{error && (
|
||||
<Grid item style={{ marginTop: theme.spacing(1) }}>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
<Alert severity="error" data-testid="error">{error}</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
@@ -293,6 +293,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="createButton"
|
||||
disableElevation
|
||||
style={{ marginBottom: theme.spacing(1) }}
|
||||
disabled={isLoading}
|
||||
@@ -304,6 +305,7 @@ const CreateAccountContent = ({ showSignIn }: { showSignIn: () => void }) => {
|
||||
fullWidth
|
||||
variant="text"
|
||||
onClick={showSignIn}
|
||||
data-testid="signInButton"
|
||||
startIcon={<ArrowBack />}
|
||||
>
|
||||
Sign in
|
||||
|
||||
@@ -29,8 +29,10 @@ export const Unbond = () => {
|
||||
{ownership?.hasOwnership && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
data-testid="bondNoded"
|
||||
action={
|
||||
<Button
|
||||
data-testid="unBond"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true)
|
||||
@@ -48,7 +50,7 @@ export const Unbond = () => {
|
||||
</Alert>
|
||||
)}
|
||||
{!ownership.hasOwnership && (
|
||||
<Alert severity="info" style={{ margin: theme.spacing(3) }}>
|
||||
<Alert severity="info" style={{ margin: theme.spacing(3) }} data-testid="noBond">
|
||||
You don't currently have a bonded node
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const UndelegateForm = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
<Alert severity="info" data-testid="feeAmount">
|
||||
{`A fee of ${
|
||||
watchNodeType === EnumNodeType.mixnode
|
||||
? fees.mixnode.amount
|
||||
@@ -140,6 +140,7 @@ export const UndelegateForm = ({
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="submitButton"
|
||||
disableElevation
|
||||
disabled={isSubmitting}
|
||||
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||
|
||||
@@ -102,14 +102,14 @@ export const Undelegate = () => {
|
||||
<RequestStatus
|
||||
status={status}
|
||||
Error={
|
||||
<Alert severity="error">
|
||||
<Alert severity="error" data-testid="requestError">
|
||||
An error occurred with the request: {message}
|
||||
</Alert>
|
||||
}
|
||||
Success={
|
||||
<Alert severity="success">
|
||||
{' '}
|
||||
<AlertTitle>Undelegation complete</AlertTitle>
|
||||
<AlertTitle data-testid="undelegateSuccess">Undelegation complete</AlertTitle>
|
||||
{message}
|
||||
</Alert>
|
||||
}
|
||||
@@ -125,6 +125,7 @@ export const Undelegate = () => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="finishButton"
|
||||
onClick={() => {
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
initialize()
|
||||
|
||||
@@ -58,6 +58,9 @@ const API_VALIDATORS_ARG: &str = "api-validators";
|
||||
#[cfg(feature = "coconut")]
|
||||
const KEYPAIR_ARG: &str = "keypair";
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
const COCONUT_ONLY_FLAG: &str = "coconut-only";
|
||||
|
||||
const EPOCH_LENGTH_ARG: &str = "epoch-length";
|
||||
const FIRST_REWARDING_EPOCH_ARG: &str = "first-epoch";
|
||||
const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold";
|
||||
@@ -162,11 +165,15 @@ fn parse_args<'a>() -> ArgMatches<'a> {
|
||||
);
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
let base_app = base_app.arg(
|
||||
let base_app = base_app.arg(
|
||||
Arg::with_name(KEYPAIR_ARG)
|
||||
.help("Path to the secret key file")
|
||||
.takes_value(true)
|
||||
.long(KEYPAIR_ARG),
|
||||
).arg(
|
||||
Arg::with_name(COCONUT_ONLY_FLAG)
|
||||
.help("Flag to indicate whether validator api should only be used for credential issuance with no blockchain connection")
|
||||
.long(COCONUT_ONLY_FLAG),
|
||||
);
|
||||
|
||||
base_app.get_matches()
|
||||
@@ -429,11 +436,24 @@ async fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
let matches = parse_args();
|
||||
|
||||
let config = override_config(config, &matches);
|
||||
// if we just wanted to write data to the config, exit
|
||||
if matches.is_present(WRITE_CONFIG_ARG) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
if matches.is_present(COCONUT_ONLY_FLAG) {
|
||||
// this simplifies everything - we just want to run coconut things
|
||||
return rocket::build()
|
||||
.attach(setup_cors()?)
|
||||
.attach(InternalSignRequest::stage(config.keypair()))
|
||||
.launch()
|
||||
.await
|
||||
.map_err(|err| err.into());
|
||||
}
|
||||
|
||||
let liftoff_notify = Arc::new(Notify::new());
|
||||
|
||||
// let's build our rocket!
|
||||
|
||||