Compare commits

...

33 Commits

Author SHA1 Message Date
Tommy Verrall a2e982a677 update workflow to include network-stat 2022-08-10 09:44:13 +01:00
tommy 7b15f350cd add service-provider vesioning to match other binaries 2022-08-10 10:14:07 +02:00
tommy 2b4917b8b1 fix messaging on init for nym-client 2022-08-10 10:11:00 +02:00
tommy 921e558660 Update versions on binaries 2022-08-09 11:56:54 +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
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
Dave Hrycyszyn b957b939cf Typo fix 2022-08-04 11:01:36 +01: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
104 changed files with 3451 additions and 606 deletions
@@ -45,3 +45,4 @@ jobs:
target/release/nym-socks5-client
target/release/nym-validator-api
target/release/nym-network-requester
target/release/nym-network-statistics
+2
View File
@@ -37,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])
- 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])
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
### Changed
@@ -77,6 +78,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1478]: https://github.com/nymtech/nym/pull/1478
[#1482]: https://github.com/nymtech/nym/pull/1482
[#1487]: https://github.com/nymtech/nym/pull/1487
[#1496]: https://github.com/nymtech/nym/pull/1496
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
Generated
+1
View File
@@ -3325,6 +3325,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"task",
"thiserror",
"time 0.3.9",
"tokio",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.0.1"
version = "1.0.2"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -82,7 +82,7 @@ fn version_check(cfg: &Config) -> bool {
if binary_version == config_version {
true
} else {
warn!("The mixnode binary has different version than what is specified in config file! {} and {}", binary_version, config_version);
warn!("The native-client binary has different version than what is specified in config file! {} and {}", binary_version, config_version);
if is_minor_version_compatible(binary_version, config_version) {
info!("but they are still semver compatible. However, consider running the `upgrade` command");
true
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.0.1"
version = "1.0.2"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+1 -1
View File
@@ -507,7 +507,7 @@ mod test {
for (expected, raw_display) in values {
let coin = DecCoin {
denom: Network::MAINNET.mix_denom().display.into(),
denom: Network::MAINNET.mix_denom().display,
amount: raw_display.parse().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
VALIDATOR_API_URL=https://validator.nymtech.net
VALIDATOR_URL=https://rpc.nyx.nodes.guru
BIG_DIPPER_URL=https://blocks.nymtech.net
CURRENCY_DENOM=unym
CURRENCY_STAKING_DENOM=unyx
+2 -1
View File
@@ -1,5 +1,6 @@
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
CURRENCY_DENOM=unymt
CURRENCY_STAKING_DENOM=unyxt
+2 -1
View File
@@ -1,6 +1,7 @@
// master APIs
export const API_BASE_URL = process.env.EXPLORER_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;
// 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 MIXNODE_API = `${API_BASE_URL}/mix-node`;
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 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.
+15 -8
View File
@@ -6,7 +6,6 @@ import {
DialogContent,
DialogActions,
DialogTitle,
IconButton,
Slider,
Typography,
Box,
@@ -25,6 +24,7 @@ import { useIsMobile } from '../../hooks/useIsMobile';
const FilterItem = ({
label,
id,
tooltipInfo,
value,
marks,
scale,
@@ -36,6 +36,7 @@ const FilterItem = ({
}) => (
<Box sx={{ p: 2 }}>
<Typography gutterBottom>{label}</Typography>
<Typography fontSize={12}>{tooltipInfo}</Typography>
<Slider
value={value}
onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])}
@@ -50,7 +51,7 @@ const FilterItem = ({
);
export const Filters = () => {
const { filterMixnodes, fetchMixnodes } = useMainContext();
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext();
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
const isMobile = useIsMobile();
@@ -129,17 +130,23 @@ export const Filters = () => {
variant={isMobile ? 'standard' : 'outlined'}
action={
<Button size="small" onClick={onClearFilters}>
Clear
CLEAR FILTERS
</Button>
}
sx={{ width: 300 }}
>
Filters applied
{mixnodes?.data?.length} mixnodes matched your criteria
</Alert>
</Snackbar>
<IconButton size="large" onClick={handleToggleShowFilters}>
<Tune />
</IconButton>
<Button
size="large"
variant="text"
color="inherit"
endIcon={<Tune />}
onClick={handleToggleShowFilters}
sx={{ textTransform: 'none' }}
>
Advanced filters
</Button>
<Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth>
<DialogTitle>Mixnode filters</DialogTitle>
<DialogContent dividers>
+20 -54
View File
@@ -18,6 +18,8 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
{ label: '90', value: 90 },
{ label: '100', value: 100 },
],
tooltipInfo:
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
},
stakeSaturation: {
label: 'Stake saturation (%)',
@@ -43,65 +45,29 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
},
],
max: upperSaturationValue,
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
},
stake: {
label: 'Stake (NYM)',
id: EnumFilterKey.stake,
value: [20, 90],
min: 20,
max: 90,
routingScore: {
label: 'Routing score (%)',
id: EnumFilterKey.routingScore,
value: [0, 100],
marks: [
{
value: 0,
label: '1',
},
{
value: 10,
label: '10',
},
{
value: 20,
label: '100',
},
{
value: 30,
label: '1k',
},
{
value: 40,
label: '10k',
},
{
value: 50,
label: '100k',
},
{
value: 60,
label: '1M',
},
{
value: 70,
label: '10M',
},
{
value: 80,
label: '100M',
},
{
value: 90,
label: '1B',
},
{ label: '0', value: 0 },
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '30', value: 30 },
{ label: '40', value: 40 },
{ label: '50', value: 50 },
{ label: '60', value: 60 },
{ label: '70', value: 70 },
{ label: '80', value: 80 },
{ label: '90', value: 90 },
{ label: '100', value: 100 },
],
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 lowerValue = value_1 / 100;
const upperValue = value_2 / 100;
@@ -110,7 +76,7 @@ const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
};
export const formatOnSave = (filters: TFilters) => ({
stake: formatStakeValuesToMinorDenom(filters.stake.value),
routingScore: filters.routingScore.value,
profitMargin: filters.profitMargin.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.stake_saturation >= filters.stakeSaturation[0] &&
m.stake_saturation <= filters.stakeSaturation[1] &&
+m.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] &&
+m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1],
m.avg_uptime >= filters.routingScore[0] &&
m.avg_uptime <= filters.routingScore[1],
);
setMixnodes({ data: filtered, isLoading: false });
};
+2 -1
View File
@@ -4,7 +4,7 @@ import { Mark } from '@mui/base';
export enum EnumFilterKey {
profitMargin = 'profitMargin',
stakeSaturation = 'stakeSaturation',
stake = 'stake',
routingScore = 'routingScore',
}
export type TFilterItem = {
@@ -15,6 +15,7 @@ export type TFilterItem = {
min?: number;
max?: number;
scale?: (value: number) => number;
tooltipInfo?: string;
};
export type TFilters = { [key in EnumFilterKey]: TFilterItem };
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.0.1"
version = "1.0.2"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Mixnet Gateway"
edition = "2021"
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.0.1"
version = "1.0.2"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -32,7 +32,7 @@ yarn install
## Development mode
You can compile nym-connectin development mode by running the following command inside the `nym-connect` directory:
You can compile nym-connect in development mode by running the following command inside the `nym-connect` directory:
```
yarn dev
+87 -10
View File
@@ -3273,7 +3273,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"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]]
@@ -3290,6 +3300,19 @@ dependencies = [
"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]]
name = "password-hash"
version = "0.3.2"
@@ -4550,6 +4573,15 @@ dependencies = [
"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]]
name = "signature"
version = "1.3.2"
@@ -4635,9 +4667,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "state"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5"
checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
dependencies = [
"loom",
]
@@ -4656,7 +4688,7 @@ checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"parking_lot",
"parking_lot 0.11.2",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
@@ -4838,7 +4870,7 @@ dependencies = [
"ndk-glue",
"ndk-sys",
"objc",
"parking_lot",
"parking_lot 0.11.2",
"raw-window-handle",
"scopeguard",
"serde",
@@ -5241,7 +5273,9 @@ dependencies = [
"mio",
"num_cpus",
"once_cell",
"parking_lot 0.12.1",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"winapi",
@@ -5898,11 +5932,11 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.30.0",
"windows_i686_gnu 0.30.0",
"windows_i686_msvc 0.30.0",
"windows_x86_64_gnu 0.30.0",
"windows_x86_64_msvc 0.30.0",
]
[[package]]
@@ -5915,12 +5949,31 @@ dependencies = [
"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]]
name = "windows_aarch64_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_gen"
version = "0.30.0"
@@ -5937,12 +5990,24 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_macros"
version = "0.30.0"
@@ -5973,12 +6038,24 @@ version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "windows_x86_64_msvc"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "winreg"
version = "0.7.0"
+2 -2
View File
@@ -32,14 +32,14 @@ log = "0.4"
once_cell = "1.7.2"
pretty_env_logger = "0.4"
rand = "0.6.5"
reqwest = "0.11.9"
reqwest = {version = "0.11.9", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.23", features = ["derive"] }
tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.10", features = ["sync", "time"] }
tokio = { version = "1.10", features = ["full"] }
toml = "0.5.8"
url = "2.2"
+2
View File
@@ -58,6 +58,8 @@ fn main() {
mixnet::bond::unbond_gateway,
mixnet::bond::unbond_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::get_delegator_rewards,
mixnet::delegate::get_pending_delegation_events,
@@ -1,3 +1,5 @@
use std::time::Duration;
use crate::error::BackendError;
use crate::state::WalletState;
use crate::{Gateway, MixNode};
@@ -5,8 +7,18 @@ use nym_types::currency::DecCoin;
use nym_types::gateway::GatewayBond;
use nym_types::mixnode::MixNodeBond;
use nym_types::transaction::TransactionExecuteResult;
use reqwest::Error as ReqwestError;
use serde::{Deserialize, Serialize};
use validator_client::nymd::{Coin, Fee};
#[derive(Debug, Serialize, Deserialize)]
pub struct NodeDescription {
name: String,
description: String,
link: String,
location: String,
}
#[tauri::command]
pub async fn bond_gateway(
gateway: Gateway,
@@ -198,3 +210,51 @@ pub async fn get_operator_rewards(
);
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 });
}
@@ -20,7 +20,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Close />
</IconButton>
</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
</Typography>
</DialogTitle>
@@ -29,7 +29,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle
<Alert
severity="warning"
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>Follow steps below to create password.</Typography>
+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 { AppContext } from '../context/main';
import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node';
import { MultiAccounts } from './Accounts';
import { config } from '../config';
export const AppBar = () => {
const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } =
useContext(AppContext);
const { logOut, handleShowTerminal, appEnv, mode, handleSwitchMode } = useContext(AppContext);
const navigate = useNavigate();
return (
@@ -31,7 +29,7 @@ export const AppBar = () => {
<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' ? (
{mode === 'dark' ? (
<LightModeOutlinedIcon fontSize="small" />
) : (
<ModeNightOutlinedIcon fontSize="small" sx={{ transform: 'rotate(180deg)' }} />
@@ -45,15 +43,6 @@ export const AppBar = () => {
</IconButton>
</Grid>
)}
<Grid item>
<IconButton
onClick={handleShowSettings}
sx={{ color: showSettings ? 'primary.main' : 'text.primary' }}
size="small"
>
<NodeIcon fontSize="small" />
</IconButton>
</Grid>
<Grid item>
<IconButton
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>
);
};
@@ -10,9 +10,9 @@ export default {
const Template: ComponentStory<typeof ConfirmTx> = (args) => (
<ConfirmTx {...args}>
<ModalListItem label="Transaction type" value="Bond" divider />
<ModalListItem label="Current bond" value="100 NYM" divider />
<ModalListItem label="Additional bond" value="50 NYM" divider />
<ModalListItem label="Transaction type:" value="Bond" divider />
<ModalListItem label="Current bond:" value="100 NYM" divider />
<ModalListItem label="Additional bond:" value="50 NYM" divider />
</ConfirmTx>
);
@@ -36,7 +36,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
color: 'text.primary',
}}
>
{!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
</IconButton>
</Tooltip>
);
@@ -168,8 +168,8 @@ export const DelegateModal: React.FC<{
onPrev={resetFeeState}
onConfirm={handleOk}
>
<ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
<ModalListItem label="Node identity key:" value={identityKey} divider />
<ModalListItem label="Amount:" value={`${amount} ${denom.toUpperCase()}`} divider />
</ConfirmTx>
);
}
@@ -184,23 +184,24 @@ export const DelegateModal: React.FC<{
}
}}
header={header || 'Delegate'}
subHeader="Delegate to mixnode"
okLabel={buttonText || 'Delegate stake'}
okDisabled={!isValidated}
sx={sx}
backdropProps={backdropProps}
>
<IdentityKeyFormField
required
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
/>
<Box sx={{ mt: 2 }}>
<IdentityKeyFormField
required
fullWidth
placeholder="Node identity key"
onChanged={handleIdentityKeyChanged}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
/>
</Box>
<Typography
component="div"
textAlign="left"
@@ -230,29 +231,30 @@ export const DelegateModal: React.FC<{
{errorAmount}
</Typography>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance} divider />
<ModalListItem label="Account balance:" value={accountBalance?.toUpperCase()} divider strong />
</Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
<ModalListItem label="Rewards payout interval:" value={rewardInterval} hidden divider />
<ModalListItem
label="Node profit margin"
label="Node profit margin:"
value={`${profitMarginPercentage ? `${profitMarginPercentage}%` : '-'}`}
hidden={profitMarginPercentage === undefined}
divider
/>
<ModalListItem
label="Node uptime"
label="Node avg. uptime:"
value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`}
hidden={nodeUptimePercentage === undefined}
divider
/>
<ModalListItem
label="Node est. reward per epoch"
label="Node est. reward per epoch:"
value={`${estimatedReward} ${denom.toUpperCase()}`}
hidden
divider
/>
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
</SimpleModal>
);
};
@@ -1,19 +1,8 @@
import React from 'react';
import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import { MoreVertSharp } from '@mui/icons-material';
import React, { useState } from 'react';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { DelegationEventKind } from '@nymproject/types';
import { Delegate, Undelegate } from '../../svg-icons';
import { ActionsMenu, ActionsMenuItem } from '../ActionsMenu';
import { DelegateListItemPending } from './types';
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<{
onActionClick?: (action: DelegationListItemActions) => void;
isPending?: DelegationEventKind;
disableRedeemingRewards?: boolean;
disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setAnchorEl(null);
const handleOpenMenu = () => setIsOpen(true);
const handleOnClose = () => setIsOpen(false);
const handleActionSelect = (action: DelegationListItemActions) => {
handleClose();
onActionClick?.(action);
handleOnClose();
};
if (isPending) {
@@ -126,37 +93,23 @@ export const DelegationsActionsMenu: React.FC<{
}
return (
<>
<IconButton onClick={handleClick}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<DelegationActionsMenuItem
title="Delegate more"
Icon={<Delegate />}
onClick={() => handleActionSelect?.('delegate')}
/>
<DelegationActionsMenuItem
title="Undelegate"
Icon={<Undelegate />}
onClick={() => handleActionSelect?.('undelegate')}
disabled={false}
/>
<DelegationActionsMenuItem
title="Redeem"
description="Transfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect?.('redeem')}
disabled={disableRedeemingRewards}
/>
<DelegationActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect?.('compound')}
disabled={disableCompoundRewards}
/>
</Menu>
</>
<ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}>
<ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} />
<ActionsMenuItem title="Undelegate" Icon={<Undelegate />} onClick={() => handleActionSelect('undelegate')} />
<ActionsMenuItem
title="Redeem"
description="Trasfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect('redeem')}
disabled={disableRedeemingRewards}
/>
<ActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect('compound')}
disabled={disableCompoundRewards}
/>
</ActionsMenu>
);
};
@@ -29,9 +29,6 @@ const transactionForDarkTheme = {
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO',
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0',
};
const balance = '104 NYMT';
const balanceVested = '12 NYMT';
const recipient = 'nymt1923pujepxfnv8dqyxqrl078s4ysf3xn2p7z2xa';
const Content: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
children,
@@ -78,8 +75,6 @@ export const DelegateSuccess = () => {
status="success"
action="delegate"
message="You delegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -99,8 +94,6 @@ export const UndelegateSuccess = () => {
status="success"
action="undelegate"
message="You undelegated 5 NYM"
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -120,8 +113,6 @@ export const RedeemSuccess = () => {
status="success"
action="redeem"
message="42 NYM"
recipient={recipient}
balance={balance}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -145,9 +136,6 @@ export const RedeemWithVestedSuccess = () => {
status="success"
action="redeem"
message="42 NYM"
recipient={recipient}
balance={balance}
balanceVested={balanceVested}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -171,8 +159,6 @@ export const RedeemAllSuccess = () => {
status="success"
action="redeem-all"
message="42 NYM"
recipient={recipient}
balance={balance}
transactions={
theme.palette.mode === 'light'
? [transaction, transaction]
@@ -196,8 +182,6 @@ export const Error = () => {
status="error"
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."
recipient={recipient}
balance={balance}
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
{...storybookStyles(theme)}
/>
@@ -1,9 +1,10 @@
import React from 'react';
import { Box, Button, Modal, Typography, SxProps } from '@mui/material';
import { Box, Button, Modal, Typography, SxProps, Stack } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { Console } from 'src/utils/console';
import { modalStyle } from '../Modals/styles';
import { LoadingModal } from '../Modals/LoadingModal';
import { ConfirmationModal } from '../Modals/ConfirmationModal';
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
@@ -15,9 +16,9 @@ const actionToHeader = (action: ActionType): string => {
case 'redeem-all':
return 'All rewards redeemed successfully';
case 'delegate':
return 'Delegation complete';
return 'Delegation successful';
case 'undelegate':
return 'Undelegation complete';
return 'Undelegation successful';
case 'compound':
return 'Rewards compounded successfully';
default:
@@ -29,9 +30,6 @@ export type DelegationModalProps = {
status: 'loading' | 'success' | 'error';
action: ActionType;
message?: string;
recipient?: string;
balance?: string;
balanceVested?: string;
transactions?: {
url: string;
hash: string;
@@ -45,20 +43,7 @@ export const DelegationModal: React.FC<
sx?: SxProps;
backdropProps?: object;
}
> = ({
status,
action,
message,
recipient,
balance,
balanceVested,
transactions,
open,
onClose,
children,
sx,
backdropProps,
}) => {
> = ({ status, action, message, transactions, open, onClose, children, sx, backdropProps }) => {
if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />;
if (status === 'error') {
@@ -81,54 +66,28 @@ export const DelegationModal: React.FC<
}
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 && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Recipient: {recipient}
</Typography>
return (
<ConfirmationModal
open={open}
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 ? (
<>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Your current balance: {balance?.toUpperCase()}
</Typography>
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
({balanceVested.toUpperCase()} is unlocked in your vesting account)
</Typography>
</>
) : (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Your current balance: {balance?.toUpperCase()}
</Typography>
)}
{transactions && (
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
Check the transaction {transactions.length > 1 ? 'hashes' : 'hash'}:
{transactions.map((transaction) => (
<Link
key={transaction.hash}
href={transaction.url}
target="_blank"
sx={{ ml: 1 }}
text={transaction.hash.slice(0, 6)}
/>
{transactions && transactions.length > 1 && (
<Stack alignItems="center" spacing={1}>
<Typography>View the transactions on blockchain:</Typography>
{transactions.map(({ url, hash }) => (
<Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon />
))}
</Typography>
</Stack>
)}
{children}
<Button variant="contained" sx={{ mt: 3 }} size="large" onClick={onClose}>
Finish
</Button>
</Box>
</Modal>
</Stack>
</ConfirmationModal>
);
};
@@ -55,7 +55,7 @@ export const UndelegateModal: React.FC<{
/>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider />
<ModalListItem label="Delegation amount:" value={`${amount} ${currency}`} divider />
</Box>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
+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,
}: ConfirmationModalProps) => {
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}
{subTitle &&
(typeof subTitle === 'string' ? (
@@ -0,0 +1,26 @@
import React from 'react';
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
import { modalStyle } from './styles';
export const ErrorModal: React.FC<{
open: boolean;
message?: string;
sx?: SxProps;
backdropProps?: object;
onClose: () => void;
}> = ({ children, open, 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}>
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 { CircularProgress } from '@mui/material';
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) => {
if (isLoading) return <CircularProgress size={15} />;
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 '-';
};
export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => (
<ModalListItem label="Estimated fee for this operation" value={getValue({ fee, isLoading, error })} />
export const ModalFee = ({ fee, isLoading, error, divider }: TFeeProps) => (
<>
<ModalListItem label="Fee for this operation:" value={getValue({ fee, isLoading, error })} />
{divider && <ModalDivider />}
</>
);
@@ -7,20 +7,18 @@ export const ModalListItem: React.FC<{
divider?: boolean;
hidden?: boolean;
strong?: boolean;
value: React.ReactNode;
value?: React.ReactNode;
}> = ({ label, value, hidden, divider, strong }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between">
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
{label}:
</Typography>
<Typography
fontSize="smaller"
fontWeight={strong ? 600 : undefined}
sx={{ color: 'text.primary', textTransform: 'uppercase' }}
>
{value}
{label}
</Typography>
{value && (
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
{value}
</Typography>
)}
</Stack>
{divider && <ModalDivider />}
</Box>
@@ -14,7 +14,7 @@ export const SimpleModal: React.FC<{
onClose?: () => void;
onOk?: () => Promise<void>;
onBack?: () => void;
header: string;
header: string | React.ReactNode;
subHeader?: string;
okLabel: string;
okDisabled?: boolean;
@@ -41,9 +41,13 @@ export const SimpleModal: React.FC<{
<Box sx={{ ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
{header}
</Typography>
{typeof header === 'string' ? (
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
{header}
</Typography>
) : (
header
)}
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
</Stack>
{subHeader && (
+34 -15
View File
@@ -2,8 +2,8 @@ import React, { useState, useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { AppContext } from '../context';
import { Bond, Delegate, Unbond } from '../svg-icons';
import { AppContext } from '../context/main';
import { Delegate, Bonding } from '../svg-icons';
export const Nav = () => {
const location = useLocation();
@@ -29,16 +29,10 @@ export const Nav = () => {
onClick: handleShowReceiveModal,
},
{
label: 'Bond',
route: '/bond',
Icon: Bond,
onClick: () => navigate('/bond'),
},
{
label: 'Unbond',
route: '/unbond',
Icon: Unbond,
onClick: () => navigate('/unbond'),
label: 'Bonding',
route: '/bonding',
Icon: Bonding,
onClick: () => navigate('/bonding'),
},
{
label: 'Delegation',
@@ -68,9 +62,16 @@ export const Nav = () => {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
marginLeft: 12,
marginRight: 12,
}}
>
<List disablePadding>
<List
disablePadding
sx={{
width: '100%',
}}
>
{routesSchema
.filter(({ mode }) => {
if (!mode) {
@@ -86,17 +87,35 @@ export const Nav = () => {
}
})
.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
sx={{
height: '20px',
minWidth: 30,
color: location.pathname === route ? 'primary.main' : 'text.primary',
}}
>
<Icon sx={{ fontSize: 20 }} />
<Icon
sx={{
fontSize: 20,
}}
/>
</ListItemIcon>
<ListItemText
sx={{
height: '20px',
margin: 0,
color: location.pathname === route ? 'primary.main' : 'text.primary',
'& .MuiListItemText-primary': {
fontSize: 14,
+1 -1
View File
@@ -26,7 +26,7 @@ export const NymCard: React.FC<{
subheader={subheader}
data-testid={dataTestid || title}
subheaderTypographyProps={{ variant: 'subtitle1' }}
action={<Box sx={{ mt: 1, mr: 1 }}>{Action}</Box>}
action={Action}
/>
{noPadding ? (
<CardContentNoPadding>{children}</CardContentNoPadding>
@@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
@@ -7,6 +6,7 @@ import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalFee } from '../Modals/ModalFee';
import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const CompoundModal: React.FC<{
open: boolean;
@@ -42,20 +42,13 @@ export const CompoundModal: React.FC<{
subHeader="Compound rewards from delegations"
okLabel="Compound rewards"
>
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography>Rewards amount:</Typography>
<Typography>
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller">
Rewards will be transferred to account you are logged in with now
</Typography>
{identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)}
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
<ModalListItem label="Rewards will be added to this delegation" value="" divider />
</SimpleModal>
);
};
@@ -1,12 +1,13 @@
import React, { useEffect } from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { SxProps } from '@mui/material';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee';
import { SimpleModal } from '../Modals/SimpleModal';
import { FeeWarning } from '../FeeWarning';
import { ModalListItem } from '../Modals/ModalListItem';
export const RedeemModal: React.FC<{
open: boolean;
@@ -47,20 +48,13 @@ export const RedeemModal: React.FC<{
sx={sx}
backdropProps={backdropProps}
>
{identityKey && <IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} />}
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography sx={{ color: 'text.primary' }}>Rewards amount:</Typography>
<Typography sx={{ color: 'text.primary' }}>
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
Rewards will be transferred to account you are logged in with now
</Typography>
{identityKey && (
<IdentityKeyFormField readOnly fullWidth initialValue={identityKey} showTickOnValid={false} sx={{ mb: 2 }} />
)}
<ModalListItem label="Rewards amount" value={` ${amount} ${denom.toUpperCase()}`} divider />
{fee && <FeeWarning amount={amount} fee={fee} />}
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} />
<ModalFee fee={fee} isLoading={isFeeLoading} error={feeError} divider />
<ModalListItem label="Rewards will be transferred to account you are logged in with now" value="" divider />
</SimpleModal>
);
};
@@ -30,6 +30,7 @@ export const SendInput = () => {
<SendInputModal
toAddress=""
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
denom="nym"
onNext={() => {}}
onClose={() => {}}
onAddressChange={() => {}}
@@ -1,9 +1,9 @@
import React from 'react';
import { Stack, SxProps } from '@mui/material';
import { CurrencyDenom } from '@nymproject/types';
import { FeeDetails, DecCoin } from '@nymproject/types';
import { FeeDetails, DecCoin, CurrencyDenom } from '@nymproject/types';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
import { ModalFee } from '../Modals/ModalFee';
export const SendDetailsModal = ({
amount,
@@ -39,14 +39,10 @@ export const SendDetailsModal = ({
backdropProps={backdropProps}
>
<Stack gap={0.5} sx={{ mt: 4 }}>
<ModalListItem label="From" value={fromAddress} divider />
<ModalListItem label="To" value={toAddress} divider />
<ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalListItem
label="Fee for this transaction"
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
divider
/>
<ModalListItem label="From:" value={fromAddress} divider />
<ModalListItem label="To:" value={toAddress} divider />
<ModalListItem label="Amount:" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalFee fee={fee} divider isLoading={false} />
</Stack>
</SimpleModal>
);
@@ -77,8 +77,8 @@ export const SendInputModal = ({
</Typography>
</Stack>
<Stack gap={0.5} sx={{ mt: 2 }}>
<ModalListItem label="Account balance" value={balance} divider strong />
<ModalListItem label="Your address" value={fromAddress} divider />
<ModalListItem label="Account balance:" value={balance?.toUpperCase()} divider strong />
<ModalListItem label="Your address:" value={fromAddress} divider />
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
Est. fee for this transaction will be show on the next page
</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>
);
@@ -14,11 +14,13 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
clientDetails,
} = useContext(AppContext);
const fetchBalances = async () => {
await fetchBalance();
await fetchTokenAllocation();
};
useEffect(() => {
(async () => {
await fetchBalance();
await fetchTokenAllocation();
})();
fetchBalances();
}, []);
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);
+1
View File
@@ -1,3 +1,4 @@
export * from './main';
export * from './auth';
export * from './accounts';
export * from './bonding';
+2 -9
View File
@@ -33,7 +33,6 @@ type TLoginType = 'mnemonic' | 'password';
export type TAppContext = {
mode: 'light' | 'dark';
handleSwitchMode: () => void;
appEnv?: AppEnv;
appVersion?: string;
clientDetails?: Account;
@@ -47,10 +46,10 @@ export type TAppContext = {
isAdminAddress: boolean;
error?: string;
loginType?: TLoginType;
showSettings: boolean;
showSendModal: boolean;
showReceiveModal: boolean;
handleShowSettings: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
handleSwitchMode: () => void;
handleShowSendModal: () => void;
handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void;
@@ -62,7 +61,6 @@ export type TAppContext = {
handleShowTerminal: () => void;
signInWithPassword: (password: string) => void;
logOut: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
};
export const AppContext = createContext({} as TAppContext);
@@ -81,7 +79,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [error, setError] = useState<string>();
const [appVersion, setAppVersion] = useState<string>();
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSettings, setShowSettings] = useState(false);
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
@@ -235,7 +232,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowTerminal = () => setShowTerminal((show) => !show);
const switchNetwork = (_network: Network) => setNetwork(_network);
const handleShowSettings = () => setShowSettings((show) => !show);
const handleShowSendModal = () => setShowSendModal((show) => !show);
const handleShowReceiveModal = () => setShowReceiveModal((show) => !show);
const handleSwitchMode = () =>
@@ -259,7 +255,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
userBalance,
showAdmin,
showTerminal,
showSettings,
network,
loginType,
setIsLoading,
@@ -272,7 +267,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
logIn,
logOut,
onAccountChange,
handleShowSettings,
showSendModal,
showReceiveModal,
handleShowSendModal,
@@ -294,7 +288,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
network,
storedAccounts,
showTerminal,
showSettings,
showSendModal,
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>;
};
+3 -4
View File
@@ -26,21 +26,20 @@ export const ApplicationLayout: React.FC = ({ children }) => {
sx={{
background: (t) => t.palette.nym.nymWallet.nav.background,
overflow: 'auto',
py: 3,
px: 5,
py: 5,
}}
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<Box>
<Box sx={{ mb: 4 }}>
<Box sx={{ ml: 5, mb: 3 }}>
<NymWordmark height={14} />
</Box>
<Nav />
</Box>
{appVersion && (
<Box color="#888" mt={8}>
<Box color="#888" ml={5} mt={8}>
Version {appVersion}
</Box>
)}
@@ -87,12 +87,12 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
) : (
<>
<ModalListItem
label="Unlocked transferrable tokens"
label="Unlocked transferrable tokens:"
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`}
divider
/>
<ModalListItem
label="Est. fee for this transaction"
label="Est. fee for this transaction:"
value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />}
divider
/>
@@ -1,5 +1,6 @@
/* eslint-disable react/no-array-index-key */
import React, { useContext } from 'react';
import { useTheme } from '@mui/material/styles';
import { Box, Tooltip, Typography } from '@mui/material';
import { format } from 'date-fns';
import { AppContext } from '../../../context/main';
@@ -21,6 +22,9 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
userBalance: { currentVestingPeriod, vestingAccountInfo },
} = useContext(AppContext);
const theme = useTheme();
const { mode } = theme.palette;
const nextPeriod =
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
@@ -29,19 +33,31 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
return (
<Box display="flex" flexDirection="column" gap={1} position="relative" width="100%">
<svg width="100%" height="12">
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill="#121726" />
<rect y="2" width="100%" height="6" rx="0" fill="text.dark" />
<rect
y="2"
width={`${percentageComplete}%`}
height="6"
rx="0"
fill={mode === 'light' ? 'text.dark' : theme.palette.success.main}
/>
{vestingAccountInfo?.periods.map((period, i, arr) => (
<Marker
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')}
key={i}
/>
))}
<Marker
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"
/>
</svg>
+28 -4
View File
@@ -65,20 +65,44 @@ const VestingSchedule = () => {
))}
</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}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</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)}
</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}>
<Typography variant="body2">{`${vestedPercentage}%`}</Typography>
<VestingTimeline percentageComplete={vestedPercentage} />
</Box>
</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}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
@@ -20,8 +20,8 @@ export const ConfirmationModal = ({
}) => (
<SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Mixnode identity" value={identity} />
<ModalListItem label="Amount" value={`${amount.amount} ${amount.denom}`} />
<ModalListItem label="Mixnode identity:" value={identity} />
<ModalListItem label="Amount:" value={`${amount.amount} ${amount.denom}`} />
<ModalFee fee={fee} isLoading={false} />
</Box>
</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;
}
+3 -7
View File
@@ -138,7 +138,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
setConfirmationModalProps({
status: 'success',
action: 'delegate',
message: 'Delegations can take up to one hour to process',
message: 'This operation can take up to one hour to process',
...balances,
transactions: [
{ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash },
@@ -240,11 +240,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try {
const txs = await claimRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
action: 'redeem',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash,
@@ -270,11 +268,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
try {
const txs = await compoundRewards(identityKey, fee);
const bal = await userBalance();
setConfirmationModalProps({
status: 'success',
action: 'compound',
balance: bal?.printable_balance || '-',
transactions: txs.map((tx) => ({
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
hash: tx.transaction_hash,
@@ -292,7 +288,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
return (
<>
<Paper elevation={0} sx={{ p: 4, mt: 4 }}>
<Paper elevation={0} sx={{ p: 3, mt: 4 }}>
<Stack spacing={5}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Delegations</Typography>
@@ -327,7 +323,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
</Paper>
{pendingDelegations && (
<Paper elevation={0} sx={{ p: 4, mt: 2 }}>
<Paper elevation={0} sx={{ p: 3, mt: 2 }}>
<Stack spacing={5}>
<Typography variant="h6">Pending Delegation Events</Typography>
<PendingEvents pendingEvents={pendingDelegations} explorerUrl={urls(network).networkExplorer} />
+4 -5
View File
@@ -1,8 +1,7 @@
export * from './auth';
export * from './Admin';
export * from './balance';
export * from './bond';
export * from './internal-docs';
export * from './auth';
export * from './settings';
export * from './unbond';
export * from './bonding';
export * from './delegation';
export * from './internal-docs';
export * from './unbond';
-67
View File
@@ -1,67 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Dialog } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { NymCard } from '../../components';
import { AppContext } from '../../context/main';
import { Tabs } from './tabs';
import { Profile } from './profile';
import { SystemVariables } from './system-variables';
import { NodeStats } from './node-stats';
import { useSettingsState } from './useSettingsState';
import { NodeStatus } from '../../components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node';
const tabs = ['Profile', 'System variables', 'Node stats'];
export const Settings = () => {
const [selectedTab, setSelectedTab] = useState(0);
const { mixnodeDetails, showSettings, getBondDetails, handleShowSettings } = useContext(AppContext);
const { status, saturation, rewardEstimation, inclusionProbability, updateAllMixnodeStats } = useSettingsState();
const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab);
useEffect(() => {
getBondDetails();
if (mixnodeDetails) {
updateAllMixnodeStats(mixnodeDetails.mix_node.identity_key);
}
}, [showSettings, selectedTab]);
return showSettings ? (
<Dialog open onClose={handleShowSettings} maxWidth="md" fullWidth>
<NymCard
title={
<Box width="100%" display="flex" justifyContent="space-between">
<Box display="flex" alignItems="center">
<NodeIcon sx={{ mr: 1 }} />
Node Settings
</Box>
<CloseIcon onClick={handleShowSettings} cursor="pointer" />
</Box>
}
Action={<NodeStatus status={status} />}
dataTestid="node-settings"
noPadding
>
<>
<Tabs tabs={tabs} selectedTab={selectedTab} onChange={handleTabChange} disabled={!mixnodeDetails} />
{!mixnodeDetails && (
<Alert severity="info" sx={{ m: 4 }}>
You do not currently have a node running
</Alert>
)}
{selectedTab === 0 && <Profile />}
{selectedTab === 1 && (
<SystemVariables
saturation={saturation}
rewardEstimation={rewardEstimation}
inclusionProbability={inclusionProbability}
/>
)}
{selectedTab === 2 && mixnodeDetails && <NodeStats mixnodeId={mixnodeDetails.mix_node.identity_key} />}
</>
</NymCard>
</Dialog>
) : null;
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const bondMixNode = async (args: TBondMixNodeArgs) =>
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
export const updateMixnode = async (profitMarginPercent: number) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent });
export const updateMixnode = async (profitMarginPercent: number, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode', { profitMarginPercent, fee });
export const send = async (args: { amount: DecCoin; address: string; memo: string; fee?: Fee }) =>
invokeWrapper<SendTxResult>('send', args);
+9 -1
View File
@@ -6,8 +6,9 @@ import {
InclusionProbabilityResponse,
DecCoin,
MixNodeBond,
GatewayBond,
} from '@nymproject/types';
import { Epoch } from 'src/types';
import { Epoch, TNodeDescription } from 'src/types';
import { invokeWrapper } from './wrapper';
export const getReverseMixDelegations = async () =>
@@ -25,6 +26,7 @@ export const getAllPendingDelegations = async () =>
invokeWrapper<DelegationEvent[]>('get_all_pending_delegation_events');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeBond | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_operator_rewards', { address });
@@ -46,3 +48,9 @@ export const getInclusionProbability = async (identity: string) =>
invokeWrapper<InclusionProbabilityResponse>('mixnode_inclusion_probability', { identity });
export const getCurrentEpoch = async () => invokeWrapper<Epoch>('get_current_epoch');
export const getNumberOfMixnodeDelegators = async (identity: string) =>
invokeWrapper<number>('get_number_of_mixnode_delegators', { identity });
export const getNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_mix_node_description', { host, port });
+5 -4
View File
@@ -1,10 +1,11 @@
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { Fee, FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const claimOperatorRewards = async () => invokeWrapper<TransactionExecuteResult[]>('claim_operator_reward');
export const claimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('claim_operator_reward', { fee });
export const compoundOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult[]>('compound_operator_reward');
export const compoundOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('compound_operator_reward', { fee });
export const claimDelegatorRewards = async (mixIdentity: string, fee?: FeeDetails) =>
invokeWrapper<TransactionExecuteResult[]>('claim_locked_and_unlocked_delegator_reward', {
+14 -2
View File
@@ -12,7 +12,8 @@ export const simulateBondMixnode = async (args: TBondMixNodeArgs) =>
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
export const simulateUpdateMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_update_mixnode', args);
export const simulateUpdateMixnode = async (args: { profitMarginPercent: number }) =>
invokeWrapper<FeeDetails>('simulate_update_mixnode', args);
export const simulateDelegateToMixnode = async (args: { identity: string; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_delegate_to_mixnode', args);
@@ -49,7 +50,7 @@ export const simulateVestingBondMixnode = async (args: { mixnode: MixNode; pledg
export const simulateVestingUnbondMixnode = async () => invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode');
export const simulateVestingUpdateMixnode = async (args: any) =>
export const simulateVestingUpdateMixnode = async (args: { profitMarginPercent: number }) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode', args);
export const simulateWithdrawVestedCoins = async (args: any) =>
@@ -57,3 +58,14 @@ export const simulateWithdrawVestedCoins = async (args: any) =>
export const simulateSend = async ({ address, amount }: { address: string; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_send', { address, amount });
export const simulateClaimOperatorReward = async () => invokeWrapper<FeeDetails>('simulate_claim_operator_reward');
export const simulateVestingClaimOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_operator_reward');
export const simulateCompoundOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_compound_operator_reward');
export const simulateVestingCompoundOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_compound_operator_reward');
+6 -6
View File
@@ -60,8 +60,8 @@ export const vestingUnbondMixnode = async (fee?: Fee) =>
export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount, fee });
export const vestingUpdateMixnode = async (profitMarginPercent: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode', { profitMarginPercent });
export const vestingUpdateMixnode = async (profitMarginPercent: number, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode', { profitMarginPercent, fee });
export const vestingDelegateToMixnode = async ({
identity,
@@ -101,11 +101,11 @@ export const vestingUnbond = async (type: TNodeType) => {
return vestingUnbondGateway();
};
export const vestingClaimOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward');
export const vestingClaimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward', { fee });
export const vestingCompoundOperatorRewards = async () =>
invokeWrapper<TransactionExecuteResult>('vesting_compound_operator_reward');
export const vestingCompoundOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_compound_operator_reward', { fee });
export const vestingClaimDelegatorRewards = async (mixIdentity: string) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_delegator_reward', { mixIdentity });
+2 -3
View File
@@ -4,17 +4,16 @@ import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Receive } from '../components/Receive';
import { Bond, Balance, InternalDocs, Unbond, DelegationPage, Admin, Settings } from '../pages';
import { Balance, InternalDocs, Unbond, DelegationPage, Admin, BondingPage } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
<Terminal />
<Settings />
<Send />
<Receive />
<Routes>
<Route path="/balance" element={<Balance />} />
<Route path="/bond" element={<Bond />} />
<Route path="/bonding" element={<BondingPage />} />
<Route path="/unbond" element={<Unbond />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';
export const Bonding = (props: SvgIconProps) => (
<SvgIcon {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 17C14.8954 17 14 16.1046 14 15L14 9C14 7.89543 14.8954 7 16 7L22 7C23.1046 7 24 7.89543 24 9L24 15C24 16.1046 23.1046 17 22 17L16 17ZM16 9L16 15L22 15L22 9L16 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 17C0.89543 17 -1.00647e-07 16.1046 -2.24801e-07 15L-8.99206e-07 9C-1.02336e-06 7.89543 0.895429 7 2 7L8 7C9.10457 7 10 7.89543 10 9L10 15C10 16.1046 9.10457 17 8 17L2 17ZM2 9L2 15L8 15L8 9L2 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z"
/>
</SvgIcon>
);
+1
View File
@@ -2,3 +2,4 @@ export * from './delegate';
export * from './undelegate';
export * from './bond';
export * from './unbond';
export * from './bonding';
+3
View File
@@ -64,6 +64,9 @@ declare module '@mui/material/styles' {
nav: {
background: string;
};
hover: {
background: string;
};
}
/**
+14 -8
View File
@@ -56,12 +56,15 @@ const darkMode: NymPaletteVariant = {
nav: {
background: '#292E34',
},
hover: {
background: '#36393E',
},
};
const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#E5E5E5',
main: '#F4F6F8',
paper: '#FFFFFF',
warn: '#FFE600',
grey: '#F5F5F5',
@@ -80,6 +83,9 @@ const lightMode: NymPaletteVariant = {
nav: {
background: '#FFFFFF',
},
hover: {
background: '#F9F9F9',
},
};
/**
@@ -223,13 +229,6 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
},
},
components: {
MuiTypography: {
styleOverrides: {
root: {
fontSize: 14,
},
},
},
MuiButton: {
styleOverrides: {
root: {
@@ -273,6 +272,13 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
underline: 'none',
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
fontWeight: 600,
},
},
},
},
palette,
};
+13
View File
@@ -1,5 +1,6 @@
import { Gateway, DecCoin, MixNode, PledgeData } from '@nymproject/types';
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
import { TBondedGateway, TBondedMixnode } from 'src/context';
export enum EnumNodeType {
mixnode = 'mixnode',
@@ -39,6 +40,13 @@ export type TBondMixNodeArgs = {
fee?: Fee;
};
export type TNodeDescription = {
name: string;
description: string;
link: string;
location: string;
};
export type TDelegateArgs = {
identity: string;
amount: DecCoin;
@@ -51,3 +59,8 @@ export type TAccount = {
address: string;
mnemonic: string;
};
export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode =>
(node as TBondedMixnode).profitMargin !== undefined;
export const isGateway = (node: TBondedMixnode | TBondedGateway): node is TBondedGateway => !isMixnode(node);
+2
View File
@@ -121,3 +121,5 @@ export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string)
return hasEnoughFunds;
};
export const isDecimal = (value: number) => value - Math.floor(value) !== 0;
@@ -3,7 +3,7 @@
[package]
name = "nym-network-requester"
version = "1.0.1"
version = "1.0.2"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
@@ -1,6 +1,6 @@
[package]
name = "nym-network-statistics"
version = "0.1.0"
version = "1.0.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -13,16 +13,21 @@ export const IdentityKeyFormField: React.FC<{
readOnly?: boolean;
initialValue?: string;
placeholder?: string;
label?: string;
helperText?: string;
onChanged?: (newValue: string) => void;
onValidate?: (isValid: boolean, error?: string) => void;
textFieldProps?: TextFieldProps;
errorText?: string;
sx?: SxProps;
}> = ({
required,
fullWidth,
placeholder,
label,
readOnly,
initialValue,
errorText,
sx,
onChanged,
onValidate,
@@ -55,7 +60,11 @@ export const IdentityKeyFormField: React.FC<{
if (initialValue) {
doValidation(initialValue);
}
}, [initialValue]);
if (errorText) {
setValidationError(errorText);
}
}, [initialValue, errorText]);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
@@ -82,6 +91,7 @@ export const IdentityKeyFormField: React.FC<{
),
}}
placeholder={placeholder}
label={label}
sx={sx}
{...textFieldProps}
aria-readonly={readOnly}
+2 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-validator-api"
version = "1.0.1"
version = "1.0.2"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -58,6 +58,7 @@ gateway-client = { path="../common/client-libs/gateway-client" }
mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" }
multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
nymsphinx = { path="../common/nymsphinx" }
task = { path = "../common/task" }
topology = { path="../common/topology" }
validator-api-requests = { path = "validator-api-requests" }
validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] }
+17 -10
View File
@@ -14,6 +14,7 @@ use okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use task::ShutdownListener;
use rocket::fairing::AdHoc;
use serde::Serialize;
@@ -252,20 +253,26 @@ impl<C> ValidatorCacheRefresher<C> {
Ok(())
}
pub(crate) async fn run(&self)
pub(crate) async fn run(&self, mut shutdown: ShutdownListener)
where
C: CosmWasmClient + Sync,
{
let mut interval = time::interval(self.caching_interval);
loop {
interval.tick().await;
if let Err(err) = self.refresh_cache().await {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.tick() => {
if let Err(err) = self.refresh_cache().await {
error!("Failed to refresh validator cache - {}", err);
} else {
// relaxed memory ordering is fine here. worst case scenario network monitor
// will just have to wait for an additional backoff to see the change.
// And so this will not really incur any performance penalties by setting it every loop iteration
self.cache.initialised.store(true, Ordering::Relaxed)
}
}
_ = shutdown.recv() => {
trace!("UpdateHandler: Received shutdown");
}
}
}
}
+50 -12
View File
@@ -31,6 +31,7 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::{fs, process};
use task::ShutdownNotifier;
use tokio::sync::Notify;
// use validator_client::nymd::SigningNymdClient;
// use validator_client::ValidatorClientError;
@@ -226,14 +227,44 @@ fn parse_args<'a>() -> ArgMatches<'a> {
base_app.get_matches()
}
async fn wait_for_interrupt() {
if let Err(e) = tokio::signal::ctrl_c().await {
error!(
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
e
);
async fn wait_for_interrupt(mut shutdown: ShutdownNotifier) {
wait_for_signal().await;
log::info!("Sending shutdown");
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym validator API");
}
#[cfg(unix)]
async fn wait_for_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel");
let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel");
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
_ = sigterm.recv() => {
log::info!("Received SIGTERM");
}
_ = sigquit.recv() => {
log::info!("Received SIGQUIT");
}
}
}
#[cfg(not(unix))]
async fn wait_for_signal() {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
}
println!("Received SIGINT - the network monitor will terminate now");
}
fn setup_logging() {
@@ -547,6 +578,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
let signing_nymd_client = Client::new_signing(&config);
let liftoff_notify = Arc::new(Notify::new());
let shutdown = ShutdownNotifier::default();
// let's build our rocket!
let rocket = setup_rocket(
@@ -569,7 +601,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
// setup our daily uptime updater. Note that if network monitor is disabled, then we have
// no data for the updates and hence we don't need to start it up
let uptime_updater = HistoricalUptimeUpdater::new(storage.clone());
tokio::spawn(async move { uptime_updater.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
// spawn the cache refresher
let validator_cache_refresher = ValidatorCacheRefresher::new(
@@ -578,7 +611,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
validator_cache.clone(),
Some(storage.clone()),
);
tokio::spawn(async move { validator_cache_refresher.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
// spawn rewarded set updater
let mut rewarded_set_updater =
@@ -593,11 +627,15 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
None,
);
let shutdown_listener = shutdown.subscribe();
// spawn our cacher
tokio::spawn(async move { validator_cache_refresher.run().await });
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
}
// launch the rocket!
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
// with that of the rest of the tasks.
// Currently it's runtime is forcefully terminated once the validator-api exits.
let shutdown_handle = rocket.shutdown();
tokio::spawn(rocket.launch());
@@ -610,12 +648,12 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
// we're ready to go! spawn the network monitor!
let runnables = monitor_builder.build().await;
runnables.spawn_tasks();
runnables.spawn_tasks(&shutdown);
} else {
info!("Network monitoring is disabled.");
}
wait_for_interrupt().await;
wait_for_interrupt(shutdown).await;
shutdown_handle.notify();
Ok(())
+6 -3
View File
@@ -6,6 +6,7 @@ use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use std::sync::Arc;
use task::ShutdownNotifier;
use crate::config::Config;
use crate::contract_cache::ValidatorCache;
@@ -131,11 +132,13 @@ impl NetworkMonitorRunnables {
// TODO: note, that is not exactly doing what we want, because when
// `ReceivedProcessor` is constructed, it already spawns a future
// this needs to be refactored!
pub(crate) fn spawn_tasks(self) {
pub(crate) fn spawn_tasks(self, shutdown: &ShutdownNotifier) {
let mut packet_receiver = self.packet_receiver;
let mut monitor = self.monitor;
tokio::spawn(async move { packet_receiver.run().await });
tokio::spawn(async move { monitor.run().await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { packet_receiver.run(shutdown_listener).await });
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { monitor.run(shutdown_listener).await });
}
}

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