Compare commits

..

48 Commits

Author SHA1 Message Date
Jon Häggblad 606bb9aa3b contrats: clippy in beta toolchain 2022-08-11 08:24:49 +02:00
Jon Häggblad df03daf2cc socks5: remove pub mod not needed anymore (#1517)
* socks5: remove pub mod not needed anymore

* all: dedup parse_validators and move to common

* rustfmt
2022-08-10 21:14:12 +02:00
Raphaël Walther b3f5a4f496 Added audit workflow 2022-08-10 16:35:49 +02:00
Raphaël Walther d080d661f7 Added audit workflow 2022-08-10 16:29:54 +02:00
Raphaël Walther 6deb481e5d Added audit workflow 2022-08-10 16:09:05 +02:00
Raphaël Walther 5b98e18a4e Added audit workflow 2022-08-10 16:03:11 +02:00
Raphaël Walther 506a0da89c Added audit workflow 2022-08-10 16:00:50 +02:00
Fouad f7d38a7ec6 modal style updates (#1508)
remove commented code

remove unused props

fix props
2022-08-10 10:14:48 +01:00
Pierre Dommerc 4459aca933 fix(wallet-delegation): add error feedback on wrong address (#1510)
* fix(wallet-delegation): add error feedback on wrong address

* fix(wallet-delegation): use generic component for modal error
2022-08-10 09:31:14 +01:00
Pierre Dommerc 5b84c58985 feat(wallet-balance): hide vesting ui when vesting balance is empty (#1505)
* feat(wallet-balance): hide vesting ui when vesting balance is empty

* refactor(wallet-balance): feedback review
2022-08-10 09:29:01 +01:00
Fouad 4301d91f6c Feature/wallet style updates (#1506)
* update buttons hover colours

* fix nymcard title size

* style updates for accounts!

* fix delegations page lock-up on network switch
2022-08-10 09:28:03 +01:00
Fouad 03d28c115e delegations page style updates (#1507) 2022-08-10 09:27:03 +01:00
Jędrzej Stuczyński de78ca8d9b Removed unnecessary to-owned conversion 2022-08-09 15:25:45 +01:00
Gala 58d09e382a Merge pull request #1504 from nymtech/smooth-filter-slider
Smooth filter slider
2022-08-09 14:59:11 +02:00
Gala 0cef12d05b Merge pull request #1502 from nymtech/1501-contrast-text
fixing text color
2022-08-09 14:58:07 +02:00
Jon Häggblad 30e73ee795 Explorer: tweak selection probability categories (#1503)
* validator-api: tweak SelectionChance

* wallet: update SelectionChance colorMap

* changelog: add note
2022-08-09 12:57:47 +02:00
Tommy Verrall d918b69664 Update .env.qa
Fix wrong denoms for the qa.env
2022-08-09 11:53:11 +01:00
Gala b3b8d2ab46 adding the value to the cursor 2022-08-08 17:03:05 +02:00
Gala 67130a1289 adding smooth prop 2022-08-08 16:30:10 +02:00
Fouad 0dabff72bd Feature/bonding refactor (#1481)
* feat(wallet-bonding): bonding page, new bond form wip

* feat(wallet-bonding): add node table component

feat(wallet-bonding): new dialog component

* feat(wallet-bonding): node settings flow

* feat(wallet-bonding): bond more flow (done)

* feat(wallet): use confirmation modal component

* feat(wallet-bonding): node menu ui

* refactor(wallet-bonding): bonding flow with new gasFee estimation

* feat(wallet-bonding): unbond with gasFee and request

* refactor(wallet-bonding): switch to simpledialog component to keep modals consistency

* feat(wallet-bonding): fetch mixnode status

* update coin types in new bonding page

* fix displayed denom

* rebuild BondedNodeCard using existing shared components

* create reuseable ActionMenu component

* new mixnode form

* add gateway bond form

* check balance and fetch fee on bond mixnode request

* node settings

* get node description

* fix up rust request

* lint fixes + used NodeTypeSelector component

* temporarily remove estimated operator reward

* update return on rust function

* dont display node name UI if name doesnt exist

* rebase develop

* fix uppercase address bug

Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: pierre <dommerc.pierre@gmail.com>
2022-08-08 14:09:57 +01:00
Gala e8e2f195e6 fixing text color 2022-08-08 14:09:33 +02:00
Jędrzej Stuczyński fa354016e0 Removed useless into() conversion 2022-08-08 09:49:52 +01:00
Tommy Verrall 935ee765e9 Merge pull request #1498 from nymtech/feature/fix-envs
fix .env files for explorer.
2022-08-05 16:36:08 +01:00
tommy 4c8e59e6fc fix .env files for explorer. 2022-08-05 17:11:35 +02:00
Bogdan-Ștefan Neacşu 067f3e6f1a validator-api: handle SIGTERM (#1496)
* validator-api: handle SIGTERM

* Update CHANGELOG
2022-08-05 15:59:34 +03:00
Gala 6f09d46dce Merge pull request #1492 from nymtech/feature/delegating_ui_update
feat(wallet-delegation): new confirmation modal ui
2022-08-05 12:08:51 +02:00
Gala bdef48331b Merge pull request #1489 from nymtech/332-fix-vesting-ui
Wallet: Fixing dark mode colours in vesting shedule
2022-08-05 12:05:19 +02:00
Gala 51a6936e51 Merge pull request #1491 from nymtech/330-mix-wallet-ui
Wallet: Fix light mode bg and nav bar UI
2022-08-05 12:02:50 +02:00
Gala fd456d2952 Merge pull request #1490 from nymtech/334-ne-changes
NE: Filter by use routing score instead of stake parameter and adding filters info
2022-08-04 18:24:04 +02:00
Gala eee1abe593 removing extra styles 2022-08-04 18:15:12 +02:00
Gala fffad43937 Avoiding CAPS in button 2022-08-04 17:51:24 +02:00
Gala 3a79f43a8d styling 2022-08-04 17:23:32 +02:00
pierre 2e495f87ab refactor(wallet-delegation): ModalListItem component, pass colon in label value 2022-08-04 16:35:14 +02:00
Gala 57a9f18f5a fixing padding marging and wording 2022-08-04 16:30:33 +02:00
Gala 0c6a0a9cae Merge branch 'develop' into 334-ne-changes 2022-08-04 15:18:54 +02:00
Gala c80d8d354a adding nodes number info 2022-08-04 15:17:44 +02:00
Gala 3f544dbc69 adding Advanced filtering text and hover fix 2022-08-04 14:59:17 +02:00
pierre d1e1f15db0 Merge branch 'develop' into feature/delegating_ui_update 2022-08-04 13:26:41 +02:00
pierre 651c314182 feat(wallet-delegation): new confirmation modal ui 2022-08-04 13:20:00 +02:00
Gala a57545521d some refactor 2022-08-04 11:28:04 +02:00
Gala da60606921 hover bg color in dark and light mode 2022-08-03 17:20:46 +02:00
Gala 14f9bf7234 left nav spacing 2022-08-03 16:50:02 +02:00
Gala c1fa92869a fix light bg color 2022-08-03 16:49:47 +02:00
Gala c8533e3ec8 cleaning 2022-08-03 14:15:03 +02:00
Gala 06c4dd601d filter by use routing score instead of stake parameter 2022-08-03 14:06:33 +02:00
Gala 4ff80bbab2 fixing dark mode progress bar 2022-08-03 14:02:50 +02:00
Gala d7220b1fec adding info text in filters and renaming 2022-08-03 13:42:48 +02:00
Gala d92df9ada3 fixing dark mode colours 2022-08-03 12:24:21 +02:00
162 changed files with 3666 additions and 2397 deletions
+36
View File
@@ -0,0 +1,36 @@
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
+4 -1
View File
@@ -25,7 +25,6 @@ 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
@@ -38,6 +37,7 @@ 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,6 +52,7 @@ 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
@@ -78,6 +79,8 @@ 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
+1 -32
View File
@@ -3325,6 +3325,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"task",
"thiserror", "thiserror",
"time 0.3.9", "time 0.3.9",
"tokio", "tokio",
@@ -5397,15 +5398,6 @@ 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"
@@ -6293,29 +6285,6 @@ 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"
+1 -2
View File
@@ -68,8 +68,7 @@ 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,9 +69,6 @@ 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
+2 -14
View File
@@ -3,7 +3,6 @@
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 =
@@ -97,28 +96,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 {
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(parse_validators(&raw_validators)); .set_custom_validator_apis(config::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(parse_validators(&raw_validators)); .set_custom_validator_apis(config::parse_validators(&raw_validators));
} }
if args.disable_socket { if args.disable_socket {
+1 -12
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 url::Url; use config::parse_validators;
pub mod init; pub mod init;
pub(crate) mod run; pub(crate) mod run;
@@ -96,17 +96,6 @@ 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,9 +2,4 @@
// 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,3 +88,14 @@ 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, Serialize, JsonSchema)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, 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, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, 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, JsonSchema)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, 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.into(), denom: Network::MAINNET.mix_denom().display,
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,5 +1,6 @@
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
+4 -3
View File
@@ -1,5 +1,6 @@
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.nymtech.net VALIDATOR_API_URL=https://qa-validator-api.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=unymt CURRENCY_DENOM=unym
CURRENCY_STAKING_DENOM=unyxt CURRENCY_STAKING_DENOM=unyx
+2 -1
View File
@@ -1,6 +1,7 @@
// 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
@@ -9,7 +10,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_API_BASE_URL}/validators`; export const VALIDATORS_API = `${VALIDATOR_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.
+19 -10
View File
@@ -6,7 +6,6 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
DialogTitle, DialogTitle,
IconButton,
Slider, Slider,
Typography, Typography,
Box, Box,
@@ -25,7 +24,9 @@ import { useIsMobile } from '../../hooks/useIsMobile';
const FilterItem = ({ const FilterItem = ({
label, label,
id, id,
tooltipInfo,
value, value,
isSmooth,
marks, marks,
scale, scale,
min, min,
@@ -36,12 +37,13 @@ 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="off" valueLabelDisplay={isSmooth ? 'auto' : 'off'}
marks={marks} marks={marks}
step={null} step={isSmooth ? 1 : null}
scale={scale} scale={scale}
min={min} min={min}
max={max} max={max}
@@ -50,7 +52,7 @@ const FilterItem = ({
); );
export const Filters = () => { export const Filters = () => {
const { filterMixnodes, fetchMixnodes } = useMainContext(); const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext();
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -127,19 +129,26 @@ 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 CLEAR FILTERS
</Button> </Button>
} }
sx={{ width: 300 }}
> >
Filters applied {mixnodes?.data?.length} mixnodes matched your criteria
</Alert> </Alert>
</Snackbar> </Snackbar>
<IconButton size="large" onClick={handleToggleShowFilters}> <Button
<Tune /> size="large"
</IconButton> variant="text"
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>
+21 -54
View File
@@ -5,6 +5,7 @@ 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 },
@@ -18,6 +19,8 @@ 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 (%)',
@@ -43,65 +46,29 @@ 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",
}, },
stake: { routingScore: {
label: 'Stake (NYM)', label: 'Routing score (%)',
id: EnumFilterKey.stake, id: EnumFilterKey.routingScore,
value: [20, 90], value: [0, 100],
min: 20,
max: 90,
marks: [ marks: [
{ { label: '0', value: 0 },
value: 0, { label: '10', value: 10 },
label: '1', { label: '20', value: 20 },
}, { label: '30', value: 30 },
{ { label: '40', value: 40 },
value: 10, { label: '50', value: 50 },
label: '10', { label: '60', value: 60 },
}, { label: '70', value: 70 },
{ { label: '80', value: 80 },
value: 20, { label: '90', value: 90 },
label: '100', { label: '100', value: 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;
@@ -110,7 +77,7 @@ const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
}; };
export const formatOnSave = (filters: TFilters) => ({ export const formatOnSave = (filters: TFilters) => ({
stake: formatStakeValuesToMinorDenom(filters.stake.value), routingScore: filters.routingScore.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.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] && m.avg_uptime >= filters.routingScore[0] &&
+m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1], m.avg_uptime <= filters.routingScore[1],
); );
setMixnodes({ data: filtered, isLoading: false }); setMixnodes({ data: filtered, isLoading: false });
}; };
+3 -1
View File
@@ -4,17 +4,19 @@ import { Mark } from '@mui/base';
export enum EnumFilterKey { export enum EnumFilterKey {
profitMargin = 'profitMargin', profitMargin = 'profitMargin',
stakeSaturation = 'stakeSaturation', stakeSaturation = 'stakeSaturation',
stake = 'stake', routingScore = 'routingScore',
} }
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 };
+1 -12
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,17 +69,6 @@ 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 {
+4 -13
View File
@@ -6,9 +6,11 @@ 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::defaults::var_names::{API_VALIDATOR, BECH32_PREFIX, CONFIGURED}; use config::{
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;
@@ -61,17 +63,6 @@ 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 {
+3 -2
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, commands::parse_validators}; use nym_socks5::client::config::Config as Socks5Config;
use crate::{ use crate::{
error::{BackendError, Result}, error::{BackendError, Result},
@@ -134,10 +134,11 @@ 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(parse_validators(&raw_validators)); .set_custom_validator_apis(config_common::parse_validators(&raw_validators));
} }
let gateway = setup_gateway( let gateway = setup_gateway(
+87 -10
View File
@@ -3273,7 +3273,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
"parking_lot_core", "parking_lot_core 0.8.5",
]
[[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]]
@@ -3290,6 +3300,19 @@ 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"
@@ -4550,6 +4573,15 @@ 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"
@@ -4635,9 +4667,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "state" name = "state"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5" checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
dependencies = [ dependencies = [
"loom", "loom",
] ]
@@ -4656,7 +4688,7 @@ checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot", "parking_lot 0.11.2",
"phf_shared 0.10.0", "phf_shared 0.10.0",
"precomputed-hash", "precomputed-hash",
"serde", "serde",
@@ -4838,7 +4870,7 @@ dependencies = [
"ndk-glue", "ndk-glue",
"ndk-sys", "ndk-sys",
"objc", "objc",
"parking_lot", "parking_lot 0.11.2",
"raw-window-handle", "raw-window-handle",
"scopeguard", "scopeguard",
"serde", "serde",
@@ -5241,7 +5273,9 @@ 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",
@@ -5898,11 +5932,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", "windows_aarch64_msvc 0.30.0",
"windows_i686_gnu", "windows_i686_gnu 0.30.0",
"windows_i686_msvc", "windows_i686_msvc 0.30.0",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.30.0",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.30.0",
] ]
[[package]] [[package]]
@@ -5915,12 +5949,31 @@ 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"
@@ -5937,12 +5990,24 @@ 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"
@@ -5973,12 +6038,24 @@ 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 = "0.11.9" reqwest = {version = "0.11.9", features = ["json"] }
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 = ["sync", "time"] } tokio = { version = "1.10", features = ["full"] }
toml = "0.5.8" toml = "0.5.8"
url = "2.2" url = "2.2"
+2
View File
@@ -58,6 +58,8 @@ 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,3 +1,5 @@
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};
@@ -5,8 +7,18 @@ 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,
@@ -198,3 +210,51 @@ 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,8 +1,12 @@
import React from 'react'; import React from 'react';
import { Avatar } from '@mui/material'; import { Avatar, Typography } 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 }: Pick<TAccount, 'name'>) => ( export const AccountAvatar = ({ name, small }: { name: TAccount['name']; small?: boolean }) => (
<Avatar sx={{ bgcolor: stc(name), width: 20, height: 20 }}>{name?.split('')[0]}</Avatar> <Avatar sx={{ bgcolor: stc(name), ...(small ? { width: 25, height: 25 } : {}) }}>
<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} />} startIcon={<AccountAvatar name={account.id} small />}
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: (t) => t.palette.nym.text.muted }}> <Typography variant="body1" sx={{ color: (theme) => theme.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: (t) => t.palette.background.paper } : {})} sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (theme) => theme.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,6 +9,7 @@ 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';
@@ -51,7 +52,7 @@ export const AccountsModal = () => {
<Close /> <Close />
</IconButton> </IconButton>
</Box> </Box>
<Typography variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}> <Typography fontSize="small" sx={{ color: 'grey.600' }}>
Switch between accounts Switch between accounts
</Typography> </Typography>
</DialogTitle> </DialogTitle>
@@ -69,6 +70,7 @@ 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,17 +7,16 @@ 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',
@@ -30,14 +29,15 @@ const importAccountSteps = [
'Confirm the password used to login to your wallet', 'Confirm the password used to login to your wallet',
]; ];
const MnemonicStep = ({ mnemonic, onNext }: { mnemonic: string; onNext: () => void }) => { const MnemonicStep = ({ mnemonic, onNext, onBack }: { mnemonic: string; onNext: () => void; onBack: () => 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 }}> <DialogActions sx={{ p: 3, pt: 0, gap: 2 }}>
<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,10 +50,12 @@ 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>();
@@ -77,7 +79,8 @@ const ImportMnemonic = ({
}} }}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}> <DialogActions sx={{ p: 3, pt: 0, gap: 2 }}>
<StyledBackButton onBack={onBack} />
<Button <Button
disabled={value.length === 0} disabled={value.length === 0}
fullWidth fullWidth
@@ -93,7 +96,7 @@ const ImportMnemonic = ({
); );
}; };
const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => { const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBack: () => void }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
@@ -121,7 +124,8 @@ const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
fullWidth fullWidth
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}> <DialogActions sx={{ p: 3, pt: 0, gap: 2 }}>
<StyledBackButton onBack={onBack} />
<Button <Button
disabled={!value.length} disabled={!value.length}
fullWidth fullWidth
@@ -178,9 +182,6 @@ 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]}
@@ -190,12 +191,17 @@ export const AddAccountModal = () => {
switch (step) { switch (step) {
case 0: case 0:
return dialogToDisplay === 'Add' ? ( return dialogToDisplay === 'Add' ? (
<MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} /> <MnemonicStep
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:
@@ -205,6 +211,7 @@ 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:
@@ -222,6 +229,7 @@ export const AddAccountModal = () => {
} }
} }
}} }}
onCancel={() => setStep((s) => s - 1)}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
/> />
@@ -19,17 +19,18 @@ export const ConfirmPasswordModal = ({
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth> <Dialog open={Boolean(accountName)} onClose={onClose} fullWidth>
<Paper> <Paper>
<DialogTitle> <DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Typography variant="h6">Switch account</Typography>
<Typography variant="h6">Switch account</Typography> <Typography fontSize="small" sx={{ color: 'grey.600' }}>
<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 onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" /> <ConfirmPassword
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 variant="body1" sx={{ color: (theme) => theme.palette.text.disabled }}> <Typography fontSize="small" sx={{ color: 'grey.600' }}>
New wallet address New wallet address
</Typography> </Typography>
</DialogTitle> </DialogTitle>
@@ -1,69 +0,0 @@
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
@@ -0,0 +1,42 @@
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>
);
+2 -13
View File
@@ -7,13 +7,11 @@ 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, handleShowSettings, showSettings, mode, handleSwitchMode } = const { logOut, handleShowTerminal, appEnv, mode, handleSwitchMode } = useContext(AppContext);
useContext(AppContext);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@@ -31,7 +29,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 === 'light' ? ( {mode === 'dark' ? (
<LightModeOutlinedIcon fontSize="small" /> <LightModeOutlinedIcon fontSize="small" />
) : ( ) : (
<ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} /> <ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} />
@@ -45,15 +43,6 @@ 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"
@@ -1,65 +0,0 @@
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>
);
};
@@ -1 +0,0 @@
export * from './AppBar';
@@ -0,0 +1,44 @@
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>
);
@@ -0,0 +1,84 @@
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>
);
};
@@ -0,0 +1,31 @@
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>
);
};
@@ -0,0 +1,138 @@
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>
);
};
@@ -0,0 +1,48 @@
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>
);
};
@@ -0,0 +1,50 @@
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>
);
@@ -0,0 +1,214 @@
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}
/>
)}
</>
);
@@ -0,0 +1,223 @@
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}
/>
)}
</>
);
@@ -0,0 +1,59 @@
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;
}),
}),
});
@@ -0,0 +1,51 @@
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),
});
@@ -0,0 +1,161 @@
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>
);
};
@@ -0,0 +1,160 @@
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>
);
};
@@ -0,0 +1,115 @@
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>
);
};
@@ -0,0 +1,53 @@
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>
);
};
@@ -0,0 +1,52 @@
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>
);
};
@@ -0,0 +1,128 @@
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>
);
};
@@ -0,0 +1,53 @@
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>
);
};
@@ -0,0 +1,72 @@
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,16 +2,19 @@ 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('');
@@ -39,7 +42,8 @@ export const ConfirmPassword = ({
disabled={isLoading} disabled={isLoading}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}> <DialogActions sx={{ p: 3, pt: 0, gap: 2 }}>
{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 fontSize="small" /> : <Check color="success" />} {!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
@@ -13,12 +13,13 @@ 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;
@@ -62,7 +63,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 } = useGetFee(); const { fee, getFee, resetFeeState, feeError } = useGetFee();
const handleCheckStakeSaturation = async (identity: string) => { const handleCheckStakeSaturation = async (identity: string) => {
try { try {
@@ -168,12 +169,24 @@ 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}
@@ -184,23 +197,24 @@ 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}
> >
<IdentityKeyFormField <Box sx={{ mt: 2 }}>
required <IdentityKeyFormField
fullWidth required
placeholder="Node identity key" fullWidth
onChanged={handleIdentityKeyChanged} label="Node identity key"
initialValue={identityKey} onChanged={handleIdentityKeyChanged}
readOnly={Boolean(initialIdentityKey)} initialValue={identityKey}
textFieldProps={{ readOnly={Boolean(initialIdentityKey)}
autoFocus: !initialIdentityKey, textFieldProps={{
}} autoFocus: !initialIdentityKey,
/> }}
/>
</Box>
<Typography <Typography
component="div" component="div"
textAlign="left" textAlign="left"
@@ -214,7 +228,7 @@ export const DelegateModal: React.FC<{
<CurrencyFormField <CurrencyFormField
required required
fullWidth fullWidth
placeholder="Amount" label="Amount"
initialValue={amount} initialValue={amount}
autoFocus={Boolean(initialIdentityKey)} autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged} onChanged={handleAmountChanged}
@@ -230,7 +244,7 @@ export const DelegateModal: React.FC<{
{errorAmount} {errorAmount}
</Typography> </Typography>
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance} divider /> <ModalListItem label="Account balance" value={accountBalance?.toUpperCase()} divider fontWeight={600} />
</Box> </Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider /> <ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
@@ -241,7 +255,7 @@ export const DelegateModal: React.FC<{
divider divider
/> />
<ModalListItem <ModalListItem
label="Node uptime" label="Node avg. uptime"
value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`} value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`}
hidden={nodeUptimePercentage === undefined} hidden={nodeUptimePercentage === undefined}
divider divider
@@ -253,6 +267,7 @@ 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,19 +1,8 @@
import React from 'react'; import React, { useState } from 'react';
import { import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
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';
@@ -75,42 +64,20 @@ 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 [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const [isOpen, setIsOpen] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => setAnchorEl(null); const handleOpenMenu = () => setIsOpen(true);
const handleOnClose = () => setIsOpen(false);
const handleActionSelect = (action: DelegationListItemActions) => { const handleActionSelect = (action: DelegationListItemActions) => {
handleClose();
onActionClick?.(action); onActionClick?.(action);
handleOnClose();
}; };
if (isPending) { if (isPending) {
@@ -126,37 +93,23 @@ export const DelegationsActionsMenu: React.FC<{
} }
return ( return (
<> <ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}>
<IconButton onClick={handleClick}> <ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} />
<MoreVertSharp /> <ActionsMenuItem title="Undelegate" Icon={<Undelegate />} onClick={() => handleActionSelect('undelegate')} />
</IconButton> <ActionsMenuItem
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}> title="Redeem"
<DelegationActionsMenuItem description="Trasfer your rewards to your balance"
title="Delegate more" Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
Icon={<Delegate />} onClick={() => handleActionSelect('redeem')}
onClick={() => handleActionSelect?.('delegate')} disabled={disableRedeemingRewards}
/> />
<DelegationActionsMenuItem <ActionsMenuItem
title="Undelegate" title="Compound"
Icon={<Undelegate />} description="Add your rewards to this delegation"
onClick={() => handleActionSelect?.('undelegate')} Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
disabled={false} onClick={() => handleActionSelect('compound')}
/> disabled={disableCompoundRewards}
<DelegationActionsMenuItem />
title="Redeem" </ActionsMenu>
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,9 +29,6 @@ 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,
@@ -78,8 +75,6 @@ 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)}
/> />
@@ -99,8 +94,6 @@ 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)}
/> />
@@ -120,8 +113,6 @@ 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]
@@ -145,9 +136,6 @@ 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]
@@ -171,8 +159,6 @@ 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]
@@ -196,8 +182,6 @@ 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,9 +1,10 @@
import React from 'react'; import React from 'react';
import { Box, Button, Modal, Typography, SxProps } from '@mui/material'; import { Typography, SxProps, Stack } 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';
@@ -15,9 +16,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 complete'; return 'Delegation successful';
case 'undelegate': case 'undelegate':
return 'Undelegation complete'; return 'Undelegation successful';
case 'compound': case 'compound':
return 'Rewards compounded successfully'; return 'Rewards compounded successfully';
default: default:
@@ -29,9 +30,6 @@ 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;
@@ -41,94 +39,44 @@ 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 (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}> <ErrorModal message={message} sx={sx} open={open} onClose={onClose}>
<Box sx={{ ...modalStyle, ...sx }} textAlign="center"> {children}
<Typography color={(theme) => theme.palette.error.main} mb={1}> </ErrorModal>
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 (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
<Typography color={(theme) => theme.palette.success.main} mb={1}>
{actionToHeader(action)}
</Typography>
<Typography mb={3} color="text.primary">
{message}
</Typography>
{recipient && ( return (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}> <ConfirmationModal
Recipient: {recipient} open={open}
</Typography> onConfirm={onClose || (() => {})}
title={actionToHeader(action)}
confirmButton="Done"
>
<Stack alignItems="center" spacing={2} mb={0}>
{message && <Typography>{message}</Typography>}
{transactions?.length === 1 && (
<Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon />
)} )}
{balanceVested ? ( {transactions && transactions.length > 1 && (
<> <Stack alignItems="center" spacing={1}>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}> <Typography>View the transactions on blockchain:</Typography>
Your current balance: {balance?.toUpperCase()} {transactions.map(({ url, hash }) => (
</Typography> <Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon />
<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)}
/>
))} ))}
</Typography> </Stack>
)} )}
{children} </Stack>
<Button variant="contained" sx={{ mt: 3 }} size="large" onClick={onClose}> </ConfirmationModal>
Finish
</Button>
</Box>
</Modal>
); );
}; };
@@ -40,7 +40,6 @@ 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}
@@ -55,14 +54,10 @@ export const UndelegateModal: React.FC<{
/> />
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider /> <ModalListItem label="Delegation amount" value={`${amount} ${currency.toUpperCase()}`} 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
@@ -0,0 +1,13 @@
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={{ py: 3, pb: 2, fontWeight: 600 }} color="text.primary"> <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
{title} {title}
{subTitle && {subTitle &&
(typeof subTitle === 'string' ? ( (typeof subTitle === 'string' ? (
@@ -0,0 +1,27 @@
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,16 +2,20 @@ 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 }; type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string; divider?: boolean };
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}`; if (fee) return `${fee.amount?.amount} ${fee.amount?.denom.toUpperCase()}`;
return '-'; return '-';
}; };
export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => ( export const ModalFee = ({ fee, isLoading, error, divider }: 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,26 +1,28 @@
import React from 'react'; import React from 'react';
import { Box, Stack, Typography } from '@mui/material'; import { Box, Stack, Typography, TypographyProps } 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;
strong?: boolean; fontWeight?: TypographyProps['fontWeight'];
value: React.ReactNode; light?: boolean;
}> = ({ label, value, hidden, divider, strong }) => ( value?: React.ReactNode;
}> = ({ 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={strong ? 600 : undefined} sx={{ color: 'text.primary' }}> <Typography fontSize="smaller" fontWeight={fontWeight} 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; header: string | React.ReactNode;
subHeader?: string; subHeader?: string;
okLabel: string; okLabel: string;
okDisabled?: boolean; okDisabled?: boolean;
@@ -41,22 +41,25 @@ 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">
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}> {typeof header === 'string' ? (
{header} <Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
</Typography> {header}
</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}
+34 -15
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'; import { AppContext } from '../context/main';
import { Bond, Delegate, Unbond } from '../svg-icons'; import { Delegate, Bonding } from '../svg-icons';
export const Nav = () => { export const Nav = () => {
const location = useLocation(); const location = useLocation();
@@ -29,16 +29,10 @@ export const Nav = () => {
onClick: handleShowReceiveModal, onClick: handleShowReceiveModal,
}, },
{ {
label: 'Bond', label: 'Bonding',
route: '/bond', route: '/bonding',
Icon: Bond, Icon: Bonding,
onClick: () => navigate('/bond'), onClick: () => navigate('/bonding'),
},
{
label: 'Unbond',
route: '/unbond',
Icon: Unbond,
onClick: () => navigate('/unbond'),
}, },
{ {
label: 'Delegation', label: 'Delegation',
@@ -68,9 +62,16 @@ export const Nav = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'flex-start',
marginLeft: 12,
marginRight: 12,
}} }}
> >
<List disablePadding> <List
disablePadding
sx={{
width: '100%',
}}
>
{routesSchema {routesSchema
.filter(({ mode }) => { .filter(({ mode }) => {
if (!mode) { if (!mode) {
@@ -86,17 +87,35 @@ export const Nav = () => {
} }
}) })
.map(({ Icon, onClick, label, route }) => ( .map(({ Icon, onClick, label, route }) => (
<ListItem disableGutters key={label} onClick={onClick} sx={{ cursor: 'pointer' }}> <ListItem
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 sx={{ fontSize: 20 }} /> <Icon
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="primary" color="inherit"
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={<Box sx={{ mt: 1, mr: 1 }}>{Action}</Box>} action={Action}
/> />
{noPadding ? ( {noPadding ? (
<CardContentNoPadding>{children}</CardContentNoPadding> <CardContentNoPadding>{children}</CardContentNoPadding>
@@ -1,42 +1,33 @@
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 } from '@mui/material'; import { Box, Stack, Typography, SxProps, Dialog, DialogTitle, DialogContent } 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 = ({ export const ReceiveModal = ({ onClose }: { onClose: () => void; sx?: SxProps; backdropProps?: object }) => {
onClose,
open,
sx,
backdropProps,
}: {
onClose: () => void;
open: boolean;
sx?: SxProps;
backdropProps?: object;
}) => {
const { clientDetails } = useContext(AppContext); const { clientDetails } = useContext(AppContext);
return ( return (
<SimpleModal <Dialog open maxWidth="sm" fullWidth onClose={onClose}>
header="Receive" <DialogTitle>
okLabel="Ok" <Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
onClose={onClose} <Typography fontSize={20} fontWeight={600}>
open={open} Receive
sx={{ width: 'small', ...sx }} </Typography>
backdropProps={backdropProps} <CloseIcon onClick={onClose} cursor="pointer" />
> </Box>
<Stack spacing={3} sx={{ mt: 1.6 }}> </DialogTitle>
<Stack direction="row" alignItems="center" gap={4}> <DialogContent sx={{ p: 0 }}>
<Typography>Your address:</Typography> <Box sx={{ px: 3 }}>
<ClientAddress withCopy showEntireAddress /> <ModalListItem label="Your address:" value={<ClientAddress withCopy showEntireAddress />} />
</Stack> </Box>
<Stack alignItems="center"> <Stack alignItems="center" sx={{ px: 0, py: 3, mt: 3, bgcolor: 'rgba(251, 110, 78, 5%)' }}>
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.highlight}`, borderRadius: 2, p: 2 }}> <Box sx={{ border: (t) => `1px solid ${t.palette.nym.highlight}`, bgcolor: 'white', borderRadius: 2, p: 3 }}>
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />} {clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
</Box> </Box>
</Stack> </Stack>
</Stack> </DialogContent>
</SimpleModal> </Dialog>
); );
}; };
+1 -2
View File
@@ -5,8 +5,7 @@ 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) if (showReceiveModal) return <ReceiveModal onClose={handleShowReceiveModal} {...hasStorybookStyles} />;
return <ReceiveModal onClose={handleShowReceiveModal} open={showReceiveModal} {...hasStorybookStyles} />;
return null; return null;
}; };
@@ -1,5 +1,4 @@
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';
@@ -7,6 +6,7 @@ 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,20 +42,13 @@ export const CompoundModal: React.FC<{
subHeader="Compound rewards from delegations" subHeader="Compound rewards from delegations"
okLabel="Compound rewards" okLabel="Compound rewards"
> >
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />} {identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}> )}
<Typography>Rewards amount:</Typography> <ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
<Typography> <ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
{amount} {denom.toUpperCase()} <ModalListItem label="Rewards will be added to this delegation" value="" divider />
</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,12 +1,13 @@
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;
@@ -47,20 +48,13 @@ export const RedeemModal: React.FC<{
sx={sx} sx={sx}
backdropProps={backdropProps} backdropProps={backdropProps}
> >
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />} {identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}> )}
<Typography sx={{ color: 'text.primary' }}>Rewards amount:</Typography> <ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
<Typography sx={{ color: 'text.primary' }}> <ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
{amount} {denom.toUpperCase()} <ModalListItem label="Rewards will be transferred to account you are logged in with now" value="" divider />
</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,6 +1,7 @@
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;
@@ -9,15 +10,17 @@ 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" alignItems="center"> <Stack direction="row" justifyContent="space-between">
<Stack direction="row" spacing={4}> <Stack direction="row" spacing={4}>
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={1} alignItems="center">
<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={2}> <Stack direction="row" spacing={1} alignItems="center">
<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,6 +30,7 @@ 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 { CurrencyDenom } from '@nymproject/types'; import { FeeDetails, DecCoin, 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,14 +39,10 @@ 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 />
<ModalListItem <ModalFee fee={fee} divider isLoading={false} />
label="Fee for this transaction"
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
divider
/>
</Stack> </Stack>
</SimpleModal> </SimpleModal>
); );
@@ -56,6 +56,7 @@ 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
@@ -76,9 +77,8 @@ export const SendInputModal = ({
{error} {error}
</Typography> </Typography>
</Stack> </Stack>
<Stack gap={0.5} sx={{ mt: 2 }}> <Stack gap={0.5} sx={{ mt: 1 }}>
<ModalListItem label="Account balance" value={balance} divider strong /> <ModalListItem label="Account balance:" value={balance?.toUpperCase()} divider fontWeight={600} />
<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
@@ -0,0 +1,35 @@
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="h6" sx={{ fontWeight: 600 }}> <Typography width="100%" variant="h5" sx={{ fontWeight: 600 }}>
{title} {title}
</Typography> </Typography>
</Box> </Box>
@@ -14,11 +14,13 @@ 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(() => {
(async () => { fetchBalances();
await fetchBalance();
await fetchTokenAllocation();
})();
}, []); }, []);
useEffect(() => { useEffect(() => {
+358
View File
@@ -0,0 +1,358 @@
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,6 +81,7 @@ 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();
@@ -94,12 +95,11 @@ 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,3 +1,4 @@
export * from './main'; export * from './main';
export * from './auth'; export * from './auth';
export * from './accounts'; export * from './accounts';
export * from './bonding';
+2 -9
View File
@@ -33,7 +33,6 @@ 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;
@@ -47,10 +46,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;
handleShowSettings: () => void; onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
handleSwitchMode: () => void;
handleShowSendModal: () => void; handleShowSendModal: () => void;
handleShowReceiveModal: () => void; handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void; setIsLoading: (isLoading: boolean) => void;
@@ -62,7 +61,6 @@ 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);
@@ -81,7 +79,6 @@ 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);
@@ -235,7 +232,6 @@ 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 = () =>
@@ -259,7 +255,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
userBalance, userBalance,
showAdmin, showAdmin,
showTerminal, showTerminal,
showSettings,
network, network,
loginType, loginType,
setIsLoading, setIsLoading,
@@ -272,7 +267,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
logIn, logIn,
logOut, logOut,
onAccountChange, onAccountChange,
handleShowSettings,
showSendModal, showSendModal,
showReceiveModal, showReceiveModal,
handleShowSendModal, handleShowSendModal,
@@ -294,7 +288,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
network, network,
storedAccounts, storedAccounts,
showTerminal, showTerminal,
showSettings,
showSendModal, showSendModal,
showReceiveModal, showReceiveModal,
], ],
+181
View File
@@ -0,0 +1,181 @@
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>;
};
+4 -12
View File
@@ -1,5 +1,4 @@
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';
@@ -29,11 +28,8 @@ export const RewardsContext = createContext<TRewardsContext>({
}, },
}); });
export const RewardsContextProvider: FC<{ export const RewardsContextProvider: FC<{}> = ({ children }) => {
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 () => {
@@ -41,12 +37,8 @@ export const RewardsContextProvider: FC<{
}; };
useEffect(() => { useEffect(() => {
if (currentNetwork !== network) { resetState();
// reset state and refresh }, []);
resetState();
setCurrentNetwork(network);
}
}, [network]);
const memoizedValue = useMemo( const memoizedValue = useMemo(
() => ({ () => ({
@@ -60,7 +52,7 @@ export const RewardsContextProvider: FC<{
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
}), }),
[isLoading, error, totalRewards, network], [isLoading, error, totalRewards],
); );
return <RewardsContext.Provider value={memoizedValue}>{children}</RewardsContext.Provider>; return <RewardsContext.Provider value={memoizedValue}>{children}</RewardsContext.Provider>;
+3 -4
View File
@@ -26,21 +26,20 @@ 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: 3, py: 5,
px: 5,
}} }}
display="flex" display="flex"
flexDirection="column" flexDirection="column"
justifyContent="space-between" justifyContent="space-between"
> >
<Box> <Box>
<Box sx={{ mb: 4 }}> <Box sx={{ ml: 5, mb: 3 }}>
<NymWordmark height={14} /> <NymWordmark height={14} />
</Box> </Box>
<Nav /> <Nav />
</Box> </Box>
{appVersion && ( {appVersion && (
<Box color="#888" mt={8}> <Box color="#888" ml={5} 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,8 +1,9 @@
/* 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/main'; import { AppContext } from '../../../context';
const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index; const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index;
@@ -21,6 +22,9 @@ 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)
@@ -29,19 +33,31 @@ 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="#E6E6E6" /> <rect y="2" width="100%" height="6" rx="0" fill="text.dark" />
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill="#121726" /> <rect
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={+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i) ? '#121726' : '#B9B9B9'} color={
+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 ? '#121726' : '#B9B9B9'} color={percentageComplete === 100 ? (mode === 'light' ? 'text.dark' : theme.palette.success.main) : '#B9B9B9'}
tooltipText="End of vesting schedule" tooltipText="End of vesting schedule"
/> />
</svg> </svg>
+18 -2
View File
@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { AppContext } from '../../context/main'; import { AppContext } from '../../context/main';
@@ -9,9 +9,25 @@ 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);
@@ -21,7 +37,7 @@ export const Balance = () => {
<PageLayout> <PageLayout>
<Box display="flex" flexDirection="column" gap={2}> <Box display="flex" flexDirection="column" gap={2}>
<BalanceCard /> <BalanceCard />
<VestingCard onTransfer={handleShowTransferModal} /> {showVestingCard && <VestingCard onTransfer={handleShowTransferModal} />}
{showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />} {showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />}
</Box> </Box>
</PageLayout> </PageLayout>
+28 -4
View File
@@ -65,20 +65,44 @@ const VestingSchedule = () => {
))} ))}
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }}> <TableCell
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 align="left" sx={{ borderBottom: 'none' }}> <TableCell
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 sx={{ borderBottom: 'none' }}> <TableCell
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 sx={{ borderBottom: 'none', textTransform: 'uppercase' }} align="right"> <TableCell
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>
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,252 @@
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
@@ -0,0 +1,70 @@
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;
}
+7 -11
View File
@@ -85,7 +85,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
useEffect(() => { useEffect(() => {
refresh(); refresh();
}, [network, clientDetails, confirmationModalProps]); }, [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: 'Delegations can take up to one hour to process', message: 'This operation 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,11 +240,9 @@ 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,
@@ -270,11 +268,9 @@ 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,
@@ -292,7 +288,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
return ( return (
<> <>
<Paper elevation={0} sx={{ p: 4, mt: 4 }}> <Paper elevation={0} sx={{ p: 3, 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>
@@ -306,7 +302,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
noIcon noIcon
/> />
</Box> </Box>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="end">
<RewardsSummary isLoading={isLoading} totalDelegation={totalDelegations} totalRewards={totalRewards} /> <RewardsSummary isLoading={isLoading} totalDelegation={totalDelegations} totalRewards={totalRewards} />
<Button <Button
variant="contained" variant="contained"
@@ -327,7 +323,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
</Paper> </Paper>
{pendingDelegations && ( {pendingDelegations && (
<Paper elevation={0} sx={{ p: 4, mt: 2 }}> <Paper elevation={0} sx={{ p: 3, 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} />
@@ -431,8 +427,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 network={network}> <DelegationContextProvider>
<RewardsContextProvider network={network}> <RewardsContextProvider>
<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