Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b5cc1bb54 | |||
| e980441e5f | |||
| f1214e29f9 | |||
| 604742bddb | |||
| e935b2c60f | |||
| 4cb04e9192 | |||
| 4fa5c7f2d3 | |||
| 3321331cb0 | |||
| 1e031391fa | |||
| e9f5f0223f | |||
| 8e8adc45f3 | |||
| 158ad15d37 | |||
| 9954334b30 | |||
| 8ddd9e37e0 | |||
| d7deb9819b | |||
| 3ec0fcb0a4 | |||
| ba6584e4b6 | |||
| 44bec774c3 | |||
| a1f15cda16 | |||
| 2a42090eb7 | |||
| d31d4630b7 | |||
| a1890e49cf | |||
| 21e636616d | |||
| f04fc452dc | |||
| be90d03129 | |||
| 0a3e42700c | |||
| 55d554701c | |||
| 19c4769260 | |||
| 71aadc8e1b | |||
| 95340b5817 | |||
| 12751665bb | |||
| 01b86bcc0d | |||
| c6ce8caaf7 | |||
| 265713b9d2 | |||
| c9af4721f3 | |||
| 8c0ab7c697 | |||
| 92b220ca4b | |||
| c218cba96c | |||
| c958975fff |
@@ -34,7 +34,7 @@ on:
|
||||
- 'tools/ts-rs-cli/**'
|
||||
|
||||
env:
|
||||
NETWORK: mainnet
|
||||
NETWORK: mainnet
|
||||
|
||||
jobs:
|
||||
publish-nym:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: Prepare build output directory
|
||||
shell: bash
|
||||
env:
|
||||
@@ -110,4 +110,3 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
|
||||
+12
-3
@@ -4,13 +4,22 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
# [Unreleased]
|
||||
|
||||
# [v1.1.9] (2023-02-07)
|
||||
|
||||
### Added
|
||||
|
||||
- remove coconut feature and unify builds ([#2890])
|
||||
- native-client: is now capable of listening for requests on sockets different than `127.0.0.1` ([#2939]). This can be specified via `--host` flag during `init` or `run`. Alternatively a custom `host` can be set in `config.toml` file under `socket` section.
|
||||
- Separate `nym-api` endpoints with values of "total-supply" and "circulating-supply" in `nym` ([#2964])
|
||||
- Add `host` option to client init ([#2912])
|
||||
- Remove Coconut feature flag ([#2793])
|
||||
- Don't drop in mixnet connection handler ([#2963])
|
||||
|
||||
### Changed
|
||||
- native-client: is now capable of listening for requests on sockets different than `127.0.0.1` ([#2939]). This can be specified via `--host` flag during `init` or `run`. Alternatively a custom `host` can be set in `config.toml` file under `socket` section.
|
||||
- mixnode, gateway: fix unexpected shutdown on corrupted connection ([#2963])
|
||||
|
||||
[#2890]: https://github.com/nymtech/nym/pull/2890
|
||||
[#2939]: https://github.com/nymtech/nym/pull/2939
|
||||
[#2963]: https://github.com/nymtech/nym/pull/2963
|
||||
|
||||
|
||||
# [v1.1.8] (2023-01-31)
|
||||
|
||||
|
||||
Generated
-1
@@ -3743,7 +3743,6 @@ dependencies = [
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nym-sdk",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
"pretty_env_logger",
|
||||
|
||||
@@ -51,22 +51,11 @@ pub mod non_wasm_helpers;
|
||||
|
||||
pub mod helpers;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientInput {
|
||||
pub connection_command_sender: ConnectionCommandSender,
|
||||
pub input_sender: InputMessageSender,
|
||||
}
|
||||
|
||||
impl ClientInput {
|
||||
pub async fn send(
|
||||
&self,
|
||||
message: InputMessage,
|
||||
) -> Result<(), tokio::sync::mpsc::error::SendError<InputMessage>> {
|
||||
self.input_sender.send(message).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientOutput {
|
||||
pub received_buffer_request_sender: ReceivedBufferRequestSender,
|
||||
}
|
||||
|
||||
@@ -342,6 +342,7 @@ where
|
||||
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
|
||||
match id {
|
||||
ConnectionCommand::Close(id) => self.on_close_connection(id),
|
||||
ConnectionCommand::ActiveConnections(_) => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +421,7 @@ where
|
||||
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
|
||||
match id {
|
||||
ConnectionCommand::Close(id) => self.on_close_connection(id),
|
||||
ConnectionCommand::ActiveConnections(_) => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use client_core::client::{
|
||||
};
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use proxy_helpers::connection_controller::Controller;
|
||||
use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller};
|
||||
use std::net::SocketAddr;
|
||||
use tap::TapFallible;
|
||||
use task::TaskClient;
|
||||
@@ -69,7 +69,7 @@ impl SphinxSocksServer {
|
||||
// controller for managing all active connections
|
||||
let (mut active_streams_controller, controller_sender) = Controller::new(
|
||||
client_connection_tx,
|
||||
//BroadcastActiveConnections::Off,
|
||||
BroadcastActiveConnections::Off,
|
||||
self.shutdown.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -25,6 +25,12 @@ pub enum ConnectionCommand {
|
||||
// Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard
|
||||
// transmission lanes.
|
||||
Close(ConnectionId),
|
||||
|
||||
// In the network requester for example, we usually want to broadcast active connections
|
||||
// regularly, so we know what connections we need to request lane queue lengths for from the
|
||||
// client.
|
||||
// In the socks5-client, this is not needed since have direct access to the lane queue lengths.
|
||||
ActiveConnections(Vec<ConnectionId>),
|
||||
}
|
||||
|
||||
// The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down
|
||||
|
||||
@@ -7,8 +7,12 @@ use futures::StreamExt;
|
||||
use log::*;
|
||||
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData};
|
||||
use socks5_requests::ConnectionId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
use task::TaskClient;
|
||||
use tokio::time;
|
||||
|
||||
/// A generic message produced after reading from a socket/connection. It includes data that was
|
||||
/// actually read alongside boolean indicating whether the connection got closed so that
|
||||
@@ -83,6 +87,10 @@ pub struct Controller {
|
||||
// Broadcast closed connections
|
||||
client_connection_tx: ConnectionCommandSender,
|
||||
|
||||
// The controller can broadcast active connections. This is useful in the network-requester
|
||||
// where its used to query the client for lane queue lengths
|
||||
broadcast_connections: BroadcastActiveConnections,
|
||||
|
||||
// TODO: this can potentially be abused to ddos and kill provider. Not sure at this point
|
||||
// how to handle it more gracefully
|
||||
|
||||
@@ -96,6 +104,7 @@ pub struct Controller {
|
||||
impl Controller {
|
||||
pub fn new(
|
||||
client_connection_tx: ConnectionCommandSender,
|
||||
broadcast_connections: BroadcastActiveConnections,
|
||||
shutdown: TaskClient,
|
||||
) -> (Self, ControllerSender) {
|
||||
let (sender, receiver) = mpsc::unbounded();
|
||||
@@ -105,6 +114,7 @@ impl Controller {
|
||||
receiver,
|
||||
recently_closed: HashSet::new(),
|
||||
client_connection_tx,
|
||||
broadcast_connections,
|
||||
pending_messages: HashMap::new(),
|
||||
shutdown,
|
||||
},
|
||||
@@ -155,6 +165,15 @@ impl Controller {
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_active_connections(&mut self) {
|
||||
// What about the recently closed ones? Hopefully we can ignore them ...
|
||||
let conn_ids = self.active_connections.keys().copied().collect();
|
||||
|
||||
self.client_connection_tx
|
||||
.unbounded_send(ConnectionCommand::ActiveConnections(conn_ids))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) {
|
||||
if let Some(active_connection) = self.active_connections.get_mut(&conn_id) {
|
||||
if !payload.is_empty() {
|
||||
@@ -211,6 +230,8 @@ impl Controller {
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
let mut interval = time::interval(Duration::from_millis(500));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
command = self.receiver.next() => match command {
|
||||
@@ -226,6 +247,11 @@ impl Controller {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
if self.broadcast_connections == BroadcastActiveConnections::On {
|
||||
self.broadcast_active_connections();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
self.shutdown.recv_timeout().await;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
## UNRELEASED
|
||||
|
||||
- nothing yet
|
||||
|
||||
## [nym-explorer-v1.0.5](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.5) (2023-02-07)
|
||||
|
||||
- NE - link `Owner` field on the node detail page to the account details on NG explorer ([#2923])
|
||||
- NE - Upgrade Sandbox and make below changes: ([#2332])
|
||||
|
||||
[#2923]: https://github.com/nymtech/nym/issues/2923
|
||||
[#2332]: https://github.com/nymtech/nym/issues/2332
|
||||
|
||||
## [nym-explorer-v1.0.4](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.4) (2023-01-31)
|
||||
|
||||
- Add routing score on gateway list ([#2913])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import { Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
@@ -37,9 +37,19 @@ function formatCellValues(val: string | number, field: string) {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (field === 'bond') {
|
||||
return unymToNym(val, 6);
|
||||
}
|
||||
|
||||
if (field === 'owner') {
|
||||
return (
|
||||
<Link underline="none" color="inherit" target="_blank" href={`https://mixnet.explorers.guru/account/${val}`}>
|
||||
{val}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ impl<St: Storage> ConnectionHandler<St> {
|
||||
mut shutdown: TaskClient,
|
||||
) {
|
||||
debug!("Starting connection handler for {:?}", remote);
|
||||
shutdown.mark_as_success();
|
||||
let mut framed_conn = Framed::new(conn, SphinxCodec);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
|
||||
@@ -77,6 +77,7 @@ impl ConnectionHandler {
|
||||
mut shutdown: TaskClient,
|
||||
) {
|
||||
debug!("Starting connection handler for {:?}", remote);
|
||||
shutdown.mark_as_success();
|
||||
let mut framed_conn = Framed::new(conn, SphinxCodec);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
|
||||
@@ -15,7 +15,11 @@ pub(crate) mod routes;
|
||||
|
||||
/// Merges the routes with http information and returns it to Rocket for serving
|
||||
pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![settings: routes::get_circulating_supply]
|
||||
openapi_get_routes_spec![
|
||||
settings: routes::get_full_circulating_supply,
|
||||
routes::get_total_supply,
|
||||
routes::get_circulating_supply
|
||||
]
|
||||
}
|
||||
|
||||
/// Spawn the circulating supply cache refresher.
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
|
||||
use crate::node_status_api::models::ErrorResponse;
|
||||
use nym_api_requests::models::CirculatingSupplyResponse;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use validator_client::nyxd::Coin;
|
||||
|
||||
// TODO: this is not the best place to put it, it should be more centralised,
|
||||
// but for a quick fix, that's good enough for now...
|
||||
// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent
|
||||
// value from the mix denom here.
|
||||
const UNYM_RATIO: f64 = 1000000.;
|
||||
|
||||
fn unym_coin_to_float_unym(coin: Coin) -> f64 {
|
||||
// our total supply can't exceed 1B so an overflow here is impossible
|
||||
// (if it happened, then we SHOULD crash)
|
||||
coin.amount as f64 / UNYM_RATIO
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply")]
|
||||
pub(crate) async fn get_circulating_supply(
|
||||
pub(crate) async fn get_full_circulating_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<CirculatingSupplyResponse>, ErrorResponse> {
|
||||
match cache.get_circulating_supply().await {
|
||||
@@ -20,3 +35,43 @@ pub(crate) async fn get_circulating_supply(
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply/total-supply-value")]
|
||||
pub(crate) async fn get_total_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<f64>, ErrorResponse> {
|
||||
let full_circulating_supply = match cache.get_circulating_supply().await {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(ErrorResponse::new(
|
||||
"unavailable",
|
||||
Status::InternalServerError,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(unym_coin_to_float_unym(
|
||||
full_circulating_supply.total_supply.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[openapi(tag = "circulating-supply")]
|
||||
#[get("/circulating-supply/circulating-supply-value")]
|
||||
pub(crate) async fn get_circulating_supply(
|
||||
cache: &State<CirculatingSupplyCache>,
|
||||
) -> Result<Json<f64>, ErrorResponse> {
|
||||
let full_circulating_supply = match cache.get_circulating_supply().await {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return Err(ErrorResponse::new(
|
||||
"unavailable",
|
||||
Status::InternalServerError,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(unym_coin_to_float_unym(
|
||||
full_circulating_supply.circulating_supply.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub(crate) mod tests {
|
||||
use coconut_dkg_common::dealer::DealerDetails;
|
||||
use cosmwasm_std::Addr;
|
||||
use dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use dkg::bte::{Params, PublicKeyWithProof};
|
||||
use dkg::bte::Params;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -186,18 +186,8 @@ pub(crate) mod tests {
|
||||
let mut bytes = bs58::decode(details.bte_public_key_with_proof.clone())
|
||||
.into_vec()
|
||||
.unwrap();
|
||||
// Find another value for last byte that still deserializes to a public key with proof
|
||||
let initial_byte = *bytes.last_mut().unwrap();
|
||||
loop {
|
||||
let last_byte = bytes.last_mut().unwrap();
|
||||
let (ret, _) = last_byte.overflowing_add(1);
|
||||
*last_byte = ret;
|
||||
// stop when we find that value, or if we do a full round trip of u8 values
|
||||
// and can't find one, in which case this test is invalid
|
||||
if PublicKeyWithProof::try_from_bytes(&bytes).is_ok() || ret == initial_byte {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let last_byte = bytes.last_mut().unwrap();
|
||||
*last_byte += 1;
|
||||
details.bte_public_key_with_proof = bs58::encode(&bytes).into_string();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
## UNRELEASED
|
||||
|
||||
## [nym-connect-v1.1.9](https://github.com/nymtech/nym/tree/nym-connect-v1.1.9) (2023-02-07)
|
||||
|
||||
- NC - Button animations ([#2949])
|
||||
- NC - add effect when the button is clicked ([#2947])
|
||||
- NC - UI to select gateways based on some performance criteria by checking gateways' routing score from nym-api ([#2942])
|
||||
- NC - client health check when connecting ([#2859])
|
||||
|
||||
[#2949]: https://github.com/nymtech/nym/issues/2949
|
||||
[#2947]: https://github.com/nymtech/nym/issues/2947
|
||||
[#2942]: https://github.com/nymtech/nym/issues/2942
|
||||
[#2859]: https://github.com/nymtech/nym/issues/2859
|
||||
|
||||
## [nym-connect-v1.1.8](https://github.com/nymtech/nym/tree/nym-connect-v1.1.8) (2023-01-31)
|
||||
|
||||
- Add supported apps in the menu + update guide ([#2868])
|
||||
|
||||
@@ -45,6 +45,8 @@ url = "2.2"
|
||||
yaml-rust = "0.4"
|
||||
|
||||
client-core = { path = "../../clients/client-core" }
|
||||
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common"}
|
||||
config-common = { path = "../../common/config", package = "config" }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
logging = { path = "../../common/logging"}
|
||||
|
||||
@@ -52,6 +52,7 @@ fn main() {
|
||||
crate::operations::connection::status::get_gateway_connection_status,
|
||||
crate::operations::connection::status::start_connection_health_check_task,
|
||||
crate::operations::directory::get_services,
|
||||
crate::operations::directory::get_gateways_detailed,
|
||||
crate::operations::export::export_keys,
|
||||
crate::operations::window::hide_window,
|
||||
crate::operations::growth::test_and_earn::growth_tne_get_client_id,
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models::{DirectoryService, HarbourMasterService, PagedResult};
|
||||
use crate::models::{
|
||||
DirectoryService, DirectoryServiceProvider, HarbourMasterService, PagedResult,
|
||||
};
|
||||
use contracts_common::types::Percent;
|
||||
use nym_api_requests::models::GatewayBondAnnotated;
|
||||
|
||||
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
|
||||
"https://nymtech.net/.wellknown/connect/service-providers.json";
|
||||
|
||||
static HARBOUR_MASTER_URL: &str = "https://harbourmaster.nymtech.net/v1/services/?size=100";
|
||||
|
||||
static GATEWAYS_DETAILED_URL: &str =
|
||||
"https://validator.nymtech.net/api/v1/status/gateways/detailed";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_services() -> Result<Vec<DirectoryService>> {
|
||||
pub async fn get_services() -> Result<Vec<DirectoryServiceProvider>> {
|
||||
log::trace!("Fetching services");
|
||||
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
|
||||
let services_res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
|
||||
.await?
|
||||
.json::<Vec<DirectoryService>>()
|
||||
.await?;
|
||||
log::trace!("Received: {:#?}", res);
|
||||
log::trace!("Received: {:#?}", services_res);
|
||||
|
||||
log::trace!("Fetching gateways");
|
||||
let gateway_res = reqwest::get(GATEWAYS_DETAILED_URL)
|
||||
.await?
|
||||
.json::<Vec<GatewayBondAnnotated>>()
|
||||
.await?;
|
||||
log::trace!("Received: {:#?}", gateway_res);
|
||||
|
||||
// TODO: get paged
|
||||
log::trace!("Fetching active services");
|
||||
@@ -27,7 +41,7 @@ pub async fn get_services() -> Result<Vec<DirectoryService>> {
|
||||
|
||||
let mut filtered: Vec<DirectoryService> = vec![];
|
||||
|
||||
for service in &res {
|
||||
for service in &services_res {
|
||||
let items: _ = service
|
||||
.items
|
||||
.clone()
|
||||
@@ -47,5 +61,32 @@ pub async fn get_services() -> Result<Vec<DirectoryService>> {
|
||||
})
|
||||
}
|
||||
|
||||
Ok(filtered)
|
||||
let perf_threshold = Percent::from_percentage_value(90).unwrap();
|
||||
|
||||
// Use only services that are active AND have a performance of >= 90%
|
||||
let services_with_good_performance: Vec<DirectoryServiceProvider> = filtered
|
||||
.iter_mut()
|
||||
.fold(vec![], |mut acc, sp| {
|
||||
acc.append(&mut sp.items);
|
||||
acc
|
||||
})
|
||||
.into_iter()
|
||||
.filter(|sp| {
|
||||
gateway_res.iter().any(|gateway| {
|
||||
gateway.gateway_bond.gateway.identity_key == sp.gateway
|
||||
&& gateway.performance >= perf_threshold
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(services_with_good_performance)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_gateways_detailed() -> Result<Vec<GatewayBondAnnotated>> {
|
||||
let res = reqwest::get(GATEWAYS_DETAILED_URL)
|
||||
.await?
|
||||
.json::<Vec<GatewayBondAnnotated>>()
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { forage } from '@tauri-apps/tauri-forage';
|
||||
import { useClientContext } from './context/main';
|
||||
import { useTauriEvents } from './utils';
|
||||
import { AppRoutes } from './routes';
|
||||
import { Connected } from './pages/connection/Connected';
|
||||
|
||||
export const App: FCWithChildren = () => {
|
||||
const context = useClientContext();
|
||||
const [busy, setBusy] = React.useState<boolean>();
|
||||
|
||||
useTauriEvents('help://clear-storage', (_event) => {
|
||||
console.log('About to clear local storage...');
|
||||
// clear local storage
|
||||
try {
|
||||
forage.clear()();
|
||||
console.log('Local storage cleared');
|
||||
} catch (e) {
|
||||
console.error('Failed to clear local storage', e);
|
||||
}
|
||||
});
|
||||
|
||||
const handleConnectClick = React.useCallback(async () => {
|
||||
const currentStatus = context.connectionStatus;
|
||||
if (currentStatus === 'connected' || currentStatus === 'disconnected') {
|
||||
setBusy(true);
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (currentStatus) {
|
||||
case 'disconnected':
|
||||
await context.startConnecting();
|
||||
context.setConnectedSince(DateTime.now());
|
||||
break;
|
||||
case 'connected':
|
||||
await context.startDisconnecting();
|
||||
context.setConnectedSince(undefined);
|
||||
break;
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
}, [context.connectionStatus]);
|
||||
|
||||
if (context.connectionStatus === 'disconnected' || context.connectionStatus === 'connecting') {
|
||||
return <AppRoutes />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Connected
|
||||
status={context.connectionStatus}
|
||||
showInfoModal={context.showInfoModal}
|
||||
closeInfoModal={() => context.setShowInfoModal(false)}
|
||||
busy={busy}
|
||||
onConnectClick={handleConnectClick}
|
||||
ipAddress="127.0.0.1"
|
||||
port={1080}
|
||||
gatewayPerformance={context.gatewayPerformance}
|
||||
connectedSince={context.connectedSince}
|
||||
serviceProvider={context.selectedProvider}
|
||||
stats={[
|
||||
{
|
||||
label: 'in:',
|
||||
totalBytes: 1024,
|
||||
rateBytesPerSecond: 1024 * 1024 * 1024 + 10,
|
||||
},
|
||||
{
|
||||
label: 'out:',
|
||||
totalBytes: 1024 * 1024 * 1024 * 1024 * 20,
|
||||
rateBytesPerSecond: 1024 * 1024 + 10,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -83,7 +83,6 @@ export const ConnectionStatus: FCWithChildren<{
|
||||
serviceProvider?: ServiceProvider;
|
||||
}> = ({ status, serviceProvider, gatewayPerformance }) => {
|
||||
const color = status === 'connected' || status === 'disconnecting' ? '#21D072' : 'white';
|
||||
console.log(gatewayPerformance);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ConnectionStatusKind } from 'src/types';
|
||||
|
||||
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
|
||||
if (isError && hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
if (isError) {
|
||||
return '#40475C';
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'disconnected':
|
||||
if (hover) {
|
||||
return '#FFF';
|
||||
}
|
||||
return '#BBB';
|
||||
case 'connecting':
|
||||
return '#FFF';
|
||||
case 'disconnecting':
|
||||
return '#FFF';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return '#E43E3E';
|
||||
}
|
||||
return '#21D072';
|
||||
}
|
||||
};
|
||||
|
||||
export const PowerButton: FCWithChildren<{
|
||||
onClick?: (status: ConnectionStatusKind) => void;
|
||||
isError?: boolean;
|
||||
disabled?: boolean;
|
||||
status: ConnectionStatusKind;
|
||||
busy?: boolean;
|
||||
}> = ({ onClick, disabled, status, isError }) => {
|
||||
const [hover, setHover] = React.useState<boolean>(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled === true) {
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(status);
|
||||
}
|
||||
};
|
||||
|
||||
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="190"
|
||||
height="190"
|
||||
viewBox="0 0 200 200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={handleClick}
|
||||
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
|
||||
onMouseEnter={() => !disabled && setHover(true)}
|
||||
onMouseLeave={() => !disabled && setHover(false)}
|
||||
>
|
||||
<g transform="translate(-30, -25) ">
|
||||
<circle cx={131} cy={131} r={70} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
|
||||
<circle cx={131} cy={131} r={22} strokeWidth={1} stroke={statusFillColor} filter="url(#blur)" opacity="0.3" />
|
||||
<circle opacity={0.6} cx={131} cy={131} r={68.5} stroke={statusFillColor} />
|
||||
<g filter="url(#filter1_d_944_9033)">
|
||||
<circle cx={131} cy={131} r={64} fill="url(#paint1_radial_944_9033)" />
|
||||
<circle cx={131} cy={131} r={63} stroke={statusFillColor} strokeWidth={2} />
|
||||
</g>
|
||||
<g opacity={0.5} filter="url(#filter2_f_944_9033)">
|
||||
<g clipPath="url(#clip0_944_9033)">
|
||||
<path
|
||||
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
|
||||
stroke={statusFillColor}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g clipPath="url(#clip1_944_9033)">
|
||||
<path
|
||||
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
|
||||
fill={statusFillColor}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_f_944_9033"
|
||||
x={0}
|
||||
y={0}
|
||||
width={240}
|
||||
height={240}
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity={0} result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation={40} result="effect1_foregroundBlur_944_9033" />
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_944_9033"
|
||||
x={52}
|
||||
y={58}
|
||||
width={158}
|
||||
height={158}
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity={0} result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_944_9033" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_944_9033" result="shape" />
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_f_944_9033"
|
||||
x={97}
|
||||
y={97}
|
||||
width={68}
|
||||
height={68}
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity={0} result="BackgroundImageFix" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<feGaussianBlur stdDeviation={5} result="effect1_foregroundBlur_944_9033" />
|
||||
</filter>
|
||||
<filter id="blur">
|
||||
<feGaussianBlur stdDeviation="5" />
|
||||
</filter>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { ConnectionStatusKind } from 'src/types';
|
||||
import './power-button.css';
|
||||
|
||||
const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => {
|
||||
if (isError && hover) {
|
||||
return '#21D072';
|
||||
}
|
||||
if (isError) {
|
||||
return '#40475C';
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'disconnected':
|
||||
if (hover) {
|
||||
return '#FFF';
|
||||
}
|
||||
return '#BBB';
|
||||
case 'connecting':
|
||||
return '#FFF';
|
||||
case 'disconnecting':
|
||||
return '#FFF';
|
||||
default:
|
||||
// connected
|
||||
if (hover) {
|
||||
return '#E43E3E';
|
||||
}
|
||||
return '#21D072';
|
||||
}
|
||||
};
|
||||
|
||||
export const PowerButton: FCWithChildren<{
|
||||
onClick?: (status: ConnectionStatusKind) => void;
|
||||
isError?: boolean;
|
||||
disabled?: boolean;
|
||||
status: ConnectionStatusKind;
|
||||
busy?: boolean;
|
||||
}> = ({ onClick, disabled, status, isError }) => {
|
||||
const [hover, setHover] = React.useState<boolean>(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled === true) {
|
||||
return;
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(status);
|
||||
}
|
||||
};
|
||||
|
||||
const statusFillColor = getStatusFillColor(status, hover, Boolean(isError));
|
||||
|
||||
const getClassName = useCallback(() => {
|
||||
if (hover) {
|
||||
switch (status) {
|
||||
case 'disconnected':
|
||||
return 'expand';
|
||||
default:
|
||||
return 'contract';
|
||||
}
|
||||
}
|
||||
if (!hover) {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'expand';
|
||||
default:
|
||||
return 'contract';
|
||||
}
|
||||
}
|
||||
}, [status, hover]);
|
||||
|
||||
const buttonPulse = () => {
|
||||
if (status === 'connecting' || status === 'disconnecting') return 'pulse';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="190"
|
||||
height="190"
|
||||
viewBox="0 0 200 200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onClick={handleClick}
|
||||
style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}
|
||||
onMouseEnter={() => !disabled && setHover(true)}
|
||||
onMouseLeave={() => !disabled && setHover(false)}
|
||||
>
|
||||
<g transform="translate(-30, -25) ">
|
||||
<circle cx={131} cy={131} r={75} strokeWidth={4} stroke={statusFillColor} filter="url(#blur)" opacity="0.6" />
|
||||
<circle cx={131} cy={131} r={25} strokeWidth={2} stroke={statusFillColor} filter="url(#blur)" opacity="0.5" />
|
||||
<g id="Button power">
|
||||
<circle cx="131" cy="131" r="68.5" stroke={statusFillColor} strokeWidth="0.5" />
|
||||
<circle id="ring-one" className={getClassName()} cx="131" cy="131" r="73" stroke={statusFillColor} />
|
||||
<circle id="ring-two" className={getClassName()} cx="131" cy="131" r="77" stroke={statusFillColor} />
|
||||
<circle id="ring-three" className={getClassName()} cx="131" cy="131" r="81" stroke={statusFillColor} />
|
||||
<circle id="ring-four" className={getClassName()} cx="131" cy="131" r="85" stroke={statusFillColor} />
|
||||
<g id="button bg">
|
||||
<circle cx="131" cy="131" r="63" stroke={statusFillColor} strokeWidth="3" className={buttonPulse()} />
|
||||
</g>
|
||||
<g id="Power icon">
|
||||
<g id="Icon">
|
||||
<g id="Group 672_2">
|
||||
<g id="power_settings_new_black_24dp (1) 1_2" clipPath="url(#clip1_944_8739)">
|
||||
<path
|
||||
id="Vector_2"
|
||||
d="M131 113C129.9 113 129 113.9 129 115V131C129 132.1 129.9 133 131 133C132.1 133 133 132.1 133 131V115C133 113.9 132.1 113 131 113ZM141.28 118.72C140.5 119.5 140.52 120.72 141.26 121.5C143.52 123.9 144.92 127.1 145 130.64C145.18 138.3 138.84 144.9 131.18 144.98C123.36 145.1 117 138.8 117 131C117 127.32 118.42 123.98 120.74 121.48C121.48 120.7 121.48 119.48 120.72 118.72C119.92 117.92 118.62 117.94 117.86 118.76C114.96 121.84 113.14 125.94 113 130.48C112.72 140.24 120.66 148.68 130.42 148.98C140.62 149.3 149 141.12 149 130.98C149 126.24 147.16 121.96 144.16 118.76C143.4 117.94 142.08 117.92 141.28 118.72Z"
|
||||
fill={statusFillColor}
|
||||
className={buttonPulse()}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%" x="-50%" y="-50%">
|
||||
<feGaussianBlur stdDeviation="12.5" />
|
||||
</filter>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
#ring-expand {
|
||||
animation-name: rings-expand;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
#ring-one.expand {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
#ring-two.expand {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#ring-three.expand {
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#ring-four.expand {
|
||||
opacity: 0.05;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
#ring-one.contract {
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
#ring-two.contract {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
#ring-three.contract {
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
#ring-four.contract {
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
circle,
|
||||
path {
|
||||
transition: stroke 0.5s, fill 0.5s;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation-name: pulse;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { forage } from '@tauri-apps/tauri-forage';
|
||||
import { Error } from 'src/types/error';
|
||||
import { TauriEvent } from 'src/types/event';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { ConnectionStatusKind, GatewayPerformance } from '../types';
|
||||
import { ConnectionStatsItem } from '../components/ConnectionStats';
|
||||
import { ServiceProvider, Services } from '../types/directory';
|
||||
import { useEvents } from 'src/hooks/events';
|
||||
|
||||
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
|
||||
|
||||
@@ -57,21 +55,21 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
|
||||
|
||||
const timerId = useRef<NodeJS.Timeout>();
|
||||
|
||||
const flattenProviders = (services: Services) => {
|
||||
return services.reduce((a: ServiceProvider[], b) => {
|
||||
return [...a, ...b.items];
|
||||
}, []);
|
||||
};
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const services = await invoke('get_services');
|
||||
const allServiceProviders = flattenProviders(services as Services);
|
||||
const AppVersion = await getAppVersion();
|
||||
console.log(services);
|
||||
|
||||
setAppVersion(AppVersion);
|
||||
setServiceProviders(allServiceProviders);
|
||||
setServiceProviders(services as ServiceProvider[]);
|
||||
};
|
||||
|
||||
useEvents({
|
||||
onError: (error) => setError(error),
|
||||
onGatewayPerformanceChange: (performance) => setGatewayPerformance(performance),
|
||||
onStatusChange: (status) => setConnectionStatus(status),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initialiseApp();
|
||||
}, []);
|
||||
@@ -84,49 +82,6 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten: UnlistenFn[] = [];
|
||||
|
||||
// TODO: fix typings
|
||||
listen(TAURI_EVENT_STATUS_CHANGED, (event) => {
|
||||
const { status } = event.payload as any;
|
||||
console.log(TAURI_EVENT_STATUS_CHANGED, { status, event });
|
||||
setConnectionStatus(status);
|
||||
})
|
||||
.then((result) => {
|
||||
unlisten.push(result);
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
listen('socks5-event', (e: TauriEvent) => {
|
||||
console.log(e);
|
||||
|
||||
setError(e.payload);
|
||||
}).then((result) => {
|
||||
unlisten.push(result);
|
||||
});
|
||||
|
||||
listen('socks5-status-event', (e: TauriEvent) => {
|
||||
if (e.payload.message.includes('slow')) {
|
||||
setGatewayPerformance('Poor');
|
||||
|
||||
if (timerId.current) {
|
||||
clearTimeout(timerId.current);
|
||||
}
|
||||
|
||||
timerId.current = setTimeout(() => {
|
||||
setGatewayPerformance('Good');
|
||||
}, 10000);
|
||||
}
|
||||
}).then((result) => {
|
||||
unlisten.push(result);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startConnecting = useCallback(async () => {
|
||||
try {
|
||||
await invoke('start_connecting');
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { ConnectionStatusKind, GatewayPerformance } from 'src/types';
|
||||
import { Error } from 'src/types/error';
|
||||
import { TauriEvent } from 'src/types/event';
|
||||
|
||||
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
|
||||
|
||||
export const useEvents = ({
|
||||
onError,
|
||||
onStatusChange,
|
||||
onGatewayPerformanceChange,
|
||||
}: {
|
||||
onError: (error: Error) => void;
|
||||
onStatusChange: (status: ConnectionStatusKind) => void;
|
||||
onGatewayPerformanceChange: (status: GatewayPerformance) => void;
|
||||
}) => {
|
||||
const timerId = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten: UnlistenFn[] = [];
|
||||
|
||||
// TODO: fix typings
|
||||
listen(TAURI_EVENT_STATUS_CHANGED, (event) => {
|
||||
const { status } = event.payload as any;
|
||||
console.log(TAURI_EVENT_STATUS_CHANGED, { status, event });
|
||||
onStatusChange(status);
|
||||
})
|
||||
.then((result) => {
|
||||
unlisten.push(result);
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
|
||||
listen('socks5-event', (e: TauriEvent) => {
|
||||
console.log(e);
|
||||
onError(e.payload);
|
||||
}).then((result) => {
|
||||
unlisten.push(result);
|
||||
});
|
||||
|
||||
listen('socks5-status-event', (e: TauriEvent) => {
|
||||
if (e.payload.message.includes('slow')) {
|
||||
onGatewayPerformanceChange('Poor');
|
||||
|
||||
if (timerId?.current) {
|
||||
clearTimeout(timerId.current);
|
||||
}
|
||||
|
||||
timerId.current = setTimeout(() => {
|
||||
onGatewayPerformanceChange('Good');
|
||||
}, 10000);
|
||||
}
|
||||
}).then((result) => {
|
||||
unlisten.push(result);
|
||||
});
|
||||
|
||||
listen('socks5-connection-fail-event', (e: TauriEvent) => {
|
||||
onError({ title: 'Connection failed', message: `${e.payload.message} - Please disconnect and reconnect.` });
|
||||
onGatewayPerformanceChange('Poor');
|
||||
}).then((result) => {
|
||||
unlisten.push(result);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ClientContextProvider } from './context/main';
|
||||
import { ErrorFallback } from './components/Error';
|
||||
import { NymMixnetTheme } from './theme';
|
||||
import { App } from './App';
|
||||
import { AppWindowFrame } from './components/AppWindowFrame';
|
||||
import { TestAndEarnContextProvider } from './components/Growth/context/TestAndEarnContext';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
@@ -10,9 +10,12 @@ import { IpAddressAndPort } from 'src/components/IpAddressAndPort';
|
||||
import { ServiceProvider } from 'src/types/directory';
|
||||
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
|
||||
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
|
||||
import { PowerButton } from 'src/components/PowerButton';
|
||||
import { PowerButton } from 'src/components/PowerButton/PowerButton';
|
||||
import { Error } from 'src/types/error';
|
||||
import { InfoModal } from 'src/components/InfoModal';
|
||||
|
||||
export const Connected: FCWithChildren<{
|
||||
error?: Error;
|
||||
status: ConnectionStatusKind;
|
||||
showInfoModal: boolean;
|
||||
gatewayPerformance: GatewayPerformance;
|
||||
@@ -23,9 +26,11 @@ export const Connected: FCWithChildren<{
|
||||
busy?: boolean;
|
||||
isError?: boolean;
|
||||
serviceProvider?: ServiceProvider;
|
||||
clearError: () => void;
|
||||
onConnectClick: (status: ConnectionStatusKind) => void;
|
||||
closeInfoModal: () => void;
|
||||
}> = ({
|
||||
error,
|
||||
status,
|
||||
showInfoModal,
|
||||
gatewayPerformance,
|
||||
@@ -35,11 +40,13 @@ export const Connected: FCWithChildren<{
|
||||
busy,
|
||||
isError,
|
||||
serviceProvider,
|
||||
clearError,
|
||||
onConnectClick,
|
||||
closeInfoModal,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{error && <InfoModal show title={error.title} description={error.message} onClose={clearError} />}
|
||||
<IpAddressAndPortModal show={showInfoModal} onClose={closeInfoModal} ipAddress={ipAddress} port={port} />
|
||||
<ConnectionLayout
|
||||
TopContent={
|
||||
@@ -58,7 +65,7 @@ export const Connected: FCWithChildren<{
|
||||
busy={busy}
|
||||
onClick={onConnectClick}
|
||||
isError={isError}
|
||||
disabled={status === 'connecting' || status === 'disconnecting'}
|
||||
disabled={status === 'disconnecting'}
|
||||
/>
|
||||
}
|
||||
BottomContent={
|
||||
|
||||
@@ -2,14 +2,12 @@ import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { ConnectionStatus } from 'src/components/ConnectionStatus';
|
||||
import { ConnectionTimer } from 'src/components/ConntectionTimer';
|
||||
import { useClientContext } from 'src/context/main';
|
||||
import { InfoModal } from 'src/components/InfoModal';
|
||||
import { Error } from 'src/types/error';
|
||||
import { ExperimentalWarning } from 'src/components/ExperimentalWarning';
|
||||
import { ServiceProvider, Services } from 'src/types/directory';
|
||||
import { ConnectionStatusKind } from 'src/types';
|
||||
import { ConnectionButton } from 'src/components/ConnectionButton';
|
||||
import { PowerButton } from 'src/components/PowerButton';
|
||||
import { PowerButton } from 'src/components/PowerButton/PowerButton';
|
||||
import { Box } from '@mui/system';
|
||||
import { ConnectionLayout } from 'src/layouts/ConnectionLayout';
|
||||
|
||||
@@ -22,7 +20,7 @@ export const Disconnected: FCWithChildren<{
|
||||
serviceProvider?: ServiceProvider;
|
||||
clearError: () => void;
|
||||
onConnectClick: (status: ConnectionStatusKind) => void;
|
||||
}> = ({ status, error, onConnectClick, clearError, serviceProvider }) => {
|
||||
}> = ({ status, error, onConnectClick, clearError }) => {
|
||||
return (
|
||||
<>
|
||||
{error && <InfoModal show title={error.title} description={error.message} onClose={clearError} />}
|
||||
@@ -33,7 +31,7 @@ export const Disconnected: FCWithChildren<{
|
||||
<ConnectionTimer />
|
||||
</Box>
|
||||
}
|
||||
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={false} />}
|
||||
ConnectButton={<PowerButton onClick={onConnectClick} status={status} disabled={status === 'connecting'} />}
|
||||
BottomContent={
|
||||
<Stack justifyContent="space-between" pt={1}>
|
||||
<Typography
|
||||
|
||||
@@ -47,6 +47,8 @@ export const ConnectionPage = () => {
|
||||
if (context.connectionStatus === 'connected')
|
||||
return (
|
||||
<Connected
|
||||
error={context.error}
|
||||
clearError={context.clearError}
|
||||
status={context.connectionStatus}
|
||||
showInfoModal={context.showInfoModal}
|
||||
busy={busy}
|
||||
|
||||
@@ -87,6 +87,7 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
|
||||
return (
|
||||
<AppWindowFrame>
|
||||
<Connected
|
||||
clearError={() => {}}
|
||||
gatewayPerformance="Good"
|
||||
showInfoModal={false}
|
||||
closeInfoModal={() => undefined}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default {
|
||||
export const Default: ComponentStory<typeof Connected> = () => (
|
||||
<Box p={2} width={242} sx={{ bgcolor: 'nym.background.dark' }}>
|
||||
<Connected
|
||||
clearError={() => {}}
|
||||
gatewayPerformance="Good"
|
||||
showInfoModal={false}
|
||||
closeInfoModal={() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { PowerButton } from 'src/components/PowerButton';
|
||||
import { PowerButton } from 'src/components/PowerButton/PowerButton';
|
||||
import { ConnectionStatusKind } from 'src/types';
|
||||
|
||||
export default {
|
||||
@@ -13,7 +13,7 @@ export const Disconnected: ComponentStory<typeof PowerButton> = () => (
|
||||
);
|
||||
|
||||
export const Connecting: ComponentStory<typeof PowerButton> = () => (
|
||||
<PowerButton status={ConnectionStatusKind.connecting} />
|
||||
<PowerButton status={ConnectionStatusKind.connecting} disabled />
|
||||
);
|
||||
|
||||
export const Connected: ComponentStory<typeof PowerButton> = () => (
|
||||
@@ -21,9 +21,9 @@ export const Connected: ComponentStory<typeof PowerButton> = () => (
|
||||
);
|
||||
|
||||
export const Disconnecting: ComponentStory<typeof PowerButton> = () => (
|
||||
<PowerButton status={ConnectionStatusKind.disconnecting} />
|
||||
<PowerButton status={ConnectionStatusKind.disconnecting} disabled />
|
||||
);
|
||||
|
||||
export const Disabled: ComponentStory<typeof PowerButton> = () => (
|
||||
<PowerButton status={ConnectionStatusKind.connecting} disabled />
|
||||
<PowerButton status={ConnectionStatusKind.disconnected} disabled />
|
||||
);
|
||||
|
||||
@@ -1312,9 +1312,9 @@ hosted-git-info@^2.1.4:
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
http-cache-semantics@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||
|
||||
http2-wrapper@^1.0.0-beta.5.2:
|
||||
version "1.0.3"
|
||||
@@ -2471,9 +2471,9 @@ type-fest@^0.21.3:
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
ua-parser-js@^0.7.21:
|
||||
version "0.7.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
|
||||
|
||||
unbzip2-stream@1.3.3:
|
||||
version "1.3.3"
|
||||
|
||||
@@ -36,8 +36,8 @@ mod connection_state;
|
||||
mod keys;
|
||||
mod paths;
|
||||
|
||||
pub use client::{MixnetClient, MixnetClientBuilder, MixnetClientSender};
|
||||
pub use client_core::{client::inbound_messages::InputMessage, config::GatewayEndpointConfig};
|
||||
pub use client::{MixnetClient, MixnetClientBuilder};
|
||||
pub use client_core::config::GatewayEndpointConfig;
|
||||
pub use config::Config;
|
||||
pub use keys::{Keys, KeysArc};
|
||||
pub use nymsphinx::{
|
||||
|
||||
@@ -268,6 +268,7 @@ pub struct MixnetClient {
|
||||
|
||||
/// The current state of the client that is exposed to the user. This includes things like
|
||||
/// current message send queue length.
|
||||
#[allow(dead_code)]
|
||||
client_state: ClientState,
|
||||
|
||||
/// A channel for messages arriving from the mixnet after they have been reconstructed.
|
||||
@@ -420,23 +421,6 @@ impl MixnetClient {
|
||||
&self.nym_address
|
||||
}
|
||||
|
||||
/// Get a shallow clone of [`MixnetClientSender`]
|
||||
pub fn sender(&self) -> MixnetClientSender {
|
||||
MixnetClientSender {
|
||||
client_input: self.client_input.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a shallow clone of [`ConnectionCommandSender`].
|
||||
pub fn connection_command_sender(&self) -> client_connections::ConnectionCommandSender {
|
||||
self.client_input.connection_command_sender.clone()
|
||||
}
|
||||
|
||||
/// Get a shallow clone of [`LaneQueueLengths`].
|
||||
pub fn shared_lane_queue_lengths(&self) -> client_connections::LaneQueueLengths {
|
||||
self.client_state.shared_lane_queue_lengths.clone()
|
||||
}
|
||||
|
||||
/// Sends stringy data to the supplied Nym address
|
||||
pub async fn send_str(&self, address: Recipient, message: &str) {
|
||||
let message_bytes = message.to_string().into_bytes();
|
||||
@@ -446,7 +430,7 @@ impl MixnetClient {
|
||||
/// Sends stringy data to the supplied Nym address, and skip sending reply-SURBs
|
||||
pub async fn send_str_direct(&self, address: Recipient, message: &str) {
|
||||
let message_bytes = message.to_string().into_bytes();
|
||||
self.send_bytes_direct(address, message_bytes).await;
|
||||
self.send_bytes(address, message_bytes).await;
|
||||
}
|
||||
|
||||
/// Sends bytes to the supplied Nym address
|
||||
@@ -467,12 +451,13 @@ impl MixnetClient {
|
||||
pub async fn send_bytes(&self, address: Recipient, message: Vec<u8>) {
|
||||
let lane = TransmissionLane::General;
|
||||
let input_msg = InputMessage::new_anonymous(address, message, 20, lane);
|
||||
self.send_input_message(input_msg).await
|
||||
}
|
||||
|
||||
/// Sends a [`InputMessage`] to the mixnet.
|
||||
async fn send_input_message(&self, message: InputMessage) {
|
||||
if self.client_input.send(message).await.is_err() {
|
||||
if self
|
||||
.client_input
|
||||
.input_sender
|
||||
.send(input_msg)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Failed to send message");
|
||||
}
|
||||
}
|
||||
@@ -516,15 +501,3 @@ impl MixnetClient {
|
||||
self.task_manager.wait_for_shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MixnetClientSender {
|
||||
client_input: ClientInput,
|
||||
}
|
||||
|
||||
impl MixnetClientSender {
|
||||
pub async fn send_input_message(&mut self, message: InputMessage) {
|
||||
if self.client_input.send(message).await.is_err() {
|
||||
log::error!("Failed to send message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,9 @@ tokio-tungstenite = "0.17.2"
|
||||
# internal
|
||||
client-connections = { path = "../../common/client-connections" }
|
||||
completions = { path = "../../common/completions" }
|
||||
logging = { path = "../../common/logging"}
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
|
||||
nymsphinx = { path = "../../common/nymsphinx" }
|
||||
logging = { path = "../../common/logging"}
|
||||
ordered-buffer = {path = "../../common/socks5/ordered-buffer"}
|
||||
proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
|
||||
socks5-requests = { path = "../../common/socks5/requests" }
|
||||
|
||||
@@ -4,16 +4,22 @@ use crate::allowed_hosts;
|
||||
use crate::allowed_hosts::OutboundRequestFilter;
|
||||
use crate::error::NetworkRequesterError;
|
||||
use crate::statistics::ServiceStatisticsCollector;
|
||||
use crate::websocket;
|
||||
use crate::websocket::TSWebsocketStream;
|
||||
use crate::{reply, socks5};
|
||||
use client_connections::LaneQueueLengths;
|
||||
use client_connections::{
|
||||
ConnectionCommand, ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane,
|
||||
};
|
||||
use futures::channel::mpsc;
|
||||
use futures::stream::{SplitSink, SplitStream};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use proxy_helpers::{
|
||||
connection_controller::{Controller, ControllerCommand, ControllerSender},
|
||||
proxy_runner::{MixProxyReader, MixProxySender},
|
||||
use proxy_helpers::connection_controller::{
|
||||
BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender,
|
||||
};
|
||||
use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender};
|
||||
use socks5_requests::{
|
||||
ConnectRequest, ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request,
|
||||
Response,
|
||||
@@ -22,11 +28,14 @@ use statistics_common::collector::StatisticsSender;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use task::TaskClient;
|
||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||
use websocket_requests::{requests::ClientRequest, responses::ServerResponse};
|
||||
|
||||
// Since it's an atomic, it's safe to be kept static and shared across threads
|
||||
static ACTIVE_PROXIES: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub struct ServiceProvider {
|
||||
websocket_address: String,
|
||||
outbound_request_filter: OutboundRequestFilter,
|
||||
open_proxy: bool,
|
||||
enable_statistics: bool,
|
||||
@@ -35,6 +44,7 @@ pub struct ServiceProvider {
|
||||
|
||||
impl ServiceProvider {
|
||||
pub async fn new(
|
||||
websocket_address: String,
|
||||
open_proxy: bool,
|
||||
enable_statistics: bool,
|
||||
stats_provider_addr: Option<Recipient>,
|
||||
@@ -57,6 +67,7 @@ impl ServiceProvider {
|
||||
|
||||
let outbound_request_filter = OutboundRequestFilter::new(allowed_hosts, unknown_hosts);
|
||||
ServiceProvider {
|
||||
websocket_address,
|
||||
outbound_request_filter,
|
||||
open_proxy,
|
||||
enable_statistics,
|
||||
@@ -67,9 +78,10 @@ impl ServiceProvider {
|
||||
/// Listens for any messages from `mix_reader` that should be written back to the mix network
|
||||
/// via the `websocket_writer`.
|
||||
async fn mixnet_response_listener(
|
||||
mut mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender,
|
||||
mut websocket_writer: SplitSink<TSWebsocketStream, Message>,
|
||||
mut mix_reader: MixProxyReader<(Socks5Message, reply::ReturnAddress)>,
|
||||
stats_collector: Option<ServiceStatisticsCollector>,
|
||||
mut client_connection_rx: ConnectionCommandReceiver,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -90,19 +102,102 @@ impl ServiceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// make 'request' to native-websocket client
|
||||
let conn_id = msg.conn_id();
|
||||
let response_message = return_address.send_back_to(msg.into_bytes(), conn_id);
|
||||
|
||||
mixnet_client_sender.send_input_message(response_message).await;
|
||||
let message = Message::Binary(response_message.serialize());
|
||||
websocket_writer.send(message).await.unwrap();
|
||||
} else {
|
||||
log::error!("Exiting: channel closed!");
|
||||
break;
|
||||
}
|
||||
},
|
||||
Some(command) = client_connection_rx.next() => {
|
||||
match command {
|
||||
ConnectionCommand::Close(id) => {
|
||||
let msg = ClientRequest::ClosedConnection(id);
|
||||
let ws_msg = Message::Binary(msg.serialize());
|
||||
websocket_writer.send(ws_msg).await.unwrap();
|
||||
}
|
||||
ConnectionCommand::ActiveConnections(ids) => {
|
||||
// We can optimize this by sending a single request, but this is
|
||||
// usually in the low single digits, max a few tens, so we leave that
|
||||
// for a rainy day.
|
||||
// Also that means fiddling with the currently manual
|
||||
// serialize/deserialize we do with ClientRequests ...
|
||||
for id in ids {
|
||||
log::trace!("Requesting lane queue length for: {}", id);
|
||||
let msg = ClientRequest::GetLaneQueueLength(id);
|
||||
let ws_msg = Message::Binary(msg.serialize());
|
||||
websocket_writer.send(ws_msg).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_lane_queue_length_response(
|
||||
lane_queue_lengths: &LaneQueueLengths,
|
||||
lane: u64,
|
||||
queue_length: usize,
|
||||
) {
|
||||
log::trace!("Received LaneQueueLength lane: {lane}, queue_length: {queue_length}");
|
||||
if let Ok(mut lane_queue_lengths) = lane_queue_lengths.lock() {
|
||||
let lane = TransmissionLane::ConnectionId(lane);
|
||||
lane_queue_lengths.map.insert(lane, queue_length);
|
||||
} else {
|
||||
log::warn!("Unable to lock lane queue lengths, skipping updating received lane length")
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_websocket_message(
|
||||
websocket_reader: &mut SplitStream<TSWebsocketStream>,
|
||||
lane_queue_lengths: LaneQueueLengths,
|
||||
) -> Option<ReconstructedMessage> {
|
||||
while let Some(msg) = websocket_reader.next().await {
|
||||
let data = match msg {
|
||||
Ok(msg) => msg.into_data(),
|
||||
Err(err) => {
|
||||
log::error!("Failed to read from the websocket: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// try to recover the actual message from the mix network...
|
||||
let deserialized_message = match ServerResponse::deserialize(&data) {
|
||||
Ok(deserialized) => deserialized,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to deserialize received websocket message! - {}",
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let received = match deserialized_message {
|
||||
ServerResponse::Received(received) => received,
|
||||
ServerResponse::LaneQueueLength { lane, queue_length } => {
|
||||
Self::handle_lane_queue_length_response(
|
||||
&lane_queue_lengths,
|
||||
lane,
|
||||
queue_length,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
ServerResponse::Error(err) => {
|
||||
panic!("received error from native client! - {err}")
|
||||
}
|
||||
_ => unimplemented!("probably should never be reached?"),
|
||||
};
|
||||
return Some(received);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn start_proxy(
|
||||
conn_id: ConnectionId,
|
||||
remote_addr: String,
|
||||
@@ -300,8 +395,10 @@ impl ServiceProvider {
|
||||
|
||||
/// Start all subsystems
|
||||
pub async fn run(&mut self) -> Result<(), NetworkRequesterError> {
|
||||
// Connect to the mixnet
|
||||
let mut mixnet_client = nym_sdk::mixnet::MixnetClient::connect().await.unwrap();
|
||||
let websocket_stream = self.connect_websocket(&self.websocket_address).await?;
|
||||
|
||||
// split the websocket so that we could read and write from separate threads
|
||||
let (websocket_writer, mut websocket_reader) = websocket_stream.split();
|
||||
|
||||
// channels responsible for managing messages that are to be sent to the mix network. The receiver is
|
||||
// going to be used by `mixnet_response_listener`
|
||||
@@ -311,9 +408,21 @@ impl ServiceProvider {
|
||||
// Used to notify tasks to shutdown. Not all tasks fully supports this (yet).
|
||||
let shutdown = task::TaskManager::default();
|
||||
|
||||
// Channel for announcing client connection state by the controller.
|
||||
// The `mixnet_response_listener` will use this to either report closed connection to the
|
||||
// client or request lane queue lengths.
|
||||
let (client_connection_tx, client_connection_rx) = mpsc::unbounded();
|
||||
|
||||
// Shared queue length data. Published by the `OutQueueController` in the client, and used
|
||||
// primarily to throttle incoming connections
|
||||
let shared_lane_queue_lengths = LaneQueueLengths::new();
|
||||
|
||||
// Controller for managing all active connections.
|
||||
// We provide it with a ShutdownListener since it requires it, even though for the network
|
||||
// requester shutdown signalling is not yet fully implemented.
|
||||
let (mut active_connections_controller, mut controller_sender) = Controller::new(
|
||||
mixnet_client.connection_command_sender(),
|
||||
client_connection_tx,
|
||||
BroadcastActiveConnections::On,
|
||||
shutdown.subscribe(),
|
||||
);
|
||||
|
||||
@@ -337,37 +446,58 @@ impl ServiceProvider {
|
||||
};
|
||||
|
||||
let stats_collector_clone = stats_collector.clone();
|
||||
let mixnet_client_sender = mixnet_client.sender();
|
||||
|
||||
// start the listener for mix messages
|
||||
tokio::spawn(async move {
|
||||
Self::mixnet_response_listener(
|
||||
mixnet_client_sender,
|
||||
websocket_writer,
|
||||
mix_input_receiver,
|
||||
stats_collector_clone,
|
||||
client_connection_rx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
let nym_address = mixnet_client.nym_address();
|
||||
log::info!("Our nym address is: {nym_address}");
|
||||
log::info!("All systems go. Press CTRL-C to stop the server.");
|
||||
|
||||
while let Some(received) = mixnet_client.wait_for_messages().await {
|
||||
for received in received {
|
||||
self.handle_proxy_message(
|
||||
received,
|
||||
&mut controller_sender,
|
||||
&mix_input_sender,
|
||||
mixnet_client.shared_lane_queue_lengths(),
|
||||
stats_collector.clone(),
|
||||
shutdown.subscribe(),
|
||||
// for each incoming message from the websocket... (which in 99.99% cases is going to be a mix message)
|
||||
loop {
|
||||
let Some(received) = Self::read_websocket_message(
|
||||
&mut websocket_reader,
|
||||
shared_lane_queue_lengths.clone()
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
else {
|
||||
log::error!("The websocket stream has finished!");
|
||||
return Err(NetworkRequesterError::ConnectionClosed);
|
||||
};
|
||||
|
||||
self.handle_proxy_message(
|
||||
received,
|
||||
&mut controller_sender,
|
||||
&mix_input_sender,
|
||||
shared_lane_queue_lengths.clone(),
|
||||
stats_collector.clone(),
|
||||
shutdown.subscribe(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the websocket connection so we can receive incoming Mixnet messages.
|
||||
async fn connect_websocket(
|
||||
&self,
|
||||
uri: &str,
|
||||
) -> Result<TSWebsocketStream, NetworkRequesterError> {
|
||||
match websocket::Connection::new(uri).connect().await {
|
||||
Ok(ws_stream) => {
|
||||
log::info!("* connected to local websocket server at {}", uri);
|
||||
Ok(ws_stream)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Error: websocket connection attempt failed, is the Nym client running?"
|
||||
);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
log::error!("Network requester exited unexpectedly");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use crate::websocket::WebsocketConnectionError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum NetworkRequesterError {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Websocket error")]
|
||||
WebsocketConnectionError(#[from] WebsocketConnectionError),
|
||||
|
||||
#[error("Websocket connection closed")]
|
||||
ConnectionClosed,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ mod error;
|
||||
mod reply;
|
||||
mod socks5;
|
||||
mod statistics;
|
||||
mod websocket;
|
||||
|
||||
const ENABLE_STATISTICS: &str = "enable-statistics";
|
||||
|
||||
@@ -55,8 +56,16 @@ impl Run {
|
||||
.transpose()
|
||||
.unwrap_or(None);
|
||||
|
||||
let websocket_address = format!(
|
||||
"ws://localhost:{}",
|
||||
self.websocket_port
|
||||
.as_ref()
|
||||
.unwrap_or(&network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT.to_string())
|
||||
);
|
||||
|
||||
log::info!("Starting socks5 service provider");
|
||||
let mut server = core::ServiceProvider::new(
|
||||
websocket_address,
|
||||
self.open_proxy,
|
||||
self.enable_statistics,
|
||||
stats_provider_addr,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use client_connections::TransmissionLane;
|
||||
use nym_sdk::mixnet::InputMessage;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use websocket_requests::requests::ClientRequest;
|
||||
|
||||
/// A return address is a way to send a message back to the original sender. It can be either
|
||||
/// an explicitly known Recipient, or a surb AnonymousSenderTag.
|
||||
@@ -25,17 +24,17 @@ impl ReturnAddress {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn send_back_to(self, message: Vec<u8>, connection_id: u64) -> InputMessage {
|
||||
pub(super) fn send_back_to(self, message: Vec<u8>, connection_id: u64) -> ClientRequest {
|
||||
match self {
|
||||
ReturnAddress::Known(recipient) => InputMessage::Regular {
|
||||
ReturnAddress::Known(recipient) => ClientRequest::Send {
|
||||
recipient: *recipient,
|
||||
data: message,
|
||||
lane: TransmissionLane::ConnectionId(connection_id),
|
||||
message,
|
||||
connection_id: Some(connection_id),
|
||||
},
|
||||
ReturnAddress::Anonymous(sender_tag) => InputMessage::Reply {
|
||||
recipient_tag: sender_tag,
|
||||
data: message,
|
||||
lane: TransmissionLane::ConnectionId(connection_id),
|
||||
ReturnAddress::Anonymous(sender_tag) => ClientRequest::Reply {
|
||||
message,
|
||||
sender_tag,
|
||||
connection_id: Some(connection_id),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::{connect_async, MaybeTlsStream};
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub(crate) type TSWebsocketStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||
|
||||
pub struct Connection {
|
||||
uri: String,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(uri: &str) -> Connection {
|
||||
Connection {
|
||||
uri: String::from(uri),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&self) -> Result<TSWebsocketStream, WebsocketConnectionError> {
|
||||
match connect_async(&self.uri).await {
|
||||
Ok((ws_stream, _)) => Ok(ws_stream),
|
||||
Err(e) => Err(WebsocketConnectionError::ConnectionNotEstablished(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WebsocketConnectionError {
|
||||
#[error("Connection not established")]
|
||||
ConnectionNotEstablished(tokio_tungstenite::tungstenite::Error),
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
## Binary init checker
|
||||
|
||||
|
||||
### WIP
|
||||
A simple tool to ensure that all binaries init with the correct format, using the assert.sh library
|
||||
|
||||
Simply run `./build_and_run.sh $GIT_BRANCH`
|
||||
|
||||
This will run through all the binaries anc check the fields that we expect to be initialised when passing the parameters into nyms core binaries
|
||||
|
||||
## TODO
|
||||
- Introduce all binaries
|
||||
- Sort paths and locations
|
||||
- Tidy up
|
||||
- Run in CI
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
#!/bin/bash
|
||||
# assert.sh 1.1 - bash unit testing framework
|
||||
# Copyright (C) 2009-2015 Robert Lehmann
|
||||
#
|
||||
# http://github.com/lehmannro/assert.sh
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export DISCOVERONLY=${DISCOVERONLY:-}
|
||||
export DEBUG=${DEBUG:-}
|
||||
export STOP=${STOP:-}
|
||||
export INVARIANT=${INVARIANT:-}
|
||||
export CONTINUE=${CONTINUE:-}
|
||||
|
||||
args="$(getopt -n "$0" -l \
|
||||
verbose,help,stop,discover,invariant,continue vhxdic $*)" \
|
||||
|| exit -1
|
||||
for arg in $args; do
|
||||
case "$arg" in
|
||||
-h)
|
||||
echo "$0 [-vxidc]" \
|
||||
"[--verbose] [--stop] [--invariant] [--discover] [--continue]"
|
||||
echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]"
|
||||
exit 0;;
|
||||
--help)
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
Language-agnostic unit tests for subprocesses.
|
||||
|
||||
Options:
|
||||
-v, --verbose generate output for every individual test case
|
||||
-x, --stop stop running tests after the first failure
|
||||
-i, --invariant do not measure timings to remain invariant between runs
|
||||
-d, --discover collect test suites only, do not run any tests
|
||||
-c, --continue do not modify exit code to test suite status
|
||||
-h show brief usage information and exit
|
||||
--help show this help message and exit
|
||||
EOF
|
||||
exit 0;;
|
||||
-v|--verbose)
|
||||
DEBUG=1;;
|
||||
-x|--stop)
|
||||
STOP=1;;
|
||||
-i|--invariant)
|
||||
INVARIANT=1;;
|
||||
-d|--discover)
|
||||
DISCOVERONLY=1;;
|
||||
-c|--continue)
|
||||
CONTINUE=1;;
|
||||
esac
|
||||
done
|
||||
|
||||
_indent=$'\n\t' # local format helper
|
||||
|
||||
_assert_reset() {
|
||||
tests_ran=0
|
||||
tests_failed=0
|
||||
tests_errors=()
|
||||
tests_starttime="$(date +%s%N)" # nanoseconds_since_epoch
|
||||
}
|
||||
|
||||
assert_end() {
|
||||
# assert_end [suite ..]
|
||||
tests_endtime="$(date +%s%N)"
|
||||
# required visible decimal place for seconds (leading zeros if needed)
|
||||
local tests_time="$( \
|
||||
printf "%010d" "$(( ${tests_endtime/%N/000000000}
|
||||
- ${tests_starttime/%N/000000000} ))")" # in ns
|
||||
tests="$tests_ran ${*:+$* }tests"
|
||||
[[ -n "$DISCOVERONLY" ]] && echo "collected $tests." && _assert_reset && return
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
# to get report_time split tests_time on 2 substrings:
|
||||
# ${tests_time:0:${#tests_time}-9} - seconds
|
||||
# ${tests_time:${#tests_time}-9:3} - milliseconds
|
||||
[[ -z "$INVARIANT" ]] \
|
||||
&& report_time=" in ${tests_time:0:${#tests_time}-9}.${tests_time:${#tests_time}-9:3}s" \
|
||||
|| report_time=
|
||||
|
||||
if [[ "$tests_failed" -eq 0 ]]; then
|
||||
echo "all $tests passed$report_time."
|
||||
else
|
||||
for error in "${tests_errors[@]}"; do echo "$error"; done
|
||||
echo "$tests_failed of $tests failed$report_time."
|
||||
fi
|
||||
tests_failed_previous=$tests_failed
|
||||
[[ $tests_failed -gt 0 ]] && tests_suite_status=1
|
||||
_assert_reset
|
||||
}
|
||||
|
||||
assert() {
|
||||
# assert <command> <expected stdout> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -z "$DISCOVERONLY" ]] || return
|
||||
expected=$(echo -ne "${2:-}")
|
||||
result="$(eval 2>/dev/null $1 <<< ${3:-})" || true
|
||||
if [[ "$result" == "$expected" ]]; then
|
||||
[[ -z "$DEBUG" ]] || echo -n .
|
||||
return
|
||||
fi
|
||||
result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")"
|
||||
[[ -z "$result" ]] && result="nothing" || result="\"$result\""
|
||||
[[ -z "$2" ]] && expected="nothing" || expected="\"$2\""
|
||||
_assert_fail "expected $expected${_indent}got $result" "$1" "$3"
|
||||
}
|
||||
|
||||
assert_raises() {
|
||||
# assert_raises <command> <expected code> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -z "$DISCOVERONLY" ]] || return
|
||||
status=0
|
||||
(eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$?
|
||||
expected=${2:-0}
|
||||
if [[ "$status" -eq "$expected" ]]; then
|
||||
[[ -z "$DEBUG" ]] || echo -n .
|
||||
return
|
||||
fi
|
||||
_assert_fail "program terminated with code $status instead of $expected" "$1" "$3"
|
||||
}
|
||||
|
||||
_assert_fail() {
|
||||
# _assert_fail <failure> <command> <stdin>
|
||||
[[ -n "$DEBUG" ]] && echo -n X
|
||||
report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1"
|
||||
if [[ -n "$STOP" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
echo "$report"
|
||||
exit 1
|
||||
fi
|
||||
tests_errors[$tests_failed]="$report"
|
||||
(( tests_failed++ )) || :
|
||||
}
|
||||
|
||||
skip_if() {
|
||||
# skip_if <command ..>
|
||||
(eval $@) > /dev/null 2>&1 && status=0 || status=$?
|
||||
[[ "$status" -eq 0 ]] || return
|
||||
skip
|
||||
}
|
||||
|
||||
skip() {
|
||||
# skip (no arguments)
|
||||
shopt -q extdebug && tests_extdebug=0 || tests_extdebug=1
|
||||
shopt -q -o errexit && tests_errexit=0 || tests_errexit=1
|
||||
# enable extdebug so returning 1 in a DEBUG trap handler skips next command
|
||||
shopt -s extdebug
|
||||
# disable errexit (set -e) so we can safely return 1 without causing exit
|
||||
set +o errexit
|
||||
tests_trapped=0
|
||||
trap _skip DEBUG
|
||||
}
|
||||
_skip() {
|
||||
if [[ $tests_trapped -eq 0 ]]; then
|
||||
# DEBUG trap for command we want to skip. Do not remove the handler
|
||||
# yet because *after* the command we need to reset extdebug/errexit (in
|
||||
# another DEBUG trap.)
|
||||
tests_trapped=1
|
||||
[[ -z "$DEBUG" ]] || echo -n s
|
||||
return 1
|
||||
else
|
||||
trap - DEBUG
|
||||
[[ $tests_extdebug -eq 0 ]] || shopt -u extdebug
|
||||
[[ $tests_errexit -eq 1 ]] || set -o errexit
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
_assert_reset
|
||||
: ${tests_suite_status:=0} # remember if any of the tests failed so far
|
||||
_assert_cleanup() {
|
||||
local status=$?
|
||||
# modify exit code if it's not already non-zero
|
||||
[[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status
|
||||
}
|
||||
trap _assert_cleanup EXIT
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
PWD="../"
|
||||
GIT_BRANCH=$1
|
||||
VERSION_NUMBER=$2
|
||||
|
||||
# lets make sure the branch is up to date
|
||||
# ---------------------------------------
|
||||
git checkout develop
|
||||
git fetch origin
|
||||
git checkout $GIT_BRANCH
|
||||
git pull origin $GIT_BRANCH
|
||||
# ---------------------------------------
|
||||
|
||||
echo "working directory ${PWD}"
|
||||
|
||||
#build all binaries...
|
||||
#expect the cargo tool chain to be installed on the machine
|
||||
cargo build --release --all
|
||||
|
||||
#here there should be the applicable binaries to test inits
|
||||
|
||||
./nym-mixnode-binary-check.sh "$VERSION_NUMBER"
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "running gateway binary check"
|
||||
#./nym-gateway-binary-check.sh "$VERSION_NUMBER"
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
. assert.sh -v -x
|
||||
|
||||
PWD="../"
|
||||
RELEASE_DIRECTORY="target/release"
|
||||
VERSION_NUMBER=$1
|
||||
WALLET_ADDRESS_CONST=n1435n84se65tn7yv536am0sfvng4yyrwj7thhxr
|
||||
MOCK_HOST="1.2.3.4"
|
||||
OUTPUT=$(for i in {1..8}; do echo -n $(( $RANDOM % 10 )); done)
|
||||
ID="test-${OUTPUT}"
|
||||
|
||||
echo "the version number is ${VERSION_NUMBER}"
|
||||
|
||||
cd ${PWD}${RELEASE_DIRECTORY}
|
||||
|
||||
if [ -f nym-gateway ]; then
|
||||
echo "running init tests"
|
||||
# we wont use config env files for now
|
||||
# unless we want to use a specific environment
|
||||
OUTPUT=$(./nym-gateway --output json init --id ${ID} --host ${MOCK_HOST} --wallet-address ${WALLET_ADDRESS_CONST}) > /dev/null 2>&1
|
||||
|
||||
# get jq values for things we can assert against
|
||||
VALUE=$(echo ${OUTPUT} | jq .wallet_address)
|
||||
VALUE=${VALUE#\"}
|
||||
VALUE=${VALUE%\"}
|
||||
|
||||
# do asserts here based upon the output on init
|
||||
|
||||
assert $(cat ${VALUE}) $(echo ${WALLET_ADDRESS_CONST})
|
||||
assert_end nym-gateway-tests
|
||||
else
|
||||
echo "exting test no binary found"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
. assert.sh -v -x
|
||||
|
||||
PWD="../"
|
||||
RELEASE_DIRECTORY="target/release"
|
||||
VERSION_NUMBER=$1
|
||||
WALLET_ADDRESS_CONST=n1435n84se65tn7yv536am0sfvng4yyrwj7thhxr
|
||||
MOCK_HOST="1.2.3.4"
|
||||
OUTPUT=$(for i in {1..8}; do echo -n $(( $RANDOM % 10 )); done)
|
||||
ID="test-${OUTPUT}"
|
||||
|
||||
echo "the version number is ${VERSION_NUMBER}"
|
||||
|
||||
cd $PWD/$RELEASE_DIRECTORY
|
||||
|
||||
if [ -f nym-mixnode ]; then
|
||||
echo "running init tests"
|
||||
# we wont use config env files for now
|
||||
OUTPUT=$(./nym-mixnode --output json init --id ${ID} --host ${MOCK_HOST} --wallet-address ${WALLET_ADDRESS_CONST}) > /dev/null 2>&1
|
||||
|
||||
# get jq values for things we can assert against
|
||||
# tidy this bit up - okay for first push
|
||||
VALUE=$(echo ${OUTPUT} | jq .wallet_address)
|
||||
VALUE=${VALUE#\"}
|
||||
VALUE=${VALUE%\"}
|
||||
|
||||
# do asserts here based upon the output on init
|
||||
|
||||
assert $(cat ${VALUE}) $(echo ${WALLET_ADDRESS_CONST})
|
||||
assert_end nym-mixnode-tests
|
||||
else
|
||||
echo "exting test no binary found"
|
||||
fi
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
# TODO
|
||||
@@ -10855,9 +10855,9 @@ htmlparser2@^6.1.0:
|
||||
entities "^2.0.0"
|
||||
|
||||
http-cache-semantics@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||
|
||||
http-deceiver@^1.2.7:
|
||||
version "1.2.7"
|
||||
|
||||
Reference in New Issue
Block a user