Compare commits

..

9 Commits

Author SHA1 Message Date
tommy a6ac1cf5e7 one more removal 2022-08-08 15:20:14 +02:00
tommy 0ba2a3b724 update text to reflect the correct opts args 2022-08-08 15:18:54 +02:00
tommy 96d5fd7612 update the version 2022-08-08 15:15:52 +02:00
tommy a1b7b43da2 Resolve PR comments 2022-08-08 15:11:24 +02:00
tommy 123cbf7aff add change log entry 2022-08-05 15:42:40 +02:00
tommy c4d12e7a8e cargo fmt - and the project to cargo.toml 2022-08-05 15:34:24 +02:00
tommy bf22a13b8f Revert "Validator Command Binary - Draft"
This reverts commit 3429e0a8e9.
2022-08-05 12:19:12 +02:00
tommy 8f5dd00027 minor changes 2022-08-04 13:41:53 +02:00
tommy 84c43ebf54 Draft - Validator CLI
- validator  binary - to enable easy to use commands on the network
- contains all operations (vesting / normal)
2022-08-04 13:38:59 +02:00
162 changed files with 2396 additions and 3665 deletions
-36
View File
@@ -1,36 +0,0 @@
name: Daily security audit
on: workflow_dispatch
jobs:
security_audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
notification:
if: ${{ failure() }}
needs: security_audit
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym daily audit"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBTECH_TEAM }}"
KEYBASE_NYM_CHANNEL: "test"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+1 -4
View File
@@ -25,6 +25,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator-client: add `denom` argument and add simple test for querying an account balance - validator-client: add `denom` argument and add simple test for querying an account balance
- gateway, validator-api: Checks for coconut credential double spending attempts, taking the coconut bandwidth contract as source of truth ([#1457]) - gateway, validator-api: Checks for coconut credential double spending attempts, taking the coconut bandwidth contract as source of truth ([#1457])
- coconut-bandwidth-contract: Record the state of a coconut credential; create specific proposal for releasing funds ([#1457]) - coconut-bandwidth-contract: Record the state of a coconut credential; create specific proposal for releasing funds ([#1457])
- add validator-client script rust binary /tools/validator-client-scripts
### Fixed ### Fixed
@@ -37,7 +38,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329]) - validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]). - explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
- network-requester: fix filter for suffix-only domains ([#1487]) - network-requester: fix filter for suffix-only domains ([#1487])
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
### Changed ### Changed
@@ -52,7 +52,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- multisig-contract: Limit the proposal creating functionality to one address (coconut-bandwidth-contract address) ([#1457]) - multisig-contract: Limit the proposal creating functionality to one address (coconut-bandwidth-contract address) ([#1457])
- All binaries and cosmwasm blobs are configured at runtime now; binaries are configured using environment variables or .env files and contracts keep the configuration parameters in storage ([#1463]) - All binaries and cosmwasm blobs are configured at runtime now; binaries are configured using environment variables or .env files and contracts keep the configuration parameters in storage ([#1463])
- gateway, network-statistics: include gateway id in the sent statistical data ([#1478]) - gateway, network-statistics: include gateway id in the sent statistical data ([#1478])
- network explorer: tweak how active set probability is shown ([#1503])
[#1249]: https://github.com/nymtech/nym/pull/1249 [#1249]: https://github.com/nymtech/nym/pull/1249
@@ -79,8 +78,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1478]: https://github.com/nymtech/nym/pull/1478 [#1478]: https://github.com/nymtech/nym/pull/1478
[#1482]: https://github.com/nymtech/nym/pull/1482 [#1482]: https://github.com/nymtech/nym/pull/1482
[#1487]: https://github.com/nymtech/nym/pull/1487 [#1487]: https://github.com/nymtech/nym/pull/1487
[#1496]: https://github.com/nymtech/nym/pull/1496
[#1503]: https://github.com/nymtech/nym/pull/1503
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22) ## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
Generated
+32 -1
View File
@@ -3325,7 +3325,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"task",
"thiserror", "thiserror",
"time 0.3.9", "time 0.3.9",
"tokio", "tokio",
@@ -5398,6 +5397,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "streaming-stats"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d670ce4e348a2081843569e0f79b21c99c91bb9028b3b3ecb0f050306de547"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.2" version = "0.1.2"
@@ -6285,6 +6293,29 @@ dependencies = [
"vesting-contract-common", "vesting-contract-common",
] ]
[[package]]
name = "validator-client-scripts"
version = "0.1.0"
dependencies = [
"base64",
"bip39",
"bs58",
"clap 3.2.8",
"csv",
"dotenv",
"log",
"mixnet-contract-common",
"network-defaults",
"pretty_env_logger",
"serde",
"serde_json",
"streaming-stats",
"tokio",
"url",
"validator-client",
"vesting-contract-common",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
+2 -1
View File
@@ -68,7 +68,8 @@ members = [
"service-providers/network-statistics", "service-providers/network-statistics",
"validator-api", "validator-api",
"validator-api/validator-api-requests", "validator-api/validator-api-requests",
"tools/ts-rs-cli" "tools/ts-rs-cli",
"tools/validator-client-scripts"
] ]
default-members = [ default-members = [
+3
View File
@@ -69,6 +69,9 @@ build-wallet:
build-connect: build-connect:
cargo build --manifest-path nym-connect/Cargo.toml --workspace cargo build --manifest-path nym-connect/Cargo.toml --workspace
build-validator-scripts:
cargo build --release --manifest-path tools/validator-client-scripts/Cargo.toml
fmt-main: fmt-main:
cargo fmt --all cargo fmt --all
+14 -2
View File
@@ -3,6 +3,7 @@
use crate::client::config::{Config, SocketType}; use crate::client::config::{Config, SocketType};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use url::Url;
#[cfg(not(feature = "coconut"))] #[cfg(not(feature = "coconut"))]
pub(crate) const DEFAULT_ETH_ENDPOINT: &str = pub(crate) const DEFAULT_ETH_ENDPOINT: &str =
@@ -96,17 +97,28 @@ pub(crate) async fn execute(args: &Cli) {
} }
} }
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.validators { if let Some(raw_validators) = args.validators {
config config
.get_base_mut() .get_base_mut()
.set_custom_validator_apis(config::parse_validators(&raw_validators)); .set_custom_validator_apis(parse_validators(&raw_validators));
} else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() { } else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() {
let raw_validators = std::env::var(network_defaults::var_names::API_VALIDATOR) let raw_validators = std::env::var(network_defaults::var_names::API_VALIDATOR)
.expect("api validator not set"); .expect("api validator not set");
config config
.get_base_mut() .get_base_mut()
.set_custom_validator_apis(config::parse_validators(&raw_validators)); .set_custom_validator_apis(parse_validators(&raw_validators));
} }
if args.disable_socket { if args.disable_socket {
+12 -1
View File
@@ -3,7 +3,7 @@
use crate::client::config::Config; use crate::client::config::Config;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use config::parse_validators; use url::Url;
pub mod init; pub mod init;
pub(crate) mod run; pub(crate) mod run;
@@ -96,6 +96,17 @@ pub(crate) async fn execute(args: &Cli) {
} }
} }
pub fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.validators { if let Some(raw_validators) = args.validators {
config config
+5
View File
@@ -2,4 +2,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pub mod client; pub mod client;
// This is only used as we reach into the init functions in nym-connect. We need to refactor the
// init functions so that nym-connect can just call the same init function as the regular socks5
// client.
#[allow(unused)]
pub mod commands;
pub mod socks; pub mod socks;
-11
View File
@@ -88,14 +88,3 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
} }
} }
pub fn parse_validators(raw: &str) -> Vec<url::Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
@@ -37,7 +37,7 @@ impl SpendCredentialData {
} }
} }
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub enum SpendCredentialStatus { pub enum SpendCredentialStatus {
InProgress, InProgress,
Spent, Spent,
@@ -213,7 +213,7 @@ pub enum QueryMsg {
}, },
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MigrateMsg { pub struct MigrateMsg {
pub mixnet_denom: String, pub mixnet_denom: String,
@@ -226,7 +226,7 @@ impl MigrateMsg {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct NodeToRemove { pub struct NodeToRemove {
owner: String, owner: String,
proxy: Option<String>, proxy: Option<String>,
+1 -1
View File
@@ -507,7 +507,7 @@ mod test {
for (expected, raw_display) in values { for (expected, raw_display) in values {
let coin = DecCoin { let coin = DecCoin {
denom: Network::MAINNET.mix_denom().display, denom: Network::MAINNET.mix_denom().display.into(),
amount: raw_display.parse().unwrap(), amount: raw_display.parse().unwrap(),
}; };
let base = reg.attempt_convert_to_base_coin(coin).unwrap(); let base = reg.attempt_convert_to_base_coin(coin).unwrap();
-1
View File
@@ -1,6 +1,5 @@
EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 EXPLORER_API_URL=https://explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://validator.nymtech.net VALIDATOR_API_URL=https://validator.nymtech.net
VALIDATOR_URL=https://rpc.nyx.nodes.guru
BIG_DIPPER_URL=https://blocks.nymtech.net BIG_DIPPER_URL=https://blocks.nymtech.net
CURRENCY_DENOM=unym CURRENCY_DENOM=unym
CURRENCY_STAKING_DENOM=unyx CURRENCY_STAKING_DENOM=unyx
+3 -4
View File
@@ -1,6 +1,5 @@
EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://qa-validator-api.nymtech.net VALIDATOR_API_URL=https://qa-validator.nymtech.net
VALIDATOR_URL=https://qa-validator.nymtech.net
BIG_DIPPER_URL=https://qa-blocks.nymtech.net BIG_DIPPER_URL=https://qa-blocks.nymtech.net
CURRENCY_DENOM=unym CURRENCY_DENOM=unymt
CURRENCY_STAKING_DENOM=unyx CURRENCY_STAKING_DENOM=unyxt
+1 -2
View File
@@ -1,7 +1,6 @@
// master APIs // master APIs
export const API_BASE_URL = process.env.EXPLORER_API_URL; export const API_BASE_URL = process.env.EXPLORER_API_URL;
export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL; export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL;
export const VALIDATOR_URL = process.env.VALIDATOR_URL;
export const BIG_DIPPER = process.env.BIG_DIPPER_URL; export const BIG_DIPPER = process.env.BIG_DIPPER_URL;
// specific API routes // specific API routes
@@ -10,7 +9,7 @@ export const MIXNODE_PING = `${API_BASE_URL}/ping`;
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
export const MIXNODE_API = `${API_BASE_URL}/mix-node`; export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
export const GATEWAYS_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`; export const GATEWAYS_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`;
export const VALIDATORS_API = `${VALIDATOR_URL}/validators`; export const VALIDATORS_API = `${VALIDATOR_API_BASE_URL}/validators`;
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`; export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
+10 -19
View File
@@ -6,6 +6,7 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
DialogTitle, DialogTitle,
IconButton,
Slider, Slider,
Typography, Typography,
Box, Box,
@@ -24,9 +25,7 @@ import { useIsMobile } from '../../hooks/useIsMobile';
const FilterItem = ({ const FilterItem = ({
label, label,
id, id,
tooltipInfo,
value, value,
isSmooth,
marks, marks,
scale, scale,
min, min,
@@ -37,13 +36,12 @@ const FilterItem = ({
}) => ( }) => (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Typography gutterBottom>{label}</Typography> <Typography gutterBottom>{label}</Typography>
<Typography fontSize={12}>{tooltipInfo}</Typography>
<Slider <Slider
value={value} value={value}
onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])} onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])}
valueLabelDisplay={isSmooth ? 'auto' : 'off'} valueLabelDisplay="off"
marks={marks} marks={marks}
step={isSmooth ? 1 : null} step={null}
scale={scale} scale={scale}
min={min} min={min}
max={max} max={max}
@@ -52,7 +50,7 @@ const FilterItem = ({
); );
export const Filters = () => { export const Filters = () => {
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext(); const { filterMixnodes, fetchMixnodes } = useMainContext();
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -129,26 +127,19 @@ export const Filters = () => {
<Alert <Alert
severity="info" severity="info"
variant={isMobile ? 'standard' : 'outlined'} variant={isMobile ? 'standard' : 'outlined'}
sx={{ color: (t) => t.palette.info.light }}
action={ action={
<Button size="small" onClick={onClearFilters}> <Button size="small" onClick={onClearFilters}>
CLEAR FILTERS Clear
</Button> </Button>
} }
sx={{ width: 300 }}
> >
{mixnodes?.data?.length} mixnodes matched your criteria Filters applied
</Alert> </Alert>
</Snackbar> </Snackbar>
<Button <IconButton size="large" onClick={handleToggleShowFilters}>
size="large" <Tune />
variant="text" </IconButton>
color="inherit"
endIcon={<Tune />}
onClick={handleToggleShowFilters}
sx={{ textTransform: 'none' }}
>
Advanced filters
</Button>
<Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth> <Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth>
<DialogTitle>Mixnode filters</DialogTitle> <DialogTitle>Mixnode filters</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
+54 -21
View File
@@ -5,7 +5,6 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
label: 'Profit margin (%)', label: 'Profit margin (%)',
id: EnumFilterKey.profitMargin, id: EnumFilterKey.profitMargin,
value: [0, 100], value: [0, 100],
isSmooth: true,
marks: [ marks: [
{ label: '0', value: 0 }, { label: '0', value: 0 },
{ label: '10', value: 10 }, { label: '10', value: 10 },
@@ -19,8 +18,6 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
{ label: '90', value: 90 }, { label: '90', value: 90 },
{ label: '100', value: 100 }, { label: '100', value: 100 },
], ],
tooltipInfo:
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
}, },
stakeSaturation: { stakeSaturation: {
label: 'Stake saturation (%)', label: 'Stake saturation (%)',
@@ -46,29 +43,65 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
}, },
], ],
max: upperSaturationValue, max: upperSaturationValue,
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
}, },
routingScore: { stake: {
label: 'Routing score (%)', label: 'Stake (NYM)',
id: EnumFilterKey.routingScore, id: EnumFilterKey.stake,
value: [0, 100], value: [20, 90],
min: 20,
max: 90,
marks: [ marks: [
{ label: '0', value: 0 }, {
{ label: '10', value: 10 }, value: 0,
{ label: '20', value: 20 }, label: '1',
{ label: '30', value: 30 }, },
{ label: '40', value: 40 }, {
{ label: '50', value: 50 }, value: 10,
{ label: '60', value: 60 }, label: '10',
{ label: '70', value: 70 }, },
{ label: '80', value: 80 }, {
{ label: '90', value: 90 }, value: 20,
{ label: '100', value: 100 }, label: '100',
},
{
value: 30,
label: '1k',
},
{
value: 40,
label: '10k',
},
{
value: 50,
label: '100k',
},
{
value: 60,
label: '1M',
},
{
value: 70,
label: '10M',
},
{
value: 80,
label: '100M',
},
{
value: 90,
label: '1B',
},
], ],
tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards',
}, },
}); });
const formatStakeValuesToMinorDenom = ([value_1, value_2]: number[]) => {
const lowerValue = 10 ** (value_1 / 10) * 1_000_000;
const upperValue = 10 ** (value_2 / 10) * 1_000_000;
return [lowerValue, upperValue];
};
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
const lowerValue = value_1 / 100; const lowerValue = value_1 / 100;
const upperValue = value_2 / 100; const upperValue = value_2 / 100;
@@ -77,7 +110,7 @@ const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
}; };
export const formatOnSave = (filters: TFilters) => ({ export const formatOnSave = (filters: TFilters) => ({
routingScore: filters.routingScore.value, stake: formatStakeValuesToMinorDenom(filters.stake.value),
profitMargin: filters.profitMargin.value, profitMargin: filters.profitMargin.value,
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
}); });
+2 -2
View File
@@ -102,8 +102,8 @@ export const MainContextProvider: React.FC = ({ children }) => {
m.mix_node.profit_margin_percent <= filters.profitMargin[1] && m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
m.stake_saturation >= filters.stakeSaturation[0] && m.stake_saturation >= filters.stakeSaturation[0] &&
m.stake_saturation <= filters.stakeSaturation[1] && m.stake_saturation <= filters.stakeSaturation[1] &&
m.avg_uptime >= filters.routingScore[0] && +m.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] &&
m.avg_uptime <= filters.routingScore[1], +m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1],
); );
setMixnodes({ data: filtered, isLoading: false }); setMixnodes({ data: filtered, isLoading: false });
}; };
+1 -3
View File
@@ -4,19 +4,17 @@ import { Mark } from '@mui/base';
export enum EnumFilterKey { export enum EnumFilterKey {
profitMargin = 'profitMargin', profitMargin = 'profitMargin',
stakeSaturation = 'stakeSaturation', stakeSaturation = 'stakeSaturation',
routingScore = 'routingScore', stake = 'stake',
} }
export type TFilterItem = { export type TFilterItem = {
label: string; label: string;
id: EnumFilterKey; id: EnumFilterKey;
value: number[]; value: number[];
isSmooth?: boolean;
marks: Mark[]; marks: Mark[];
min?: number; min?: number;
max?: number; max?: number;
scale?: (value: number) => number; scale?: (value: number) => number;
tooltipInfo?: string;
}; };
export type TFilters = { [key in EnumFilterKey]: TFilterItem }; export type TFilters = { [key in EnumFilterKey]: TFilterItem };
+12 -1
View File
@@ -6,11 +6,11 @@ use std::{process, str::FromStr};
use crate::{config::Config, Cli}; use crate::{config::Config, Cli};
use clap::Subcommand; use clap::Subcommand;
use colored::Colorize; use colored::Colorize;
use config::parse_validators;
use crypto::bech32_address_validation; use crypto::bech32_address_validation;
use network_defaults::var_names::{ use network_defaults::var_names::{
API_VALIDATOR, BECH32_PREFIX, CONFIGURED, NYMD_VALIDATOR, STATISTICS_SERVICE_DOMAIN_ADDRESS, API_VALIDATOR, BECH32_PREFIX, CONFIGURED, NYMD_VALIDATOR, STATISTICS_SERVICE_DOMAIN_ADDRESS,
}; };
use url::Url;
pub(crate) mod init; pub(crate) mod init;
pub(crate) mod node_details; pub(crate) mod node_details;
@@ -69,6 +69,17 @@ pub(crate) async fn execute(args: Cli) {
} }
} }
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
let mut was_host_overridden = false; let mut was_host_overridden = false;
if let Some(host) = args.host { if let Some(host) = args.host {
+13 -4
View File
@@ -6,11 +6,9 @@ use std::process;
use crate::{config::Config, Cli}; use crate::{config::Config, Cli};
use clap::Subcommand; use clap::Subcommand;
use colored::Colorize; use colored::Colorize;
use config::{ use config::defaults::var_names::{API_VALIDATOR, BECH32_PREFIX, CONFIGURED};
defaults::var_names::{API_VALIDATOR, BECH32_PREFIX, CONFIGURED},
parse_validators,
};
use crypto::bech32_address_validation; use crypto::bech32_address_validation;
use url::Url;
mod describe; mod describe;
mod init; mod init;
@@ -63,6 +61,17 @@ pub(crate) async fn execute(args: Cli) {
} }
} }
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
fn override_config(mut config: Config, args: OverrideConfig) -> Config { fn override_config(mut config: Config, args: OverrideConfig) -> Config {
let mut was_host_overridden = false; let mut was_host_overridden = false;
if let Some(host) = args.host { if let Some(host) = args.host {
+2 -3
View File
@@ -7,7 +7,7 @@ use tokio::sync::RwLock;
use client_core::config::Config as BaseConfig; use client_core::config::Config as BaseConfig;
use config_common::NymConfig; use config_common::NymConfig;
use nym_socks5::client::config::Config as Socks5Config; use nym_socks5::{client::config::Config as Socks5Config, commands::parse_validators};
use crate::{ use crate::{
error::{BackendError, Result}, error::{BackendError, Result},
@@ -134,11 +134,10 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
config config
.get_base_mut() .get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY); .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
if let Ok(raw_validators) = std::env::var(config_common::defaults::var_names::API_VALIDATOR) { if let Ok(raw_validators) = std::env::var(config_common::defaults::var_names::API_VALIDATOR) {
config config
.get_base_mut() .get_base_mut()
.set_custom_validator_apis(config_common::parse_validators(&raw_validators)); .set_custom_validator_apis(parse_validators(&raw_validators));
} }
let gateway = setup_gateway( let gateway = setup_gateway(
+10 -87
View File
@@ -3273,17 +3273,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
"parking_lot_core 0.8.5", "parking_lot_core",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core 0.9.3",
] ]
[[package]] [[package]]
@@ -3300,19 +3290,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "parking_lot_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec 1.8.0",
"windows-sys",
]
[[package]] [[package]]
name = "password-hash" name = "password-hash"
version = "0.3.2" version = "0.3.2"
@@ -4573,15 +4550,6 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "signature" name = "signature"
version = "1.3.2" version = "1.3.2"
@@ -4667,9 +4635,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "state" name = "state"
version = "0.5.3" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5"
dependencies = [ dependencies = [
"loom", "loom",
] ]
@@ -4688,7 +4656,7 @@ checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot 0.11.2", "parking_lot",
"phf_shared 0.10.0", "phf_shared 0.10.0",
"precomputed-hash", "precomputed-hash",
"serde", "serde",
@@ -4870,7 +4838,7 @@ dependencies = [
"ndk-glue", "ndk-glue",
"ndk-sys", "ndk-sys",
"objc", "objc",
"parking_lot 0.11.2", "parking_lot",
"raw-window-handle", "raw-window-handle",
"scopeguard", "scopeguard",
"serde", "serde",
@@ -5273,9 +5241,7 @@ dependencies = [
"mio", "mio",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"parking_lot 0.12.1",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"winapi", "winapi",
@@ -5932,11 +5898,11 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0"
dependencies = [ dependencies = [
"windows_aarch64_msvc 0.30.0", "windows_aarch64_msvc",
"windows_i686_gnu 0.30.0", "windows_i686_gnu",
"windows_i686_msvc 0.30.0", "windows_i686_msvc",
"windows_x86_64_gnu 0.30.0", "windows_x86_64_gnu",
"windows_x86_64_msvc 0.30.0", "windows_x86_64_msvc",
] ]
[[package]] [[package]]
@@ -5949,31 +5915,12 @@ dependencies = [
"windows_reader", "windows_reader",
] ]
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.30.0" version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]] [[package]]
name = "windows_gen" name = "windows_gen"
version = "0.30.0" version = "0.30.0"
@@ -5990,24 +5937,12 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.30.0" version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]] [[package]]
name = "windows_macros" name = "windows_macros"
version = "0.30.0" version = "0.30.0"
@@ -6038,24 +5973,12 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.30.0" version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.7.0" version = "0.7.0"
+2 -2
View File
@@ -32,14 +32,14 @@ log = "0.4"
once_cell = "1.7.2" once_cell = "1.7.2"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
rand = "0.6.5" rand = "0.6.5"
reqwest = {version = "0.11.9", features = ["json"] } reqwest = "0.11.9"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
strum = { version = "0.23", features = ["derive"] } strum = { version = "0.23", features = ["derive"] }
tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] } tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
tendermint-rpc = "0.23.0" tendermint-rpc = "0.23.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.10", features = ["full"] } tokio = { version = "1.10", features = ["sync", "time"] }
toml = "0.5.8" toml = "0.5.8"
url = "2.2" url = "2.2"
-2
View File
@@ -58,8 +58,6 @@ fn main() {
mixnet::bond::unbond_gateway, mixnet::bond::unbond_gateway,
mixnet::bond::unbond_mixnode, mixnet::bond::unbond_mixnode,
mixnet::bond::update_mixnode, mixnet::bond::update_mixnode,
mixnet::bond::get_number_of_mixnode_delegators,
mixnet::bond::get_mix_node_description,
mixnet::delegate::delegate_to_mixnode, mixnet::delegate::delegate_to_mixnode,
mixnet::delegate::get_delegator_rewards, mixnet::delegate::get_delegator_rewards,
mixnet::delegate::get_pending_delegation_events, mixnet::delegate::get_pending_delegation_events,
@@ -1,5 +1,3 @@
use std::time::Duration;
use crate::error::BackendError; use crate::error::BackendError;
use crate::state::WalletState; use crate::state::WalletState;
use crate::{Gateway, MixNode}; use crate::{Gateway, MixNode};
@@ -7,18 +5,8 @@ use nym_types::currency::DecCoin;
use nym_types::gateway::GatewayBond; use nym_types::gateway::GatewayBond;
use nym_types::mixnode::MixNodeBond; use nym_types::mixnode::MixNodeBond;
use nym_types::transaction::TransactionExecuteResult; use nym_types::transaction::TransactionExecuteResult;
use reqwest::Error as ReqwestError;
use serde::{Deserialize, Serialize};
use validator_client::nymd::{Coin, Fee}; use validator_client::nymd::{Coin, Fee};
#[derive(Debug, Serialize, Deserialize)]
pub struct NodeDescription {
name: String,
description: String,
link: String,
location: String,
}
#[tauri::command] #[tauri::command]
pub async fn bond_gateway( pub async fn bond_gateway(
gateway: Gateway, gateway: Gateway,
@@ -210,51 +198,3 @@ pub async fn get_operator_rewards(
); );
Ok(display_coin) Ok(display_coin)
} }
#[tauri::command]
pub async fn get_number_of_mixnode_delegators(
identity: String,
state: tauri::State<'_, WalletState>,
) -> Result<usize, BackendError> {
let guard = state.read().await;
let client = guard.current_client()?;
let paged_delegations = client
.nymd
.get_mix_delegations_paged(identity, None, Some(20))
.await?;
Ok(paged_delegations.delegations.len())
}
async fn fetch_mix_node_description(
host: &str,
port: u16,
) -> Result<NodeDescription, ReqwestError> {
let milli_second = Duration::from_millis(1000);
let client = reqwest::Client::builder().timeout(milli_second).build()?;
let response = client
.get(format!("http://{}:{}/description", host, port))
.send()
.await;
match response {
Ok(res) => {
let json = res.json::<NodeDescription>().await;
match json {
Ok(json) => Ok(json),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
}
}
#[tauri::command]
pub async fn get_mix_node_description(
host: &str,
port: u16,
) -> Result<NodeDescription, BackendError> {
return fetch_mix_node_description(host, port)
.await
.map_err(|e| BackendError::ReqwestError { source: e });
}
@@ -1,12 +1,8 @@
import React from 'react'; import React from 'react';
import { Avatar, Typography } from '@mui/material'; import { Avatar } from '@mui/material';
import stc from 'string-to-color'; import stc from 'string-to-color';
import { TAccount } from 'src/types'; import { TAccount } from 'src/types';
export const AccountAvatar = ({ name, small }: { name: TAccount['name']; small?: boolean }) => ( export const AccountAvatar = ({ name }: Pick<TAccount, 'name'>) => (
<Avatar sx={{ bgcolor: stc(name), ...(small ? { width: 25, height: 25 } : {}) }}> <Avatar sx={{ bgcolor: stc(name), width: 20, height: 20 }}>{name?.split('')[0]}</Avatar>
<Typography fontSize={small ? 14 : 'inherit'} fontWeight={600}>
{name?.split('')[0]}
</Typography>
</Avatar>
); );
@@ -5,10 +5,10 @@ import { AccountAvatar } from './AccountAvatar';
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
<Button <Button
startIcon={<AccountAvatar name={account.id} small />} startIcon={<AccountAvatar name={account.id} />}
sx={{ color: 'text.primary' }} sx={{ color: 'text.primary' }}
color="inherit"
onClick={onClick} onClick={onClick}
disableRipple
> >
{account.id} {account.id}
</Button> </Button>
@@ -20,7 +20,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Close /> <Close />
</IconButton> </IconButton>
</Box> </Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.nym.text.muted }}> <Typography variant="body1" sx={{ color: (t) => t.palette.nym.text.muted }}>
How to set up multiple accounts How to set up multiple accounts
</Typography> </Typography>
</DialogTitle> </DialogTitle>
@@ -29,7 +29,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Alert <Alert
severity="warning" severity="warning"
icon={false} icon={false}
sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (theme) => theme.palette.background.paper } : {})} sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (t) => t.palette.background.paper } : {})}
> >
<Typography>In order to create multiple accounts your wallet needs a password.</Typography> <Typography>In order to create multiple accounts your wallet needs a password.</Typography>
<Typography>Follow steps below to create password.</Typography> <Typography>Follow steps below to create password.</Typography>
@@ -9,7 +9,6 @@ import {
DialogTitle, DialogTitle,
IconButton, IconButton,
Typography, Typography,
Divider,
} from '@mui/material'; } from '@mui/material';
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material'; import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context'; import { AccountsContext } from 'src/context';
@@ -52,7 +51,7 @@ export const AccountsModal = () => {
<Close /> <Close />
</IconButton> </IconButton>
</Box> </Box>
<Typography fontSize="small" sx={{ color: 'grey.600' }}> <Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Switch between accounts Switch between accounts
</Typography> </Typography>
</DialogTitle> </DialogTitle>
@@ -70,7 +69,6 @@ export const AccountsModal = () => {
/> />
))} ))}
</DialogContent> </DialogContent>
<Divider variant="middle" sx={{ mt: 3 }} />
<DialogActions sx={{ p: 3 }}> <DialogActions sx={{ p: 3 }}>
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}> <Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
Import account Import account
@@ -7,16 +7,17 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
IconButton,
TextField, TextField,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { ArrowBackSharp } from '@mui/icons-material';
import { useClipboard } from 'use-clipboard-copy'; import { useClipboard } from 'use-clipboard-copy';
import { createMnemonic, validateMnemonic } from 'src/requests'; import { createMnemonic, validateMnemonic } from 'src/requests';
import { Console } from 'src/utils/console'; import { Console } from 'src/utils/console';
import { AccountsContext } from 'src/context'; import { AccountsContext } from 'src/context';
import { ConfirmPassword, Mnemonic } from 'src/components'; import { ConfirmPassword, Mnemonic } from 'src/components';
import { MnemonicInput } from 'src/components/textfields'; import { MnemonicInput } from 'src/components/textfields';
import { StyledBackButton } from 'src/components/StyledBackButton';
const createAccountSteps = [ const createAccountSteps = [
'Copy and save mnemonic for your new account', 'Copy and save mnemonic for your new account',
@@ -29,15 +30,14 @@ const importAccountSteps = [
'Confirm the password used to login to your wallet', 'Confirm the password used to login to your wallet',
]; ];
const MnemonicStep = ({ mnemonic, onNext, onBack }: { mnemonic: string; onNext: () => void; onBack: () => void }) => { const MnemonicStep = ({ mnemonic, onNext }: { mnemonic: string; onNext: () => void }) => {
const { copy, copied } = useClipboard({ copiedTimeout: 5000 }); const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
return ( return (
<Box sx={{ mt: 1 }}> <Box sx={{ mt: 1 }}>
<DialogContent> <DialogContent>
<Mnemonic mnemonic={mnemonic} handleCopy={copy} copied={copied} /> <Mnemonic mnemonic={mnemonic} handleCopy={copy} copied={copied} />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0, gap: 2 }}> <DialogActions sx={{ p: 3, pt: 0 }}>
<StyledBackButton onBack={onBack} />
<Button disabled={!copied} fullWidth disableElevation variant="contained" size="large" onClick={onNext}> <Button disabled={!copied} fullWidth disableElevation variant="contained" size="large" onClick={onNext}>
I saved my mnemonic I saved my mnemonic
</Button> </Button>
@@ -50,12 +50,10 @@ const ImportMnemonic = ({
value, value,
onChange, onChange,
onNext, onNext,
onBack,
}: { }: {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onNext: () => void; onNext: () => void;
onBack: () => void;
}) => { }) => {
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
@@ -79,8 +77,7 @@ const ImportMnemonic = ({
}} }}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0, gap: 2 }}> <DialogActions sx={{ p: 3, pt: 0 }}>
<StyledBackButton onBack={onBack} />
<Button <Button
disabled={value.length === 0} disabled={value.length === 0}
fullWidth fullWidth
@@ -96,7 +93,7 @@ const ImportMnemonic = ({
); );
}; };
const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBack: () => void }) => { const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
@@ -124,8 +121,7 @@ const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBa
fullWidth fullWidth
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0, gap: 2 }}> <DialogActions sx={{ p: 3, pt: 0 }}>
<StyledBackButton onBack={onBack} />
<Button <Button
disabled={!value.length} disabled={!value.length}
fullWidth fullWidth
@@ -182,6 +178,9 @@ export const AddAccountModal = () => {
<DialogTitle sx={{ pb: 0 }}> <DialogTitle sx={{ pb: 0 }}>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{`${dialogToDisplay} new account`}</Typography> <Typography variant="h6">{`${dialogToDisplay} new account`}</Typography>
<IconButton onClick={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}>
<ArrowBackSharp />
</IconButton>
</Box> </Box>
<Typography sx={{ mt: 2 }}> <Typography sx={{ mt: 2 }}>
{dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]} {dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]}
@@ -191,17 +190,12 @@ export const AddAccountModal = () => {
switch (step) { switch (step) {
case 0: case 0:
return dialogToDisplay === 'Add' ? ( return dialogToDisplay === 'Add' ? (
<MnemonicStep <MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} />
mnemonic={data.mnemonic}
onNext={() => setStep((s) => s + 1)}
onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}
/>
) : ( ) : (
<ImportMnemonic <ImportMnemonic
value={data.mnemonic} value={data.mnemonic}
onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))} onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))}
onNext={() => setStep((s) => s + 1)} onNext={() => setStep((s) => s + 1)}
onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}
/> />
); );
case 1: case 1:
@@ -211,7 +205,6 @@ export const AddAccountModal = () => {
setData((d) => ({ ...d, accountName })); setData((d) => ({ ...d, accountName }));
setStep((s) => s + 1); setStep((s) => s + 1);
}} }}
onBack={() => setStep((s) => s - 1)}
/> />
); );
case 2: case 2:
@@ -229,7 +222,6 @@ export const AddAccountModal = () => {
} }
} }
}} }}
onCancel={() => setStep((s) => s - 1)}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
/> />
@@ -19,18 +19,17 @@ export const ConfirmPasswordModal = ({
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth> <Dialog open={Boolean(accountName)} onClose={onClose} fullWidth>
<Paper> <Paper>
<DialogTitle> <DialogTitle>
<Typography variant="h6">Switch account</Typography> <Box display="flex" justifyContent="space-between" alignItems="center">
<Typography fontSize="small" sx={{ color: 'grey.600' }}> <Typography variant="h6">Switch account</Typography>
<IconButton onClick={onClose}>
<ArrowBack />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Confirm password Confirm password
</Typography> </Typography>
</DialogTitle> </DialogTitle>
<ConfirmPassword <ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
onConfirm={onConfirm}
error={error}
isLoading={isLoading}
buttonTitle="Switch account"
onCancel={onClose}
/>
</Paper> </Paper>
</Dialog> </Dialog>
); );
@@ -33,7 +33,7 @@ export const EditAccountModal = () => {
<Close /> <Close />
</IconButton> </IconButton>
</Box> </Box>
<Typography fontSize="small" sx={{ color: 'grey.600' }}> <Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
New wallet address New wallet address
</Typography> </Typography>
</DialogTitle> </DialogTitle>
@@ -0,0 +1,69 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
} from '@mui/material';
import { Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
export const ImportAccountModal = () => {
const [mnemonic, setMnemonic] = useState('');
const { dialogToDisplay, setDialogToDisplay, handleImportAccount } = useContext(AccountsContext);
const handleClose = () => {
setMnemonic('');
setDialogToDisplay('Accounts');
};
return (
<Dialog open={dialogToDisplay === 'Import'} onClose={handleClose} fullWidth>
<Paper>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Import account</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}>
Provide mnemonic of account you want to import
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
placeholder="Paste or type your mnemonic here"
fullWidth
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
autoFocus
multiline
rows={3}
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => handleImportAccount({ id: '', address: '' })}
disabled={!mnemonic.length}
>
Import account
</Button>
</DialogActions>
</Paper>
</Dialog>
);
};
-42
View File
@@ -1,42 +0,0 @@
import React, { useRef } from 'react';
import { MoreVertSharp } from '@mui/icons-material';
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
export const ActionsMenu: React.FC<{ open: boolean; onOpen: () => void; onClose: () => void }> = ({
children,
open,
onOpen,
onClose,
}) => {
const anchorEl: any = useRef<HTMLElement>();
return (
<>
<IconButton ref={anchorEl} onClick={onOpen}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl.current} open={open} onClose={onClose}>
{children}
</Menu>
</>
);
};
export const ActionsMenuItem = ({
title,
description,
onClick,
Icon,
disabled,
}: {
title: string;
description?: string;
onClick?: () => void;
Icon?: React.ReactNode;
disabled?: boolean;
}) => (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
+13 -2
View File
@@ -7,11 +7,13 @@ import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'; import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppContext } from '../context/main'; import { AppContext } from '../context/main';
import { NetworkSelector } from './NetworkSelector'; import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node';
import { MultiAccounts } from './Accounts'; import { MultiAccounts } from './Accounts';
import { config } from '../config'; import { config } from '../config';
export const AppBar = () => { export const AppBar = () => {
const { logOut, handleShowTerminal, appEnv, mode, handleSwitchMode } = useContext(AppContext); const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } =
useContext(AppContext);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@@ -29,7 +31,7 @@ export const AppBar = () => {
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}> <Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item> <Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}> <IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'dark' ? ( {mode === 'light' ? (
<LightModeOutlinedIcon fontSize="small" /> <LightModeOutlinedIcon fontSize="small" />
) : ( ) : (
<ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} /> <ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} />
@@ -43,6 +45,15 @@ export const AppBar = () => {
</IconButton> </IconButton>
</Grid> </Grid>
)} )}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<NodeIcon fontSize="small" />
</IconButton>
</Grid>
<Grid item> <Grid item>
<IconButton <IconButton
size="small" size="small"
@@ -0,0 +1,65 @@
import React, { useContext } from 'react';
import { Logout } from '@mui/icons-material';
import TerminalIcon from '@mui/icons-material/Terminal';
import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined';
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { Node } from 'src/svg-icons/node';
import { config } from '../../config';
import { AppContext } from '../../context/main';
import { MultiAccounts } from '../Accounts';
import { NetworkSelector } from '../NetworkSelector';
export const AppBar = () => {
const { showSettings, handleShowTerminal, appEnv, handleShowSettings, logOut, mode, handleSwitchMode } =
useContext(AppContext);
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none' }}>
<Toolbar disableGutters>
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
<Grid item container alignItems="center" spacing={1}>
<Grid item>
<MultiAccounts />
</Grid>
<Grid item>
<NetworkSelector />
</Grid>
</Grid>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
<Grid item>
<IconButton size="small" onClick={handleSwitchMode} sx={{ color: 'text.primary' }}>
{mode === 'light' ? (
<ModeNightOutlinedIcon fontSize="small" />
) : (
<LightModeOutlinedIcon fontSize="small" />
)}
</IconButton>
</Grid>
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
<Grid item>
<IconButton size="small" onClick={handleShowTerminal} sx={{ color: 'text.primary' }}>
<TerminalIcon fontSize="small" />
</IconButton>
</Grid>
)}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<Node fontSize="small" />
</IconButton>
</Grid>
<Grid item>
<IconButton size="small" onClick={logOut} sx={{ color: 'text.primary' }}>
<Logout fontSize="small" />
</IconButton>
</Grid>
</Grid>
</Grid>
</Toolbar>
</MuiAppBar>
);
};
@@ -0,0 +1 @@
export * from './AppBar';
@@ -1,44 +0,0 @@
import React from 'react';
import { Box, Button, Typography } from '@mui/material';
import { NymCard } from '../NymCard';
export const Bond = ({
onBond,
disabled,
}: {
onBond: () => void;
disabled: boolean;
}) => (
<NymCard title="Bonding" borderless>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography>Bond a mixnode or a gateway</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
gap: 2,
}}
>
<Button
size="large"
variant="contained"
color="primary"
type="button"
disableElevation
onClick={onBond}
disabled={disabled}
>
Bond
</Button>
</Box>
</Box>
</NymCard>
);
@@ -1,84 +0,0 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedGateway, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction';
const headers: Header[] = [
{
header: 'IP',
id: 'ip',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
id: 'menu-button',
sx: { width: 34, maxWidth: 34 },
},
];
export const BondedGateway = ({
gateway,
network,
onActionSelect,
}: {
gateway: TBondedGateway;
network?: Network;
onActionSelect: (action: TBondedGatwayActions) => void;
}) => {
const { name, bond, ip, identityKey } = gateway;
const cells: Cell[] = [
{
cell: ip,
id: 'stake-saturation-cell',
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'stake-cell',
sx: { pl: 0 },
},
{
cell: <BondedGatewayActions onActionSelect={onActionSelect} />,
id: 'actions-cell',
align: 'right',
},
];
return (
<NymCard
borderless
title={
<Stack gap={2}>
<Typography variant="h5" fontWeight={600}>
Gateway
</Typography>
{name && (
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/gateways`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
);
};
@@ -1,31 +0,0 @@
import React, { useState } from 'react';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Unbond as UnbondIcon } from '../../svg-icons';
export type TBondedGatwayActions = 'unbond';
export const BondedGatewayActions = ({
onActionSelect,
}: {
onActionSelect: (action: TBondedGatwayActions) => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (action: TBondedGatwayActions) => {
onActionSelect(action);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
</ActionsMenu>
);
};
@@ -1,138 +0,0 @@
import React from 'react';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedMixnode, urls } from 'src/context';
import { NymCard } from 'src/components';
import { Network } from 'src/types';
import { IdentityKey } from 'src/components/IdentityKey';
import { NodeStatus } from 'src/components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions';
const headers: Header[] = [
{
header: 'Stake',
id: 'stake',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
header: 'Stake saturation',
id: 'stake-saturation',
},
{
header: 'PM',
id: 'profit-margin',
tooltipText:
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Operator rewards',
id: 'operator-rewards',
tooltipText:
'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.',
},
{
header: 'No. delegators',
id: 'delegators',
},
{
id: 'menu-button',
sx: { width: 34, maxWidth: 34 },
},
];
export const BondedMixnode = ({
mixnode,
network,
onActionSelect,
}: {
mixnode: TBondedMixnode;
network?: Network;
onActionSelect: (action: TBondedMixnodeActions) => void;
}) => {
const { name, stake, bond, stakeSaturation, profitMargin, operatorRewards, delegators, status, identityKey } =
mixnode;
const cells: Cell[] = [
{
cell: `${stake.amount} ${stake.denom}`,
id: 'stake-cell',
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: `${stakeSaturation}%`,
id: 'stake-saturation-cell',
},
{
cell: `${profitMargin}%`,
id: 'pm-cell',
},
{
cell: `${operatorRewards.amount} ${operatorRewards.denom}`,
id: 'operator-rewards-cell',
},
{
cell: delegators,
id: 'delegators-cell',
},
{
cell: (
<BondedMixnodeActions
onActionSelect={onActionSelect}
disabledRedeemAndCompound={Number(mixnode.operatorRewards.amount) === 0}
/>
),
id: 'actions-cell',
align: 'right',
},
];
return (
<NymCard
borderless
title={
<Stack gap={2}>
<Box display="flex" alignItems="center" gap={2}>
<Typography variant="h5" fontWeight={600}>
Mix node
</Typography>
<NodeStatus status={status} />
</Box>
{name && (
<Typography fontWeight="regular" variant="h6">
{name}
</Typography>
)}
<IdentityKey identityKey={identityKey} />
</Stack>
}
Action={
<Button
variant="text"
color="secondary"
onClick={() => onActionSelect('nodeSettings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
}
>
<NodeTable headers={headers} cells={cells} />
{network && (
<Typography sx={{ mt: 2, fontSize: 'small' }}>
Check more stats of your node on the{' '}
<Link href={`${urls(network).networkExplorer}/network-components/mixnodes`} target="_blank">
explorer
</Link>
</Typography>
)}
</NymCard>
);
};
@@ -1,48 +0,0 @@
import React, { useState } from 'react';
import { Typography } from '@mui/material';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Unbond as UnbondIcon } from '../../svg-icons';
export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem' | 'compound';
export const BondedMixnodeActions = ({
onActionSelect,
disabledRedeemAndCompound,
}: {
onActionSelect: (action: TBondedMixnodeActions) => void;
disabledRedeemAndCompound: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (action: TBondedMixnodeActions) => {
onActionSelect(action);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
<ActionsMenuItem
title="Compound rewards"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
description={disabledRedeemAndCompound ? 'No rewards to compound' : 'Add your rewards to you balance'}
onClick={() => handleActionClick('compound')}
disabled={disabledRedeemAndCompound}
/>
<ActionsMenuItem
title="Redeem rewards"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
description={disabledRedeemAndCompound ? 'No rewards to redeem' : 'Add your rewards to you balance'}
onClick={() => handleActionClick('redeem')}
disabled={disabledRedeemAndCompound}
/>
</ActionsMenu>
);
};
@@ -1,50 +0,0 @@
import React from 'react';
import {
Stack,
SxProps,
Table,
TableBody,
TableCell,
TableCellProps,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@mui/material';
import { InfoTooltip } from '../InfoToolTip';
export type Header = { header?: string; id: string; tooltipText?: string; sx?: SxProps };
export type Cell = { cell: string | React.ReactNode; id: string; align?: TableCellProps['align']; sx?: SxProps };
export interface TableProps {
headers: Header[];
cells: Cell[];
}
export const NodeTable = ({ headers, cells }: TableProps) => (
<TableContainer>
<Table aria-label="node-table">
<TableHead>
<TableRow>
{headers.map(({ header, id, tooltipText }) => (
<TableCell key={id}>
<Stack direction="row" gap={1}>
{tooltipText && <InfoTooltip title={tooltipText} />}
<Typography>{header}</Typography>
</Stack>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow key="node-data">
{cells.map(({ cell, id, align }) => (
<TableCell key={id} align={align} sx={{ textTransform: 'uppercase' }}>
{cell}
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
@@ -1,214 +0,0 @@
import React, { useEffect, useState } from 'react';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { useForm } from 'react-hook-form';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { NodeTypeSelector, TokenPoolSelector } from 'src/components';
import { yupResolver } from '@hookform/resolvers/yup';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import { gatewayValidationSchema, amountSchema } from './gatewayValidationSchema';
const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNext: (data: GatewayData) => void }) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const {
register,
formState: { errors },
handleSubmit,
setValue,
} = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData });
const handleRequestValidation = (event: { detail: { step: number } }) => {
if (event.detail.step === 1) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<IdentityKeyFormField
required
fullWidth
label="Identity Key"
initialValue={gatewayData?.identityKey}
errorText={errors.identityKey?.message}
onChanged={(value) => setValue('identityKey', value)}
/>
<TextField
{...register('sphinxKey')}
name="sphinxKey"
label="Sphinx key"
error={Boolean(errors.sphinxKey)}
helperText={errors.sphinxKey?.message}
/>
<TextField
{...register('ownerSignature')}
name="ownerSignature"
label="Owner signature"
error={Boolean(errors.ownerSignature)}
helperText={errors.ownerSignature?.message}
/>
<TextField
{...register('location')}
name="location"
label="Location"
error={Boolean(errors.location)}
helperText={errors.location?.message}
required
sx={{ flexBasis: '50%' }}
/>
<Stack direction="row" gap={2}>
<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
sx={{ flexBasis: '50%' }}
/>
<TextField
{...register('version')}
name="version"
label="Version"
error={Boolean(errors.version)}
helperText={errors.version?.message}
required
sx={{ flexBasis: '50%' }}
/>
</Stack>
<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={2} sx={{ mb: 2 }}>
<TextField
{...register('mixPort')}
name="mixPort"
label="Mix port"
error={Boolean(errors.mixPort)}
helperText={errors.mixPort?.message}
fullWidth
/>
<TextField
{...register('clientsPort')}
name="clientsPort"
label="Client WS API port"
error={Boolean(errors.clientsPort)}
helperText={errors.clientsPort?.message}
fullWidth
/>
</Stack>
)}
</Stack>
);
};
const AmountFormData = ({
denom,
amountData,
hasVestingTokens,
onNext,
}: {
denom: CurrencyDenom;
amountData: GatewayAmount;
hasVestingTokens: boolean;
onNext: (data: any) => void;
}) => {
const {
formState: { errors },
handleSubmit,
setValue,
getValues,
setError,
} = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData });
const handleRequestValidation = async (event: { detail: { step: number } }) => {
let hasSufficientTokens = true;
const values = getValues();
if (values.tokenPool === 'balance') {
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
}
if (values.tokenPool === 'locked') {
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
}
if (event.detail.step === 2 && hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('amount.amount', { message: 'Not enough tokens' });
}
};
useEffect(() => {
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<Box display="flex" gap={2} justifyContent="center" sx={{ mt: 2 }}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => setValue('amount', newValue, { shouldValidate: true })}
validationError={errors.amount?.amount?.message}
denom={denom}
initialValue={amountData.amount.amount}
/>
</Box>
</Stack>
);
};
export const BondGatewayForm = ({
step,
denom,
gatewayData,
amountData,
hasVestingTokens,
onValidateGatewayData,
onValidateAmountData,
onSelectNodeType,
}: {
step: 1 | 2 | 3;
gatewayData: GatewayData;
amountData: GatewayAmount;
denom: CurrencyDenom;
hasVestingTokens: boolean;
onValidateGatewayData: (data: GatewayData) => void;
onValidateAmountData: (data: GatewayAmount) => Promise<void>;
onSelectNodeType: (nodeType: TNodeType) => void;
}) => (
<>
{step === 1 && (
<>
<Box sx={{ mb: 2 }}>
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="gateway" />
</Box>
<NodeFormData onNext={onValidateGatewayData} gatewayData={gatewayData} />
</>
)}
{step === 2 && (
<AmountFormData
denom={denom}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onNext={onValidateAmountData}
/>
)}
</>
);
@@ -1,223 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils';
import { NodeTypeSelector, TokenPoolSelector } from 'src/components';
import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types';
import { amountSchema, mixnodeValidationSchema } from './mixnodeValidationSchema';
const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const {
register,
formState: { errors },
handleSubmit,
setValue,
} = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData });
const handleRequestValidation = (event: { detail: { step: number } }) => {
if (event.detail.step === 1) {
handleSubmit(onNext)();
}
};
useEffect(() => {
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<IdentityKeyFormField
required
fullWidth
label="Identity Key"
initialValue={mixnodeData?.identityKey}
errorText={errors.identityKey?.message}
onChanged={(value) => setValue('identityKey', value)}
/>
<TextField
{...register('sphinxKey')}
name="sphinxKey"
label="Sphinx key"
error={Boolean(errors.sphinxKey)}
helperText={errors.sphinxKey?.message}
/>
<TextField
{...register('ownerSignature')}
name="ownerSignature"
label="Owner signature"
error={Boolean(errors.ownerSignature)}
helperText={errors.ownerSignature?.message}
/>
<Stack direction="row" gap={2}>
<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
sx={{ flexBasis: '50%' }}
/>
<TextField
{...register('version')}
name="version"
label="Version"
error={Boolean(errors.version)}
helperText={errors.version?.message}
required
sx={{ flexBasis: '50%' }}
/>
</Stack>
<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={2} sx={{ mb: 2 }}>
<TextField
{...register('mixPort')}
name="mixPort"
label="Mix port"
error={Boolean(errors.mixPort)}
helperText={errors.mixPort?.message}
fullWidth
/>
<TextField
{...register('verlocPort')}
name="verlocPort"
label="Verloc port"
error={Boolean(errors.verlocPort)}
helperText={errors.verlocPort?.message}
fullWidth
/>
<TextField
{...register('httpApiPort')}
name="httpApiPort"
label="HTTP api port"
error={Boolean(errors.httpApiPort)}
helperText={errors.httpApiPort?.message}
fullWidth
/>
</Stack>
)}
</Stack>
);
};
const AmountFormData = ({
amountData,
hasVestingTokens,
denom,
onNext,
}: {
amountData: MixnodeAmount;
hasVestingTokens: boolean;
denom: CurrencyDenom;
onNext: (data: MixnodeAmount) => void;
}) => {
const {
register,
formState: { errors },
handleSubmit,
setValue,
getValues,
setError,
} = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData });
const handleRequestValidation = async (event: { detail: { step: number } }) => {
let hasSufficientTokens = true;
const values = getValues();
if (values.tokenPool === 'balance') {
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
}
if (values.tokenPool === 'locked') {
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
}
if (event.detail.step === 2 && hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('amount.amount', { message: 'Not enough tokens' });
}
};
useEffect(() => {
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
}, []);
return (
<Stack gap={2}>
<Box display="flex" gap={2} justifyContent="center" sx={{ mt: 2 }}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => {
setValue('amount', newValue, { shouldValidate: true });
}}
validationError={errors.amount?.amount?.message}
denom={denom}
initialValue={amountData.amount.amount}
/>
</Box>
<TextField
{...register('profitMargin')}
name="profitMargin"
label="Profit margin"
error={Boolean(errors.profitMargin)}
helperText={errors.profitMargin?.message}
/>
</Stack>
);
};
export const BondMixnodeForm = ({
step,
denom,
mixnodeData,
amountData,
hasVestingTokens,
onValidateMixnodeData,
onValidateAmountData,
onSelectNodeType,
}: {
step: 1 | 2 | 3;
mixnodeData: MixnodeData;
amountData: MixnodeAmount;
denom: CurrencyDenom;
hasVestingTokens: boolean;
onValidateMixnodeData: (data: MixnodeData) => void;
onValidateAmountData: (data: MixnodeAmount) => Promise<void>;
onSelectNodeType: (nodeType: TNodeType) => void;
}) => (
<>
{step === 1 && (
<>
<Box sx={{ mb: 2 }}>
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="mixnode" />
</Box>
<NodeFormData onNext={onValidateMixnodeData} mixnodeData={mixnodeData} />
</>
)}
{step === 2 && (
<AmountFormData
denom={denom}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onNext={onValidateAmountData}
/>
)}
</>
);
@@ -1,59 +0,0 @@
import * as Yup from 'yup';
import {
isValidHostname,
validateAmount,
validateKey,
validateLocation,
validateRawPort,
validateVersion,
} from 'src/utils';
export const gatewayValidationSchema = Yup.object().shape({
identityKey: Yup.string()
.required('An indentity key is required')
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
sphinxKey: Yup.string()
.required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
ownerSignature: Yup.string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
host: Yup.string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: Yup.string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
location: Yup.string()
.required('A location is required')
.test('valid-location', 'A valid version is required', (locationValueTest) =>
locationValueTest ? validateLocation(locationValueTest) : false,
),
mixPort: Yup.number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
clientsPort: Yup.number()
.required('A clients port is required')
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
});
export const amountSchema = Yup.object().shape({
amount: Yup.object().shape({
amount: Yup.string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
});
@@ -1,51 +0,0 @@
import * as Yup from 'yup';
import { isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils';
export const mixnodeValidationSchema = Yup.object().shape({
identityKey: Yup.string()
.required('An identity key is required')
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
sphinxKey: Yup.string()
.required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
ownerSignature: Yup.string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
host: Yup.string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: Yup.string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
mixPort: Yup.number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
verlocPort: Yup.number()
.required('A verloc port is required')
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
httpApiPort: Yup.number()
.required('A http-api port is required')
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
});
export const amountSchema = Yup.object().shape({
amount: Yup.object().shape({
amount: Yup.string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100),
});
@@ -1,161 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box } from '@mui/material';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests';
import { TBondGatewayArgs } from 'src/types';
import { BondGatewayForm } from '../forms/BondGatewayForm';
const defaultMixnodeValues: GatewayData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
location: '',
host: '',
version: '',
mixPort: 1789,
clientsPort: 1790,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
tokenPool: 'balance',
});
export const BondGatewayModal = ({
denom,
hasVestingTokens,
onBondGateway,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultMixnodeValues);
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
const { fee, getFee, resetFeeState, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const validateStep = async (s: number) => {
const event = new CustomEvent('validate_bond_gateway_step', { detail: { step: s } });
window.dispatchEvent(event);
};
const handleBack = () => {
setStep(1);
};
const handleUpdateGatwayData = (data: GatewayData) => {
setGatewayData(data);
setStep(2);
};
const handleUpdateAmountData = async (data: GatewayAmount) => {
setAmountData(data);
const payload = {
pledge: data.amount,
ownerSignature: gatewayData.ownerSignature,
gateway: {
...gatewayData,
host: gatewayData.host,
version: gatewayData.version,
mix_port: gatewayData.mixPort,
clients_port: gatewayData.clientsPort,
sphinx_key: gatewayData.sphinxKey,
identity_key: gatewayData.identityKey,
location: gatewayData.location,
},
};
if (data.tokenPool === 'balance') {
await getFee<TBondGatewayArgs>(simulateBondGateway, payload);
} else {
await getFee<TBondGatewayArgs>(simulateVestingBondGateway, payload);
}
};
const handleConfirm = async () => {
await onBondGateway(
{
pledge: amountData.amount,
ownerSignature: gatewayData.ownerSignature,
gateway: {
...gatewayData,
host: gatewayData.host,
version: gatewayData.version,
mix_port: gatewayData.mixPort,
clients_port: gatewayData.clientsPort,
sphinx_key: gatewayData.sphinxKey,
identity_key: gatewayData.identityKey,
location: gatewayData.location,
},
},
amountData.tokenPool as TPoolOption,
);
};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={gatewayData.identityKey} divider />
<ModalListItem
label="Amount"
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
divider
/>
</ConfirmTx>
);
}
return (
<SimpleModal
open
onOk={async () => {
await validateStep(step);
}}
onBack={step === 2 ? handleBack : undefined}
onClose={onClose}
header="Bond gateway"
subHeader={`Step ${step}/2`}
okLabel="Next"
>
<Box sx={{ mb: 2 }}>
<BondGatewayForm
step={step}
denom={denom}
gatewayData={gatewayData}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onValidateGatewayData={handleUpdateGatwayData}
onValidateAmountData={handleUpdateAmountData}
onSelectNodeType={onSelectNodeType}
/>
</Box>
</SimpleModal>
);
};
@@ -1,160 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box } from '@mui/material';
import { CurrencyDenom, TNodeType } from '@nymproject/types';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TPoolOption } from 'src/components/TokenPoolSelector';
import { useGetFee } from 'src/hooks/useGetFee';
import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types';
import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests';
import { TBondMixNodeArgs } from 'src/types';
import { BondMixnodeForm } from '../forms/BondMixnodeForm';
const defaultMixnodeValues: MixnodeData = {
identityKey: '',
sphinxKey: '',
ownerSignature: '',
host: '',
version: '',
mixPort: 1789,
verlocPort: 1790,
httpApiPort: 8000,
};
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
profitMargin: 10,
tokenPool: 'balance',
});
export const BondMixnodeModal = ({
denom,
hasVestingTokens,
onBondMixnode,
onSelectNodeType,
onClose,
onError,
}: {
denom: CurrencyDenom;
hasVestingTokens: boolean;
onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void;
onSelectNodeType: (type: TNodeType) => void;
onClose: () => void;
onError: (e: string) => void;
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
const [mixnodeData, setMixnodeData] = useState<MixnodeData>(defaultMixnodeValues);
const [amountData, setAmountData] = useState<MixnodeAmount>(defaultAmountValues(denom));
const { fee, getFee, resetFeeState, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
const validateStep = async (s: number) => {
const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } });
window.dispatchEvent(event);
};
const handleBack = () => {
setStep(1);
};
const handleUpdateMixnodeData = (data: MixnodeData) => {
setMixnodeData(data);
setStep(2);
};
const handleUpdateAmountData = async (data: MixnodeAmount) => {
setAmountData(data);
const payload = {
pledge: data.amount,
ownerSignature: mixnodeData.ownerSignature,
mixnode: {
...mixnodeData,
mix_port: mixnodeData.mixPort,
http_api_port: mixnodeData.httpApiPort,
verloc_port: mixnodeData.verlocPort,
sphinx_key: mixnodeData.sphinxKey,
identity_key: mixnodeData.identityKey,
profit_margin_percent: data.profitMargin,
},
};
if (data.tokenPool === 'balance') {
await getFee<TBondMixNodeArgs>(simulateBondMixnode, payload);
} else {
await getFee<TBondMixNodeArgs>(simulateVestingBondMixnode, payload);
}
};
const handleConfirm = async () => {
await onBondMixnode(
{
pledge: amountData.amount,
ownerSignature: mixnodeData.ownerSignature,
mixnode: {
...mixnodeData,
mix_port: mixnodeData.mixPort,
http_api_port: mixnodeData.httpApiPort,
verloc_port: mixnodeData.verlocPort,
sphinx_key: mixnodeData.sphinxKey,
identity_key: mixnodeData.identityKey,
profit_margin_percent: amountData.profitMargin,
},
},
amountData.tokenPool as TPoolOption,
);
};
if (fee) {
return (
<ConfirmTx
open
header="Bond details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleConfirm}
>
<ModalListItem label="Node identity key" value={mixnodeData.identityKey} divider />
<ModalListItem
label="Amount"
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
divider
/>
</ConfirmTx>
);
}
return (
<SimpleModal
open
onOk={async () => {
await validateStep(step);
}}
onBack={step === 2 ? handleBack : undefined}
onClose={onClose}
header="Bond mixnode"
subHeader={`Step ${step}/2`}
okLabel="Next"
>
<Box sx={{ mb: 2 }}>
<BondMixnodeForm
step={step}
denom={denom}
mixnodeData={mixnodeData}
amountData={amountData}
hasVestingTokens={hasVestingTokens}
onValidateMixnodeData={handleUpdateMixnodeData}
onValidateAmountData={handleUpdateAmountData}
onSelectNodeType={onSelectNodeType}
/>
</Box>
</SimpleModal>
);
};
@@ -1,115 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box, FormHelperText, Stack, TextField } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { DecCoin } from '@nymproject/types';
import { TokenPoolSelector, TPoolOption } from 'src/components/TokenPoolSelector';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { validateAmount, validateKey } from 'src/utils';
export const BondMoreModal = ({
currentBond,
userBalance,
hasVestingTokens,
onConfirm,
onClose,
}: {
currentBond: DecCoin;
userBalance?: string;
hasVestingTokens: boolean;
onConfirm: (args: { additionalBond: DecCoin; signature: string; tokenPool: TPoolOption }) => Promise<void>;
onClose: () => void;
}) => {
const { fee, resetFeeState } = useGetFee();
const [additionalBond, setAdditionalBond] = useState<DecCoin>({ amount: '0', denom: currentBond.denom });
const [signature, setSignature] = useState<string>('');
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [errorAmount, setErrorAmount] = useState(false);
const [errorSignature, setErrorSignature] = useState(false);
const handleOnOk = async () => {
const errors = {
amount: false,
signature: false,
};
if (!validateKey(signature || '', 64)) {
errors.signature = true;
}
if (!additionalBond?.amount) {
errors.amount = true;
}
if (additionalBond && !(await validateAmount(additionalBond.amount, '1'))) {
errors.amount = true;
}
if (!errors.amount && !errors.signature) {
onConfirm({ additionalBond, signature, tokenPool });
} else {
setErrorAmount(errors.amount);
setErrorSignature(errors.signature);
}
};
useEffect(() => {
setErrorAmount(false);
}, [additionalBond]);
if (fee)
return (
<ConfirmTx
header="Bond more details"
open
fee={fee}
onConfirm={async () => onConfirm({ additionalBond, signature, tokenPool })}
onPrev={resetFeeState}
>
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Additional bond" value={`${additionalBond?.amount} ${additionalBond?.denom}`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
header="Bond more"
subHeader="Bond more tokens on your node and receive more rewards"
okLabel="Next"
onOk={handleOnOk}
okDisabled={errorAmount || errorSignature}
onClose={onClose}
>
<Stack gap={2}>
<Box display="flex" gap={1}>
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
<CurrencyFormField
autoFocus
label="Bond amount"
denom={currentBond.denom}
onChanged={(value) => {
setAdditionalBond(value);
setErrorSignature(false);
}}
fullWidth
validationError={errorAmount ? 'Please enter a valid amount' : undefined}
/>
</Box>
<Box>
<TextField fullWidth label="Signature" value={signature} onChange={(e) => setSignature(e.target.value)} />
{errorSignature && <FormHelperText sx={{ color: 'error.main' }}>Invalid signature</FormHelperText>}
</Box>
<Box>
<ModalListItem label="Account balance" value={userBalance?.toUpperCase() || '-'} divider />
<ModalListItem label="Current bond" value={`${currentBond.amount} ${currentBond.denom}`} divider />
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
</Box>
</Stack>
</SimpleModal>
);
};
@@ -1,53 +0,0 @@
import React, { useEffect } from 'react';
import { FeeDetails } from '@nymproject/types';
import { ModalFee } from 'src/components/Modals/ModalFee';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateCompoundOperatorReward, simulateVestingCompoundOperatorReward } from 'src/requests';
export const CompoundRewardsModal = ({
node,
onConfirm,
onClose,
onError,
}: {
node: TBondedMixnode;
onClose: () => void;
onConfirm: (fee?: FeeDetails) => void;
onError: (err: string) => void;
}) => {
const { fee, getFee, feeError, isFeeLoading } = useGetFee();
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
useEffect(() => {
if (node.proxy) getFee(simulateVestingCompoundOperatorReward, {});
else getFee(simulateCompoundOperatorReward, {});
}, []);
const handleOnOK = async () => onConfirm(fee);
return (
<SimpleModal
open
header="Compound rewards"
subHeader="Get more rewards by compounding"
okLabel="Compound"
okDisabled={isFeeLoading}
onOk={handleOnOK}
onClose={onClose}
>
<ModalListItem
label="Rewards to redeem"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
<ModalFee fee={fee} isLoading={isFeeLoading} divider />
<ModalListItem label="Rewards will be transferred to the account you are logged in with" value="" />
</SimpleModal>
);
};
@@ -1,52 +0,0 @@
import React from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal } from 'src/components/Modals/ConfirmationModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
export type ConfirmationDetailProps = {
status: 'success' | 'error';
title: string;
subtitle?: string;
txUrl?: string;
};
export const ConfirmationDetailsModal = ({
title,
subtitle,
txUrl,
status,
onClose,
sx,
backdropProps,
}: ConfirmationDetailProps & {
onClose: () => void;
sx?: SxProps;
backdropProps?: object;
}) => {
if (status === 'error') {
<ErrorModal open message={subtitle} onClose={onClose} />;
}
return (
<ConfirmationModal
open
onConfirm={onClose}
onClose={onClose}
title=""
confirmButton="Done"
maxWidth="xs"
fullWidth
sx={sx}
backdropProps={backdropProps}
>
<Stack alignItems="center" spacing={2}>
<Typography variant="h6" fontWeight={600}>
{title}
</Typography>
<Typography>{subtitle}</Typography>
{txUrl && <Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />}
</Stack>
</ConfirmationModal>
);
};
@@ -1,128 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { Node as NodeIcon } from 'src/svg-icons/node';
import { TBondedMixnode } from 'src/context';
import { Tabs } from 'src/components/Tabs';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { isDecimal } from 'src/utils';
import { useGetFee } from 'src/hooks/useGetFee';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { simulateUpdateMixnode, simulateVestingUpdateMixnode } from 'src/requests';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { FeeDetails } from '@nymproject/types';
export const NodeSettings = ({
currentPm,
isVesting,
onConfirm,
onClose,
onError,
}: {
currentPm: TBondedMixnode['profitMargin'];
isVesting: boolean;
onConfirm: (profitMargin: number, fee?: FeeDetails) => Promise<void>;
onClose: () => void;
onError: (err: string) => void;
}) => {
const [pm, setPm] = useState(currentPm.toString());
const [error, setError] = useState(false);
const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee();
const handleValidate = async () => {
let isValid = true;
const pmAsNumber = Number(pm);
if (!pmAsNumber) {
isValid = false;
}
if (isDecimal(pmAsNumber)) {
isValid = false;
}
if (pmAsNumber > 100) {
isValid = false;
}
if (pmAsNumber < 0) {
isValid = false;
}
if (!isValid) {
setError(true);
return;
}
if (isVesting) {
await getFee(simulateVestingUpdateMixnode, { profitMarginPercent: pmAsNumber });
} else {
await getFee(simulateUpdateMixnode, { profitMarginPercent: pmAsNumber });
}
};
useEffect(() => {
setError(false);
}, [pm]);
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
if (isFeeLoading) return <LoadingModal />;
if (fee)
return (
<ConfirmTx
open
header="Profit margin change"
fee={fee}
onPrev={resetFeeState}
onClose={onClose}
onConfirm={() => onConfirm(Number(pm), fee)}
>
<ModalListItem label="Current profit margin" value={`${currentPm}%`} divider />
<ModalListItem label="New profit margin" value={`${pm}%`} divider />
</ConfirmTx>
);
return (
<SimpleModal
open
hideCloseIcon
sx={{ p: 0 }}
header={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 3 }}>
<NodeIcon />
<Typography variant="h6" fontWeight={600}>
Node Settings
</Typography>
</Box>
}
okLabel="Next"
onClose={onClose}
>
<Tabs tabs={['System variables']} selectedTab={0} disableActiveTabHighlight />
<Box sx={{ p: 3 }}>
<Typography fontWeight={600} sx={{ mb: 1 }}>
Set profit margin
</Typography>
<Box sx={{ mb: 3 }}>
<TextField placeholder="Profit margin" value={pm} onChange={(e) => setPm(e.target.value)} fullWidth />
{error && (
<FormHelperText sx={{ color: 'error.main' }}>
Profit margin should be a whole number between 0 and 100
</FormHelperText>
)}
<FormHelperText>Your new profit margin will be applied in the next epoch</FormHelperText>
</Box>
<Box sx={{ mb: 3 }}>
<ModalListItem label="Est. fee for this operation will be caculated in the next page" value="" />
</Box>
<Button variant="contained" fullWidth size="large" onClick={handleValidate} disabled={error}>
Next
</Button>
</Box>
</SimpleModal>
);
};
@@ -1,53 +0,0 @@
import React, { useEffect } from 'react';
import { FeeDetails } from '@nymproject/types';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { ModalFee } from 'src/components/Modals/ModalFee';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests';
import { TBondedMixnode } from 'src/context';
export const RedeemRewardsModal = ({
node,
onConfirm,
onError,
onClose,
}: {
node: TBondedMixnode;
onConfirm: (fee?: FeeDetails) => Promise<void>;
onError: (err: string) => void;
onClose: () => void;
}) => {
const { fee, getFee, isFeeLoading, feeError } = useGetFee();
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
useEffect(() => {
if (node.proxy) getFee(simulateVestingClaimOperatorReward, {});
else getFee(simulateClaimOperatorReward, {});
}, []);
const handleOnOK = async () => onConfirm(fee);
return (
<SimpleModal
open
header="Redeem rewards"
subHeader="Claim you rewards"
okLabel="Redeem"
okDisabled={isFeeLoading}
onOk={handleOnOK}
onClose={onClose}
>
<ModalListItem
label="Rewards to redeem"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
<ModalFee fee={fee} isLoading={isFeeLoading} divider />
<ModalListItem label="Rewards will be transferred to the account you are logged in with" value="" />
</SimpleModal>
);
};
@@ -1,72 +0,0 @@
import * as React from 'react';
import { Typography } from '@mui/material';
import { useEffect } from 'react';
import { TBondedGateway, TBondedMixnode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { isGateway, isMixnode } from 'src/types';
import { ModalFee } from '../../Modals/ModalFee';
import { ModalListItem } from '../../Modals/ModalListItem';
import { SimpleModal } from '../../Modals/SimpleModal';
import {
simulateUnbondGateway,
simulateUnbondMixnode,
simulateVestingUnbondGateway,
simulateVestingUnbondMixnode,
} from '../../../requests';
interface Props {
node: TBondedMixnode | TBondedGateway;
onConfirm: () => Promise<void>;
onClose: () => void;
onError: (e: string) => void;
}
export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
const { fee, isFeeLoading, getFee, feeError } = useGetFee();
useEffect(() => {
if (feeError) {
onError(feeError);
}
}, [feeError]);
useEffect(() => {
if (isMixnode(node) && !node.proxy) {
getFee(simulateUnbondMixnode, {});
}
if (isMixnode(node) && node.proxy) {
getFee(simulateVestingUnbondMixnode, {});
}
if (isGateway(node) && !node.proxy) {
getFee(simulateUnbondGateway, {});
}
if (isGateway(node) && node.proxy) {
getFee(simulateVestingUnbondGateway, {});
}
}, [node]);
return (
<SimpleModal
open
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
onOk={onConfirm}
onClose={onClose}
>
<ModalListItem label="Amount to unbond" value={`${node.bond.amount} ${node.bond.denom.toUpperCase()}`} divider />
{isMixnode(node) && (
<ModalListItem
label="Operator rewards"
value={`${node.operatorRewards.amount} ${node.operatorRewards.denom.toUpperCase()}`}
divider
/>
)}
<ModalFee isLoading={isFeeLoading} fee={fee} divider />
<Typography fontSize="small">Tokens will be transferred to the account you are logged in with now</Typography>
</SimpleModal>
);
};
@@ -2,19 +2,16 @@ import React, { useEffect, useState } from 'react';
import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material'; import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material';
import { useKeyPress } from 'src/hooks/useKeyPress'; import { useKeyPress } from 'src/hooks/useKeyPress';
import { PasswordInput } from './textfields'; import { PasswordInput } from './textfields';
import { StyledBackButton } from './StyledBackButton';
export const ConfirmPassword = ({ export const ConfirmPassword = ({
error, error,
isLoading, isLoading,
onConfirm, onConfirm,
onCancel,
buttonTitle, buttonTitle,
}: { }: {
error?: string; error?: string;
isLoading?: boolean; isLoading?: boolean;
buttonTitle: string; buttonTitle: string;
onCancel?: () => void;
onConfirm: (password: string) => void; onConfirm: (password: string) => void;
}) => { }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
@@ -42,8 +39,7 @@ export const ConfirmPassword = ({
disabled={isLoading} disabled={isLoading}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0, gap: 2 }}> <DialogActions sx={{ p: 3, pt: 0 }}>
{onCancel && <StyledBackButton onBack={onCancel} />}
<Button <Button
disabled={!value.length || isLoading} disabled={!value.length || isLoading}
fullWidth fullWidth
@@ -10,9 +10,9 @@ export default {
const Template: ComponentStory<typeof ConfirmTx> = (args) => ( const Template: ComponentStory<typeof ConfirmTx> = (args) => (
<ConfirmTx {...args}> <ConfirmTx {...args}>
<ModalListItem label="Transaction type:" value="Bond" divider /> <ModalListItem label="Transaction type" value="Bond" divider />
<ModalListItem label="Current bond:" value="100 NYM" divider /> <ModalListItem label="Current bond" value="100 NYM" divider />
<ModalListItem label="Additional bond:" value="50 NYM" divider /> <ModalListItem label="Additional bond" value="50 NYM" divider />
</ConfirmTx> </ConfirmTx>
); );
@@ -36,7 +36,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
color: 'text.primary', color: 'text.primary',
}} }}
> >
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />} {!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
@@ -13,13 +13,12 @@ import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector';
import { ConfirmTx } from '../ConfirmTX'; import { ConfirmTx } from '../ConfirmTX';
import { getMixnodeStakeSaturation } from '../../requests'; import { getMixnodeStakeSaturation } from '../../requests';
import { ErrorModal } from '../Modals/ErrorModal';
const MIN_AMOUNT_TO_DELEGATE = 10; const MIN_AMOUNT_TO_DELEGATE = 10;
export const DelegateModal: React.FC<{ export const DelegateModal: React.FC<{
open: boolean; open: boolean;
onClose: () => void; onClose?: () => void;
onOk?: (identityKey: string, amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails) => Promise<void>; onOk?: (identityKey: string, amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails) => Promise<void>;
identityKey?: string; identityKey?: string;
onIdentityKeyChanged?: (identityKey: string) => void; onIdentityKeyChanged?: (identityKey: string) => void;
@@ -63,7 +62,7 @@ export const DelegateModal: React.FC<{
const [tokenPool, setTokenPool] = useState<TPoolOption>('balance'); const [tokenPool, setTokenPool] = useState<TPoolOption>('balance');
const [errorIdentityKey, setErrorIdentityKey] = useState<string>(); const [errorIdentityKey, setErrorIdentityKey] = useState<string>();
const { fee, getFee, resetFeeState, feeError } = useGetFee(); const { fee, getFee, resetFeeState } = useGetFee();
const handleCheckStakeSaturation = async (identity: string) => { const handleCheckStakeSaturation = async (identity: string) => {
try { try {
@@ -169,24 +168,12 @@ export const DelegateModal: React.FC<{
onPrev={resetFeeState} onPrev={resetFeeState}
onConfirm={handleOk} onConfirm={handleOk}
> >
<ModalListItem label="Node identity key:" value={identityKey} divider /> <ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount:" value={`${amount} ${denom.toUpperCase()}`} divider /> <ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
</ConfirmTx> </ConfirmTx>
); );
} }
if (feeError) {
return (
<ErrorModal
title="Something went wrong while calculating fee. Are you sure you entered a valid node address?"
message={feeError}
sx={sx}
open={open}
onClose={onClose}
/>
);
}
return ( return (
<SimpleModal <SimpleModal
open={open} open={open}
@@ -197,24 +184,23 @@ export const DelegateModal: React.FC<{
} }
}} }}
header={header || 'Delegate'} header={header || 'Delegate'}
subHeader="Delegate to mixnode"
okLabel={buttonText || 'Delegate stake'} okLabel={buttonText || 'Delegate stake'}
okDisabled={!isValidated} okDisabled={!isValidated}
sx={sx} sx={sx}
backdropProps={backdropProps} backdropProps={backdropProps}
> >
<Box sx={{ mt: 2 }}> <IdentityKeyFormField
<IdentityKeyFormField required
required fullWidth
fullWidth placeholder="Node identity key"
label="Node identity key" onChanged={handleIdentityKeyChanged}
onChanged={handleIdentityKeyChanged} initialValue={identityKey}
initialValue={identityKey} readOnly={Boolean(initialIdentityKey)}
readOnly={Boolean(initialIdentityKey)} textFieldProps={{
textFieldProps={{ autoFocus: !initialIdentityKey,
autoFocus: !initialIdentityKey, }}
}} />
/>
</Box>
<Typography <Typography
component="div" component="div"
textAlign="left" textAlign="left"
@@ -228,7 +214,7 @@ export const DelegateModal: React.FC<{
<CurrencyFormField <CurrencyFormField
required required
fullWidth fullWidth
label="Amount" placeholder="Amount"
initialValue={amount} initialValue={amount}
autoFocus={Boolean(initialIdentityKey)} autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged} onChanged={handleAmountChanged}
@@ -244,7 +230,7 @@ export const DelegateModal: React.FC<{
{errorAmount} {errorAmount}
</Typography> </Typography>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance?.toUpperCase()} divider fontWeight={600} /> <ModalListItem label="Account balance" value={accountBalance} divider />
</Box> </Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider /> <ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
@@ -255,7 +241,7 @@ export const DelegateModal: React.FC<{
divider divider
/> />
<ModalListItem <ModalListItem
label="Node avg. uptime" label="Node uptime"
value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`} value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`}
hidden={nodeUptimePercentage === undefined} hidden={nodeUptimePercentage === undefined}
divider divider
@@ -267,7 +253,6 @@ export const DelegateModal: React.FC<{
hidden hidden
divider divider
/> />
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
</SimpleModal> </SimpleModal>
); );
}; };
@@ -1,8 +1,19 @@
import React, { useState } from 'react'; import React from 'react';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import { MoreVertSharp } from '@mui/icons-material';
import { DelegationEventKind } from '@nymproject/types'; import { DelegationEventKind } from '@nymproject/types';
import { Delegate, Undelegate } from '../../svg-icons'; import { Delegate, Undelegate } from '../../svg-icons';
import { ActionsMenu, ActionsMenuItem } from '../ActionsMenu';
import { DelegateListItemPending } from './types'; import { DelegateListItemPending } from './types';
export type DelegationListItemActions = 'delegate' | 'undelegate' | 'redeem' | 'compound'; export type DelegationListItemActions = 'delegate' | 'undelegate' | 'redeem' | 'compound';
@@ -64,20 +75,42 @@ export const DelegationActions: React.FC<{
); );
}; };
const DelegationActionsMenuItem = ({
title,
description,
onClick,
Icon,
disabled,
}: {
title: string;
description?: string;
onClick?: () => void;
Icon?: React.ReactNode;
disabled?: boolean;
}) => (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
export const DelegationsActionsMenu: React.FC<{ export const DelegationsActionsMenu: React.FC<{
onActionClick?: (action: DelegationListItemActions) => void; onActionClick?: (action: DelegationListItemActions) => void;
isPending?: DelegationEventKind; isPending?: DelegationEventKind;
disableRedeemingRewards?: boolean; disableRedeemingRewards?: boolean;
disableCompoundRewards?: boolean; disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => { }> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
const [isOpen, setIsOpen] = useState(false); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleOpenMenu = () => setIsOpen(true); const handleClose = () => setAnchorEl(null);
const handleOnClose = () => setIsOpen(false);
const handleActionSelect = (action: DelegationListItemActions) => { const handleActionSelect = (action: DelegationListItemActions) => {
handleClose();
onActionClick?.(action); onActionClick?.(action);
handleOnClose();
}; };
if (isPending) { if (isPending) {
@@ -93,23 +126,37 @@ export const DelegationsActionsMenu: React.FC<{
} }
return ( return (
<ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}> <>
<ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} /> <IconButton onClick={handleClick}>
<ActionsMenuItem title="Undelegate" Icon={<Undelegate />} onClick={() => handleActionSelect('undelegate')} /> <MoreVertSharp />
<ActionsMenuItem </IconButton>
title="Redeem" <Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
description="Trasfer your rewards to your balance" <DelegationActionsMenuItem
Icon={<Typography sx={{ pl: 1 }}>R</Typography>} title="Delegate more"
onClick={() => handleActionSelect('redeem')} Icon={<Delegate />}
disabled={disableRedeemingRewards} onClick={() => handleActionSelect?.('delegate')}
/> />
<ActionsMenuItem <DelegationActionsMenuItem
title="Compound" title="Undelegate"
description="Add your rewards to this delegation" Icon={<Undelegate />}
Icon={<Typography sx={{ pl: 1 }}>C</Typography>} onClick={() => handleActionSelect?.('undelegate')}
onClick={() => handleActionSelect('compound')} disabled={false}
disabled={disableCompoundRewards} />
/> <DelegationActionsMenuItem
</ActionsMenu> title="Redeem"
description="Transfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect?.('redeem')}
disabled={disableRedeemingRewards}
/>
<DelegationActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect?.('compound')}
disabled={disableCompoundRewards}
/>
</Menu>
</>
); );
}; };
@@ -29,6 +29,9 @@ const transactionForDarkTheme = {
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO', url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO',
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0', hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0',
}; };
const balance = '104 NYMT';
const balanceVested = '12 NYMT';
const recipient = 'nymt1923pujepxfnv8dqyxqrl078s4ysf3xn2p7z2xa';
const Content: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({ const Content: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
children, children,
@@ -75,6 +78,8 @@ export const DelegateSuccess = () => {
status="success" status="success"
action="delegate" action="delegate"
message="You delegated 5 NYM" message="You delegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]} transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)} {...storybookStyles(theme)}
/> />
@@ -94,6 +99,8 @@ export const UndelegateSuccess = () => {
status="success" status="success"
action="undelegate" action="undelegate"
message="You undelegated 5 NYM" message="You undelegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]} transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)} {...storybookStyles(theme)}
/> />
@@ -113,6 +120,8 @@ export const RedeemSuccess = () => {
status="success" status="success"
action="redeem" action="redeem"
message="42 NYM" message="42 NYM"
recipient={recipient}
balance={balance}
transactions={ transactions={
theme.palette.mode === 'light' theme.palette.mode === 'light'
? [transaction, transaction] ? [transaction, transaction]
@@ -136,6 +145,9 @@ export const RedeemWithVestedSuccess = () => {
status="success" status="success"
action="redeem" action="redeem"
message="42 NYM" message="42 NYM"
recipient={recipient}
balance={balance}
balanceVested={balanceVested}
transactions={ transactions={
theme.palette.mode === 'light' theme.palette.mode === 'light'
? [transaction, transaction] ? [transaction, transaction]
@@ -159,6 +171,8 @@ export const RedeemAllSuccess = () => {
status="success" status="success"
action="redeem-all" action="redeem-all"
message="42 NYM" message="42 NYM"
recipient={recipient}
balance={balance}
transactions={ transactions={
theme.palette.mode === 'light' theme.palette.mode === 'light'
? [transaction, transaction] ? [transaction, transaction]
@@ -182,6 +196,8 @@ export const Error = () => {
status="error" status="error"
action="redeem-all" action="redeem-all"
message="Minim esse veniam Lorem id velit Lorem eu eu est. Excepteur labore sunt do proident proident sint aliquip consequat Lorem sint non nulla ad excepteur." message="Minim esse veniam Lorem id velit Lorem eu eu est. Excepteur labore sunt do proident proident sint aliquip consequat Lorem sint non nulla ad excepteur."
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]} transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)} {...storybookStyles(theme)}
/> />
@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { Typography, SxProps, Stack } from '@mui/material'; import { Box, Button, Modal, Typography, SxProps } from '@mui/material';
import { Link } from '@nymproject/react/link/Link'; import { Link } from '@nymproject/react/link/Link';
import { Console } from 'src/utils/console'; import { Console } from 'src/utils/console';
import { modalStyle } from '../Modals/styles';
import { LoadingModal } from '../Modals/LoadingModal'; import { LoadingModal } from '../Modals/LoadingModal';
import { ConfirmationModal } from '../Modals/ConfirmationModal';
import { ErrorModal } from '../Modals/ErrorModal';
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound'; export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
@@ -16,9 +15,9 @@ const actionToHeader = (action: ActionType): string => {
case 'redeem-all': case 'redeem-all':
return 'All rewards redeemed successfully'; return 'All rewards redeemed successfully';
case 'delegate': case 'delegate':
return 'Delegation successful'; return 'Delegation complete';
case 'undelegate': case 'undelegate':
return 'Undelegation successful'; return 'Undelegation complete';
case 'compound': case 'compound':
return 'Rewards compounded successfully'; return 'Rewards compounded successfully';
default: default:
@@ -30,6 +29,9 @@ export type DelegationModalProps = {
status: 'loading' | 'success' | 'error'; status: 'loading' | 'success' | 'error';
action: ActionType; action: ActionType;
message?: string; message?: string;
recipient?: string;
balance?: string;
balanceVested?: string;
transactions?: { transactions?: {
url: string; url: string;
hash: string; hash: string;
@@ -39,44 +41,94 @@ export type DelegationModalProps = {
export const DelegationModal: React.FC< export const DelegationModal: React.FC<
DelegationModalProps & { DelegationModalProps & {
open: boolean; open: boolean;
onClose: () => void; onClose?: () => void;
sx?: SxProps; sx?: SxProps;
backdropProps?: object; backdropProps?: object;
} }
> = ({ status, action, message, transactions, open, onClose, children, sx, backdropProps }) => { > = ({
status,
action,
message,
recipient,
balance,
balanceVested,
transactions,
open,
onClose,
children,
sx,
backdropProps,
}) => {
if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />; if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />;
if (status === 'error') { if (status === 'error') {
return ( return (
<ErrorModal message={message} sx={sx} open={open} onClose={onClose}> <Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
{children} <Box sx={{ ...modalStyle, ...sx }} textAlign="center">
</ErrorModal> <Typography color={(theme) => theme.palette.error.main} mb={1}>
Oh no! Something went wrong...
</Typography>
<Typography my={5} color="text.primary">
{message}
</Typography>
{children}
<Button variant="contained" onClick={onClose}>
Close
</Button>
</Box>
</Modal>
); );
} }
transactions?.map((transaction) => Console.log('action', action, 'status', status, 'key', transaction.hash)); transactions?.map((transaction) => Console.log('action', action, 'status', status, 'key', transaction.hash));
return ( return (
<ConfirmationModal <Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
open={open} <Box sx={{ ...modalStyle, ...sx }} textAlign="center">
onConfirm={onClose || (() => {})} <Typography color={(theme) => theme.palette.success.main} mb={1}>
title={actionToHeader(action)} {actionToHeader(action)}
confirmButton="Done" </Typography>
> <Typography mb={3} color="text.primary">
<Stack alignItems="center" spacing={2} mb={0}> {message}
{message && <Typography>{message}</Typography>} </Typography>
{transactions?.length === 1 && (
<Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon /> {recipient && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Recipient: {recipient}
</Typography>
)} )}
{transactions && transactions.length > 1 && ( {balanceVested ? (
<Stack alignItems="center" spacing={1}> <>
<Typography>View the transactions on blockchain:</Typography> <Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
{transactions.map(({ url, hash }) => ( Your current balance: {balance?.toUpperCase()}
<Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon /> </Typography>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
({balanceVested.toUpperCase()} is unlocked in your vesting account)
</Typography>
</>
) : (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Your current balance: {balance?.toUpperCase()}
</Typography>
)}
{transactions && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Check the transaction {transactions.length > 1 ? 'hashes' : 'hash'}:
{transactions.map((transaction) => (
<Link
key={transaction.hash}
href={transaction.url}
target="_blank"
sx={{ ml: 1 }}
text={transaction.hash.slice(0, 6)}
/>
))} ))}
</Stack> </Typography>
)} )}
</Stack> {children}
</ConfirmationModal> <Button variant="contained" sx={{ mt: 3 }} size="large" onClick={onClose}>
Finish
</Button>
</Box>
</Modal>
); );
}; };
@@ -40,6 +40,7 @@ export const UndelegateModal: React.FC<{
onClose={onClose} onClose={onClose}
onOk={handleOk} onOk={handleOk}
header="Undelegate" header="Undelegate"
subHeader="Undelegate from mixnode"
okLabel="Undelegate stake" okLabel="Undelegate stake"
okDisabled={!fee} okDisabled={!fee}
sx={sx} sx={sx}
@@ -54,10 +55,14 @@ export const UndelegateModal: React.FC<{
/> />
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<ModalListItem label="Delegation amount" value={`${amount} ${currency.toUpperCase()}`} divider /> <ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider />
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
<ModalListItem label=" Tokens will be transferred to account you are logged in with now" value="" divider />
</Box> </Box>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
Tokens will be transferred to account you are logged in with now
</Typography>
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
</SimpleModal> </SimpleModal>
); );
}; };
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { splice } from 'src/utils';
export const IdentityKey = ({ identityKey }: { identityKey: string }) => (
<Stack direction="row">
<Typography variant="body2" component="span" fontWeight={400} sx={{ mr: 1, color: 'text.primary' }}>
{splice(6, identityKey)}
</Typography>
<CopyToClipboard value={identityKey} sx={{ fontSize: 18 }} />
</Stack>
);
@@ -41,7 +41,7 @@ export const ConfirmationModal = ({
backdropProps, backdropProps,
}: ConfirmationModalProps) => { }: ConfirmationModalProps) => {
const Title = ( const Title = (
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> <DialogTitle id="responsive-dialog-title" sx={{ py: 3, pb: 2, fontWeight: 600 }} color="text.primary">
{title} {title}
{subTitle && {subTitle &&
(typeof subTitle === 'string' ? ( (typeof subTitle === 'string' ? (
@@ -1,27 +0,0 @@
import React from 'react';
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
import { modalStyle } from './styles';
export const ErrorModal: React.FC<{
open: boolean;
title?: string;
message?: string;
sx?: SxProps;
backdropProps?: object;
onClose: () => void;
}> = ({ children, open, title, message, sx, backdropProps, onClose }) => (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
<Typography color={(theme) => theme.palette.error.main} mb={1}>
{title || 'Oh no! Something went wrong...'}
</Typography>
<Typography my={5} color="text.primary">
{message}
</Typography>
{children}
<Button variant="contained" onClick={onClose}>
Close
</Button>
</Box>
</Modal>
);
@@ -2,20 +2,16 @@ import React from 'react';
import { FeeDetails } from '@nymproject/types'; import { FeeDetails } from '@nymproject/types';
import { CircularProgress } from '@mui/material'; import { CircularProgress } from '@mui/material';
import { ModalListItem } from './ModalListItem'; import { ModalListItem } from './ModalListItem';
import { ModalDivider } from './ModalDivider';
type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string; divider?: boolean }; type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string };
const getValue = ({ fee, isLoading, error }: TFeeProps) => { const getValue = ({ fee, isLoading, error }: TFeeProps) => {
if (isLoading) return <CircularProgress size={15} />; if (isLoading) return <CircularProgress size={15} />;
if (error && !isLoading) return 'n/a'; if (error && !isLoading) return 'n/a';
if (fee) return `${fee.amount?.amount} ${fee.amount?.denom.toUpperCase()}`; if (fee) return `${fee.amount?.amount} ${fee.amount?.denom}`;
return '-'; return '-';
}; };
export const ModalFee = ({ fee, isLoading, error, divider }: TFeeProps) => ( export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => (
<> <ModalListItem label="Estimated fee for this operation" value={getValue({ fee, isLoading, error })} />
<ModalListItem label="Fee for this operation" value={getValue({ fee, isLoading, error })} />
{divider && <ModalDivider />}
</>
); );
@@ -1,28 +1,26 @@
import React from 'react'; import React from 'react';
import { Box, Stack, Typography, TypographyProps } from '@mui/material'; import { Box, Stack, Typography } from '@mui/material';
import { ModalDivider } from './ModalDivider'; import { ModalDivider } from './ModalDivider';
import { fontWeight } from '@mui/system';
type TFontWeight = 'strong' | 'light';
export const ModalListItem: React.FC<{ export const ModalListItem: React.FC<{
label: string; label: string;
divider?: boolean; divider?: boolean;
hidden?: boolean; hidden?: boolean;
fontWeight?: TypographyProps['fontWeight']; strong?: boolean;
light?: boolean; value: React.ReactNode;
value?: React.ReactNode; }> = ({ label, value, hidden, divider, strong }) => (
}> = ({ label, value, hidden, fontWeight, divider }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}> <Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between"> <Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary' }}> <Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
{label} {label}:
</Typography>
<Typography
fontSize="smaller"
fontWeight={strong ? 600 : undefined}
sx={{ color: 'text.primary', textTransform: 'uppercase' }}
>
{value}
</Typography> </Typography>
{value && (
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary' }}>
{value}
</Typography>
)}
</Stack> </Stack>
{divider && <ModalDivider />} {divider && <ModalDivider />}
</Box> </Box>
@@ -14,7 +14,7 @@ export const SimpleModal: React.FC<{
onClose?: () => void; onClose?: () => void;
onOk?: () => Promise<void>; onOk?: () => Promise<void>;
onBack?: () => void; onBack?: () => void;
header: string | React.ReactNode; header: string;
subHeader?: string; subHeader?: string;
okLabel: string; okLabel: string;
okDisabled?: boolean; okDisabled?: boolean;
@@ -41,25 +41,22 @@ export const SimpleModal: React.FC<{
<Box sx={{ ...modalStyle, ...sx }}> <Box sx={{ ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Stack direction="row" justifyContent="space-between" alignItems="center">
{typeof header === 'string' ? ( <Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}> {header}
{header} </Typography>
</Typography>
) : (
header
)}
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
</Stack> </Stack>
{subHeader && (
<Typography <Typography
mt={0.5} mt={0.5}
mb={3} mb={3}
fontSize={12} fontSize={12}
color={(theme) => theme.palette.text.secondary} color={(theme) => theme.palette.text.secondary}
sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }} sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }}
> >
{subHeader} {subHeader}
</Typography> </Typography>
)}
{children} {children}
+15 -34
View File
@@ -2,8 +2,8 @@ import React, { useState, useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material'; import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { AppContext } from '../context/main'; import { AppContext } from '../context';
import { Delegate, Bonding } from '../svg-icons'; import { Bond, Delegate, Unbond } from '../svg-icons';
export const Nav = () => { export const Nav = () => {
const location = useLocation(); const location = useLocation();
@@ -29,10 +29,16 @@ export const Nav = () => {
onClick: handleShowReceiveModal, onClick: handleShowReceiveModal,
}, },
{ {
label: 'Bonding', label: 'Bond',
route: '/bonding', route: '/bond',
Icon: Bonding, Icon: Bond,
onClick: () => navigate('/bonding'), onClick: () => navigate('/bond'),
},
{
label: 'Unbond',
route: '/unbond',
Icon: Unbond,
onClick: () => navigate('/unbond'),
}, },
{ {
label: 'Delegation', label: 'Delegation',
@@ -62,16 +68,9 @@ export const Nav = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'flex-start',
marginLeft: 12,
marginRight: 12,
}} }}
> >
<List <List disablePadding>
disablePadding
sx={{
width: '100%',
}}
>
{routesSchema {routesSchema
.filter(({ mode }) => { .filter(({ mode }) => {
if (!mode) { if (!mode) {
@@ -87,35 +86,17 @@ export const Nav = () => {
} }
}) })
.map(({ Icon, onClick, label, route }) => ( .map(({ Icon, onClick, label, route }) => (
<ListItem <ListItem disableGutters key={label} onClick={onClick} sx={{ cursor: 'pointer' }}>
disableGutters
key={label}
onClick={onClick}
sx={{
cursor: 'pointer',
py: 2,
paddingLeft: 3.5,
borderRadius: 1,
'&:hover': { backgroundColor: (theme) => theme.palette.nym.nymWallet.hover.background },
}}
>
<ListItemIcon <ListItemIcon
sx={{ sx={{
height: '20px',
minWidth: 30, minWidth: 30,
color: location.pathname === route ? 'primary.main' : 'text.primary', color: location.pathname === route ? 'primary.main' : 'text.primary',
}} }}
> >
<Icon <Icon sx={{ fontSize: 20 }} />
sx={{
fontSize: 20,
}}
/>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
height: '20px',
margin: 0,
color: location.pathname === route ? 'primary.main' : 'text.primary', color: location.pathname === route ? 'primary.main' : 'text.primary',
'& .MuiListItemText-primary': { '& .MuiListItemText-primary': {
fontSize: 14, fontSize: 14,
@@ -39,7 +39,7 @@ export const NetworkSelector = () => {
<> <>
<Button <Button
variant="text" variant="text"
color="inherit" color="primary"
sx={{ color: 'text.primary' }} sx={{ color: 'text.primary' }}
onClick={handleClick} onClick={handleClick}
disableElevation disableElevation
+1 -1
View File
@@ -26,7 +26,7 @@ export const NymCard: React.FC<{
subheader={subheader} subheader={subheader}
data-testid={dataTestid || title} data-testid={dataTestid || title}
subheaderTypographyProps={{ variant: 'subtitle1' }} subheaderTypographyProps={{ variant: 'subtitle1' }}
action={Action} action={<Box sx={{ mt: 1, mr: 1 }}>{Action}</Box>}
/> />
{noPadding ? ( {noPadding ? (
<CardContentNoPadding>{children}</CardContentNoPadding> <CardContentNoPadding>{children}</CardContentNoPadding>
@@ -1,33 +1,42 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { AppContext } from 'src/context'; import { AppContext } from 'src/context';
import { Box, Stack, Typography, SxProps, Dialog, DialogTitle, DialogContent } from '@mui/material'; import { Box, Stack, Typography, SxProps } from '@mui/material';
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import { SimpleModal } from '../Modals/SimpleModal';
import { ClientAddress } from '../ClientAddress'; import { ClientAddress } from '../ClientAddress';
import { ModalListItem } from '../Modals/ModalListItem';
import { Close as CloseIcon } from '@mui/icons-material';
export const ReceiveModal = ({ onClose }: { onClose: () => void; sx?: SxProps; backdropProps?: object }) => { export const ReceiveModal = ({
onClose,
open,
sx,
backdropProps,
}: {
onClose: () => void;
open: boolean;
sx?: SxProps;
backdropProps?: object;
}) => {
const { clientDetails } = useContext(AppContext); const { clientDetails } = useContext(AppContext);
return ( return (
<Dialog open maxWidth="sm" fullWidth onClose={onClose}> <SimpleModal
<DialogTitle> header="Receive"
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> okLabel="Ok"
<Typography fontSize={20} fontWeight={600}> onClose={onClose}
Receive open={open}
</Typography> sx={{ width: 'small', ...sx }}
<CloseIcon onClick={onClose} cursor="pointer" /> backdropProps={backdropProps}
</Box> >
</DialogTitle> <Stack spacing={3} sx={{ mt: 1.6 }}>
<DialogContent sx={{ p: 0 }}> <Stack direction="row" alignItems="center" gap={4}>
<Box sx={{ px: 3 }}> <Typography>Your address:</Typography>
<ModalListItem label="Your address:" value={<ClientAddress withCopy showEntireAddress />} /> <ClientAddress withCopy showEntireAddress />
</Box> </Stack>
<Stack alignItems="center" sx={{ px: 0, py: 3, mt: 3, bgcolor: 'rgba(251, 110, 78, 5%)' }}> <Stack alignItems="center">
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.highlight}`, bgcolor: 'white', borderRadius: 2, p: 3 }}> <Box sx={{ border: (t) => `1px solid ${t.palette.nym.highlight}`, borderRadius: 2, p: 2 }}>
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />} {clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
</Box> </Box>
</Stack> </Stack>
</DialogContent> </Stack>
</Dialog> </SimpleModal>
); );
}; };
+2 -1
View File
@@ -5,7 +5,8 @@ import { ReceiveModal } from './ReceiveModal';
export const Receive = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => { export const Receive = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => {
const { showReceiveModal, handleShowReceiveModal } = useContext(AppContext); const { showReceiveModal, handleShowReceiveModal } = useContext(AppContext);
if (showReceiveModal) return <ReceiveModal onClose={handleShowReceiveModal} {...hasStorybookStyles} />; if (showReceiveModal)
return <ReceiveModal onClose={handleShowReceiveModal} open={showReceiveModal} {...hasStorybookStyles} />;
return null; return null;
}; };
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types'; import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests'; import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
@@ -6,7 +7,6 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal'; import { SimpleModal } from '../Modals/SimpleModal';
import { ModalFee } from '../Modals/ModalFee'; import { ModalFee } from '../Modals/ModalFee';
import { FeeWarning } from '../FeeWarning'; import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const CompoundModal: React.FC<{ export const CompoundModal: React.FC<{
open: boolean; open: boolean;
@@ -42,13 +42,20 @@ export const CompoundModal: React.FC<{
subHeader="Compound rewards from delegations" subHeader="Compound rewards from delegations"
okLabel="Compound rewards" okLabel="Compound rewards"
> >
{identityKey && ( {identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)} <Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider /> <Typography>Rewards amount:</Typography>
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider /> <Typography>
<ModalListItem label="Rewards will be added to this delegation" value="" divider /> {amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller">
Rewards will be transferred to account you are logged in with now
</Typography>
{fee && <FeeWarning amount={amount} fee={fee} />} {fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
</SimpleModal> </SimpleModal>
); );
}; };
@@ -1,13 +1,12 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types'; import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { SxProps } from '@mui/material';
import { useGetFee } from 'src/hooks/useGetFee'; import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests'; import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee'; import { ModalFee } from '../Modals/ModalFee';
import { SimpleModal } from '../Modals/SimpleModal'; import { SimpleModal } from '../Modals/SimpleModal';
import { FeeWarning } from '../FeeWarning'; import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const RedeemModal: React.FC<{ export const RedeemModal: React.FC<{
open: boolean; open: boolean;
@@ -48,13 +47,20 @@ export const RedeemModal: React.FC<{
sx={sx} sx={sx}
backdropProps={backdropProps} backdropProps={backdropProps}
> >
{identityKey && ( {identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)} <Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider /> <Typography sx={{ color: 'text.primary' }}>Rewards amount:</Typography>
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider /> <Typography sx={{ color: 'text.primary' }}>
<ModalListItem label="Rewards will be transferred to account you are logged in with now" value="" divider /> {amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
Rewards will be transferred to account you are logged in with now
</Typography>
{fee && <FeeWarning amount={amount} fee={fee} />} {fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
</SimpleModal> </SimpleModal>
); );
}; };
@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { CircularProgress, Stack, Typography } from '@mui/material'; import { CircularProgress, Stack, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { InfoTooltip } from '../InfoToolTip';
export const RewardsSummary: React.FC<{ export const RewardsSummary: React.FC<{
isLoading?: boolean; isLoading?: boolean;
@@ -10,17 +9,15 @@ export const RewardsSummary: React.FC<{
}> = ({ isLoading, totalDelegation, totalRewards }) => { }> = ({ isLoading, totalDelegation, totalRewards }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Stack direction="row" justifyContent="space-between"> <Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row" spacing={4}> <Stack direction="row" spacing={4}>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={2}>
<InfoTooltip title="This is the total amount you have delgated across multiple nodes" />
<Typography>Total delegations:</Typography> <Typography>Total delegations:</Typography>
<Typography fontWeight={600} fontSize={16} textTransform="uppercase"> <Typography fontWeight={600} fontSize={16} textTransform="uppercase">
{isLoading ? <CircularProgress size={theme.typography.fontSize} /> : totalDelegation || '-'} {isLoading ? <CircularProgress size={theme.typography.fontSize} /> : totalDelegation || '-'}
</Typography> </Typography>
</Stack> </Stack>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={2}>
<InfoTooltip title="Awaiting rewards accrue per epoch (hourly). You can redeem or compound them" />
<Typography>New rewards:</Typography> <Typography>New rewards:</Typography>
<Typography fontWeight={600} fontSize={16} textTransform="uppercase"> <Typography fontWeight={600} fontSize={16} textTransform="uppercase">
{isLoading ? <CircularProgress size={theme.typography.fontSize} /> : totalRewards || '-'} {isLoading ? <CircularProgress size={theme.typography.fontSize} /> : totalRewards || '-'}
@@ -30,7 +30,6 @@ export const SendInput = () => {
<SendInputModal <SendInputModal
toAddress="" toAddress=""
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u" fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
denom="nym"
onNext={() => {}} onNext={() => {}}
onClose={() => {}} onClose={() => {}}
onAddressChange={() => {}} onAddressChange={() => {}}
@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { Stack, SxProps } from '@mui/material'; import { Stack, SxProps } from '@mui/material';
import { FeeDetails, DecCoin, CurrencyDenom } from '@nymproject/types'; import { CurrencyDenom } from '@nymproject/types';
import { FeeDetails, DecCoin } from '@nymproject/types';
import { SimpleModal } from '../Modals/SimpleModal'; import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem'; import { ModalListItem } from '../Modals/ModalListItem';
import { ModalFee } from '../Modals/ModalFee';
export const SendDetailsModal = ({ export const SendDetailsModal = ({
amount, amount,
@@ -39,10 +39,14 @@ export const SendDetailsModal = ({
backdropProps={backdropProps} backdropProps={backdropProps}
> >
<Stack gap={0.5} sx={{ mt: 4 }}> <Stack gap={0.5} sx={{ mt: 4 }}>
<ModalListItem label="From:" value={fromAddress} divider /> <ModalListItem label="From" value={fromAddress} divider />
<ModalListItem label="To:" value={toAddress} divider /> <ModalListItem label="To" value={toAddress} divider />
<ModalListItem label="Amount:" value={`${amount?.amount} ${denom.toUpperCase()}`} divider /> <ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalFee fee={fee} divider isLoading={false} /> <ModalListItem
label="Fee for this transaction"
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
divider
/>
</Stack> </Stack>
</SimpleModal> </SimpleModal>
); );
@@ -56,7 +56,6 @@ export const SendInputModal = ({
backdropProps={backdropProps} backdropProps={backdropProps}
> >
<Stack gap={2} sx={{ mt: 2 }}> <Stack gap={2} sx={{ mt: 2 }}>
<ModalListItem label="Your address:" value={fromAddress} fontWeight="light" />
<TextField <TextField
placeholder="Recipient address" placeholder="Recipient address"
fullWidth fullWidth
@@ -77,8 +76,9 @@ export const SendInputModal = ({
{error} {error}
</Typography> </Typography>
</Stack> </Stack>
<Stack gap={0.5} sx={{ mt: 1 }}> <Stack gap={0.5} sx={{ mt: 2 }}>
<ModalListItem label="Account balance:" value={balance?.toUpperCase()} divider fontWeight={600} /> <ModalListItem label="Account balance" value={balance} divider strong />
<ModalListItem label="Your address" value={fromAddress} divider />
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}> <Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
Est. fee for this transaction will be show on the next page Est. fee for this transaction will be show on the next page
</Typography> </Typography>
-35
View File
@@ -1,35 +0,0 @@
import React from 'react';
import { Tab, Tabs as MuiTabs } from '@mui/material';
export const Tabs: React.FC<{
tabs: string[];
selectedTab: number;
disabled?: boolean;
onChange?: (event: React.SyntheticEvent, tab: number) => void;
disableActiveTabHighlight?: boolean;
}> = ({ tabs, selectedTab, disabled, disableActiveTabHighlight, onChange }) => (
<MuiTabs
value={selectedTab}
onChange={onChange}
sx={{
bgcolor: (theme) => theme.palette.nym.nymWallet.background.grey,
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: (theme) => theme.palette.nym.nymWallet.background.greyStroke,
}}
textColor="inherit"
TabIndicatorProps={
disableActiveTabHighlight
? {
style: {
opacity: 0,
},
}
: {}
}
>
{tabs.map((tabName) => (
<Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} />
))}
</MuiTabs>
);
+1 -1
View File
@@ -4,7 +4,7 @@ import { Box, Typography } from '@mui/material';
export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => ( export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => (
<Box width="100%" display="flex" alignItems="center"> <Box width="100%" display="flex" alignItems="center">
{Icon} {Icon}
<Typography width="100%" variant="h5" sx={{ fontWeight: 600 }}> <Typography width="100%" variant="h6" sx={{ fontWeight: 600 }}>
{title} {title}
</Typography> </Typography>
</Box> </Box>
@@ -14,13 +14,11 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
clientDetails, clientDetails,
} = useContext(AppContext); } = useContext(AppContext);
const fetchBalances = async () => {
await fetchBalance();
await fetchTokenAllocation();
};
useEffect(() => { useEffect(() => {
fetchBalances(); (async () => {
await fetchBalance();
await fetchTokenAllocation();
})();
}, []); }, []);
useEffect(() => { useEffect(() => {
-358
View File
@@ -1,358 +0,0 @@
import { FeeDetails, DecCoin, MixnodeStatus, TransactionExecuteResult } from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { isGateway, isMixnode, Network, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { Console } from 'src/utils/console';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
claimOperatorReward,
compoundOperatorReward,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixnodeRequest,
vestingBondGateway,
vestingBondMixNode,
vestingUnbondGateway,
vestingUnbondMixnode,
updateMixnode as updateMixnodeRequest,
vestingUpdateMixnode as updateMixnodeVestingRequest,
getNodeDescription as getNodeDescriptioRequest,
getGatewayBondDetails,
getMixnodeBondDetails,
getMixnodeStatus,
getOperatorRewards,
getMixnodeStakeSaturation,
getNumberOfMixnodeDelegators,
vestingClaimOperatorReward,
vestingCompoundOperatorReward,
} from '../requests';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
const bonded: TBondedMixnode = {
name: 'Monster node',
identityKey: 'B2Xx4haarLWMajX8w259oHjtRZsC7nHwagbWrJNiA3QC',
bond: { denom: 'nym', amount: '1234' },
delegators: 123,
operatorRewards: { denom: 'nym', amount: '12' },
profitMargin: 10,
stake: { denom: 'nym', amount: '99' },
stakeSaturation: 99,
status: 'active',
};
// TODO add relevant data
export type TBondedMixnode = {
name?: string;
identityKey: string;
stake: DecCoin;
bond: DecCoin;
stakeSaturation: number;
profitMargin: number;
operatorRewards: DecCoin;
delegators: number;
status: MixnodeStatus;
proxy?: string;
};
// TODO add relevant data
export interface TBondedGateway {
name: string;
identityKey: string;
ip: string;
bond: DecCoin;
location?: string; // TODO not yet available, only available in Network Explorer API
proxy?: string;
}
export type TokenPool = 'locked' | 'balance';
export type TBondingContext = {
isLoading: boolean;
error?: string;
bondedNode?: TBondedMixnode | TBondedGateway;
refresh: () => Promise<void>;
bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise<TransactionExecuteResult | undefined>;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
compoundRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
updateMixnode: (pm: number, fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
checkOwnership: () => Promise<void>;
};
const calculateStake = (pledge: DecCoin, delegations: DecCoin) => {
const total = Number(pledge.amount) + Number(delegations.amount);
return { amount: total.toString(), denom: pledge.denom };
};
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
refresh: async () => undefined,
bondMixnode: async () => {
throw new Error('Not implemented');
},
bondGateway: async () => {
throw new Error('Not implemented');
},
unbond: async () => {
throw new Error('Not implemented');
},
redeemRewards: async () => {
throw new Error('Not implemented');
},
compoundRewards: async () => {
throw new Error('Not implemented');
},
updateMixnode: async () => {
throw new Error('Not implemented');
},
checkOwnership(): Promise<void> {
throw new Error('Not implemented');
},
});
export const BondingContextProvider = ({ children }: { children?: React.ReactNode }): JSX.Element => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const [bondedNode, setBondedNode] = useState<TBondedMixnode | TBondedGateway>();
const { userBalance, clientDetails } = useContext(AppContext);
const { ownership, isLoading: isOwnershipLoading, checkOwnership } = useCheckOwnership();
const isVesting = Boolean(ownership.vestingPledge);
const resetState = () => {
setError(undefined);
setBondedNode(undefined);
};
const getAdditionalMixnodeDetails = async (identityKey: string) => {
const additionalDetails: { status: MixnodeStatus; stakeSaturation: number; numberOfDelegators: number } = {
status: 'not_found',
stakeSaturation: 0,
numberOfDelegators: 0,
};
try {
const statusResponse = await getMixnodeStatus(identityKey);
additionalDetails.status = statusResponse.status;
} catch (e) {
Console.log(e);
}
try {
const stakeSaturationResponse = await getMixnodeStakeSaturation(identityKey);
additionalDetails.stakeSaturation = Math.round(stakeSaturationResponse.saturation * 100);
} catch (e) {
Console.log(e);
}
try {
const numberOfDelegators = await getNumberOfMixnodeDelegators(identityKey);
additionalDetails.numberOfDelegators = numberOfDelegators;
} catch (e) {
Console.log(e);
}
return additionalDetails;
};
const getNodeDescription = async (host: string, port: number) => {
try {
const nodeDescription = await getNodeDescriptioRequest(host, port);
return nodeDescription;
} catch (e) {
Console.log(e);
}
return undefined;
};
const refresh = useCallback(async () => {
setIsLoading(true);
if (ownership.hasOwnership && ownership.nodeType === 'mixnode' && clientDetails) {
try {
const data = await getMixnodeBondDetails();
const operatorRewards = await getOperatorRewards(clientDetails?.client_address);
if (data) {
const { status, stakeSaturation, numberOfDelegators } = await getAdditionalMixnodeDetails(
data.mix_node.identity_key,
);
const nodeDescription = await getNodeDescription(data.mix_node.host, data.mix_node.http_api_port);
setBondedNode({
name: nodeDescription?.name,
identityKey: data.mix_node.identity_key,
ip: '',
stake: calculateStake(data.pledge_amount, data.total_delegation),
bond: data.pledge_amount,
profitMargin: data.mix_node.profit_margin_percent,
nodeRewards: data.accumulated_rewards,
delegators: numberOfDelegators,
proxy: data.proxy,
operatorRewards,
status,
stakeSaturation,
} as TBondedMixnode);
}
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (ownership.hasOwnership && ownership.nodeType === 'gateway') {
try {
const data = await getGatewayBondDetails();
if (data) {
const nodeDescription = await getNodeDescription(data.gateway.host, data.gateway.clients_port);
setBondedNode({
name: nodeDescription?.name,
identityKey: data.gateway.identity_key,
ip: data.gateway.host,
location: data.gateway.location,
bond: data.pledge_amount,
delegators: bonded.delegators,
proxy: data.proxy,
} as TBondedGateway);
}
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
}
if (!ownership.hasOwnership) {
resetState();
}
setIsLoading(false);
}, [ownership]);
useEffect(() => {
refresh();
}, [ownership, refresh]);
const bondMixnode = async (data: TBondMixNodeArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
};
const bondGateway = async (data: TBondGatewayArgs, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
setIsLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(data);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(data);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return undefined;
};
const unbond = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode && isMixnode(bondedNode) && bondedNode.proxy) tx = await vestingUnbondMixnode(fee?.fee);
if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee);
if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee);
if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee);
} catch (e) {
setError(`an error occurred: ${e as string}`);
} finally {
setIsLoading(false);
}
return tx;
};
const updateMixnode = async (pm: number, fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await updateMixnodeVestingRequest(pm, fee?.fee);
else tx = await updateMixnodeRequest(pm, fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const redeemRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await vestingClaimOperatorReward(fee?.fee);
else tx = await claimOperatorReward(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const compoundRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
try {
if (bondedNode?.proxy) tx = await vestingCompoundOperatorReward(fee?.fee);
else tx = await compoundOperatorReward(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) =>
// TODO to implement
undefined;
const memoizedValue = useMemo(
() => ({
isLoading: isLoading || isOwnershipLoading,
error,
bondMixnode,
bondedNode,
bondGateway,
unbond,
updateMixnode,
refresh,
redeemRewards,
compoundRewards,
bondMore,
checkOwnership,
}),
[isLoading, isOwnershipLoading, error, bondedNode, isVesting],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
export const useBondingContext = () => useContext<TBondingContext>(BondingContext);
+3 -3
View File
@@ -81,7 +81,6 @@ export const DelegationContextProvider: FC<{
}; };
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
resetState();
setIsLoading(true); setIsLoading(true);
try { try {
const data = await getDelegationSummary(); const data = await getDelegationSummary();
@@ -95,11 +94,12 @@ export const DelegationContextProvider: FC<{
setError((e as Error).message); setError((e as Error).message);
} }
setIsLoading(false); setIsLoading(false);
}, []); }, [network]);
useEffect(() => { useEffect(() => {
resetState();
refresh(); refresh();
}, []); }, [network]);
const memoizedValue = useMemo( const memoizedValue = useMemo(
() => ({ () => ({
-1
View File
@@ -1,4 +1,3 @@
export * from './main'; export * from './main';
export * from './auth'; export * from './auth';
export * from './accounts'; export * from './accounts';
export * from './bonding';
+9 -2
View File
@@ -33,6 +33,7 @@ type TLoginType = 'mnemonic' | 'password';
export type TAppContext = { export type TAppContext = {
mode: 'light' | 'dark'; mode: 'light' | 'dark';
handleSwitchMode: () => void;
appEnv?: AppEnv; appEnv?: AppEnv;
appVersion?: string; appVersion?: string;
clientDetails?: Account; clientDetails?: Account;
@@ -46,10 +47,10 @@ export type TAppContext = {
isAdminAddress: boolean; isAdminAddress: boolean;
error?: string; error?: string;
loginType?: TLoginType; loginType?: TLoginType;
showSettings: boolean;
showSendModal: boolean; showSendModal: boolean;
showReceiveModal: boolean; showReceiveModal: boolean;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void; handleShowSettings: () => void;
handleSwitchMode: () => void;
handleShowSendModal: () => void; handleShowSendModal: () => void;
handleShowReceiveModal: () => void; handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void; setIsLoading: (isLoading: boolean) => void;
@@ -61,6 +62,7 @@ export type TAppContext = {
handleShowTerminal: () => void; handleShowTerminal: () => void;
signInWithPassword: (password: string) => void; signInWithPassword: (password: string) => void;
logOut: () => void; logOut: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
}; };
export const AppContext = createContext({} as TAppContext); export const AppContext = createContext({} as TAppContext);
@@ -79,6 +81,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [appVersion, setAppVersion] = useState<string>(); const [appVersion, setAppVersion] = useState<string>();
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false); const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSettings, setShowSettings] = useState(false);
const [showSendModal, setShowSendModal] = useState(false); const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false);
@@ -232,6 +235,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const handleShowAdmin = () => setShowAdmin((show) => !show); const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowTerminal = () => setShowTerminal((show) => !show); const handleShowTerminal = () => setShowTerminal((show) => !show);
const switchNetwork = (_network: Network) => setNetwork(_network); const switchNetwork = (_network: Network) => setNetwork(_network);
const handleShowSettings = () => setShowSettings((show) => !show);
const handleShowSendModal = () => setShowSendModal((show) => !show); const handleShowSendModal = () => setShowSendModal((show) => !show);
const handleShowReceiveModal = () => setShowReceiveModal((show) => !show); const handleShowReceiveModal = () => setShowReceiveModal((show) => !show);
const handleSwitchMode = () => const handleSwitchMode = () =>
@@ -255,6 +259,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
userBalance, userBalance,
showAdmin, showAdmin,
showTerminal, showTerminal,
showSettings,
network, network,
loginType, loginType,
setIsLoading, setIsLoading,
@@ -267,6 +272,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
logIn, logIn,
logOut, logOut,
onAccountChange, onAccountChange,
handleShowSettings,
showSendModal, showSendModal,
showReceiveModal, showReceiveModal,
handleShowSendModal, handleShowSendModal,
@@ -288,6 +294,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
network, network,
storedAccounts, storedAccounts,
showTerminal, showTerminal,
showSettings,
showSendModal, showSendModal,
showReceiveModal, showReceiveModal,
], ],
-181
View File
@@ -1,181 +0,0 @@
import { FeeDetails, DecCoin, TransactionExecuteResult } from '@nymproject/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { TBondedGateway, TBondedMixnode, BondingContext } from '../bonding';
import { mockSleep } from './utils';
const SLEEP_MS = 1000;
const bondedMixnodeMock: TBondedMixnode = {
name: 'Monster node',
identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
stake: { denom: 'nym', amount: '1234' },
bond: { denom: 'nym', amount: '1234' },
stakeSaturation: 95,
profitMargin: 15,
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
};
const bondedGatewayMock: TBondedGateway = {
name: 'Monster node',
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: { denom: 'nym', amount: '1234' },
};
const TxResultMock: TransactionExecuteResult = {
logs_json: '',
data_json: '',
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
fee: { amount: '1', denom: 'nym' },
};
const feeMock: FeeDetails = {
amount: { denom: 'nym', amount: '1' },
fee: { Auto: 1 },
};
export const MockBondingContextProvider = ({
network,
children,
}: {
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [isLoading, setIsLoading] = useState(true);
const [feeLoading, setFeeLoading] = useState(false);
const [fee, setFee] = useState<FeeDetails | undefined>();
const [error, setError] = useState<string>();
const [bondedData, setBondedData] = useState<TBondedMixnode | TBondedGateway | null>(null);
const [bondedMixnode, setBondedMixnode] = useState<TBondedMixnode | null>(null);
const [bondedGateway, setBondedGateway] = useState<TBondedGateway | null>(null);
const [trigger, setTrigger] = useState<Date>(new Date());
const triggerStateUpdate = () => setTrigger(new Date());
const resetState = () => {
setIsLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
};
// fake tauri request
const fetchBondingData: () => Promise<TBondedMixnode | TBondedGateway | null> = async () => {
await mockSleep(SLEEP_MS);
return bondedData;
};
const checkOwnership = async () => {};
const refresh = useCallback(async () => {
const bounded = await fetchBondingData();
if (bounded && 'stake' in bounded) {
setBondedMixnode(bounded);
}
if (bounded && !('stake' in bounded)) {
setBondedGateway(bounded);
}
setIsLoading(false);
}, [network]);
useEffect(() => {
resetState();
refresh();
}, [network, bondedData]);
const bondMixnode = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedMixnodeMock);
setIsLoading(false);
return TxResultMock;
};
const bondGateway = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedGatewayMock);
setIsLoading(false);
return TxResultMock;
};
const unbond = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setIsLoading(false);
return TxResultMock;
};
const redeemRewards = async (): Promise<TransactionExecuteResult | undefined> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const compoundRewards = async (): Promise<TransactionExecuteResult | undefined> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const updateMixnode = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const getFee = async (_feeOperation: any, _args: any) => {
setFeeLoading(true);
await mockSleep(SLEEP_MS);
setFeeLoading(false);
setFee(feeMock);
return feeMock;
};
const resetFeeState = () => {};
const memoizedValue = useMemo(
() => ({
isLoading,
error,
bondMixnode,
bondGateway,
unbond,
refresh,
redeemRewards,
compoundRewards,
fee,
feeLoading,
getFee,
resetFeeState,
updateMixnode,
bondMore,
checkOwnership,
}),
[isLoading, error, bondedMixnode, bondedGateway, trigger, fee],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
+12 -4
View File
@@ -1,4 +1,5 @@
import React, { createContext, FC, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, FC, useContext, useEffect, useMemo, useState } from 'react';
import { Network } from 'src/types';
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { useDelegationContext } from './delegations'; import { useDelegationContext } from './delegations';
import { claimDelegatorRewards, compoundDelegatorRewards } from '../requests'; import { claimDelegatorRewards, compoundDelegatorRewards } from '../requests';
@@ -28,8 +29,11 @@ export const RewardsContext = createContext<TRewardsContext>({
}, },
}); });
export const RewardsContextProvider: FC<{}> = ({ children }) => { export const RewardsContextProvider: FC<{
network?: Network;
}> = ({ network, children }) => {
const { isLoading, totalRewards, refresh } = useDelegationContext(); const { isLoading, totalRewards, refresh } = useDelegationContext();
const [currentNetwork, setCurrentNetwork] = useState<undefined | Network>();
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const resetState = async () => { const resetState = async () => {
@@ -37,8 +41,12 @@ export const RewardsContextProvider: FC<{}> = ({ children }) => {
}; };
useEffect(() => { useEffect(() => {
resetState(); if (currentNetwork !== network) {
}, []); // reset state and refresh
resetState();
setCurrentNetwork(network);
}
}, [network]);
const memoizedValue = useMemo( const memoizedValue = useMemo(
() => ({ () => ({
@@ -52,7 +60,7 @@ export const RewardsContextProvider: FC<{}> = ({ children }) => {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
}), }),
[isLoading, error, totalRewards], [isLoading, error, totalRewards, network],
); );
return <RewardsContext.Provider value={memoizedValue}>{children}</RewardsContext.Provider>; return <RewardsContext.Provider value={memoizedValue}>{children}</RewardsContext.Provider>;
+4 -3
View File
@@ -26,20 +26,21 @@ export const ApplicationLayout: React.FC = ({ children }) => {
sx={{ sx={{
background: (t) => t.palette.nym.nymWallet.nav.background, background: (t) => t.palette.nym.nymWallet.nav.background,
overflow: 'auto', overflow: 'auto',
py: 5, py: 3,
px: 5,
}} }}
display="flex" display="flex"
flexDirection="column" flexDirection="column"
justifyContent="space-between" justifyContent="space-between"
> >
<Box> <Box>
<Box sx={{ ml: 5, mb: 3 }}> <Box sx={{ mb: 4 }}>
<NymWordmark height={14} /> <NymWordmark height={14} />
</Box> </Box>
<Nav /> <Nav />
</Box> </Box>
{appVersion && ( {appVersion && (
<Box color="#888" ml={5} mt={8}> <Box color="#888" mt={8}>
Version {appVersion} Version {appVersion}
</Box> </Box>
)} )}
@@ -87,12 +87,12 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
) : ( ) : (
<> <>
<ModalListItem <ModalListItem
label="Unlocked transferrable tokens:" label="Unlocked transferrable tokens"
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`} value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`}
divider divider
/> />
<ModalListItem <ModalListItem
label="Est. fee for this transaction:" label="Est. fee for this transaction"
value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />} value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />}
divider divider
/> />
@@ -1,9 +1,8 @@
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTheme } from '@mui/material/styles';
import { Box, Tooltip, Typography } from '@mui/material'; import { Box, Tooltip, Typography } from '@mui/material';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { AppContext } from '../../../context'; import { AppContext } from '../../../context/main';
const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index; const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index;
@@ -22,9 +21,6 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
userBalance: { currentVestingPeriod, vestingAccountInfo }, userBalance: { currentVestingPeriod, vestingAccountInfo },
} = useContext(AppContext); } = useContext(AppContext);
const theme = useTheme();
const { mode } = theme.palette;
const nextPeriod = const nextPeriod =
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time) ? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
@@ -33,31 +29,19 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
return ( return (
<Box display="flex" flexDirection="column" gap={1} position="relative" width="100%"> <Box display="flex" flexDirection="column" gap={1} position="relative" width="100%">
<svg width="100%" height="12"> <svg width="100%" height="12">
<rect y="2" width="100%" height="6" rx="0" fill="text.dark" /> <rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
<rect <rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill="#121726" />
y="2"
width={`${percentageComplete}%`}
height="6"
rx="0"
fill={mode === 'light' ? 'text.dark' : theme.palette.success.main}
/>
{vestingAccountInfo?.periods.map((period, i, arr) => ( {vestingAccountInfo?.periods.map((period, i, arr) => (
<Marker <Marker
position={`${calculateMarkerPosition(arr.length, i)}%`} position={`${calculateMarkerPosition(arr.length, i)}%`}
color={ color={+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i) ? '#121726' : '#B9B9B9'}
+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i)
? mode === 'light'
? 'text.dark'
: theme.palette.success.main
: '#B9B9B9'
}
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')} tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
key={i} key={i}
/> />
))} ))}
<Marker <Marker
position="calc(100% - 4px)" position="calc(100% - 4px)"
color={percentageComplete === 100 ? (mode === 'light' ? 'text.dark' : theme.palette.success.main) : '#B9B9B9'} color={percentageComplete === 100 ? '#121726' : '#B9B9B9'}
tooltipText="End of vesting schedule" tooltipText="End of vesting schedule"
/> />
</svg> </svg>
+2 -18
View File
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useState } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { AppContext } from '../../context/main'; import { AppContext } from '../../context/main';
@@ -9,25 +9,9 @@ import { TransferModal } from './components/TransferModal';
export const Balance = () => { export const Balance = () => {
const [showTransferModal, setShowTransferModal] = useState(false); const [showTransferModal, setShowTransferModal] = useState(false);
const [showVestingCard, setShowVestingCard] = useState(false);
const { userBalance } = useContext(AppContext); const { userBalance } = useContext(AppContext);
useEffect(() => {
const { originalVesting, currentVestingPeriod, tokenAllocation } = userBalance;
if (
originalVesting &&
currentVestingPeriod === 'After' &&
tokenAllocation?.locked === '0' &&
tokenAllocation?.vesting === '0' &&
tokenAllocation?.spendable === '0'
) {
setShowVestingCard(false);
} else if (originalVesting) {
setShowVestingCard(true);
}
}, [userBalance]);
const handleShowTransferModal = async () => { const handleShowTransferModal = async () => {
await userBalance.refreshBalances(); await userBalance.refreshBalances();
setShowTransferModal(true); setShowTransferModal(true);
@@ -37,7 +21,7 @@ export const Balance = () => {
<PageLayout> <PageLayout>
<Box display="flex" flexDirection="column" gap={2}> <Box display="flex" flexDirection="column" gap={2}>
<BalanceCard /> <BalanceCard />
{showVestingCard && <VestingCard onTransfer={handleShowTransferModal} />} <VestingCard onTransfer={handleShowTransferModal} />
{showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />} {showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />}
</Box> </Box>
</PageLayout> </PageLayout>
+4 -28
View File
@@ -65,44 +65,20 @@ const VestingSchedule = () => {
))} ))}
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell <TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }}>
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
textTransform: 'uppercase',
}}
>
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} {userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()} {clientDetails?.display_mix_denom.toUpperCase()}
</TableCell> </TableCell>
<TableCell <TableCell align="left" sx={{ borderBottom: 'none' }}>
align="left"
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
}}
>
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)} {vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
</TableCell> </TableCell>
<TableCell <TableCell sx={{ borderBottom: 'none' }}>
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
}}
>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Typography variant="body2">{`${vestedPercentage}%`}</Typography> <Typography variant="body2">{`${vestedPercentage}%`}</Typography>
<VestingTimeline percentageComplete={vestedPercentage} /> <VestingTimeline percentageComplete={vestedPercentage} />
</Box> </Box>
</TableCell> </TableCell>
<TableCell <TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }} align="right">
sx={{
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
borderBottom: 'none',
textTransform: 'uppercase',
}}
align="right"
>
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} {userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()} {clientDetails?.display_mix_denom.toUpperCase()}
</TableCell> </TableCell>
@@ -20,8 +20,8 @@ export const ConfirmationModal = ({
}) => ( }) => (
<SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}> <SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<ModalListItem label="Mixnode identity:" value={identity} /> <ModalListItem label="Mixnode identity" value={identity} />
<ModalListItem label="Amount:" value={`${amount.amount} ${amount.denom}`} /> <ModalListItem label="Amount" value={`${amount.amount} ${amount.denom}`} />
<ModalFee fee={fee} isLoading={false} /> <ModalFee fee={fee} isLoading={false} />
</Box> </Box>
</SimpleModal> </SimpleModal>
@@ -1,13 +0,0 @@
import * as React from 'react';
import { BondingPage } from './index';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
title: 'Bonding/Flows/Mock',
};
export const Default = () => (
<MockBondingContextProvider>
<BondingPage />
</MockBondingContextProvider>
);
-252
View File
@@ -1,252 +0,0 @@
import React, { useContext, useState } from 'react';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { NodeSettings } from 'src/components/Bonding/modals/NodeSettingsModal';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { CompoundRewardsModal } from 'src/components/Bonding/modals/CompoundRewardsModal';
import { PageLayout } from '../../layouts';
import { BondingContextProvider, useBondingContext } from '../../context';
import { Box } from '@mui/material';
const Bonding = () => {
const [showModal, setShowModal] = useState<
'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem' | 'compound' | 'node-settings'
>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const {
bondedNode,
bondMixnode,
bondGateway,
unbond,
updateMixnode,
redeemRewards,
compoundRewards,
isLoading,
checkOwnership,
} = useBondingContext();
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (error: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUnbond = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUpdateProfitMargin = async (profitMargin: number, fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await updateMixnode(profitMargin, fee);
setConfirmationDetails({
status: 'success',
title: 'Profit margin update successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleRedeemReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await redeemRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleCompoundReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await compoundRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards compounded successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
setShowModal('unbond');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
case 'compound': {
setShowModal('compound');
break;
}
case 'nodeSettings': {
setShowModal('node-settings');
break;
}
default: {
return undefined;
}
}
return undefined;
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{showModal === 'compound' && bondedNode && isMixnode(bondedNode) && (
<CompoundRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleCompoundReward}
onError={handleError}
/>
)}
{showModal === 'node-settings' && bondedNode && isMixnode(bondedNode) && (
<NodeSettings
currentPm={bondedNode.profitMargin}
isVesting={Boolean(bondedNode.proxy)}
onConfirm={handleUpdateProfitMargin}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
-70
View File
@@ -1,70 +0,0 @@
import { DecCoin, TNodeType, TransactionExecuteResult } from '@nymproject/types';
import { TPoolOption } from 'src/components';
export type FormStep = 1 | 2 | 3 | 4;
export type NodeType = TNodeType;
export type BondStatus = 'init' | 'success' | 'error' | 'loading';
export type ACTIONTYPE =
| { type: 'change_bond_type'; payload: NodeType }
| { type: 'set_node_data'; payload: NodeData }
| { type: 'set_amount_data'; payload: AmountData }
| { type: 'set_step'; payload: FormStep }
| { type: 'set_tx'; payload: TransactionExecuteResult | undefined }
| { type: 'set_error'; payload: string | null | undefined }
| { type: 'set_bond_status'; payload: BondStatus }
| { type: 'next_step' }
| { type: 'prev_step' }
| { type: 'show_modal' }
| { type: 'close_modal' }
| { type: 'reset' };
export type NodeIdentity = {
identityKey: string;
sphinxKey: string;
ownerSignature: string;
host: string;
version: string;
mixPort: number;
};
export type MixnodeData = NodeIdentity & {
verlocPort: number;
httpApiPort: number;
};
export type Amount = {
amount: DecCoin;
tokenPool: string;
};
export type GatewayAmount = Amount;
export type MixnodeAmount = Amount & {
profitMargin: number;
};
export type GatewayData = NodeIdentity & {
location: string;
clientsPort: number;
};
export type NodeData<N = MixnodeData | GatewayData> = {
nodeType: TNodeType;
} & N;
export interface AmountData {
amount: DecCoin;
tokenPool: TPoolOption;
profitMargin?: number;
}
export interface BondState {
showModal: boolean;
formStep: FormStep;
nodeData?: NodeData;
amountData?: MixnodeAmount | GatewayAmount;
tx?: TransactionExecuteResult;
bondStatus: BondStatus;
error?: string | null;
}
+11 -7
View File
@@ -85,7 +85,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
useEffect(() => { useEffect(() => {
refresh(); refresh();
}, [clientDetails, confirmationModalProps]); }, [network, clientDetails, confirmationModalProps]);
const handleDelegationItemActionClick = (item: DelegationWithEverything, action: DelegationListItemActions) => { const handleDelegationItemActionClick = (item: DelegationWithEverything, action: DelegationListItemActions) => {
if ((action === 'delegate' || action === 'compound') && item.stake_saturation && item.stake_saturation > 1) { if ((action === 'delegate' || action === 'compound') && item.stake_saturation && item.stake_saturation > 1) {
@@ -138,7 +138,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
setConfirmationModalProps({ setConfirmationModalProps({
status: 'success', status: 'success',
action: 'delegate', action: 'delegate',
message: 'This operation can take up to one hour to process', message: 'Delegations can take up to one hour to process',
...balances, ...balances,
transactions: [ transactions: [
{ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash }, { url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash },
@@ -240,9 +240,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try { try {
const txs = await claimRewards(identityKey, fee); const txs = await claimRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({ setConfirmationModalProps({
status: 'success', status: 'success',
action: 'redeem', action: 'redeem',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({ transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash, hash: tx.transaction_hash,
@@ -268,9 +270,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try { try {
const txs = await compoundRewards(identityKey, fee); const txs = await compoundRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({ setConfirmationModalProps({
status: 'success', status: 'success',
action: 'compound', action: 'compound',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({ transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash, hash: tx.transaction_hash,
@@ -288,7 +292,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
return ( return (
<> <>
<Paper elevation={0} sx={{ p: 3, mt: 4 }}> <Paper elevation={0} sx={{ p: 4, mt: 4 }}>
<Stack spacing={5}> <Stack spacing={5}>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Delegations</Typography> <Typography variant="h6">Delegations</Typography>
@@ -302,7 +306,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
noIcon noIcon
/> />
</Box> </Box>
<Box display="flex" justifyContent="space-between" alignItems="end"> <Box display="flex" justifyContent="space-between" alignItems="center">
<RewardsSummary isLoading={isLoading} totalDelegation={totalDelegations} totalRewards={totalRewards} /> <RewardsSummary isLoading={isLoading} totalDelegation={totalDelegations} totalRewards={totalRewards} />
<Button <Button
variant="contained" variant="contained"
@@ -323,7 +327,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
</Paper> </Paper>
{pendingDelegations && ( {pendingDelegations && (
<Paper elevation={0} sx={{ p: 3, mt: 2 }}> <Paper elevation={0} sx={{ p: 4, mt: 2 }}>
<Stack spacing={5}> <Stack spacing={5}>
<Typography variant="h6">Pending Delegation Events</Typography> <Typography variant="h6">Pending Delegation Events</Typography>
<PendingEvents pendingEvents={pendingDelegations} explorerUrl={urls(network).networkExplorer} /> <PendingEvents pendingEvents={pendingDelegations} explorerUrl={urls(network).networkExplorer} />
@@ -427,8 +431,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
export const DelegationPage: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { export const DelegationPage: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
const { network } = useContext(AppContext); const { network } = useContext(AppContext);
return ( return (
<DelegationContextProvider> <DelegationContextProvider network={network}>
<RewardsContextProvider> <RewardsContextProvider network={network}>
<Delegation isStorybook={isStorybook} /> <Delegation isStorybook={isStorybook} />
</RewardsContextProvider> </RewardsContextProvider>
</DelegationContextProvider> </DelegationContextProvider>

Some files were not shown because too many files have changed in this diff Show More