merging develop

This commit is contained in:
Tommy
2021-10-21 11:04:57 +01:00
51 changed files with 395 additions and 194 deletions
Generated
+1
View File
@@ -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);
}
+1
View File
@@ -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" }
+15 -8
View File
@@ -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)),
}
}
+2 -94
View File
@@ -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)
}
+7 -2
View File
@@ -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
+1 -21
View File
@@ -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")]
+53 -27
View File
@@ -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,
+26
View File
@@ -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");
}
});
}
}
+1 -8
View File
@@ -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()),
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"package": {
"productName": "tauri-wallet",
"productName": "nym-wallet",
"version": "0.1.0"
},
"build": {
+1 -1
View File
@@ -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' } : {}}
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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}
/>
+3 -1
View File
@@ -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 />}
>
+2 -1
View File
@@ -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} />}
+4 -3
View File
@@ -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"
+4 -2
View File
@@ -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)
}}
+3 -2
View File
@@ -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>
+1 -1
View File
@@ -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>
+4 -3
View File
@@ -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
+6 -4
View File
@@ -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
+3 -1
View File
@@ -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} />}
+3 -2
View File
@@ -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()
+21 -1
View File
@@ -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!