Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc1060693 | |||
| 2417e0565f | |||
| ea3f70a2ac | |||
| 80664b911f | |||
| 46149012bd | |||
| de601c319a | |||
| 7318de23f2 | |||
| 56e07753ea | |||
| 21b008fae9 | |||
| 27a202cbe8 | |||
| 085538582b | |||
| f66ea05929 | |||
| 052c7188ec | |||
| f6c316eea9 | |||
| f33defc645 | |||
| 424230c3bb | |||
| cddd9e8e4c | |||
| 63bc42cd5f | |||
| 9dc8ba7b77 | |||
| e130131f16 | |||
| 64a5b4b593 | |||
| 40fbdff05a | |||
| 9934b9bc8a | |||
| 6954b383a7 | |||
| 4b5276e816 | |||
| 0278bd2c26 | |||
| 56a9527497 | |||
| a601c28a20 | |||
| 005dd7513b | |||
| 3ebdc55847 | |||
| aaf5d18692 | |||
| bcbec1f3e6 | |||
| c95005265d | |||
| b299c9e4b5 | |||
| bacbd3dfce | |||
| 55561fe1f7 | |||
| 0d01500b87 | |||
| 74e34567b4 | |||
| b77025bfd5 | |||
| 4d831efcd6 | |||
| d227d20385 | |||
| fb2d3bae3c | |||
| 967d74eb19 | |||
| 228df278d9 | |||
| 37da23ab1c | |||
| 0c9cf7b5d9 | |||
| 262149078c | |||
| 08fd1c1b47 | |||
| 3bcbb90127 | |||
| 8156ed0029 | |||
| 265f7a7c2e | |||
| 5de8c9d1ed | |||
| 7658eec9b9 | |||
| 99c49581df | |||
| 926689da1d | |||
| 714171f4e5 | |||
| 7a8ad1387d | |||
| bd72426280 | |||
| 48d0f31d7e | |||
| 4522c18a55 | |||
| b9389f1235 | |||
| b40be179ae | |||
| 32ef9e019e | |||
| 45a56a7088 | |||
| d50afd6113 | |||
| 3205b1e0e6 | |||
| 53ea8486f8 | |||
| 43ababf8d4 | |||
| 5461574023 | |||
| 01d2df7bb7 | |||
| 7cff72757b | |||
| 5bcbf45d16 | |||
| 4295d75e0f | |||
| 018666a614 | |||
| 15048524a7 | |||
| 2b792945cc | |||
| b6193270a6 | |||
| 0b4a8fe657 | |||
| 2997337948 | |||
| e1bea43ff4 | |||
| 27ef28da8d | |||
| 42bf139ebb | |||
| 352862e4d0 | |||
| 9f9ab010d8 | |||
| 87a0b05d1a | |||
| dbf82da9b6 | |||
| 01a4305883 | |||
| 704b3241ee | |||
| 4513edae46 | |||
| f020b21106 | |||
| 2587906473 | |||
| 5d3f1b86e8 | |||
| 5a17e48581 | |||
| 41356f2181 | |||
| bd7d788741 | |||
| 63107c2bca | |||
| f043639ad2 | |||
| 1d2a1b2635 | |||
| 6fb15fff8b | |||
| c69d7fa46f | |||
| c2ee02a2cf | |||
| 6f5f0c00b5 | |||
| 12707c5f1e | |||
| 0daf89eeb4 | |||
| 2aa7fa0426 | |||
| f5aa6e2db2 | |||
| ee5d1c3419 | |||
| e68c261162 | |||
| c7fe4cd24e | |||
| 1c690fc3d0 | |||
| f56edb825a | |||
| d8e6a5fb2e | |||
| b95893bb02 | |||
| 6bdff701b4 | |||
| ccdb5911c6 | |||
| eff38c8466 | |||
| 5f4fabc0b8 | |||
| e2dd1cc9ae | |||
| 42f75028bc | |||
| c7e622f284 | |||
| 248da351c6 | |||
| fe1c8a3b08 | |||
| 84af923389 | |||
| 1bc17abbaa | |||
| b8c2735520 | |||
| 67fd4367ef | |||
| 4540d2c447 | |||
| 4ad25114c3 | |||
| 3ce7888c07 | |||
| ce75769703 | |||
| 66210658cb | |||
| 728da763b3 | |||
| 3da08ee33c | |||
| 55977185fd | |||
| 1c8c0a47bc | |||
| c161b2fe71 | |||
| eb91c1180d | |||
| 13e357637b | |||
| 54c4bdb7d2 | |||
| 9b5f50913f | |||
| 7cfa35b542 | |||
| 581a6b0a6f | |||
| 85f23eb3d1 | |||
| 17100fa7da | |||
| 24af0c94bf | |||
| 8ec9e3121f | |||
| 498dbb8ba3 | |||
| 7345bd8148 | |||
| b405d2e4af |
@@ -3,7 +3,7 @@ name: Continuous integration
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' }}
|
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' }}
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Mixnet Contract
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
mixnet-contract:
|
||||||
# since it's going to be compiled into wasm, there's absolutely
|
# since it's going to be compiled into wasm, there's absolutely
|
||||||
# no point in running CI on different OS-es
|
# no point in running CI on different OS-es
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Generate TS types
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tauri-wallet-types:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libsoup2.4-dev librust-gdk-dev libwebkit2gtk-4.0-dev
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
- name: Generate TS
|
||||||
|
run: cd tauri-wallet/src-tauri && cargo test
|
||||||
|
- uses: EndBug/add-and-commit@v7.2.1 # https://github.com/marketplace/actions/add-commit
|
||||||
|
with:
|
||||||
|
add: '["tauri-wallet"]'
|
||||||
|
message: '[ci skip] Generate TS types'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -3,7 +3,7 @@ name: Wasm Client
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
wasm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ The platform is composed of multiple Rust crates. Top-level executable binary cr
|
|||||||
* nym-gateway - acts sort of like a mailbox for mixnet messages, removing the need for directly delivery to potentially offline or firewalled devices.
|
* nym-gateway - acts sort of like a mailbox for mixnet messages, removing the need for directly delivery to potentially offline or firewalled devices.
|
||||||
* nym-network-monitor - sends packets through the full system to check that they are working as expected, and stores node uptime histories as the basis of a rewards system ("mixmining" or "proof-of-mixing").
|
* nym-network-monitor - sends packets through the full system to check that they are working as expected, and stores node uptime histories as the basis of a rewards system ("mixmining" or "proof-of-mixing").
|
||||||
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
|
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
|
||||||
|
* nym-wallet (currently in development)- a desktop wallet implemented using the [Tauri](https://tauri.studio/en/docs/about/intro) framework.
|
||||||
|
|
||||||
[](https://opensource.org/licenses/Apache-2.0)
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
[](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
|
[](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ flate2 = { version = "1.0.20", optional = true }
|
|||||||
sha2 = { version = "0.9.5", optional = true }
|
sha2 = { version = "0.9.5", optional = true }
|
||||||
itertools = { version = "0.10", optional = true }
|
itertools = { version = "0.10", optional = true }
|
||||||
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", optional = true }
|
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", optional = true }
|
||||||
|
ts-rs = "3.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
nymd-client = ["async-trait", "bip39", "config", "cosmrs", "prost", "flate2", "sha2", "itertools", "cosmwasm-std"]
|
nymd-client = ["async-trait", "bip39", "config", "cosmrs", "prost", "flate2", "sha2", "itertools", "cosmwasm-std"]
|
||||||
|
|||||||
@@ -442,6 +442,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
rpc_client: HttpClient,
|
rpc_client: HttpClient,
|
||||||
signer: DirectSecp256k1HdWallet,
|
signer: DirectSecp256k1HdWallet,
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ use crate::nymd::GasPrice;
|
|||||||
use cosmrs::tx::{Fee, Gas};
|
use cosmrs::tx::{Fee, Gas};
|
||||||
use cosmrs::Coin;
|
use cosmrs::Coin;
|
||||||
use cosmwasm_std::Uint128;
|
use cosmwasm_std::Uint128;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, TS)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Upload,
|
Upload,
|
||||||
Init,
|
Init,
|
||||||
@@ -37,9 +40,30 @@ pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Operation {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Operation::Upload => f.write_str("Upload"),
|
||||||
|
Operation::Init => f.write_str("Init"),
|
||||||
|
Operation::Migrate => f.write_str("Migrate"),
|
||||||
|
Operation::ChangeAdmin => f.write_str("ChangeAdmin"),
|
||||||
|
Operation::Send => f.write_str("Send"),
|
||||||
|
Operation::BondMixnode => f.write_str("BondMixnode"),
|
||||||
|
Operation::UnbondMixnode => f.write_str("UnbondMixnode"),
|
||||||
|
Operation::DelegateToMixnode => f.write_str("DelegateToMixnode"),
|
||||||
|
Operation::UndelegateFromMixnode => f.write_str("UndelegateFromMixnode"),
|
||||||
|
Operation::BondGateway => f.write_str("BondGateway"),
|
||||||
|
Operation::UnbondGateway => f.write_str("UnbondGateway"),
|
||||||
|
Operation::DelegateToGateway => f.write_str("DelegateToGateway"),
|
||||||
|
Operation::UndelegateFromGateway => f.write_str("UndelegateFromGateway"),
|
||||||
|
Operation::UpdateStateParams => f.write_str("UpdateStateParams"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Operation {
|
impl Operation {
|
||||||
// TODO: some value tweaking
|
// TODO: some value tweaking
|
||||||
pub(crate) fn default_gas_limit(&self) -> Gas {
|
pub fn default_gas_limit(&self) -> Gas {
|
||||||
match self {
|
match self {
|
||||||
Operation::Upload => 2_500_000u64.into(),
|
Operation::Upload => 2_500_000u64.into(),
|
||||||
Operation::Init => 500_000u64.into(),
|
Operation::Init => 500_000u64.into(),
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ pub use signing_client::Client as SigningNymdClient;
|
|||||||
|
|
||||||
pub mod cosmwasm_client;
|
pub mod cosmwasm_client;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub(crate) mod fee_helpers;
|
pub mod fee_helpers;
|
||||||
pub mod gas_price;
|
pub mod gas_price;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NymdClient<C> {
|
pub struct NymdClient<C> {
|
||||||
client: C,
|
client: C,
|
||||||
contract_address: Option<AccountId>,
|
contract_address: Option<AccountId>,
|
||||||
@@ -124,6 +125,14 @@ impl<C> NymdClient<C> {
|
|||||||
self.custom_gas_limits.insert(operation, limit);
|
self.custom_gas_limits.insert(operation, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_gas_price(&self) -> GasPrice {
|
||||||
|
self.gas_price.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_custom_gas_limits(&self) -> HashMap<Operation, Gas> {
|
||||||
|
self.custom_gas_limits.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contract_address(&self) -> Result<&AccountId, NymdError> {
|
pub fn contract_address(&self) -> Result<&AccountId, NymdError> {
|
||||||
self.contract_address
|
self.contract_address
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -145,7 +154,7 @@ impl<C> NymdClient<C> {
|
|||||||
&self.client_address.as_ref().unwrap()[0]
|
&self.client_address.as_ref().unwrap()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_fee(&self, operation: Operation) -> Fee {
|
pub fn get_fee(&self, operation: Operation) -> Fee {
|
||||||
let gas_limit = self.custom_gas_limits.get(&operation).cloned();
|
let gas_limit = self.custom_gas_limits.get(&operation).cloned();
|
||||||
operation.determine_fee(&self.gas_price, gas_limit)
|
operation.determine_fee(&self.gas_price, gas_limit)
|
||||||
}
|
}
|
||||||
@@ -517,15 +526,17 @@ impl<C> NymdClient<C> {
|
|||||||
/// Delegates specified amount of stake to particular mixnode.
|
/// Delegates specified amount of stake to particular mixnode.
|
||||||
pub async fn delegate_to_mixnode(
|
pub async fn delegate_to_mixnode(
|
||||||
&self,
|
&self,
|
||||||
mix_identity: IdentityKey,
|
mix_identity: &str,
|
||||||
amount: Coin,
|
amount: &Coin,
|
||||||
) -> Result<ExecuteResult, NymdError>
|
) -> Result<ExecuteResult, NymdError>
|
||||||
where
|
where
|
||||||
C: SigningCosmWasmClient + Sync,
|
C: SigningCosmWasmClient + Sync,
|
||||||
{
|
{
|
||||||
let fee = self.get_fee(Operation::DelegateToMixnode);
|
let fee = self.get_fee(Operation::DelegateToMixnode);
|
||||||
|
|
||||||
let req = ExecuteMsg::DelegateToMixnode { mix_identity };
|
let req = ExecuteMsg::DelegateToMixnode {
|
||||||
|
mix_identity: mix_identity.to_string(),
|
||||||
|
};
|
||||||
self.client
|
self.client
|
||||||
.execute(
|
.execute(
|
||||||
self.address(),
|
self.address(),
|
||||||
@@ -533,7 +544,7 @@ impl<C> NymdClient<C> {
|
|||||||
&req,
|
&req,
|
||||||
fee,
|
fee,
|
||||||
"Delegating to mixnode from rust!",
|
"Delegating to mixnode from rust!",
|
||||||
vec![cosmwasm_coin_to_cosmos_coin(amount)],
|
vec![cosmwasm_coin_ptr_to_cosmos_coin(amount)],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -541,14 +552,16 @@ impl<C> NymdClient<C> {
|
|||||||
/// Removes stake delegation from a particular mixnode.
|
/// Removes stake delegation from a particular mixnode.
|
||||||
pub async fn remove_mixnode_delegation(
|
pub async fn remove_mixnode_delegation(
|
||||||
&self,
|
&self,
|
||||||
mix_identity: IdentityKey,
|
mix_identity: &str,
|
||||||
) -> Result<ExecuteResult, NymdError>
|
) -> Result<ExecuteResult, NymdError>
|
||||||
where
|
where
|
||||||
C: SigningCosmWasmClient + Sync,
|
C: SigningCosmWasmClient + Sync,
|
||||||
{
|
{
|
||||||
let fee = self.get_fee(Operation::UndelegateFromMixnode);
|
let fee = self.get_fee(Operation::UndelegateFromMixnode);
|
||||||
|
|
||||||
let req = ExecuteMsg::UndelegateFromMixnode { mix_identity };
|
let req = ExecuteMsg::UndelegateFromMixnode {
|
||||||
|
mix_identity: mix_identity.to_string(),
|
||||||
|
};
|
||||||
self.client
|
self.client
|
||||||
.execute(
|
.execute(
|
||||||
self.address(),
|
self.address(),
|
||||||
@@ -608,15 +621,17 @@ impl<C> NymdClient<C> {
|
|||||||
/// Delegates specified amount of stake to particular gateway.
|
/// Delegates specified amount of stake to particular gateway.
|
||||||
pub async fn delegate_to_gateway(
|
pub async fn delegate_to_gateway(
|
||||||
&self,
|
&self,
|
||||||
gateway_identity: IdentityKey,
|
gateway_identity: &str,
|
||||||
amount: Coin,
|
amount: &Coin,
|
||||||
) -> Result<ExecuteResult, NymdError>
|
) -> Result<ExecuteResult, NymdError>
|
||||||
where
|
where
|
||||||
C: SigningCosmWasmClient + Sync,
|
C: SigningCosmWasmClient + Sync,
|
||||||
{
|
{
|
||||||
let fee = self.get_fee(Operation::DelegateToGateway);
|
let fee = self.get_fee(Operation::DelegateToGateway);
|
||||||
|
|
||||||
let req = ExecuteMsg::DelegateToGateway { gateway_identity };
|
let req = ExecuteMsg::DelegateToGateway {
|
||||||
|
gateway_identity: gateway_identity.to_string(),
|
||||||
|
};
|
||||||
self.client
|
self.client
|
||||||
.execute(
|
.execute(
|
||||||
self.address(),
|
self.address(),
|
||||||
@@ -624,7 +639,7 @@ impl<C> NymdClient<C> {
|
|||||||
&req,
|
&req,
|
||||||
fee,
|
fee,
|
||||||
"Delegating to gateway from rust!",
|
"Delegating to gateway from rust!",
|
||||||
vec![cosmwasm_coin_to_cosmos_coin(amount)],
|
vec![cosmwasm_coin_ptr_to_cosmos_coin(amount)],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -632,14 +647,16 @@ impl<C> NymdClient<C> {
|
|||||||
/// Removes stake delegation from a particular gateway.
|
/// Removes stake delegation from a particular gateway.
|
||||||
pub async fn remove_gateway_delegation(
|
pub async fn remove_gateway_delegation(
|
||||||
&self,
|
&self,
|
||||||
gateway_identity: IdentityKey,
|
gateway_identity: &str,
|
||||||
) -> Result<ExecuteResult, NymdError>
|
) -> Result<ExecuteResult, NymdError>
|
||||||
where
|
where
|
||||||
C: SigningCosmWasmClient + Sync,
|
C: SigningCosmWasmClient + Sync,
|
||||||
{
|
{
|
||||||
let fee = self.get_fee(Operation::UndelegateFromGateway);
|
let fee = self.get_fee(Operation::UndelegateFromGateway);
|
||||||
|
|
||||||
let req = ExecuteMsg::UndelegateFromGateway { gateway_identity };
|
let req = ExecuteMsg::UndelegateFromGateway {
|
||||||
|
gateway_identity: gateway_identity.to_string(),
|
||||||
|
};
|
||||||
self.client
|
self.client
|
||||||
.execute(
|
.execute(
|
||||||
self.address(),
|
self.address(),
|
||||||
@@ -682,3 +699,11 @@ fn cosmwasm_coin_to_cosmos_coin(coin: Coin) -> CosmosCoin {
|
|||||||
amount: (coin.amount.u128() as u64).into(),
|
amount: (coin.amount.u128() as u64).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cosmwasm_coin_ptr_to_cosmos_coin(coin: &Coin) -> CosmosCoin {
|
||||||
|
CosmosCoin {
|
||||||
|
denom: coin.denom.parse().unwrap(),
|
||||||
|
// this might be a bit iffy, cosmwasm coin stores value as u128, while cosmos does it as u64
|
||||||
|
amount: (coin.amount.u128() as u64).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use cosmrs::tx::SignDoc;
|
|||||||
use cosmrs::{tx, AccountId};
|
use cosmrs::{tx, AccountId};
|
||||||
|
|
||||||
/// Derivation information required to derive a keypair and an address from a mnemonic.
|
/// Derivation information required to derive a keypair and an address from a mnemonic.
|
||||||
|
#[derive(Debug)]
|
||||||
struct Secp256k1Derivation {
|
struct Secp256k1Derivation {
|
||||||
hd_path: DerivationPath,
|
hd_path: DerivationPath,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
@@ -25,6 +26,7 @@ pub struct AccountData {
|
|||||||
|
|
||||||
type Secp256k1Keypair = (SigningKey, PublicKey);
|
type Secp256k1Keypair = (SigningKey, PublicKey);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct DirectSecp256k1HdWallet {
|
pub struct DirectSecp256k1HdWallet {
|
||||||
/// Base secret
|
/// Base secret
|
||||||
secret: bip39::Mnemonic,
|
secret: bip39::Mnemonic,
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-up
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
schemars = "0.8"
|
schemars = "0.8"
|
||||||
|
ts-rs = "3.0"
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ use cosmwasm_std::{coin, Addr, Coin};
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::current_block_height;
|
use crate::current_block_height;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema, TS)]
|
||||||
pub struct Gateway {
|
pub struct Gateway {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub mix_port: u16,
|
pub mix_port: u16,
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::current_block_height;
|
use crate::current_block_height;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema, TS)]
|
||||||
pub struct MixNode {
|
pub struct MixNode {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub mix_port: u16,
|
pub mix_port: u16,
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ edition = "2018"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub struct ValidatorDetails<'a> {
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ValidatorDetails {
|
||||||
// it is assumed those values are always valid since they're being provided in our defaults file
|
// it is assumed those values are always valid since they're being provided in our defaults file
|
||||||
pub nymd_url: &'a str,
|
pub nymd_url: String,
|
||||||
// Right now api_url is optional as we are not running the api reliably on all validators
|
// Right now api_url is optional as we are not running the api reliably on all validators
|
||||||
// however, later on it should be a mandatory field
|
// however, later on it should be a mandatory field
|
||||||
pub api_url: Option<&'a str>,
|
pub api_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ValidatorDetails<'a> {
|
impl ValidatorDetails {
|
||||||
pub const fn new(nymd_url: &'a str, api_url: Option<&'a str>) -> Self {
|
pub fn new(nymd_url: &str, api_url: Option<&str>) -> Self {
|
||||||
ValidatorDetails { nymd_url, api_url }
|
let api_url = api_url.map(|api_url_str| api_url_str.to_string());
|
||||||
|
ValidatorDetails {
|
||||||
|
nymd_url: nymd_url.to_string(),
|
||||||
|
api_url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nymd_url(&self) -> Url {
|
pub fn nymd_url(&self) -> Url {
|
||||||
@@ -24,27 +29,30 @@ impl<'a> ValidatorDetails<'a> {
|
|||||||
|
|
||||||
pub fn api_url(&self) -> Option<Url> {
|
pub fn api_url(&self) -> Option<Url> {
|
||||||
self.api_url
|
self.api_url
|
||||||
|
.as_ref()
|
||||||
.map(|url| url.parse().expect("the provided api url is invalid!"))
|
.map(|url| url.parse().expect("the provided api url is invalid!"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_VALIDATORS: &[ValidatorDetails] = &[
|
pub fn default_validators() -> Vec<ValidatorDetails> {
|
||||||
ValidatorDetails::new(
|
vec![
|
||||||
"https://testnet-milhon-validator1.nymtech.net",
|
ValidatorDetails::new(
|
||||||
Some("https://testnet-milhon-validator1.nymtech.net/api"),
|
"https://testnet-milhon-validator1.nymtech.net",
|
||||||
),
|
Some("https://testnet-milhon-validator1.nymtech.net/api"),
|
||||||
ValidatorDetails::new("https://testnet-milhon-validator2.nymtech.net", None),
|
),
|
||||||
];
|
ValidatorDetails::new("https://testnet-milhon-validator2.nymtech.net", None),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_nymd_endpoints() -> Vec<Url> {
|
pub fn default_nymd_endpoints() -> Vec<Url> {
|
||||||
DEFAULT_VALIDATORS
|
default_validators()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|validator| validator.nymd_url())
|
.map(|validator| validator.nymd_url())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_api_endpoints() -> Vec<Url> {
|
pub fn default_api_endpoints() -> Vec<Url> {
|
||||||
DEFAULT_VALIDATORS
|
default_validators()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|validator| validator.api_url())
|
.filter_map(|validator| validator.api_url())
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"esmodules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/preset-react",
|
||||||
|
"@babel/preset-typescript"
|
||||||
|
],
|
||||||
|
"plugins": ["@babel/plugin-transform-async-to-generator"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2019,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"airbnb",
|
||||||
|
"prettier",
|
||||||
|
"plugin:jest/recommended",
|
||||||
|
"plugin:jest/style"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"jest/prefer-strict-equal": "error",
|
||||||
|
"jest/prefer-to-have-length": "warn",
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/jsx-filename-extension": "off",
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"import/no-extraneous-dependencies": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"devDependencies": [
|
||||||
|
"**/*.test.[jt]s",
|
||||||
|
"**/*.spec.[jt]s",
|
||||||
|
"**/*.test.[jt]sx",
|
||||||
|
"**/*.spec.[jt]sx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"ts": "never",
|
||||||
|
"tsx": "never",
|
||||||
|
"js": "never",
|
||||||
|
"jsx": "never"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.+(ts|tsx)",
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint/eslint-plugin"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"no-use-before-define": [0],
|
||||||
|
"@typescript-eslint/no-use-before-define": [1],
|
||||||
|
"import/no-unresolved": 0,
|
||||||
|
"import/no-extraneous-dependencies": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"devDependencies": [
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quotes": "off",
|
||||||
|
"@typescript-eslint/quotes": [
|
||||||
|
2,
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"avoidEscape": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"root-import": {
|
||||||
|
"rootPathPrefix": "@",
|
||||||
|
"rootPathSuffix": "src",
|
||||||
|
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
14
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["src-tauri"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Nym Tauri Wallet
|
||||||
|
|
||||||
|
A Rust and Tauri desktop wallet implementation.
|
||||||
|
|
||||||
|
## Installation prerequisites
|
||||||
|
|
||||||
|
* `Yarn`
|
||||||
|
* `NodeJS >= v16.8.0`
|
||||||
|
* `Rust & cargo >= v1.51`
|
||||||
|
|
||||||
|
## Installation & usage
|
||||||
|
|
||||||
|
* `yarn install`
|
||||||
|
* `yarn dev`
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "tauri-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"webpack:dev": "yarn webpack serve",
|
||||||
|
"tauri:dev": "yarn tauri dev",
|
||||||
|
"dev": "yarn run webpack:dev & yarn run tauri:dev "
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/preset-typescript": "^7.15.0",
|
||||||
|
"@hookform/resolvers": "^2.8.0",
|
||||||
|
"@material-ui/core": "^4.12.3",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
|
"@material-ui/styles": "^4.11.4",
|
||||||
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"notistack": "^1.0.10",
|
||||||
|
"qrcode.react": "^1.0.1",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-error-boundary": "^3.1.3",
|
||||||
|
"react-hook-form": "^7.14.2",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"semver": "^6.3.0",
|
||||||
|
"yup": "^0.32.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.15.0",
|
||||||
|
"@babel/plugin-transform-async-to-generator": "^7.14.5",
|
||||||
|
"@babel/preset-env": "^7.15.0",
|
||||||
|
"@babel/preset-react": "^7.14.5",
|
||||||
|
"@tauri-apps/api": "^1.0.0-beta.6",
|
||||||
|
"@tauri-apps/cli": "^1.0.0-beta.9",
|
||||||
|
"@types/bs58": "^4.0.1",
|
||||||
|
"@types/qrcode.react": "^1.0.2",
|
||||||
|
"@types/react-router-dom": "^5.1.8",
|
||||||
|
"@types/semver": "^7.3.8",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"css-loader": "^6.2.0",
|
||||||
|
"favicons-webpack-plugin": "^5.0.2",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^5.3.2",
|
||||||
|
"style-loader": "^3.2.1",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.50.0",
|
||||||
|
"webpack-cli": "^4.8.0",
|
||||||
|
"webpack-dev-server": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Nym Wallet</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
WixTools
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym_wallet"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Nym Native Wallet"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
default-run = "nym_wallet"
|
||||||
|
edition = "2018"
|
||||||
|
build = "src/build.rs"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.0.0-beta.4" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tauri = { version = "1.0.0-beta.8", features = [] }
|
||||||
|
tokio = { version = "1.10", features = ["sync"] }
|
||||||
|
dirs = "3.0"
|
||||||
|
# url = "2.2"
|
||||||
|
bip39 = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tendermint-rpc = "0.21.0"
|
||||||
|
ts-rs = "3.0"
|
||||||
|
url = "2.0"
|
||||||
|
rand = "0.6.5"
|
||||||
|
|
||||||
|
cosmrs = { version = "0.1", features = ["rpc", "bip32", "cosmwasm"] }
|
||||||
|
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch = "0.14.1-updatedk256" }
|
||||||
|
|
||||||
|
validator-client = { path = "../../common/client-libs/validator-client", features = [
|
||||||
|
"nymd-client",
|
||||||
|
] }
|
||||||
|
mixnet-contract = { path = "../../common/mixnet-contract" }
|
||||||
|
config = { path = "../../common/config" }
|
||||||
|
coconut-interface = { path = "../../common/coconut-interface" }
|
||||||
|
credentials = { path = "../../common/credentials" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
max_width = 100
|
||||||
|
hard_tabs = false
|
||||||
|
tab_spaces = 2
|
||||||
|
newline_style = "Auto"
|
||||||
|
use_small_heuristics = "Default"
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
remove_nested_parens = true
|
||||||
|
edition = "2018"
|
||||||
|
merge_derives = true
|
||||||
|
use_try_shorthand = false
|
||||||
|
use_field_init_shorthand = false
|
||||||
|
force_explicit_abi = true
|
||||||
|
imports_granularity = "Crate"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
// This should be moved out of the wallet, and used as a primary coin type throughout the codebase
|
||||||
|
|
||||||
|
use ::config::defaults::DENOM;
|
||||||
|
use cosmrs::Decimal;
|
||||||
|
use cosmrs::Denom as CosmosDenom;
|
||||||
|
use cosmwasm_std::Coin as CosmWasmCoin;
|
||||||
|
use cosmwasm_std::Uint128;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::{Add, Sub};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use ts_rs::TS;
|
||||||
|
use validator_client::nymd::{CosmosCoin, GasPrice};
|
||||||
|
|
||||||
|
use crate::format_err;
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
pub enum Denom {
|
||||||
|
Major,
|
||||||
|
Minor,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MINOR_IN_MAJOR: f64 = 1_000_000.;
|
||||||
|
|
||||||
|
impl fmt::Display for Denom {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Denom::Major => f.write_str(&DENOM[1..]),
|
||||||
|
Denom::Minor => f.write_str(DENOM),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Denom {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Denom, String> {
|
||||||
|
let s = s.to_lowercase();
|
||||||
|
if s == DENOM.to_lowercase() || s == "minor" {
|
||||||
|
Ok(Denom::Minor)
|
||||||
|
} else if s == DENOM[1..].to_lowercase() || s == "major" {
|
||||||
|
Ok(Denom::Major)
|
||||||
|
} else {
|
||||||
|
Err(format_err!(format!(
|
||||||
|
"{} is not a valid denomination string",
|
||||||
|
s
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
pub struct Coin {
|
||||||
|
amount: String,
|
||||||
|
denom: Denom,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO convert to TryFrom
|
||||||
|
impl From<GasPrice> for Coin {
|
||||||
|
fn from(g: GasPrice) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: g.amount.to_string(),
|
||||||
|
denom: Denom::from_str(&g.denom.to_string()).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Coin {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&format!("{} {}", self.amount, self.denom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows adding minor and major denominations, output will have the LHS denom.
|
||||||
|
impl Add for Coin {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
let denom = self.denom.clone();
|
||||||
|
let lhs = self.to_minor();
|
||||||
|
let rhs = rhs.to_minor();
|
||||||
|
let lhs_amount = lhs.amount.parse::<u64>().unwrap();
|
||||||
|
let rhs_amount = rhs.amount.parse::<u64>().unwrap();
|
||||||
|
let amount = lhs_amount + rhs_amount;
|
||||||
|
let coin = Coin {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
denom: Denom::Minor,
|
||||||
|
};
|
||||||
|
match denom {
|
||||||
|
Denom::Major => coin.to_major(),
|
||||||
|
Denom::Minor => coin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows adding minor and major denominations, output will have the LHS denom.
|
||||||
|
impl Sub for Coin {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
let denom = self.denom.clone();
|
||||||
|
let lhs = self.to_minor();
|
||||||
|
let rhs = rhs.to_minor();
|
||||||
|
let lhs_amount = lhs.amount.parse::<i64>().unwrap();
|
||||||
|
let rhs_amount = rhs.amount.parse::<i64>().unwrap();
|
||||||
|
let amount = lhs_amount - rhs_amount;
|
||||||
|
let coin = Coin {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
denom: Denom::Minor,
|
||||||
|
};
|
||||||
|
match denom {
|
||||||
|
Denom::Major => coin.to_major(),
|
||||||
|
Denom::Minor => coin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coin {
|
||||||
|
pub fn major<T: ToString>(amount: T) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
denom: Denom::Major,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn minor<T: ToString>(amount: T) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
denom: Denom::Minor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new<T: ToString>(amount: T, denom: &Denom) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: amount.to_string(),
|
||||||
|
denom: denom.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_major(&self) -> Coin {
|
||||||
|
match self.denom {
|
||||||
|
Denom::Major => self.clone(),
|
||||||
|
Denom::Minor => Coin {
|
||||||
|
amount: (self.amount.parse::<f64>().unwrap() / MINOR_IN_MAJOR).to_string(),
|
||||||
|
denom: Denom::Major,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_minor(&self) -> Coin {
|
||||||
|
match self.denom {
|
||||||
|
Denom::Minor => self.clone(),
|
||||||
|
Denom::Major => Coin {
|
||||||
|
amount: (self.amount.parse::<f64>().unwrap() * MINOR_IN_MAJOR).to_string(),
|
||||||
|
denom: Denom::Minor,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn amount(&self) -> String {
|
||||||
|
self.amount.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn denom(&self) -> Denom {
|
||||||
|
self.denom.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Coin> for CosmWasmCoin {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(coin: Coin) -> Result<CosmWasmCoin, String> {
|
||||||
|
Ok(CosmWasmCoin::new(
|
||||||
|
Uint128::try_from(coin.amount.as_str()).unwrap().u128(),
|
||||||
|
coin.denom.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Coin> for CosmosCoin {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(coin: Coin) -> Result<CosmosCoin, String> {
|
||||||
|
match Decimal::from_str(&coin.amount) {
|
||||||
|
Ok(d) => Ok(CosmosCoin {
|
||||||
|
amount: d,
|
||||||
|
denom: CosmosDenom::from_str(&coin.denom.to_string()).unwrap(),
|
||||||
|
}),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CosmosCoin> for Coin {
|
||||||
|
fn from(c: CosmosCoin) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: c.amount.to_string(),
|
||||||
|
denom: Denom::from_str(&c.denom.to_string()).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CosmWasmCoin> for Coin {
|
||||||
|
fn from(c: CosmWasmCoin) -> Coin {
|
||||||
|
Coin {
|
||||||
|
amount: c.amount.to_string(),
|
||||||
|
denom: Denom::from_str(&c.denom).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::coin::{Coin, Denom};
|
||||||
|
use cosmrs::Coin as CosmosCoin;
|
||||||
|
use cosmrs::Decimal;
|
||||||
|
use cosmrs::Denom as CosmosDenom;
|
||||||
|
use cosmwasm_std::Coin as CosmWasmCoin;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_to_coin() {
|
||||||
|
let minor = json!({
|
||||||
|
"amount": "1",
|
||||||
|
"denom": "Minor"
|
||||||
|
});
|
||||||
|
|
||||||
|
let major = json!({
|
||||||
|
"amount": "1",
|
||||||
|
"denom": "Major"
|
||||||
|
});
|
||||||
|
|
||||||
|
let test_minor_coin = Coin::minor("1");
|
||||||
|
let test_major_coin = Coin::major("1");
|
||||||
|
|
||||||
|
let minor_coin = serde_json::from_value::<Coin>(minor).unwrap();
|
||||||
|
let major_coin = serde_json::from_value::<Coin>(major).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(minor_coin, test_minor_coin);
|
||||||
|
assert_eq!(major_coin, test_major_coin);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn denom_conversions() {
|
||||||
|
let minor = Coin::minor("1");
|
||||||
|
let major = minor.to_major();
|
||||||
|
|
||||||
|
assert_eq!(major, Coin::major("0.000001"));
|
||||||
|
|
||||||
|
let minor = major.to_minor();
|
||||||
|
assert_eq!(minor, Coin::minor("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn amounts() -> Vec<&'static str> {
|
||||||
|
vec![
|
||||||
|
"1",
|
||||||
|
"10",
|
||||||
|
"100",
|
||||||
|
"1000",
|
||||||
|
"10000",
|
||||||
|
"100000",
|
||||||
|
"10000000",
|
||||||
|
"100000000",
|
||||||
|
"1000000000",
|
||||||
|
"10000000000",
|
||||||
|
"100000000000",
|
||||||
|
"1000000000000",
|
||||||
|
"10000000000000",
|
||||||
|
"100000000000000",
|
||||||
|
"1000000000000000",
|
||||||
|
"10000000000000000",
|
||||||
|
"100000000000000000",
|
||||||
|
"1000000000000000000",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coin_to_cosmoswasm() {
|
||||||
|
for amount in amounts() {
|
||||||
|
let coin: Coin = Coin::minor(amount).into();
|
||||||
|
let cosmoswasm_coin: CosmWasmCoin = coin.try_into().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cosmoswasm_coin,
|
||||||
|
CosmWasmCoin::new(amount.parse::<u128>().unwrap(), Denom::Minor.to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Coin::try_from(cosmoswasm_coin).unwrap(),
|
||||||
|
Coin::minor(amount)
|
||||||
|
);
|
||||||
|
|
||||||
|
let coin: Coin = Coin::major(amount).into();
|
||||||
|
let cosmoswasm_coin: CosmWasmCoin = coin.try_into().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cosmoswasm_coin,
|
||||||
|
CosmWasmCoin::new(amount.parse::<u128>().unwrap(), Denom::Major.to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Coin::try_from(cosmoswasm_coin).unwrap(),
|
||||||
|
Coin::major(amount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coin_to_cosmos() {
|
||||||
|
for amount in amounts() {
|
||||||
|
let coin: Coin = Coin::minor(amount).into();
|
||||||
|
let cosmos_coin: CosmosCoin = coin.try_into().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cosmos_coin,
|
||||||
|
CosmosCoin {
|
||||||
|
amount: Decimal::from_str(amount).unwrap(),
|
||||||
|
denom: CosmosDenom::from_str(&Denom::Minor.to_string()).unwrap()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(Coin::try_from(cosmos_coin).unwrap(), Coin::minor(amount));
|
||||||
|
|
||||||
|
let coin: Coin = Coin::major(amount).into();
|
||||||
|
let cosmos_coin: CosmosCoin = coin.try_into().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cosmos_coin,
|
||||||
|
CosmosCoin {
|
||||||
|
amount: Decimal::from_str(amount).unwrap(),
|
||||||
|
denom: CosmosDenom::from_str(&Denom::Major.to_string()).unwrap()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(Coin::try_from(cosmos_coin).unwrap(), Coin::major(amount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
assert_eq!(Coin::minor("1") + Coin::minor("1"), Coin::minor("2"));
|
||||||
|
assert_eq!(Coin::major("1") + Coin::major("1"), Coin::major("2"));
|
||||||
|
assert_eq!(Coin::minor("1") + Coin::major("1"), Coin::minor("1000001"));
|
||||||
|
assert_eq!(Coin::major("1") + Coin::minor("1"), Coin::major("1.000001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sub() {
|
||||||
|
assert_eq!(Coin::minor("1") - Coin::minor("1"), Coin::minor("0"));
|
||||||
|
assert_eq!(Coin::major("1") - Coin::major("1"), Coin::major("0"));
|
||||||
|
assert_eq!(Coin::minor("1") - Coin::major("1"), Coin::minor("-999999"));
|
||||||
|
assert_eq!(Coin::major("1") - Coin::minor("1"), Coin::major("0.999999"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use config::defaults::{default_validators, ValidatorDetails, DEFAULT_MIXNET_CONTRACT_ADDRESS};
|
||||||
|
use config::NymConfig;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tendermint_rpc::Url;
|
||||||
|
|
||||||
|
mod template;
|
||||||
|
|
||||||
|
use template::config_template;
|
||||||
|
|
||||||
|
use crate::error::BackendError;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Config {
|
||||||
|
base: Base,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Base {
|
||||||
|
validators: Vec<ValidatorDetails>,
|
||||||
|
|
||||||
|
/// Address of the validator contract managing the network
|
||||||
|
mixnet_contract_address: String,
|
||||||
|
|
||||||
|
/// Mnemonic (currently of the network monitor) used for rewarding
|
||||||
|
mnemonic: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Base {
|
||||||
|
fn default() -> Self {
|
||||||
|
Base {
|
||||||
|
validators: default_validators(),
|
||||||
|
mixnet_contract_address: DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(),
|
||||||
|
mnemonic: String::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NymConfig for Config {
|
||||||
|
fn template() -> &'static str {
|
||||||
|
config_template()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_root_directory() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.expect("Failed to evaluate $HOME value")
|
||||||
|
.join(".nym")
|
||||||
|
.join("wallet")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_directory(&self) -> PathBuf {
|
||||||
|
Self::default_root_directory()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_directory(&self) -> PathBuf {
|
||||||
|
self.root_directory().join("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_directory(&self) -> PathBuf {
|
||||||
|
self.root_directory().join("data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn get_nymd_validator_url(&self) -> Result<Url, BackendError> {
|
||||||
|
// TODO make this a random choice
|
||||||
|
if let Some(validator_details) = self.base.validators.first() {
|
||||||
|
match tendermint_rpc::Url::from_str(&validator_details.nymd_url().to_string()) {
|
||||||
|
Ok(url) => Ok(url),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("No validators found in config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mixnet_contract_address(&self) -> String {
|
||||||
|
self.base.mixnet_contract_address.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn get_mnemonic(&self) -> String {
|
||||||
|
// self.base.mnemonic.clone()
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
pub(crate) fn config_template() -> &'static str {
|
||||||
|
r#"
|
||||||
|
# This is a TOML config file.
|
||||||
|
# For more information, see https://github.com/toml-lang/toml
|
||||||
|
|
||||||
|
##### main base tauri-wallet config options #####
|
||||||
|
|
||||||
|
[base]
|
||||||
|
|
||||||
|
# Validator server to which the API will be getting information about the network.
|
||||||
|
validator_url = '{{ base.validator_url }}'
|
||||||
|
|
||||||
|
"#
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use validator_client::nymd::error::NymdError;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum BackendError {
|
||||||
|
#[error("Error parsing bip39 mnemonic")]
|
||||||
|
Bip39Error {
|
||||||
|
#[from]
|
||||||
|
source: bip39::Error,
|
||||||
|
},
|
||||||
|
#[error("Error parsing into tendermint Url")]
|
||||||
|
TendermintError {
|
||||||
|
#[from]
|
||||||
|
source: tendermint_rpc::Error,
|
||||||
|
},
|
||||||
|
#[error("Error getting balances")]
|
||||||
|
NymdError {
|
||||||
|
#[from]
|
||||||
|
source: NymdError,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
use mixnet_contract::{Gateway, MixNode};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ts_rs::export;
|
||||||
|
use validator_client::nymd::fee_helpers::Operation;
|
||||||
|
|
||||||
|
mod coin;
|
||||||
|
mod config;
|
||||||
|
mod error;
|
||||||
|
mod operations;
|
||||||
|
mod state;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use crate::operations::account::*;
|
||||||
|
use crate::operations::admin::*;
|
||||||
|
use crate::operations::bond::*;
|
||||||
|
use crate::operations::delegate::*;
|
||||||
|
use crate::operations::send::*;
|
||||||
|
use crate::utils::*;
|
||||||
|
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::coin::{Coin, Denom};
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! format_err {
|
||||||
|
($e:expr) => {
|
||||||
|
format!("line {}: {}", line!(), $e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.manage(Arc::new(RwLock::new(State::default())))
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
connect_with_mnemonic,
|
||||||
|
get_balance,
|
||||||
|
minor_to_major,
|
||||||
|
major_to_minor,
|
||||||
|
owns_gateway,
|
||||||
|
owns_mixnode,
|
||||||
|
bond_mixnode,
|
||||||
|
unbond_mixnode,
|
||||||
|
bond_gateway,
|
||||||
|
unbond_gateway,
|
||||||
|
delegate_to_mixnode,
|
||||||
|
undelegate_from_mixnode,
|
||||||
|
delegate_to_gateway,
|
||||||
|
undelegate_from_gateway,
|
||||||
|
send,
|
||||||
|
create_new_account,
|
||||||
|
get_fee,
|
||||||
|
get_state_params,
|
||||||
|
update_state_params
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
|
||||||
|
export! {
|
||||||
|
MixNode => "../src/types/rust/mixnode.ts",
|
||||||
|
Coin => "../src/types/rust/coin.ts",
|
||||||
|
Balance => "../src/types/rust/balance.ts",
|
||||||
|
Gateway => "../src/types/rust/gateway.ts",
|
||||||
|
TauriTxResult => "../src/types/rust/tauritxresult.ts",
|
||||||
|
TransactionDetails => "../src/types/rust/transactiondetails.ts",
|
||||||
|
Operation => "../src/types/rust/operation.ts",
|
||||||
|
Denom => "../src/types/rust/denom.ts",
|
||||||
|
DelegationResult => "../src/types/rust/delegationresult.ts",
|
||||||
|
Account => "../src/types/rust/account.ts",
|
||||||
|
TauriStateParams => "../src/types/rust/stateparams.ts"
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use crate::coin::{Coin, Denom};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::BackendError;
|
||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use bip39::{Language, Mnemonic};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ts_rs::TS;
|
||||||
|
use validator_client::nymd::{AccountId, NymdClient, SigningNymdClient};
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize)]
|
||||||
|
pub struct Account {
|
||||||
|
contract_address: String,
|
||||||
|
client_address: String,
|
||||||
|
denom: Denom,
|
||||||
|
mnemonmic: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize)]
|
||||||
|
pub struct Balance {
|
||||||
|
coin: Coin,
|
||||||
|
printable_balance: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn connect_with_mnemonic(
|
||||||
|
mnemonic: String,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<Account, String> {
|
||||||
|
let mnemonic = match Mnemonic::from_str(&mnemonic) {
|
||||||
|
Ok(mnemonic) => mnemonic,
|
||||||
|
Err(e) => return Err(BackendError::from(e).to_string()),
|
||||||
|
};
|
||||||
|
let client;
|
||||||
|
{
|
||||||
|
let r_state = state.read().await;
|
||||||
|
client = _connect_with_mnemonic(mnemonic, &r_state.config());
|
||||||
|
}
|
||||||
|
|
||||||
|
let contract_address = match client.contract_address() {
|
||||||
|
Ok(address) => address.to_string(),
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let client_address = client.address().to_string();
|
||||||
|
let denom = match client.denom() {
|
||||||
|
Ok(denom) => denom,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let account = Account {
|
||||||
|
contract_address,
|
||||||
|
client_address,
|
||||||
|
denom: Denom::from_str(&denom.to_string())?,
|
||||||
|
mnemonmic: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut w_state = state.write().await;
|
||||||
|
w_state.set_client(client);
|
||||||
|
|
||||||
|
Ok(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_balance(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<Balance, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.get_balance(client.address()).await {
|
||||||
|
Ok(Some(coin)) => {
|
||||||
|
let coin = Coin::new(
|
||||||
|
&coin.amount.to_string(),
|
||||||
|
&Denom::from_str(&coin.denom.to_string())?,
|
||||||
|
);
|
||||||
|
Ok(Balance {
|
||||||
|
coin: coin.clone(),
|
||||||
|
printable_balance: coin.to_major().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(None) => Err(format!(
|
||||||
|
"No balance available for address {}",
|
||||||
|
client.address()
|
||||||
|
)),
|
||||||
|
Err(e) => Err(BackendError::from(e).to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_new_account(
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<Account, String> {
|
||||||
|
let mnemonic = random_mnemonic();
|
||||||
|
let mut client = connect_with_mnemonic(mnemonic.to_string(), state).await?;
|
||||||
|
client.mnemonmic = Some(mnemonic.to_string());
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_mnemonic() -> Mnemonic {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _connect_with_mnemonic(mnemonic: Mnemonic, config: &Config) -> NymdClient<SigningNymdClient> {
|
||||||
|
match NymdClient::connect_with_mnemonic(
|
||||||
|
config.get_nymd_validator_url().unwrap(),
|
||||||
|
Some(AccountId::from_str(&config.get_mixnet_contract_address()).unwrap()),
|
||||||
|
mnemonic,
|
||||||
|
) {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => panic!("{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use cosmwasm_std::Decimal;
|
||||||
|
use cosmwasm_std::Uint128;
|
||||||
|
use mixnet_contract::StateParams;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, TS)]
|
||||||
|
pub struct TauriStateParams {
|
||||||
|
epoch_length: u32,
|
||||||
|
minimum_mixnode_bond: String,
|
||||||
|
minimum_gateway_bond: String,
|
||||||
|
mixnode_bond_reward_rate: String,
|
||||||
|
gateway_bond_reward_rate: String,
|
||||||
|
mixnode_delegation_reward_rate: String,
|
||||||
|
gateway_delegation_reward_rate: String,
|
||||||
|
mixnode_active_set_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StateParams> for TauriStateParams {
|
||||||
|
fn from(p: StateParams) -> TauriStateParams {
|
||||||
|
TauriStateParams {
|
||||||
|
epoch_length: p.epoch_length,
|
||||||
|
minimum_mixnode_bond: p.minimum_mixnode_bond.to_string(),
|
||||||
|
minimum_gateway_bond: p.minimum_gateway_bond.to_string(),
|
||||||
|
mixnode_bond_reward_rate: p.mixnode_bond_reward_rate.to_string(),
|
||||||
|
gateway_bond_reward_rate: p.gateway_bond_reward_rate.to_string(),
|
||||||
|
mixnode_delegation_reward_rate: p.mixnode_delegation_reward_rate.to_string(),
|
||||||
|
gateway_delegation_reward_rate: p.gateway_delegation_reward_rate.to_string(),
|
||||||
|
mixnode_active_set_size: p.mixnode_active_set_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<TauriStateParams> for StateParams {
|
||||||
|
type Error = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
|
fn try_from(p: TauriStateParams) -> Result<StateParams, Self::Error> {
|
||||||
|
Ok(StateParams {
|
||||||
|
epoch_length: p.epoch_length,
|
||||||
|
minimum_mixnode_bond: Uint128::try_from(p.minimum_mixnode_bond.as_str())?,
|
||||||
|
minimum_gateway_bond: Uint128::try_from(p.minimum_gateway_bond.as_str())?,
|
||||||
|
mixnode_bond_reward_rate: Decimal::from_str(p.mixnode_bond_reward_rate.as_str())?,
|
||||||
|
gateway_bond_reward_rate: Decimal::from_str(p.gateway_bond_reward_rate.as_str())?,
|
||||||
|
mixnode_delegation_reward_rate: Decimal::from_str(p.mixnode_delegation_reward_rate.as_str())?,
|
||||||
|
gateway_delegation_reward_rate: Decimal::from_str(p.gateway_delegation_reward_rate.as_str())?,
|
||||||
|
mixnode_active_set_size: p.mixnode_active_set_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_state_params(
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<TauriStateParams, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.get_state_params().await {
|
||||||
|
Ok(params) => Ok(params.into()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_state_params(
|
||||||
|
params: TauriStateParams,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<TauriStateParams, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
let state_params: StateParams = match params.try_into() {
|
||||||
|
Ok(state_params) => state_params,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
match client.update_state_params(state_params.clone()).await {
|
||||||
|
Ok(_) => Ok(state_params.into()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::coin::Coin;
|
||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use crate::{Gateway, MixNode};
|
||||||
|
use cosmwasm_std::Coin as CosmWasmCoin;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn bond_gateway(
|
||||||
|
gateway: Gateway,
|
||||||
|
bond: Coin,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let bond: CosmWasmCoin = match bond.try_into() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.bond_gateway(gateway, bond).await {
|
||||||
|
Ok(_result) => Ok(()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unbond_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<(), String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.unbond_gateway().await {
|
||||||
|
Ok(_result) => Ok(()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unbond_mixnode(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<(), String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.unbond_mixnode().await {
|
||||||
|
Ok(_result) => Ok(()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn bond_mixnode(
|
||||||
|
mixnode: MixNode,
|
||||||
|
bond: Coin,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let bond: CosmWasmCoin = match bond.try_into() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.bond_mixnode(mixnode, bond).await {
|
||||||
|
Ok(_result) => Ok(()),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::coin::Coin;
|
||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use cosmwasm_std::Coin as CosmWasmCoin;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
#[derive(TS, Serialize, Deserialize)]
|
||||||
|
pub struct DelegationResult {
|
||||||
|
source_address: String,
|
||||||
|
target_address: String,
|
||||||
|
amount: Option<Coin>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delegate_to_mixnode(
|
||||||
|
identity: &str,
|
||||||
|
amount: Coin,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<DelegationResult, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let bond: CosmWasmCoin = match amount.try_into() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.delegate_to_mixnode(identity, &bond).await {
|
||||||
|
Ok(_result) => Ok(DelegationResult {
|
||||||
|
source_address: client.address().to_string(),
|
||||||
|
target_address: identity.to_string(),
|
||||||
|
amount: Some(bond.into()),
|
||||||
|
}),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn undelegate_from_mixnode(
|
||||||
|
identity: &str,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<DelegationResult, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.remove_mixnode_delegation(identity).await {
|
||||||
|
Ok(_result) => Ok(DelegationResult {
|
||||||
|
source_address: client.address().to_string(),
|
||||||
|
target_address: identity.to_string(),
|
||||||
|
amount: None,
|
||||||
|
}),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delegate_to_gateway(
|
||||||
|
identity: &str,
|
||||||
|
amount: Coin,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<DelegationResult, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let bond: CosmWasmCoin = match amount.try_into() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.delegate_to_gateway(identity, &bond).await {
|
||||||
|
Ok(_result) => Ok(DelegationResult {
|
||||||
|
source_address: client.address().to_string(),
|
||||||
|
target_address: identity.to_string(),
|
||||||
|
amount: Some(bond.into()),
|
||||||
|
}),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn undelegate_from_gateway(
|
||||||
|
identity: &str,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<DelegationResult, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.remove_gateway_delegation(identity).await {
|
||||||
|
Ok(_result) => Ok(DelegationResult {
|
||||||
|
source_address: client.address().to_string(),
|
||||||
|
target_address: identity.to_string(),
|
||||||
|
amount: None,
|
||||||
|
}),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod account;
|
||||||
|
pub mod admin;
|
||||||
|
pub mod bond;
|
||||||
|
pub mod delegate;
|
||||||
|
pub mod send;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::coin::Coin;
|
||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tendermint_rpc::endpoint::broadcast::tx_commit::Response;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use ts_rs::TS;
|
||||||
|
use validator_client::nymd::{AccountId, CosmosCoin};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, TS)]
|
||||||
|
pub struct TauriTxResult {
|
||||||
|
code: u32,
|
||||||
|
gas_wanted: u64,
|
||||||
|
gas_used: u64,
|
||||||
|
block_height: u64,
|
||||||
|
details: TransactionDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, TS)]
|
||||||
|
pub struct TransactionDetails {
|
||||||
|
from_address: String,
|
||||||
|
to_address: String,
|
||||||
|
amount: Coin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TauriTxResult {
|
||||||
|
fn new(t: Response, details: TransactionDetails) -> TauriTxResult {
|
||||||
|
TauriTxResult {
|
||||||
|
code: t.check_tx.code.value(),
|
||||||
|
gas_wanted: t.check_tx.gas_wanted.value(),
|
||||||
|
gas_used: t.check_tx.gas_used.value(),
|
||||||
|
block_height: t.height.value(),
|
||||||
|
details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn send(
|
||||||
|
address: &str,
|
||||||
|
amount: Coin,
|
||||||
|
memo: String,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<TauriTxResult, String> {
|
||||||
|
let address = match AccountId::from_str(address) {
|
||||||
|
Ok(addy) => addy,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let cosmos_amount: CosmosCoin = match amount.clone().try_into() {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err(format_err!(e)),
|
||||||
|
};
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.send(&address, vec![cosmos_amount], memo).await {
|
||||||
|
Ok(result) => Ok(TauriTxResult::new(
|
||||||
|
result,
|
||||||
|
TransactionDetails {
|
||||||
|
from_address: client.address().to_string(),
|
||||||
|
to_address: address.to_string(),
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use crate::config::Config;
|
||||||
|
use validator_client::nymd::{NymdClient, SigningNymdClient};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct State {
|
||||||
|
config: Config,
|
||||||
|
signing_client: Option<NymdClient<SigningNymdClient>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn client(&self) -> Result<&NymdClient<SigningNymdClient>, String> {
|
||||||
|
self.signing_client.as_ref().ok_or_else(|| {
|
||||||
|
"Client has not been initialized yet, connect with mnemonic to initialize".to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> Config {
|
||||||
|
self.config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_client(&mut self, signing_client: NymdClient<SigningNymdClient>) {
|
||||||
|
self.signing_client = Some(signing_client)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::coin::{Coin, Denom};
|
||||||
|
use crate::format_err;
|
||||||
|
use crate::state::State;
|
||||||
|
use crate::Operation;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn major_to_minor(amount: &str) -> Result<Coin, String> {
|
||||||
|
let coin = Coin::new(amount, &Denom::Major);
|
||||||
|
Ok(coin.to_minor())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn minor_to_major(amount: &str) -> Result<Coin, String> {
|
||||||
|
let coin = Coin::new(amount, &Denom::Minor);
|
||||||
|
Ok(coin.to_major())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn owns_mixnode(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<bool, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.owns_mixnode(client.address()).await {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn owns_gateway(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<bool, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
match client.owns_gateway(client.address()).await {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => Err(format_err!(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_fee(
|
||||||
|
operation: Operation,
|
||||||
|
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||||
|
) -> Result<Coin, String> {
|
||||||
|
let r_state = state.read().await;
|
||||||
|
let client = r_state.client()?;
|
||||||
|
let fee = client.get_fee(operation);
|
||||||
|
let mut coin = Coin::new("0", &Denom::Major);
|
||||||
|
for f in fee.amount {
|
||||||
|
coin = coin + f.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(coin)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"package": {
|
||||||
|
"productName": "tauri-wallet",
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"distDir": "../dist",
|
||||||
|
"devPath": "http://localhost:9000",
|
||||||
|
"beforeDevCommand": "",
|
||||||
|
"beforeBuildCommand": ""
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"identifier": "com.tauri.dev",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"resources": [],
|
||||||
|
"externalBin": [],
|
||||||
|
"copyright": "",
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"shortDescription": "",
|
||||||
|
"longDescription": "",
|
||||||
|
"deb": {
|
||||||
|
"depends": [],
|
||||||
|
"useBootstrapper": false
|
||||||
|
},
|
||||||
|
"macOS": {
|
||||||
|
"frameworks": [],
|
||||||
|
"minimumSystemVersion": "",
|
||||||
|
"useBootstrapper": false,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"signingIdentity": null,
|
||||||
|
"entitlements": null
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
"allowlist": {},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "nym-wallet",
|
||||||
|
"width": 1268,
|
||||||
|
"height": 768,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import {
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
Slide,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { ClientContext } from '../context/main'
|
||||||
|
import { NymCard } from '.'
|
||||||
|
import { getContractParams, setContractParams } from '../requests'
|
||||||
|
import { TauriStateParams } from '../types'
|
||||||
|
|
||||||
|
export const Admin: React.FC = () => {
|
||||||
|
const { showAdmin, handleShowAdmin } = useContext(ClientContext)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [params, setParams] = useState<TauriStateParams>()
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setParams(undefined)
|
||||||
|
setIsLoading(false)
|
||||||
|
handleShowAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestContractParams = async () => {
|
||||||
|
if (showAdmin) {
|
||||||
|
setIsLoading(true)
|
||||||
|
const params = await getContractParams()
|
||||||
|
setParams(params)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestContractParams()
|
||||||
|
}, [showAdmin])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Backdrop open={showAdmin} style={{ zIndex: 2, overflow: 'auto' }}>
|
||||||
|
<Slide in={showAdmin}>
|
||||||
|
<Paper style={{ margin: 'auto' }}>
|
||||||
|
<NymCard title="Admin" subheader="Contract administration" noPadding>
|
||||||
|
{isLoading && (
|
||||||
|
<Box style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress size={48} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{!isLoading && params && (
|
||||||
|
<AdminForm onCancel={onCancel} params={params} />
|
||||||
|
)}
|
||||||
|
</NymCard>
|
||||||
|
</Paper>
|
||||||
|
</Slide>
|
||||||
|
</Backdrop>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminForm: React.FC<{
|
||||||
|
params: TauriStateParams
|
||||||
|
onCancel: () => void
|
||||||
|
}> = ({ params, onCancel }) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm({ defaultValues: { ...params } })
|
||||||
|
|
||||||
|
const onSubmit = async (data: TauriStateParams) => {
|
||||||
|
await setContractParams(data)
|
||||||
|
console.log(data)
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<div
|
||||||
|
style={{ padding: theme.spacing(3, 5), maxWidth: 700, minWidth: 400 }}
|
||||||
|
>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('minimum_mixnode_bond')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="minimum_mixnode_bond"
|
||||||
|
name="minimum_mixnode_bond"
|
||||||
|
label="Minumum mixnode bond"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.minimum_mixnode_bond}
|
||||||
|
helperText={errors?.minimum_mixnode_bond?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('minimum_gateway_bond')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="minimum_gateway_bond"
|
||||||
|
name="minimum_gateway_bond"
|
||||||
|
label="Minumum gateway bond"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.minimum_gateway_bond}
|
||||||
|
helperText={errors?.minimum_gateway_bond?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('mixnode_bond_reward_rate')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="mixnode_bond_reward_rate"
|
||||||
|
name="mixnode_bond_reward_rate"
|
||||||
|
label="Mixnode bond reward rate"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.mixnode_bond_reward_rate}
|
||||||
|
helperText={errors?.mixnode_bond_reward_rate?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('gateway_bond_reward_rate')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="gateway_bond_reward_rate"
|
||||||
|
name="gateway_bond_reward_rate"
|
||||||
|
label="Gateway bond reward rate"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.gateway_bond_reward_rate}
|
||||||
|
helperText={errors?.gateway_bond_reward_rate?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('mixnode_delegation_reward_rate')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="mixnode_delegation_reward_rate"
|
||||||
|
name="mixnode_delegation_reward_rate"
|
||||||
|
label="Mixnode Delegation Reward Rate"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.mixnode_delegation_reward_rate}
|
||||||
|
helperText={errors?.mixnode_delegation_reward_rate?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('gateway_delegation_reward_rate')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="gateway_delegation_reward_rate"
|
||||||
|
name="gateway_delegation_reward_rate"
|
||||||
|
label="Gateway Delegation Reward Rate"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.gateway_delegation_reward_rate}
|
||||||
|
helperText={errors?.gateway_delegation_reward_rate?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('epoch_length')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="epochLength"
|
||||||
|
name="epochLength"
|
||||||
|
label="Epoch length (hours)"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.epoch_length}
|
||||||
|
helperText={errors?.epoch_length?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('mixnode_active_set_size', { valueAsNumber: true })}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="mixnode_active_set_size"
|
||||||
|
name="mixnode_active_set_size"
|
||||||
|
label="Mixnode Active Set Size "
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.mixnode_active_set_size}
|
||||||
|
helperText={errors?.mixnode_active_set_size?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
disableElevation
|
||||||
|
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||||
|
>
|
||||||
|
Update Contract
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Typography from '@material-ui/core/Typography'
|
||||||
|
import Grid from '@material-ui/core/Grid'
|
||||||
|
import { CircularProgress } from '@material-ui/core'
|
||||||
|
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||||
|
|
||||||
|
type ConfirmationProps = {
|
||||||
|
isLoading: boolean
|
||||||
|
progressMessage: string
|
||||||
|
SuccessMessage: React.ReactNode
|
||||||
|
failureMessage: string
|
||||||
|
error: Error | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Confirmation = ({
|
||||||
|
isLoading,
|
||||||
|
progressMessage,
|
||||||
|
SuccessMessage,
|
||||||
|
failureMessage,
|
||||||
|
error,
|
||||||
|
}: ConfirmationProps) => {
|
||||||
|
return isLoading ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{progressMessage}
|
||||||
|
</Typography>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{error === null ? (
|
||||||
|
SuccessMessage
|
||||||
|
) : (
|
||||||
|
<Alert severity="error">
|
||||||
|
<AlertTitle>{error.name}</AlertTitle>
|
||||||
|
<strong>{failureMessage}</strong> - {error.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
import { Check } from '@material-ui/icons'
|
||||||
|
import { green } from '@material-ui/core/colors'
|
||||||
|
import { clipboard } from '@tauri-apps/api'
|
||||||
|
|
||||||
|
const copy = (text: string): Promise<{ success: boolean; value: string }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => resolve({ success: true, value: text }))
|
||||||
|
.catch((e) => reject({ success: false, value: 'Failed to copy: ' + e }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleCopy = async ({
|
||||||
|
text,
|
||||||
|
cb,
|
||||||
|
}: {
|
||||||
|
text: string
|
||||||
|
cb: (success: boolean) => void
|
||||||
|
}) => {
|
||||||
|
const res = await copy(text)
|
||||||
|
if (res.success) {
|
||||||
|
setTimeout(() => {
|
||||||
|
cb(true)
|
||||||
|
}, 750)
|
||||||
|
} else {
|
||||||
|
console.log(res.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CopyToClipboard = ({ text }: { text: string }) => {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const updateCopyStatus = (isCopied: boolean) => setCopied(isCopied)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant={copied ? 'text' : 'outlined'}
|
||||||
|
aria-label="save"
|
||||||
|
onClick={() => handleCopy({ text, cb: updateCopyStatus })}
|
||||||
|
endIcon={copied && <Check />}
|
||||||
|
style={copied ? { background: green[500], color: 'white' } : {}}
|
||||||
|
>
|
||||||
|
{copied ? 'Copied' : 'Copy'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { FallbackProps } from 'react-error-boundary'
|
||||||
|
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Alert severity="error">
|
||||||
|
<AlertTitle>{error.name}</AlertTitle>
|
||||||
|
{error.message}
|
||||||
|
</Alert>
|
||||||
|
<Alert severity="error">
|
||||||
|
<AlertTitle>Stack trace</AlertTitle>
|
||||||
|
{error.stack}
|
||||||
|
</Alert>
|
||||||
|
<Button onClick={resetErrorBoundary}>Back to safety</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Box, CircularProgress } from '@material-ui/core'
|
||||||
|
|
||||||
|
type TLoading = {
|
||||||
|
size?: 'small' | 'medium' | 'large' | 'x-large'
|
||||||
|
Icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading: React.FC<TLoading> = ({ size = 'medium', Icon }) => {
|
||||||
|
return (
|
||||||
|
<Box style={{ position: 'relative', display: 'inline-flex' }}>
|
||||||
|
<CircularProgress
|
||||||
|
size={
|
||||||
|
size === 'small'
|
||||||
|
? 24
|
||||||
|
: size === 'large'
|
||||||
|
? 60
|
||||||
|
: size === 'x-large'
|
||||||
|
? 72
|
||||||
|
: 36
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{Icon && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import {
|
||||||
|
AccountBalanceWalletRounded,
|
||||||
|
ArrowBack,
|
||||||
|
ArrowForward,
|
||||||
|
AttachMoney,
|
||||||
|
Cancel,
|
||||||
|
ExitToApp,
|
||||||
|
HowToVote,
|
||||||
|
MoneyOff,
|
||||||
|
Description,
|
||||||
|
Settings,
|
||||||
|
VpnLockSharp,
|
||||||
|
} from '@material-ui/icons'
|
||||||
|
import { makeStyles, useTheme } from '@material-ui/styles'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { ADMIN_ADDRESS, ClientContext } from '../context/main'
|
||||||
|
|
||||||
|
const RoutesSchema = () => {
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
label: 'Balance',
|
||||||
|
route: '/balance',
|
||||||
|
Icon: <AccountBalanceWalletRounded />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Send',
|
||||||
|
route: '/send',
|
||||||
|
Icon: <ArrowForward />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Receive',
|
||||||
|
route: '/receive',
|
||||||
|
Icon: <ArrowBack />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bond',
|
||||||
|
route: '/bond',
|
||||||
|
Icon: <AttachMoney />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Unbond',
|
||||||
|
route: '/unbond',
|
||||||
|
Icon: <MoneyOff />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delegate',
|
||||||
|
route: '/delegate',
|
||||||
|
Icon: <HowToVote />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Undelegate',
|
||||||
|
route: '/undelegate',
|
||||||
|
Icon: <Cancel />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SOCKS5',
|
||||||
|
route: '/socks5',
|
||||||
|
Icon: <VpnLockSharp />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV) {
|
||||||
|
routes.push({
|
||||||
|
label: 'Docs',
|
||||||
|
route: '/docs',
|
||||||
|
Icon: <Description />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
navItem: {
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const Nav = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { clientDetails, handleShowAdmin, logOut } = useContext(ClientContext)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{RoutesSchema().map((r, i) => (
|
||||||
|
<ListItem button component={Link} to={r.route} key={i}>
|
||||||
|
<ListItemIcon
|
||||||
|
className={clsx([
|
||||||
|
classes.navItem,
|
||||||
|
location.pathname === r.route ? classes.selected : undefined,
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{r.Icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={r.label}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
className: clsx([
|
||||||
|
classes.navItem,
|
||||||
|
location.pathname === r.route ? classes.selected : undefined,
|
||||||
|
]),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
{clientDetails?.client_address === ADMIN_ADDRESS && (
|
||||||
|
<ListItem button onClick={handleShowAdmin}>
|
||||||
|
<ListItemIcon className={classes.navItem}>
|
||||||
|
<Settings />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Admin"
|
||||||
|
primaryTypographyProps={{
|
||||||
|
className: classes.navItem,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ListItem button onClick={logOut}>
|
||||||
|
<ListItemIcon className={classes.navItem}>
|
||||||
|
<ExitToApp />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Log out"
|
||||||
|
primaryTypographyProps={{
|
||||||
|
className: classes.navItem,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
CardContent,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { ClientContext } from '../context/main'
|
||||||
|
import {
|
||||||
|
ArrowForwardSharp,
|
||||||
|
CheckCircleOutline,
|
||||||
|
FileCopy,
|
||||||
|
PowerSettingsNew,
|
||||||
|
Refresh,
|
||||||
|
} from '@material-ui/icons'
|
||||||
|
import { NymCard } from './NymCard'
|
||||||
|
import { Alert } from '@material-ui/lab'
|
||||||
|
import { handleCopy } from './CopyToClipboard'
|
||||||
|
import { truncate } from '../utils'
|
||||||
|
import { useHistory } from 'react-router'
|
||||||
|
|
||||||
|
export const BalanceCard = () => {
|
||||||
|
const { getBalance } = useContext(ClientContext)
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
useEffect(getBalance.fetchBalance, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: theme.spacing(3) }}>
|
||||||
|
<NymCard
|
||||||
|
title="Balance"
|
||||||
|
subheader="Current wallet balance"
|
||||||
|
noPadding
|
||||||
|
Action={
|
||||||
|
<Tooltip title="Refresh balance">
|
||||||
|
<IconButton onClick={getBalance.fetchBalance} size="small">
|
||||||
|
<Refresh />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
{getBalance.isLoading ? (
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
) : getBalance.error ? (
|
||||||
|
<Alert severity="error" style={{ width: '100%' }}>
|
||||||
|
{getBalance.error}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Typography variant="h6">
|
||||||
|
{getBalance.balance?.printable_balance}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</NymCard>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
enum EnumCopyState {
|
||||||
|
copying,
|
||||||
|
copySuccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddressCard = () => {
|
||||||
|
const { clientDetails } = useContext(ClientContext)
|
||||||
|
|
||||||
|
const [copyState, setCopyState] = useState<EnumCopyState>()
|
||||||
|
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: theme.spacing(3) }}>
|
||||||
|
<NymCard
|
||||||
|
title="Address"
|
||||||
|
subheader="Wallet payments address"
|
||||||
|
noPadding
|
||||||
|
Action={
|
||||||
|
<Tooltip title={!copyState ? 'Copy address' : 'Copied'}>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={!!copyState}
|
||||||
|
onClick={async () => {
|
||||||
|
setCopyState(EnumCopyState.copying)
|
||||||
|
await handleCopy({
|
||||||
|
text: clientDetails?.client_address || '',
|
||||||
|
cb: (isCopied) => {
|
||||||
|
if (isCopied) {
|
||||||
|
setCopyState(EnumCopyState.copySuccess)
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopyState(undefined)
|
||||||
|
}, 2500)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{copyState === EnumCopyState.copying ? (
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
) : copyState === EnumCopyState.copySuccess ? (
|
||||||
|
<CheckCircleOutline
|
||||||
|
style={{ color: theme.palette.success.main }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FileCopy />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
style={{ fontWeight: theme.typography.fontWeightRegular }}
|
||||||
|
>
|
||||||
|
{truncate(clientDetails?.client_address!, 35)}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</NymCard>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SockS5 = () => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
const history = useHistory()
|
||||||
|
const { ss5IsActive, bandwidthLimit, toggleSs5 } = useContext(ClientContext)
|
||||||
|
|
||||||
|
if (bandwidthLimit === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: theme.spacing(3) }}>
|
||||||
|
<NymCard
|
||||||
|
title="Socks5"
|
||||||
|
Icon={
|
||||||
|
<IconButton onClick={toggleSs5}>
|
||||||
|
<PowerSettingsNew
|
||||||
|
style={{
|
||||||
|
color: ss5IsActive
|
||||||
|
? theme.palette.success.main
|
||||||
|
: theme.palette.error.main,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
Action={
|
||||||
|
<Box style={{ marginTop: theme.spacing(1) }}>
|
||||||
|
<IconButton onClick={() => history.push('/socks5')}>
|
||||||
|
<ArrowForwardSharp />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||||
|
|
||||||
|
export const NoClientError = () => {
|
||||||
|
return (
|
||||||
|
<Alert severity="error">
|
||||||
|
<AlertTitle>No client detected</AlertTitle>
|
||||||
|
Have you signed in? Try to go back to{' '}
|
||||||
|
<Link to="/signin">the main page</Link> and try again
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import React from 'react'
|
||||||
|
import { EnumNodeType } from '../types/global'
|
||||||
|
|
||||||
|
export const NodeTypeSelector = ({
|
||||||
|
disabled,
|
||||||
|
nodeType,
|
||||||
|
setNodeType,
|
||||||
|
}: {
|
||||||
|
disabled: boolean
|
||||||
|
nodeType: EnumNodeType
|
||||||
|
setNodeType: (nodeType: EnumNodeType) => void
|
||||||
|
}) => {
|
||||||
|
const handleNodeTypeChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNodeType(e.target.value as EnumNodeType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl component="fieldset">
|
||||||
|
<FormLabel component="legend">Select node type</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
aria-label="nodeType"
|
||||||
|
name="nodeTypeRadio"
|
||||||
|
value={nodeType}
|
||||||
|
onChange={handleNodeTypeChange}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value={EnumNodeType.mixnode}
|
||||||
|
control={<Radio />}
|
||||||
|
label="Mixnode"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value={EnumNodeType.gateway}
|
||||||
|
control={<Radio />}
|
||||||
|
label="Gateway"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Card, CardContent, CardHeader, useTheme } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const NymCard: React.FC<{
|
||||||
|
title: string
|
||||||
|
subheader?: string
|
||||||
|
Action?: React.ReactNode
|
||||||
|
Icon?: React.ReactNode
|
||||||
|
noPadding?: boolean
|
||||||
|
style?: {}
|
||||||
|
}> = ({ title, subheader, Action, noPadding, Icon, style = {}, children }) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Card variant="outlined" style={{ ...style }}>
|
||||||
|
<CardHeader
|
||||||
|
title={title}
|
||||||
|
subheader={subheader}
|
||||||
|
titleTypographyProps={{ variant: 'h5' }}
|
||||||
|
subheaderTypographyProps={{ variant: 'subtitle1' }}
|
||||||
|
action={Action}
|
||||||
|
avatar={Icon}
|
||||||
|
style={{
|
||||||
|
padding: theme.spacing(2.5),
|
||||||
|
borderBottom: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{children && (
|
||||||
|
<CardContent
|
||||||
|
style={{
|
||||||
|
background: theme.palette.grey[50],
|
||||||
|
padding: noPadding ? 0 : theme.spacing(2, 5),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { CircularProgress, Theme } from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
|
||||||
|
export enum EnumRequestStatus {
|
||||||
|
initial = 'initial',
|
||||||
|
error = 'error',
|
||||||
|
loading = 'loading',
|
||||||
|
success = 'success',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequestStatus = ({
|
||||||
|
status,
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
}: {
|
||||||
|
status: EnumRequestStatus
|
||||||
|
Success: React.ReactNode
|
||||||
|
Error: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<div style={{ padding: theme.spacing(3, 5) }}>
|
||||||
|
{status === EnumRequestStatus.loading && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress size={48} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === EnumRequestStatus.success && Success}
|
||||||
|
{status === EnumRequestStatus.error && Error}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export * from './AdminForm'
|
||||||
|
export * from './Error'
|
||||||
|
export * from './Confirmation'
|
||||||
|
export * from './CopyToClipboard'
|
||||||
|
export * from './NymCard'
|
||||||
|
export * from './Nav'
|
||||||
|
export * from './NavigationCards'
|
||||||
|
export * from './NodeTypeSelector'
|
||||||
|
export * from './RequestStatus'
|
||||||
|
export * from './NoClientError'
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { createContext, useEffect, useState } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import { TClientDetails, TSignInWithMnemonic } from '../types'
|
||||||
|
import { TUseGetBalance, useGetBalance } from '../hooks/useGetBalance'
|
||||||
|
import { Button } from '@material-ui/core'
|
||||||
|
import { theme } from '../theme'
|
||||||
|
|
||||||
|
export const ADMIN_ADDRESS = 'punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0'
|
||||||
|
|
||||||
|
type TClientContext = {
|
||||||
|
clientDetails?: TClientDetails
|
||||||
|
getBalance: TUseGetBalance
|
||||||
|
showAdmin: boolean
|
||||||
|
ss5IsActive: boolean
|
||||||
|
bandwidthLimit: number
|
||||||
|
bandwidthUsed: number
|
||||||
|
handleSetBandwidthLimit: (bandwidth: number) => void
|
||||||
|
toggleSs5: () => void
|
||||||
|
handleShowAdmin: () => void
|
||||||
|
logIn: (clientDetails: TSignInWithMnemonic) => void
|
||||||
|
logOut: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ClientContext = createContext({} as TClientContext)
|
||||||
|
|
||||||
|
export const ClientContextProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const [clientDetails, setClientDetails] = useState<TClientDetails>()
|
||||||
|
const [showAdmin, setShowAdmin] = useState(false)
|
||||||
|
const [ss5IsActive, setss5IsActive] = useState(false)
|
||||||
|
const [bandwidthLimit, setBandwidthLimit] = useState(0)
|
||||||
|
const [bandwidthUsed, setBandwidthUsed] = useState(0)
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
const getBalance = useGetBalance()
|
||||||
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
!clientDetails ? history.push('/signin') : history.push('/balance')
|
||||||
|
}, [clientDetails])
|
||||||
|
|
||||||
|
const handleSetBandwidthLimit = (bandwidth: number) =>
|
||||||
|
setBandwidthLimit(bandwidth)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer
|
||||||
|
|
||||||
|
if (ss5IsActive && bandwidthUsed < bandwidthLimit) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
setBandwidthUsed((used) => used + 50)
|
||||||
|
}, 1000)
|
||||||
|
} else if (ss5IsActive && bandwidthUsed === bandwidthLimit) {
|
||||||
|
setBandwidthLimit(0)
|
||||||
|
setBandwidthUsed(0)
|
||||||
|
setss5IsActive(false)
|
||||||
|
enqueueSnackbar(
|
||||||
|
"You're out of bandwidth. You'll need to purchase more to continue using Socks5",
|
||||||
|
{
|
||||||
|
variant: 'error',
|
||||||
|
anchorOrigin: { horizontal: 'center', vertical: 'bottom' },
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
}}
|
||||||
|
onClick={() => closeSnackbar(key)}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [ss5IsActive, bandwidthUsed, bandwidthLimit, handleSetBandwidthLimit])
|
||||||
|
|
||||||
|
const logIn = async (clientDetails: TSignInWithMnemonic) =>
|
||||||
|
setClientDetails(clientDetails)
|
||||||
|
|
||||||
|
const logOut = () => setClientDetails(undefined)
|
||||||
|
|
||||||
|
const handleShowAdmin = () => setShowAdmin((show) => !show)
|
||||||
|
|
||||||
|
const toggleSs5 = () => setss5IsActive((active) => !active)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider
|
||||||
|
value={{
|
||||||
|
clientDetails,
|
||||||
|
getBalance,
|
||||||
|
showAdmin,
|
||||||
|
ss5IsActive,
|
||||||
|
bandwidthLimit,
|
||||||
|
bandwidthUsed,
|
||||||
|
toggleSs5,
|
||||||
|
handleSetBandwidthLimit,
|
||||||
|
handleShowAdmin,
|
||||||
|
logIn,
|
||||||
|
logOut,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { checkGatewayOwnership, checkMixnodeOwnership } from '../requests'
|
||||||
|
import { EnumNodeType, TNodeOwnership } from '../types'
|
||||||
|
|
||||||
|
export const useCheckOwnership = () => {
|
||||||
|
const [ownership, setOwnership] = useState<TNodeOwnership>({
|
||||||
|
hasOwnership: false,
|
||||||
|
nodeType: undefined,
|
||||||
|
})
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
const checkOwnership = async () => {
|
||||||
|
const status = {} as TNodeOwnership
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ownsMixnode = await checkMixnodeOwnership()
|
||||||
|
const ownsGateway = await checkGatewayOwnership()
|
||||||
|
|
||||||
|
if (ownsMixnode) {
|
||||||
|
status.hasOwnership = true
|
||||||
|
status.nodeType = EnumNodeType.mixnode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownsGateway) {
|
||||||
|
status.hasOwnership = true
|
||||||
|
status.nodeType = EnumNodeType.gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
setOwnership(status)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isLoading, error, ownership, checkOwnership }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { invoke } from '@tauri-apps/api'
|
||||||
|
import { Balance } from '../types'
|
||||||
|
|
||||||
|
export type TUseGetBalance = {
|
||||||
|
error?: string
|
||||||
|
balance?: Balance
|
||||||
|
isLoading: boolean
|
||||||
|
fetchBalance: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetBalance = (): TUseGetBalance => {
|
||||||
|
const [balance, setBalance] = useState<Balance>()
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchBalance = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(undefined)
|
||||||
|
invoke('get_balance')
|
||||||
|
.then((balance) => {
|
||||||
|
setBalance(balance as Balance)
|
||||||
|
})
|
||||||
|
.catch(setError)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
balance,
|
||||||
|
fetchBalance,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 5389.9 5389.9" style="enable-background:new 0 0 5389.9 5389.9;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#121726;}
|
||||||
|
.st1{fill:url(#SVGID_1_);}
|
||||||
|
.st2{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<circle class="st0" cx="2695" cy="2695" r="2585"/>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0" y1="8058.165" x2="5390" y2="8058.165" gradientTransform="matrix(1 0 0 -1 0 10753.165)">
|
||||||
|
<stop offset="0" style="stop-color:#F77846"/>
|
||||||
|
<stop offset="1" style="stop-color:#ED3572"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path class="st1" d="M2695,5390c-182.8,0-365.5-18.4-543-54.8c-173.1-35.4-343.3-88.3-506-157.1
|
||||||
|
c-159.7-67.6-313.7-151.2-457.8-248.5c-142.7-96.4-276.8-207.1-398.8-329c-121.9-121.9-232.6-256.1-329-398.8
|
||||||
|
C363,4057.8,279.4,3903.8,211.8,3744C143,3581.4,90.2,3411.1,54.8,3238C18.4,3060.5,0,2877.8,0,2695c0-182.8,18.4-365.5,54.8-543
|
||||||
|
c35.4-173.1,88.3-343.3,157.1-506c67.6-159.7,151.2-313.7,248.5-457.8c96.4-142.7,207.1-276.8,329-398.8s256.1-232.6,398.8-329
|
||||||
|
c144.1-97.3,298.1-180.9,457.8-248.5c162.7-68.8,332.9-121.7,506-157.1C2329.5,18.4,2512.2,0,2695,0c182.8,0,365.5,18.4,543,54.8
|
||||||
|
c173.1,35.4,343.3,88.3,506,157.1c159.7,67.6,313.7,151.2,457.8,248.5c142.7,96.4,276.8,207.1,398.8,329
|
||||||
|
c121.9,121.9,232.6,256.1,329,398.8c97.3,144.1,180.9,298.1,248.5,457.8c68.8,162.7,121.7,332.9,157.1,506
|
||||||
|
c36.3,177.5,54.8,360.2,54.8,543c0,182.8-18.4,365.5-54.8,543c-35.4,173.1-88.3,343.3-157.1,506
|
||||||
|
c-67.6,159.7-151.2,313.7-248.5,457.8c-96.4,142.7-207.1,276.8-329,398.8c-121.9,121.9-256.1,232.6-398.8,329
|
||||||
|
c-144.1,97.3-298.1,180.9-457.8,248.5c-162.7,68.8-332.9,121.7-506,157.1C3060.5,5371.6,2877.8,5390,2695,5390z M2695,220
|
||||||
|
c-168,0-335.9,16.9-498.9,50.3c-158.9,32.5-315.1,81-464.4,144.2c-146.6,62-288.1,138.8-420.4,228.2
|
||||||
|
c-131.1,88.6-254.3,190.3-366.4,302.3c-112,112-213.7,235.3-302.3,366.4c-89.4,132.3-166.2,273.7-228.2,420.4
|
||||||
|
c-63.2,149.3-111.7,305.6-144.2,464.4C236.9,2359.1,220,2527,220,2695s16.9,335.9,50.3,498.9c32.5,158.9,81,315.1,144.2,464.4
|
||||||
|
c62,146.6,138.8,288.1,228.2,420.4c88.6,131.1,190.3,254.3,302.3,366.4c112,112,235.3,213.7,366.4,302.3
|
||||||
|
c132.3,89.4,273.7,166.2,420.4,228.2c149.3,63.2,305.6,111.7,464.4,144.2c163.1,33.4,330.9,50.3,498.9,50.3s335.9-16.9,498.9-50.3
|
||||||
|
c158.9-32.5,315.1-81,464.4-144.2c146.6-62,288.1-138.8,420.4-228.2c131.1-88.6,254.3-190.3,366.4-302.3
|
||||||
|
c112-112,213.7-235.3,302.3-366.4c89.4-132.3,166.2-273.7,228.2-420.4c63.2-149.3,111.7-305.6,144.2-464.4
|
||||||
|
c33.4-163.1,50.3-330.9,50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4c-62-146.6-138.8-288.1-228.2-420.4
|
||||||
|
c-88.6-131.1-190.3-254.3-302.3-366.4c-112-112-235.3-213.7-366.4-302.3c-132.3-89.4-273.7-166.2-420.4-228.2
|
||||||
|
c-149.3-63.2-305.6-111.7-464.4-144.2C3030.9,236.9,2863,220,2695,220z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path class="st2" d="M1958.5,3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8,725.1v-725.1h211.9V3160.4z M4378.9,2229.5
|
||||||
|
l-335.7,330.9l-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4,340.6c26.7,26.3,69.6,26.3,96.3,0l345.4-340.6v725.3h218.4v-930.9
|
||||||
|
H4378.9z M2589.1,2715.4v445h218.4v-445l502.7-485.9H3034l-335.9,330.9l-335.7-330.9h-276.2L2589.1,2715.4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 349 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
declare module '*.jpg'
|
||||||
|
declare module '*.png'
|
||||||
|
declare module '*.svg'
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
|
import { CssBaseline, ThemeProvider } from '@material-ui/core'
|
||||||
|
import { Routes } from './routes'
|
||||||
|
import { theme } from './theme'
|
||||||
|
import { ClientContext, ClientContextProvider } from './context/main'
|
||||||
|
import { ApplicationLayout } from './layouts'
|
||||||
|
import { SignIn } from './routes/sign-in'
|
||||||
|
import { Admin, ErrorFallback } from './components'
|
||||||
|
import { SnackbarProvider } from 'notistack'
|
||||||
|
|
||||||
|
const Pages = () => {
|
||||||
|
const { clientDetails } = useContext(ClientContext)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!clientDetails ? (
|
||||||
|
<SignIn />
|
||||||
|
) : (
|
||||||
|
<ApplicationLayout>
|
||||||
|
<>
|
||||||
|
<Admin />
|
||||||
|
<Routes />
|
||||||
|
</>
|
||||||
|
</ApplicationLayout>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<SnackbarProvider maxSnack={3}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Router>
|
||||||
|
<ClientContextProvider>
|
||||||
|
<Pages />
|
||||||
|
</ClientContextProvider>
|
||||||
|
</Router>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, root)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Divider } from '@material-ui/core'
|
||||||
|
import { AddressCard, BalanceCard, Nav, SockS5 } from '../components'
|
||||||
|
import Logo from '../images/logo-background.svg'
|
||||||
|
import { theme } from '../theme'
|
||||||
|
|
||||||
|
export const ApplicationLayout = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactElement
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '400px auto',
|
||||||
|
gridTemplateRows: '100%',
|
||||||
|
gridColumnGap: '8px',
|
||||||
|
gridRowGap: '0px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gridArea: '1 / 1 / 2 / 2',
|
||||||
|
background: '#121726',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: theme.spacing(6),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={Logo} style={{ width: 75 }} />
|
||||||
|
</div>
|
||||||
|
<Divider
|
||||||
|
light
|
||||||
|
variant="middle"
|
||||||
|
style={{
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
marginTop: theme.spacing(6),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: theme.spacing(10) }}>
|
||||||
|
<BalanceCard />
|
||||||
|
<AddressCard />
|
||||||
|
<SockS5 />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: theme.spacing(7) }}>
|
||||||
|
<Nav />
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gridArea: '1 / 2 / 2 / 3',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Grid, Theme, useTheme } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const Layout = ({ children }: { children: React.ReactElement }) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: theme.spacing(5),
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container justifyContent="center" style={{ margin: '0 auto' }}>
|
||||||
|
<Grid item xs={12} md={8}>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AppLayout'
|
||||||
|
export * from './ContentLayout'
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api'
|
||||||
|
import {
|
||||||
|
Balance,
|
||||||
|
Coin,
|
||||||
|
DelegationResult,
|
||||||
|
EnumNodeType,
|
||||||
|
Gateway,
|
||||||
|
MixNode,
|
||||||
|
Operation,
|
||||||
|
TauriStateParams,
|
||||||
|
TauriTxResult,
|
||||||
|
TCreateAccount,
|
||||||
|
TSignInWithMnemonic,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
|
export const createAccount = async (): Promise<TCreateAccount> =>
|
||||||
|
await invoke('create_new_account')
|
||||||
|
|
||||||
|
export const signInWithMnemonic = async (
|
||||||
|
mnemonic: string
|
||||||
|
): Promise<TSignInWithMnemonic> =>
|
||||||
|
await invoke('connect_with_mnemonic', { mnemonic })
|
||||||
|
|
||||||
|
export const minorToMajor = async (amount: string): Promise<Coin> =>
|
||||||
|
await invoke('minor_to_major', { amount })
|
||||||
|
|
||||||
|
export const majorToMinor = async (amount: string): Promise<Coin> =>
|
||||||
|
await invoke('major_to_minor', { amount })
|
||||||
|
|
||||||
|
export const getGasFee = async (operation: Operation): Promise<Coin> =>
|
||||||
|
await invoke('get_fee', { operation })
|
||||||
|
|
||||||
|
export const delegate = async ({
|
||||||
|
type,
|
||||||
|
identity,
|
||||||
|
amount,
|
||||||
|
}: {
|
||||||
|
type: EnumNodeType
|
||||||
|
identity: string
|
||||||
|
amount: Coin
|
||||||
|
}): Promise<DelegationResult> =>
|
||||||
|
await invoke(`delegate_to_${type}`, { identity, amount })
|
||||||
|
|
||||||
|
export const undelegate = async ({
|
||||||
|
type,
|
||||||
|
identity,
|
||||||
|
}: {
|
||||||
|
type: EnumNodeType
|
||||||
|
identity: string
|
||||||
|
}): Promise<DelegationResult> =>
|
||||||
|
await invoke(`undelegate_from_${type}`, { identity })
|
||||||
|
|
||||||
|
export const send = async (args: {
|
||||||
|
amount: Coin
|
||||||
|
address: string
|
||||||
|
memo: string
|
||||||
|
}): Promise<TauriTxResult> => await invoke('send', args)
|
||||||
|
export const checkMixnodeOwnership = async (): Promise<boolean> =>
|
||||||
|
await invoke('owns_mixnode')
|
||||||
|
|
||||||
|
export const checkGatewayOwnership = async (): Promise<boolean> =>
|
||||||
|
await invoke('owns_gateway')
|
||||||
|
|
||||||
|
export const bond = async ({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
amount,
|
||||||
|
}: {
|
||||||
|
type: EnumNodeType
|
||||||
|
data: MixNode | Gateway
|
||||||
|
amount: Coin
|
||||||
|
}): Promise<any> => await invoke(`bond_${type}`, { [type]: data, bond: amount })
|
||||||
|
|
||||||
|
export const unbond = async (type: EnumNodeType) =>
|
||||||
|
await invoke(`unbond_${type}`)
|
||||||
|
|
||||||
|
export const getBalance = async (): Promise<Balance> =>
|
||||||
|
await invoke('get_balance')
|
||||||
|
|
||||||
|
export const getContractParams = async (): Promise<TauriStateParams> =>
|
||||||
|
await invoke('get_state_params')
|
||||||
|
|
||||||
|
export const setContractParams = async (
|
||||||
|
params: TauriStateParams
|
||||||
|
): Promise<TauriStateParams> => await invoke('update_state_params', { params })
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Layout } from '../layouts'
|
||||||
|
|
||||||
|
export const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<>
|
||||||
|
<h1>404</h1>
|
||||||
|
</>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import {
|
||||||
|
Cancel,
|
||||||
|
CheckCircle,
|
||||||
|
PowerSettingsNew,
|
||||||
|
PowerSettingsNewSharp,
|
||||||
|
SecuritySharp,
|
||||||
|
} from '@material-ui/icons'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
|
||||||
|
const ActiveChip = () => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
label="Secure"
|
||||||
|
style={{
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
backgroundColor: theme.palette.success.main,
|
||||||
|
}}
|
||||||
|
icon={<CheckCircle style={{ color: theme.palette.common.white }} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InactiveChip = () => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
label="Offline"
|
||||||
|
style={{
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
backgroundColor: theme.palette.error.main,
|
||||||
|
}}
|
||||||
|
icon={<Cancel style={{ color: theme.palette.common.white }} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TopCard: React.FC<{
|
||||||
|
isActive: boolean
|
||||||
|
disabled: boolean
|
||||||
|
plan: string
|
||||||
|
toggleIsActive: () => void
|
||||||
|
}> = ({ isActive, disabled, plan, toggleIsActive }) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Card style={{ padding: theme.spacing(1.5) }} variant="outlined">
|
||||||
|
<CardHeader
|
||||||
|
title={<Typography variant="h5">Package: {plan}</Typography>}
|
||||||
|
avatar={isActive ? <ActiveChip /> : <InactiveChip />}
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleIsActive}
|
||||||
|
disabled={disabled}
|
||||||
|
style={
|
||||||
|
!disabled
|
||||||
|
? {
|
||||||
|
color: isActive
|
||||||
|
? theme.palette.success.main
|
||||||
|
: theme.palette.error.main,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PowerSettingsNew />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainCard: React.FC<{
|
||||||
|
isActive: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
buyBandwidth: () => void
|
||||||
|
toggleIsActive: () => void
|
||||||
|
}> = ({ isActive, disabled, buyBandwidth, toggleIsActive }) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%' }}>
|
||||||
|
<Card variant={'outlined'} style={{ padding: theme.spacing(2) }}>
|
||||||
|
<CardHeader
|
||||||
|
title={<Typography> SOCKS5</Typography>}
|
||||||
|
subheader={
|
||||||
|
isActive
|
||||||
|
? "You're protected with SOCKS5"
|
||||||
|
: 'SOCKS5 is not currently active'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="flex-end">
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
endIcon={<SecuritySharp />}
|
||||||
|
style={{
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
marginRight: theme.spacing(1.5),
|
||||||
|
}}
|
||||||
|
size="large"
|
||||||
|
disableElevation
|
||||||
|
onClick={buyBandwidth}
|
||||||
|
>
|
||||||
|
Puchase bandwidth
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
endIcon={<PowerSettingsNewSharp />}
|
||||||
|
size="large"
|
||||||
|
disableElevation
|
||||||
|
onClick={toggleIsActive}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{isActive ? 'Disabled' : 'Enable'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { Box, Grid, Theme } from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { NymCard } from '../../components'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
import { MainCard, TopCard } from './Cards'
|
||||||
|
import { InboundCard, LimitCard, OutboundCard } from './DataCards'
|
||||||
|
import { Info } from './Info'
|
||||||
|
|
||||||
|
type TDashboardProps = {
|
||||||
|
plan: string
|
||||||
|
buyBandwidth: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dashboard: React.FC<TDashboardProps> = ({
|
||||||
|
plan,
|
||||||
|
buyBandwidth,
|
||||||
|
}) => {
|
||||||
|
const { ss5IsActive, toggleSs5, bandwidthLimit, bandwidthUsed } =
|
||||||
|
useContext(ClientContext)
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<NymCard
|
||||||
|
title="SOCKS5 Dashboard"
|
||||||
|
subheader="Monitor your SOCKS5 usage"
|
||||||
|
Action={<Info />}
|
||||||
|
>
|
||||||
|
<Box padding={theme.spacing(0.5)}>
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TopCard
|
||||||
|
isActive={ss5IsActive}
|
||||||
|
toggleIsActive={toggleSs5}
|
||||||
|
plan={plan}
|
||||||
|
disabled={bandwidthLimit === bandwidthUsed}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<MainCard
|
||||||
|
isActive={ss5IsActive}
|
||||||
|
toggleIsActive={toggleSs5}
|
||||||
|
disabled={bandwidthLimit === bandwidthUsed}
|
||||||
|
buyBandwidth={buyBandwidth}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<OutboundCard isActive={ss5IsActive} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<InboundCard isActive={ss5IsActive} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<LimitCard isActive={ss5IsActive} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</NymCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CircularProgress,
|
||||||
|
Grid,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { ToggleData } from './Toggle'
|
||||||
|
import { ArrowDownwardOutlined, ArrowUpwardOutlined } from '@material-ui/icons'
|
||||||
|
import { makeStyles } from '@material-ui/styles'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
card: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
height: 250,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fontSize: 60,
|
||||||
|
},
|
||||||
|
iconActive: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
iconInactive: {
|
||||||
|
color: theme.palette.grey[800],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const OutboundCard: React.FC<{ isActive?: boolean }> = ({
|
||||||
|
isActive,
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
return (
|
||||||
|
<Card className={classes.card} variant="outlined">
|
||||||
|
<CardHeader title="Outbound" action={<ToggleData />} />
|
||||||
|
<CardContent>
|
||||||
|
<Grid container direction="column" alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<ArrowUpwardOutlined
|
||||||
|
className={clsx(
|
||||||
|
classes.icon,
|
||||||
|
isActive ? classes.iconActive : classes.iconInactive
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{!isActive ? (
|
||||||
|
<Typography variant="h3">-</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Typography variant="h3">
|
||||||
|
298
|
||||||
|
<Typography component="span" color="textSecondary">
|
||||||
|
mb
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InboundCard: React.FC<{ isActive?: boolean }> = ({ isActive }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { bandwidthUsed } = useContext(ClientContext)
|
||||||
|
return (
|
||||||
|
<Card className={classes.card} variant="outlined">
|
||||||
|
<CardHeader title="Inbound" action={<ToggleData />} />
|
||||||
|
<CardContent>
|
||||||
|
<Grid container direction="column" alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<ArrowDownwardOutlined
|
||||||
|
className={clsx(
|
||||||
|
classes.icon,
|
||||||
|
isActive ? classes.iconActive : classes.iconInactive
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{!isActive ? (
|
||||||
|
<Typography variant="h3">-</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Typography variant="h3">
|
||||||
|
{bandwidthUsed}
|
||||||
|
<Typography component="span" color="textSecondary">
|
||||||
|
mb
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LimitCard: React.FC<{ isActive: boolean }> = ({ isActive }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { bandwidthLimit, bandwidthUsed } = useContext(ClientContext)
|
||||||
|
return (
|
||||||
|
<Card className={classes.card} variant="outlined">
|
||||||
|
<CardHeader title="Usage" action={<ToggleData />} />
|
||||||
|
<Grid container direction="column" alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||||
|
<CircularProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={100}
|
||||||
|
size={120}
|
||||||
|
style={{
|
||||||
|
color: isActive ? '#eee' : '#aaa',
|
||||||
|
transition: 'color 0.5s ease-in-out',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<CircularProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={!isActive ? 0 : (bandwidthUsed / bandwidthLimit) * 100}
|
||||||
|
size={120}
|
||||||
|
style={{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{!isActive ? (
|
||||||
|
<Typography variant="h3">-</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Typography variant="h5">{bandwidthLimit}</Typography>
|
||||||
|
<Typography variant="caption" color="textSecondary">
|
||||||
|
mb
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Box, IconButton, Popover, Theme, Typography } from '@material-ui/core'
|
||||||
|
import { HelpOutlineSharp } from '@material-ui/icons'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
|
||||||
|
export const Info = () => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>()
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(() => event.currentTarget)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HelpOutlineSharp />
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box padding={theme.spacing(0.5)} maxWidth="400px">
|
||||||
|
<Typography variant="h6" style={{ marginBottom: theme.spacing(1) }}>
|
||||||
|
What is SOCKS5?
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
A SOCKS5 proxy is a private alternative to a VPN that protects the
|
||||||
|
traffic within a specific source, such as an application. When you
|
||||||
|
use a SOCKS5 proxy, data packets from the configured source are
|
||||||
|
routed through a remote server. This server changes the IP address
|
||||||
|
associated with these data packets before they reach their final
|
||||||
|
destination
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Chip,
|
||||||
|
Grid,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { SecuritySharp } from '@material-ui/icons'
|
||||||
|
import { Autocomplete } from '@material-ui/lab'
|
||||||
|
import { NymCard } from '../../components'
|
||||||
|
import { Info } from './Info'
|
||||||
|
|
||||||
|
type TSetupProps = {
|
||||||
|
handleSelectPlan: (plan: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Setup: React.FC<TSetupProps> = ({ handleSelectPlan }) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
const [userSelection, setUserSelection] = useState<string | null>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NymCard
|
||||||
|
title="SOCKS5 - Purchase bandwidth"
|
||||||
|
subheader="Purchase badwidth to get started with SOCKS5"
|
||||||
|
Action={<Info />}
|
||||||
|
>
|
||||||
|
<Box padding={theme.spacing(0.5)}>
|
||||||
|
<Grid container direction="column" alignItems="center" spacing={5}>
|
||||||
|
<Grid item container spacing={3} justifyContent="space-evenly">
|
||||||
|
<Grid item xs={12} lg={3}>
|
||||||
|
<OptionCard
|
||||||
|
title="500MB"
|
||||||
|
cost="500 PUNK"
|
||||||
|
onSelect={handleSelectPlan}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} lg={3}>
|
||||||
|
<OptionCard
|
||||||
|
title="1GB"
|
||||||
|
cost="750 PUNK"
|
||||||
|
onSelect={handleSelectPlan}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} lg={3}>
|
||||||
|
<OptionCard
|
||||||
|
title="10GB"
|
||||||
|
cost="7000 PUNK"
|
||||||
|
onSelect={handleSelectPlan}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h5">- OR -</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
item
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
style={{ margin: 'auto' }}
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Autocomplete
|
||||||
|
disablePortal
|
||||||
|
onChange={(_, val: string | null) => setUserSelection(val)}
|
||||||
|
style={{ width: 500 }}
|
||||||
|
id="bandwidth-options"
|
||||||
|
options={[
|
||||||
|
'1MB',
|
||||||
|
'25MB',
|
||||||
|
'50MB',
|
||||||
|
'100MB',
|
||||||
|
'500MB',
|
||||||
|
'1GB',
|
||||||
|
'5GB',
|
||||||
|
'10GB',
|
||||||
|
'20GB',
|
||||||
|
'50GB',
|
||||||
|
]}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
variant="outlined"
|
||||||
|
label="Other options"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
userSelection ? handleSelectPlan(userSelection) : undefined
|
||||||
|
}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
disabled={!userSelection}
|
||||||
|
style={{ marginLeft: theme.spacing(1) }}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</NymCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TOptionProps = {
|
||||||
|
title: string
|
||||||
|
cost: string
|
||||||
|
isPrimary?: boolean
|
||||||
|
onSelect: TSetupProps['handleSelectPlan']
|
||||||
|
}
|
||||||
|
const OptionCard: React.FC<TOptionProps> = ({
|
||||||
|
title,
|
||||||
|
cost,
|
||||||
|
isPrimary,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
variant="outlined"
|
||||||
|
style={{ padding: theme.spacing(2), position: 'relative', width: 300 }}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
title={
|
||||||
|
<Box display="flex" alignItems="end" justifyContent="flex-start">
|
||||||
|
<SecuritySharp
|
||||||
|
style={{ marginRight: theme.spacing(0.5) }}
|
||||||
|
color="action"
|
||||||
|
/>
|
||||||
|
<Typography variant="h5">{title}</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
color="primary"
|
||||||
|
action={<Chip label={cost} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CardActions>
|
||||||
|
<Box display="flex" justifyContent="center" width="100%">
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
variant={isPrimary ? 'contained' : 'outlined'}
|
||||||
|
disableElevation
|
||||||
|
size="small"
|
||||||
|
onClick={() => onSelect(title)}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Grid, Paper, Theme, Typography } from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
|
||||||
|
enum EnumOptions {
|
||||||
|
Mb = 'Mb',
|
||||||
|
Gb = 'Gb',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleOption = ({
|
||||||
|
title,
|
||||||
|
isSelected,
|
||||||
|
setSelected,
|
||||||
|
}: {
|
||||||
|
title: EnumOptions
|
||||||
|
isSelected: boolean
|
||||||
|
setSelected: (selection: EnumOptions) => void
|
||||||
|
}) => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
onClick={() => setSelected(title)}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: isSelected ? theme.palette.grey[900] : theme.palette.grey[500],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleData = () => {
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
const [selected, setSeleted] = useState(EnumOptions['Mb'])
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
style={{
|
||||||
|
width: 75,
|
||||||
|
backgroundColor: theme.palette.grey[100],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={1} justifyContent="center">
|
||||||
|
<Grid item>
|
||||||
|
<ToggleOption
|
||||||
|
title={EnumOptions['Mb']}
|
||||||
|
isSelected={selected === EnumOptions['Mb']}
|
||||||
|
setSelected={(selection) => setSeleted(selection)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<ToggleOption
|
||||||
|
title={EnumOptions['Gb']}
|
||||||
|
isSelected={selected === EnumOptions['Gb']}
|
||||||
|
setSelected={(selection) => setSeleted(selection)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useContext, useState } from 'react'
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import { SecuritySharp } from '@material-ui/icons'
|
||||||
|
import { Dashboard } from './Dashboard'
|
||||||
|
import { Layout } from '../../layouts'
|
||||||
|
import { Setup } from './Setup'
|
||||||
|
import { theme } from '../../theme'
|
||||||
|
import { Loading } from '../../components/Loading'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
|
||||||
|
export const Socks5 = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [plan, setPlan] = useState<string>()
|
||||||
|
|
||||||
|
const { handleSetBandwidthLimit } = useContext(ClientContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<>
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
padding={theme.spacing(1)}
|
||||||
|
>
|
||||||
|
<Loading
|
||||||
|
size="x-large"
|
||||||
|
Icon={<SecuritySharp color="primary" style={{ fontSize: 24 }} />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !!plan && (
|
||||||
|
<Dashboard plan={plan} buyBandwidth={() => setPlan(undefined)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !plan && (
|
||||||
|
<Setup
|
||||||
|
handleSelectPlan={(plan: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setPlan(plan)
|
||||||
|
handleSetBandwidthLimit(500)
|
||||||
|
}, 2000)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { Button, CircularProgress, Grid } from '@material-ui/core'
|
||||||
|
import { Alert } from '@material-ui/lab'
|
||||||
|
import { Refresh } from '@material-ui/icons'
|
||||||
|
import { NymCard } from '../components'
|
||||||
|
import { Layout } from '../layouts'
|
||||||
|
import { theme } from '../theme'
|
||||||
|
import { useGetBalance } from '../hooks/useGetBalance'
|
||||||
|
|
||||||
|
export const Balance = () => {
|
||||||
|
const { balance, isLoading, error, fetchBalance } = useGetBalance()
|
||||||
|
|
||||||
|
useEffect(fetchBalance, [])
|
||||||
|
|
||||||
|
const RefreshAction = () => (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
onClick={fetchBalance}
|
||||||
|
disabled={isLoading}
|
||||||
|
disableElevation
|
||||||
|
startIcon={<Refresh />}
|
||||||
|
endIcon={isLoading && <CircularProgress size={20} />}
|
||||||
|
style={{ marginRight: theme.spacing(2) }}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<NymCard title="Check Balance">
|
||||||
|
<Grid container direction="column" spacing={2}>
|
||||||
|
<Grid item>
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
action={<RefreshAction />}
|
||||||
|
style={{ padding: theme.spacing(2) }}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{!error && (
|
||||||
|
<Alert
|
||||||
|
severity="success"
|
||||||
|
style={{ padding: theme.spacing(2, 3) }}
|
||||||
|
action={<RefreshAction />}
|
||||||
|
>
|
||||||
|
{'The current balance is ' + balance?.printable_balance}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</NymCard>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { Alert } from '@material-ui/lab'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { EnumNodeType } from '../../types/global'
|
||||||
|
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||||
|
import { bond, majorToMinor } from '../../requests'
|
||||||
|
import { validationSchema } from './validationSchema'
|
||||||
|
import { Coin, Gateway, MixNode } from '../../types'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
import { checkHasEnoughFunds } from '../../utils'
|
||||||
|
|
||||||
|
type TBondFormFields = {
|
||||||
|
withAdvancedOptions: boolean
|
||||||
|
nodeType: EnumNodeType
|
||||||
|
identityKey: string
|
||||||
|
sphinxKey: string
|
||||||
|
amount: string
|
||||||
|
host: string
|
||||||
|
version: string
|
||||||
|
location?: string
|
||||||
|
mixPort: number
|
||||||
|
verlocPort: number
|
||||||
|
clientsPort: number
|
||||||
|
httpApiPort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
withAdvancedOptions: false,
|
||||||
|
nodeType: EnumNodeType.mixnode,
|
||||||
|
identityKey: '',
|
||||||
|
sphinxKey: '',
|
||||||
|
amount: '',
|
||||||
|
host: '',
|
||||||
|
version: '',
|
||||||
|
location: undefined,
|
||||||
|
mixPort: 1789,
|
||||||
|
verlocPort: 1790,
|
||||||
|
httpApiPort: 8000,
|
||||||
|
clientsPort: 9000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatData = (data: TBondFormFields) => {
|
||||||
|
const payload: { [key: string]: any } = {
|
||||||
|
identity_key: data.identityKey,
|
||||||
|
sphinx_key: data.sphinxKey,
|
||||||
|
host: data.host,
|
||||||
|
version: data.version,
|
||||||
|
mix_port: data.mixPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.nodeType === EnumNodeType.mixnode) {
|
||||||
|
payload.verloc_port = data.verlocPort
|
||||||
|
payload.http_api_port = data.httpApiPort
|
||||||
|
return payload as MixNode
|
||||||
|
} else {
|
||||||
|
payload.clients_port = data.clientsPort
|
||||||
|
payload.location = data.location
|
||||||
|
return payload as Gateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BondForm = ({
|
||||||
|
disabled,
|
||||||
|
fees,
|
||||||
|
onError,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
disabled: boolean
|
||||||
|
fees?: { [key in EnumNodeType]: Coin }
|
||||||
|
onError: (message?: string) => void
|
||||||
|
onSuccess: (message?: string) => void
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
setError,
|
||||||
|
watch,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<TBondFormFields>({
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
defaultValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getBalance } = useContext(ClientContext)
|
||||||
|
|
||||||
|
const watchNodeType = watch('nodeType', defaultValues.nodeType)
|
||||||
|
const watchAdvancedOptions = watch(
|
||||||
|
'withAdvancedOptions',
|
||||||
|
defaultValues.withAdvancedOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSubmit = async (data: TBondFormFields) => {
|
||||||
|
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
|
||||||
|
if (!hasEnoughFunds) {
|
||||||
|
return setError('amount', { message: 'Not enough funds in wallet' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedData = formatData(data)
|
||||||
|
const amount = await majorToMinor(data.amount)
|
||||||
|
|
||||||
|
await bond({ type: data.nodeType, data: formattedData, amount })
|
||||||
|
.then(() => {
|
||||||
|
getBalance.fetchBalance()
|
||||||
|
onSuccess(`Successfully bonded to ${data.identityKey}`)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
onError(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<div style={{ padding: theme.spacing(3, 5) }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid container item justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<NodeTypeSelector
|
||||||
|
nodeType={watchNodeType}
|
||||||
|
setNodeType={(nodeType) => {
|
||||||
|
setValue('nodeType', nodeType)
|
||||||
|
if (nodeType === EnumNodeType.mixnode)
|
||||||
|
setValue('location', undefined)
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{fees && (
|
||||||
|
<Grid item>
|
||||||
|
<Alert severity="info">
|
||||||
|
{`A fee of ${
|
||||||
|
watchNodeType === EnumNodeType.mixnode
|
||||||
|
? fees.mixnode.amount
|
||||||
|
: fees.gateway.amount
|
||||||
|
} PUNK will apply to this transaction`}
|
||||||
|
</Alert>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('identityKey')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="identityKey"
|
||||||
|
name="identityKey"
|
||||||
|
label="Identity key"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.identityKey}
|
||||||
|
helperText={errors.identityKey?.message}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('sphinxKey')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="sphinxKey"
|
||||||
|
name="sphinxKey"
|
||||||
|
label="Sphinx key"
|
||||||
|
error={!!errors.sphinxKey}
|
||||||
|
helperText={errors.sphinxKey?.message}
|
||||||
|
fullWidth
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={9}>
|
||||||
|
<TextField
|
||||||
|
{...register('amount')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="amount"
|
||||||
|
name="amount"
|
||||||
|
label="Amount to bond"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.amount}
|
||||||
|
helperText={errors.amount?.message}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">punks</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('host')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="host"
|
||||||
|
name="host"
|
||||||
|
label="Host"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.host}
|
||||||
|
helperText={errors.host?.message}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* if it's a gateway - get location */}
|
||||||
|
<Grid item xs={6}>
|
||||||
|
{watchNodeType === EnumNodeType.gateway && (
|
||||||
|
<TextField
|
||||||
|
{...register('location')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="location"
|
||||||
|
name="location"
|
||||||
|
label="Location"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.location}
|
||||||
|
helperText={errors.location?.message}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('version')}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
id="version"
|
||||||
|
name="version"
|
||||||
|
label="Version"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.version}
|
||||||
|
helperText={errors.version?.message}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={watchAdvancedOptions}
|
||||||
|
onChange={() => {
|
||||||
|
if (watchAdvancedOptions) {
|
||||||
|
setValue('mixPort', defaultValues.mixPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
setValue('clientsPort', defaultValues.clientsPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
setValue('verlocPort', defaultValues.verlocPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
setValue('httpApiPort', defaultValues.httpApiPort, {
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
setValue('withAdvancedOptions', false)
|
||||||
|
resizeTo
|
||||||
|
} else {
|
||||||
|
setValue('withAdvancedOptions', true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use advanced options"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{watchAdvancedOptions && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('mixPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="mixPort"
|
||||||
|
name="mixPort"
|
||||||
|
label="Mix Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.mixPort}
|
||||||
|
helperText={
|
||||||
|
errors.mixPort?.message && 'A valid port value is required'
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{watchNodeType === EnumNodeType.mixnode ? (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('verlocPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="verlocPort"
|
||||||
|
name="verlocPort"
|
||||||
|
label="Verloc Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.verlocPort}
|
||||||
|
helperText={
|
||||||
|
errors.verlocPort?.message &&
|
||||||
|
'A valid port value is required'
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('httpApiPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="httpApiPort"
|
||||||
|
name="httpApiPort"
|
||||||
|
label="HTTP API Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.httpApiPort}
|
||||||
|
helperText={
|
||||||
|
errors.httpApiPort?.message &&
|
||||||
|
'A valid port value is required'
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<TextField
|
||||||
|
{...register('clientsPort', { valueAsNumber: true })}
|
||||||
|
variant="outlined"
|
||||||
|
id="clientsPort"
|
||||||
|
name="clientsPort"
|
||||||
|
label="client WS API Port"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.clientsPort}
|
||||||
|
helperText={
|
||||||
|
errors.clientsPort?.message &&
|
||||||
|
'A valid port value is required'
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting || disabled}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
disableElevation
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||||
|
>
|
||||||
|
Bond
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
|
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
|
||||||
|
import { Alert } from '@material-ui/lab'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { BondForm } from './BondForm'
|
||||||
|
import { NymCard } from '../../components'
|
||||||
|
import {
|
||||||
|
EnumRequestStatus,
|
||||||
|
RequestStatus,
|
||||||
|
} from '../../components/RequestStatus'
|
||||||
|
import { Layout } from '../../layouts'
|
||||||
|
import { getGasFee, unbond } from '../../requests'
|
||||||
|
import { TFee } from '../../types'
|
||||||
|
import { useCheckOwnership } from '../../hooks/useCheckOwnership'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
|
||||||
|
export const Bond = () => {
|
||||||
|
const [status, setStatus] = useState(EnumRequestStatus.initial)
|
||||||
|
const [message, setMessage] = useState<string>()
|
||||||
|
const [fees, setFees] = useState<TFee>()
|
||||||
|
|
||||||
|
const { checkOwnership, ownership } = useCheckOwnership()
|
||||||
|
const { getBalance } = useContext(ClientContext)
|
||||||
|
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === EnumRequestStatus.initial) {
|
||||||
|
const initialiseForm = async () => {
|
||||||
|
await checkOwnership()
|
||||||
|
setFees({
|
||||||
|
mixnode: await getGasFee('BondMixnode'),
|
||||||
|
gateway: await getGasFee('BondGateway'),
|
||||||
|
})
|
||||||
|
setStatus(EnumRequestStatus.initial)
|
||||||
|
}
|
||||||
|
initialiseForm()
|
||||||
|
}
|
||||||
|
}, [status])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<NymCard title="Bond" subheader="Bond a node or gateway" noPadding>
|
||||||
|
{ownership?.hasOwnership && (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
disabled={status === EnumRequestStatus.loading}
|
||||||
|
onClick={async () => {
|
||||||
|
setStatus(EnumRequestStatus.loading)
|
||||||
|
await unbond(ownership.nodeType!)
|
||||||
|
getBalance.fetchBalance()
|
||||||
|
setStatus(EnumRequestStatus.initial)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unbond
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
style={{ margin: theme.spacing(2) }}
|
||||||
|
>
|
||||||
|
{`Looks like you already have a ${ownership.nodeType} bonded.`}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{status === EnumRequestStatus.loading && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={48} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{status === EnumRequestStatus.initial && (
|
||||||
|
<BondForm
|
||||||
|
fees={!ownership.hasOwnership ? fees : undefined}
|
||||||
|
onError={(e?: string) => {
|
||||||
|
setMessage(e)
|
||||||
|
setStatus(EnumRequestStatus.error)
|
||||||
|
}}
|
||||||
|
onSuccess={(message?: string) => {
|
||||||
|
setMessage(message)
|
||||||
|
setStatus(EnumRequestStatus.success)
|
||||||
|
}}
|
||||||
|
disabled={ownership?.hasOwnership}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(status === EnumRequestStatus.error ||
|
||||||
|
status === EnumRequestStatus.success) && (
|
||||||
|
<>
|
||||||
|
<RequestStatus
|
||||||
|
status={status}
|
||||||
|
Success={
|
||||||
|
<Alert severity="success">Successfully bonded node</Alert>
|
||||||
|
}
|
||||||
|
Error={
|
||||||
|
<Alert severity="error">
|
||||||
|
An error occurred with the request: {message}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setStatus(EnumRequestStatus.initial)
|
||||||
|
checkOwnership()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Again?
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</NymCard>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import * as Yup from 'yup'
|
||||||
|
import {
|
||||||
|
isValidHostname,
|
||||||
|
validateAmount,
|
||||||
|
validateKey,
|
||||||
|
validateLocation,
|
||||||
|
validateRawPort,
|
||||||
|
validateVersion,
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
export const validationSchema = Yup.object().shape({
|
||||||
|
identityKey: Yup.string()
|
||||||
|
.required('An indentity key is required')
|
||||||
|
.test('valid-id-key', 'A valid identity key is required', function (value) {
|
||||||
|
return validateKey(value || '')
|
||||||
|
}),
|
||||||
|
sphinxKey: Yup.string()
|
||||||
|
.required('A sphinx key is required')
|
||||||
|
.test(
|
||||||
|
'valid-sphinx-key',
|
||||||
|
'A valid sphinx key is required',
|
||||||
|
function (value) {
|
||||||
|
return validateKey(value || '')
|
||||||
|
}
|
||||||
|
),
|
||||||
|
amount: Yup.string()
|
||||||
|
.required('An amount is required')
|
||||||
|
.test(
|
||||||
|
'valid-amount',
|
||||||
|
'A valid amount is required (min 100 punks)',
|
||||||
|
function (value) {
|
||||||
|
return validateAmount(value || '', '100000000')
|
||||||
|
// minimum amount needs to come from the backend - replace when available
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
host: Yup.string()
|
||||||
|
.required('A host is required')
|
||||||
|
.test('valid-host', 'A valid host is required', function (value) {
|
||||||
|
return !!value ? isValidHostname(value) : false
|
||||||
|
}),
|
||||||
|
version: Yup.string()
|
||||||
|
.required('A version is required')
|
||||||
|
.test('valid-version', 'A valid version is required', function (value) {
|
||||||
|
return !!value ? validateVersion(value) : false
|
||||||
|
}),
|
||||||
|
location: Yup.lazy((value) => {
|
||||||
|
if (!!value) {
|
||||||
|
return Yup.string()
|
||||||
|
.required('A location is required')
|
||||||
|
.test(
|
||||||
|
'valid-location',
|
||||||
|
'A valid version is required',
|
||||||
|
function (value) {
|
||||||
|
return !!value ? validateLocation(value) : false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Yup.mixed().notRequired()
|
||||||
|
}),
|
||||||
|
mixPort: Yup.number()
|
||||||
|
.required('A mixport is required')
|
||||||
|
.test('valid-mixport', 'A valid mixport is required', function (value) {
|
||||||
|
return !!value ? validateRawPort(value) : false
|
||||||
|
}),
|
||||||
|
verlocPort: Yup.number()
|
||||||
|
.required('A verloc port is required')
|
||||||
|
.test('valid-verloc', 'A valid verloc port is required', function (value) {
|
||||||
|
return !!value ? validateRawPort(value) : false
|
||||||
|
}),
|
||||||
|
httpApiPort: Yup.number()
|
||||||
|
.required('A http-api port is required')
|
||||||
|
.test('valid-http', 'A valid http-api port is required', function (value) {
|
||||||
|
return !!value ? validateRawPort(value) : false
|
||||||
|
}),
|
||||||
|
clientsPort: Yup.number()
|
||||||
|
.required('A clients port is required')
|
||||||
|
.test(
|
||||||
|
'valid-clients',
|
||||||
|
'A valid clients port is required',
|
||||||
|
function (value) {
|
||||||
|
return !!value ? validateRawPort(value) : false
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import React, { useContext } from 'react'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
useTheme,
|
||||||
|
} from '@material-ui/core'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||||
|
import { EnumNodeType, TFee } from '../../types'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
import { validationSchema } from './validationSchema'
|
||||||
|
import { Alert } from '@material-ui/lab'
|
||||||
|
import { ClientContext } from '../../context/main'
|
||||||
|
import { delegate, majorToMinor } from '../../requests'
|
||||||
|
import { checkHasEnoughFunds } from '../../utils'
|
||||||
|
|
||||||
|
type TDelegateForm = {
|
||||||
|
nodeType: EnumNodeType
|
||||||
|
identity: string
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: TDelegateForm = {
|
||||||
|
nodeType: EnumNodeType.mixnode,
|
||||||
|
identity: '',
|
||||||
|
amount: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DelegateForm = ({
|
||||||
|
fees,
|
||||||
|
onError,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
fees: TFee
|
||||||
|
onError: (message?: string) => void
|
||||||
|
onSuccess: (message?: string) => void
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme<Theme>()
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<TDelegateForm>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchNodeType = watch('nodeType', defaultValues.nodeType)
|
||||||
|
|
||||||
|
const { getBalance } = useContext(ClientContext)
|
||||||
|
|
||||||
|
const onSubmit = async (data: TDelegateForm) => {
|
||||||
|
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
|
||||||
|
if (!hasEnoughFunds) {
|
||||||
|
return setError('amount', {
|
||||||
|
message: 'Not enough funds in wallet',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = await majorToMinor(data.amount)
|
||||||
|
|
||||||
|
await delegate({
|
||||||
|
type: data.nodeType,
|
||||||
|
identity: data.identity,
|
||||||
|
amount,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
onSuccess(
|
||||||
|
`Successfully delegated ${data.amount} punk to ${res.source_address}`
|
||||||
|
)
|
||||||
|
getBalance.fetchBalance()
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
onError(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<div style={{ padding: theme.spacing(3, 5) }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid container item xs={12} justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<NodeTypeSelector
|
||||||
|
nodeType={watchNodeType}
|
||||||
|
setNodeType={(nodeType) => setValue('nodeType', nodeType)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Alert severity="info">
|
||||||
|
{`A fee of ${
|
||||||
|
watchNodeType === EnumNodeType.mixnode
|
||||||
|
? fees.mixnode.amount
|
||||||
|
: fees.gateway.amount
|
||||||
|
} PUNK will apply to this transaction`}
|
||||||
|
</Alert>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
{...register('identity')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="identity"
|
||||||
|
name="identity"
|
||||||
|
label="Node identity"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.identity}
|
||||||
|
helperText={errors?.identity?.message}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} lg={6}>
|
||||||
|
<TextField
|
||||||
|
{...register('amount')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
|
id="amount"
|
||||||
|
name="amount"
|
||||||
|
label="Amount to delegate"
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.amount}
|
||||||
|
helperText={errors?.amount?.message}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">punks</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
disableElevation
|
||||||
|
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||||
|
>
|
||||||
|
Delegate stake
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
|
||||||
|
import { useTheme } from '@material-ui/styles'
|
||||||
|
import { DelegateForm } from './DelegateForm'
|
||||||
|
import { Layout } from '../../layouts'
|
||||||
|
import { NymCard } from '../../components'
|
||||||
|
import {
|
||||||
|
EnumRequestStatus,
|
||||||
|
RequestStatus,
|
||||||
|
} from '../../components/RequestStatus'
|
||||||
|
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||||
|
import { TFee } from '../../types'
|
||||||
|
import { getGasFee } from '../../requests'
|
||||||
|
|
||||||
|
export const Delegate = () => {
|
||||||
|
const [status, setStatus] = useState<EnumRequestStatus>(
|
||||||
|
EnumRequestStatus.initial
|
||||||
|
)
|
||||||
|
const [message, setMessage] = useState<string>()
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [fees, setFees] = useState<TFee>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getFees = async () => {
|
||||||
|
const mixnode = await getGasFee('DelegateToMixnode')
|
||||||
|
const gateway = await getGasFee('DelegateToGateway')
|
||||||
|
setFees({
|
||||||
|
mixnode: mixnode,
|
||||||
|
gateway: gateway,
|
||||||
|
})
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFees()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const theme: Theme = useTheme()
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<NymCard
|
||||||
|
title="Delegate"
|
||||||
|
subheader="Delegate to mixnode or gateway"
|
||||||
|
noPadding
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={48} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<>
|
||||||
|
{status === EnumRequestStatus.initial && fees && (
|
||||||
|
<DelegateForm
|
||||||
|
fees={fees}
|
||||||
|
onError={(message?: string) => {
|
||||||
|
setStatus(EnumRequestStatus.error)
|
||||||
|
setMessage(message)
|
||||||
|
}}
|
||||||
|
onSuccess={(message?: string) => {
|
||||||
|
setStatus(EnumRequestStatus.success)
|
||||||
|
setMessage(message)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{status !== EnumRequestStatus.initial && (
|
||||||
|
<>
|
||||||
|
<RequestStatus
|
||||||
|
status={status}
|
||||||
|
Error={
|
||||||
|
<Alert severity="error">
|
||||||
|
An error occurred with the request: {message}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
Success={
|
||||||
|
<Alert severity="success">
|
||||||
|
<AlertTitle>Delegation complete</AlertTitle>
|
||||||
|
{message}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||||
|
background: theme.palette.grey[100],
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setStatus(EnumRequestStatus.initial)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</NymCard>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import * as Yup from 'yup'
|
||||||
|
import { validateAmount, validateKey } from '../../utils'
|
||||||
|
|
||||||
|
export const validationSchema = Yup.object().shape({
|
||||||
|
identity: Yup.string()
|
||||||
|
.required()
|
||||||
|
.test(
|
||||||
|
'valid-id-key',
|
||||||
|
'A valid identity key is required e.g. 824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z',
|
||||||
|
(value) => (!!value ? validateKey(value) : false)
|
||||||
|
),
|
||||||
|
amount: Yup.string()
|
||||||
|
.required()
|
||||||
|
.test('valid-amount-key', 'A valid amount is required', (value) =>
|
||||||
|
!!value ? validateAmount(value, '0') : false
|
||||||
|
),
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Switch, Route } from 'react-router-dom'
|
||||||
|
import { NotFound } from './404'
|
||||||
|
import { Balance } from './balance'
|
||||||
|
import { Bond } from './bond'
|
||||||
|
import { Delegate } from './delegate'
|
||||||
|
import { Receive } from './receive'
|
||||||
|
import { Send } from './send'
|
||||||
|
import { SignIn } from './sign-in'
|
||||||
|
import { Unbond } from './unbond'
|
||||||
|
import { Undelegate } from './undelegate'
|
||||||
|
import { InternalDocs } from './internal-docs'
|
||||||
|
import { Socks5 } from './SOCKS5'
|
||||||
|
|
||||||
|
export const Routes = () => (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/signin">
|
||||||
|
<SignIn />
|
||||||
|
</Route>
|
||||||
|
<Route path="/balance">
|
||||||
|
<Balance />
|
||||||
|
</Route>
|
||||||
|
<Route path="/send">
|
||||||
|
<Send />
|
||||||
|
</Route>
|
||||||
|
<Route path="/receive">
|
||||||
|
<Receive />
|
||||||
|
</Route>
|
||||||
|
<Route path="/bond">
|
||||||
|
<Bond />
|
||||||
|
</Route>
|
||||||
|
<Route path="/unbond">
|
||||||
|
<Unbond />
|
||||||
|
</Route>
|
||||||
|
<Route path="/delegate">
|
||||||
|
<Delegate />
|
||||||
|
</Route>
|
||||||
|
<Route path="/undelegate">
|
||||||
|
<Undelegate />
|
||||||
|
</Route>
|
||||||
|
<Route path="/socks5">
|
||||||
|
<Socks5 />
|
||||||
|
</Route>
|
||||||
|
<Route path="/docs">
|
||||||
|
<InternalDocs />
|
||||||
|
</Route>
|
||||||
|
<Route path="*">
|
||||||
|
<NotFound />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||