Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43d6908d22 | |||
| e10f291ee9 | |||
| ecc89ced08 | |||
| 1e10c247bc | |||
| 4584f35a2a | |||
| 41fa03862e | |||
| 66303d7ca4 | |||
| 480f8a0a53 | |||
| ecb0f11bbb | |||
| fe223b5a60 | |||
| b9e52d22d1 | |||
| ecdf192b47 |
@@ -9,10 +9,11 @@ use nym_api_requests::coconut::{
|
||||
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
|
||||
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
|
||||
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError,
|
||||
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
|
||||
ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse,
|
||||
GatewayUptimeHistoryResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
|
||||
MixnodeUptimeHistoryResponse, RequestError, RewardEstimationResponse, StakeSaturationResponse,
|
||||
UptimeResponse,
|
||||
};
|
||||
use reqwest::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -361,6 +362,25 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn compute_mixnode_reward_estimation(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
request_body: &ComputeRewardEstParam,
|
||||
) -> Result<RewardEstimationResponse, ValidatorAPIError> {
|
||||
self.post_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
routes::COMPUTE_REWARD_ESTIMATION,
|
||||
],
|
||||
NO_PARAMS,
|
||||
request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_stake_saturation(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
|
||||
@@ -28,6 +28,7 @@ pub const STATUS: &str = "status";
|
||||
pub const REPORT: &str = "report";
|
||||
pub const HISTORY: &str = "history";
|
||||
pub const REWARD_ESTIMATION: &str = "reward-estimation";
|
||||
pub const COMPUTE_REWARD_ESTIMATION: &str = "compute-reward-estimation";
|
||||
pub const AVG_UPTIME: &str = "avg_uptime";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
|
||||
|
||||
@@ -114,7 +114,7 @@ impl MixNodeBondAnnotated {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ComputeRewardEstParam {
|
||||
pub performance: Option<Performance>,
|
||||
pub active_in_rewarded_set: Option<bool>,
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "6",
|
||||
"recharts": "^2.1.13",
|
||||
"react-to-print": "^2.14.7",
|
||||
"semver": "^6.3.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"use-clipboard-copy": "^0.2.0",
|
||||
|
||||
@@ -83,7 +83,9 @@ fn main() {
|
||||
mixnet::rewards::claim_delegator_reward,
|
||||
mixnet::rewards::claim_operator_reward,
|
||||
mixnet::rewards::claim_locked_and_unlocked_delegator_reward,
|
||||
mixnet::rewards::get_current_rewarding_parameters,
|
||||
mixnet::send::send,
|
||||
mixnet::bond::get_mixnode_uptime,
|
||||
network_config::add_validator,
|
||||
network_config::get_nym_api_urls,
|
||||
network_config::get_validator_nymd_urls,
|
||||
@@ -99,6 +101,7 @@ fn main() {
|
||||
utils::get_old_and_incorrect_hardcoded_fee,
|
||||
utils::try_convert_pubkey_to_mix_id,
|
||||
utils::default_mixnode_cost_params,
|
||||
nym_api::status::compute_mixnode_reward_estimation,
|
||||
nym_api::status::gateway_core_node_status,
|
||||
nym_api::status::mixnode_core_node_status,
|
||||
nym_api::status::mixnode_inclusion_probability,
|
||||
|
||||
@@ -341,3 +341,18 @@ pub async fn get_mix_node_description(
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_mixnode_uptime(
|
||||
mix_id: MixId,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<u8, BackendError> {
|
||||
log::info!(">>> Get mixnode uptime");
|
||||
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
let uptime = client.validator_api.get_mixnode_avg_uptime(mix_id).await?;
|
||||
|
||||
log::info!(">>> Uptime response: {}", uptime.avg_uptime);
|
||||
Ok(uptime.avg_uptime)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::error::BackendError;
|
||||
use crate::state::WalletState;
|
||||
use crate::vesting::rewards::vesting_claim_delegator_reward;
|
||||
use mixnet_contract_common::MixId;
|
||||
use mixnet_contract_common::{MixId, RewardingParams};
|
||||
use nym_types::transaction::TransactionExecuteResult;
|
||||
use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient};
|
||||
use validator_client::nymd::Fee;
|
||||
@@ -95,3 +95,17 @@ pub async fn claim_locked_and_unlocked_delegator_reward(
|
||||
log::trace!("<<< {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_current_rewarding_parameters(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<RewardingParams, BackendError> {
|
||||
log::info!(">>> Get current rewarding params",);
|
||||
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
let reward_params = client.nymd.get_rewarding_parameters().await?;
|
||||
|
||||
Ok(reward_params)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
use crate::api_client;
|
||||
use crate::error::BackendError;
|
||||
use crate::state::WalletState;
|
||||
use mixnet_contract_common::{IdentityKeyRef, MixId};
|
||||
use mixnet_contract_common::{reward_params::Performance, Coin, IdentityKeyRef, MixId, Percent};
|
||||
use validator_client::models::{
|
||||
GatewayCoreStatusResponse, GatewayStatusReportResponse, InclusionProbabilityResponse,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse,
|
||||
ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse,
|
||||
InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -59,6 +59,29 @@ pub async fn mixnode_reward_estimation(
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn compute_mixnode_reward_estimation(
|
||||
mix_id: u32,
|
||||
performance: Option<Performance>,
|
||||
pledge_amount: Option<u64>,
|
||||
total_delegation: Option<u64>,
|
||||
interval_operating_cost: Option<Coin>,
|
||||
profit_margin_percent: Option<Percent>,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<RewardEstimationResponse, BackendError> {
|
||||
let request_body = ComputeRewardEstParam {
|
||||
performance,
|
||||
active_in_rewarded_set: Some(true),
|
||||
pledge_amount,
|
||||
total_delegation,
|
||||
interval_operating_cost,
|
||||
profit_margin_percent,
|
||||
};
|
||||
Ok(api_client!(state)
|
||||
.compute_mixnode_reward_estimation(mix_id, &request_body)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mixnode_stake_saturation(
|
||||
mix_id: MixId,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -15,7 +15,7 @@ export const AppBar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none', pt: 3 }}>
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none', mt: 3 }}>
|
||||
<Toolbar disableGutters>
|
||||
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
|
||||
<Grid item container alignItems="center" spacing={1}>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { SelectionChance } from '@nymproject/types';
|
||||
|
||||
const colorMap: { [key in SelectionChance]: string } = {
|
||||
Low: 'error.main',
|
||||
Good: 'warning.main',
|
||||
High: 'success.main',
|
||||
};
|
||||
|
||||
const textMap: { [key in SelectionChance]: string } = {
|
||||
Low: 'Low',
|
||||
Good: 'Good',
|
||||
High: 'High',
|
||||
};
|
||||
|
||||
export const InclusionProbability = ({ probability }: { probability: SelectionChance }) => (
|
||||
<Typography sx={{ color: colorMap[probability] }}>{textMap[probability]}</Typography>
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Button, Grid, TextField, Typography } from '@mui/material';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { DefaultInputValues } from 'src/pages/bonding/node-settings/apy-playground';
|
||||
import { inputValidationSchema } from './inputsValidationSchema';
|
||||
|
||||
export type InputFields = {
|
||||
label: string;
|
||||
name: 'profitMargin' | 'uptime' | 'bond' | 'delegations' | 'operatorCost' | 'uptime';
|
||||
isPercentage?: boolean;
|
||||
}[];
|
||||
|
||||
export type CalculateArgs = {
|
||||
bond: string;
|
||||
delegations: string;
|
||||
uptime: string;
|
||||
profitMargin: string;
|
||||
operatorCost: string;
|
||||
};
|
||||
|
||||
const inputFields: InputFields = [
|
||||
{ label: 'Profit margin', name: 'profitMargin', isPercentage: true },
|
||||
{ label: 'Operator cost', name: 'operatorCost' },
|
||||
{ label: 'Bond', name: 'bond' },
|
||||
{ label: 'Delegations', name: 'delegations' },
|
||||
{ label: 'Uptime', name: 'uptime', isPercentage: true },
|
||||
];
|
||||
|
||||
export const Inputs = ({
|
||||
onCalculate,
|
||||
defaultValues,
|
||||
}: {
|
||||
onCalculate: (args: CalculateArgs) => Promise<void>;
|
||||
defaultValues: DefaultInputValues;
|
||||
}) => {
|
||||
const handleCalculate = useCallback(
|
||||
async (args: CalculateArgs) => {
|
||||
onCalculate({
|
||||
bond: args.bond,
|
||||
delegations: args.delegations,
|
||||
uptime: args.uptime,
|
||||
profitMargin: args.profitMargin,
|
||||
operatorCost: args.operatorCost,
|
||||
});
|
||||
},
|
||||
[onCalculate],
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(inputValidationSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{inputFields.map((field) => (
|
||||
<Grid item xs={12} lg={2} key={field.name}>
|
||||
<TextField
|
||||
{...register(field.name)}
|
||||
fullWidth
|
||||
label={field.label}
|
||||
name={field.name}
|
||||
error={Boolean(errors[field.name])}
|
||||
helperText={errors[field.name]?.message}
|
||||
InputProps={{
|
||||
endAdornment: <Typography sx={{ color: 'grey.600' }}>{field.isPercentage ? '%' : 'NYM'}</Typography>,
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
))}{' '}
|
||||
<Grid item xs={12} lg={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disableElevation
|
||||
onClick={handleSubmit(handleCalculate)}
|
||||
size="large"
|
||||
fullWidth
|
||||
disabled={Boolean(Object.keys(errors).length)}
|
||||
>
|
||||
Calculate
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Divider, Stack, Typography } from '@mui/material';
|
||||
import { SelectionChance } from '@nymproject/types';
|
||||
import { InclusionProbability } from './InclusionProbability';
|
||||
|
||||
const computeSelectionProbability = (saturation: number): SelectionChance => {
|
||||
if (saturation < 5) return 'Low';
|
||||
|
||||
if (saturation > 5 && saturation < 15) return 'Good';
|
||||
|
||||
return 'High';
|
||||
};
|
||||
|
||||
export const NodeDetails = ({ saturation }: { saturation?: string }) => {
|
||||
if (!saturation) return null;
|
||||
|
||||
return (
|
||||
<Card variant="outlined" sx={{ p: 1 }}>
|
||||
<CardContent>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight="medium">Stake saturation</Typography>
|
||||
<Typography>{saturation || '- '}%</Typography>
|
||||
</Stack>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight="medium">Selection probability</Typography>
|
||||
<InclusionProbability probability={computeSelectionProbability(parseInt(saturation))} />
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export type Results = {
|
||||
operator: {
|
||||
daily: string;
|
||||
monthly: string;
|
||||
yearly: string;
|
||||
};
|
||||
delegator: {
|
||||
daily: string;
|
||||
monthly: string;
|
||||
yearly: string;
|
||||
};
|
||||
total: {
|
||||
daily: string;
|
||||
monthly: string;
|
||||
yearly: string;
|
||||
};
|
||||
};
|
||||
|
||||
const tableHeader = [
|
||||
{ title: 'Estimated rewards', bold: true },
|
||||
{ title: 'Per day' },
|
||||
{ title: 'Per month' },
|
||||
{ title: 'Per year' },
|
||||
];
|
||||
|
||||
export const ResultsTable = ({ results }: { results: Results }) => {
|
||||
const tableRows = [
|
||||
{ title: 'Total node reward', ...results.total },
|
||||
{ title: 'Operator rewards', ...results.operator },
|
||||
{ title: 'Delegator rewards', ...results.delegator },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card variant="outlined" sx={{ p: 1 }}>
|
||||
<CardContent>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{tableHeader.map((header) => (
|
||||
<TableCell>
|
||||
<Typography fontWeight={header.bold ? 'bold' : 'regular'}>{header.title}</Typography>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tableRows.map((row) => (
|
||||
<TableRow>
|
||||
<TableCell>{row.title}</TableCell>
|
||||
<TableCell>{row.daily}</TableCell>
|
||||
<TableCell>{row.monthly}</TableCell>
|
||||
<TableCell>{row.yearly}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as Yup from 'yup';
|
||||
import { isGreaterThan, isLessThan } from 'src/utils';
|
||||
|
||||
export const inputValidationSchema = Yup.object().shape({
|
||||
profitMargin: Yup.string()
|
||||
.required('profit margin is a required field')
|
||||
.test('Is valid profit margin value', (value, ctx) => {
|
||||
const stringValueToNumber = Math.round(Number(value));
|
||||
|
||||
if (isGreaterThan(stringValueToNumber, -1) && isLessThan(stringValueToNumber, 101)) return true;
|
||||
return ctx.createError({ message: 'Profit margin must be a number from 0 and 100' });
|
||||
}),
|
||||
uptime: Yup.string()
|
||||
.required()
|
||||
.test('Is valid uptime value', (value, ctx) => {
|
||||
const stringValueToNumber = Math.round(Number(value));
|
||||
if (stringValueToNumber && isGreaterThan(stringValueToNumber, 0) && isLessThan(stringValueToNumber, 101))
|
||||
return true;
|
||||
return ctx.createError({ message: 'Uptime must be a number between 0 and 100' });
|
||||
}),
|
||||
bond: Yup.string()
|
||||
.required()
|
||||
.test('Is valid bond value', (value, ctx) => {
|
||||
if (Number(value)) return true;
|
||||
return ctx.createError({ message: 'Bond must be a valid number' });
|
||||
}),
|
||||
delegations: Yup.string()
|
||||
.required()
|
||||
.test('Is valid delegation value', (value, ctx) => {
|
||||
if (Number(value)) return true;
|
||||
return ctx.createError({ message: 'Delegations must be a valid number' });
|
||||
}),
|
||||
operatorCost: Yup.string()
|
||||
.required('operator cost is a required field')
|
||||
.test('Is valid operator cost value', (value, ctx) => {
|
||||
const stringValueToNumber = Math.round(Number(value));
|
||||
|
||||
if (isGreaterThan(stringValueToNumber, -1) && isLessThan(stringValueToNumber, 101)) return true;
|
||||
return ctx.createError({ message: 'Operator cost must be a valid number' });
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Button, Grid, Stack } from '@mui/material';
|
||||
import testNode from 'src/assets/test-node-illustration.jpg';
|
||||
import { OverviewDescription } from '../components/OverviewDescription';
|
||||
|
||||
const content = [
|
||||
{
|
||||
title: 'How is works',
|
||||
description:
|
||||
'This is your APY playground - play with the parameters on left to see estimated rewards on the right side',
|
||||
},
|
||||
{
|
||||
title: 'Test path',
|
||||
description:
|
||||
'This is your APY playground - play with the parameters on left to see estimated rewards on the right side',
|
||||
},
|
||||
{
|
||||
title: 'Results',
|
||||
description:
|
||||
'This is your APY playground - play with the parameters on left to see estimated rewards on the right side',
|
||||
},
|
||||
];
|
||||
|
||||
export const Overview = ({ onStartTest }: { onStartTest: () => void }) => (
|
||||
<Grid container spacing={2} justifyContent="center">
|
||||
<Grid item xs={12} md={6}>
|
||||
<img src={testNode} style={{ borderRadius: 8 }} />
|
||||
</Grid>
|
||||
<Grid item container direction="column" xs={12} xl={6}>
|
||||
<Grid item>
|
||||
<Stack>{content.map(OverviewDescription)}</Stack>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button variant="contained" fullWidth disableElevation onClick={onStartTest}>
|
||||
Start test
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Packets } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Test my node / Packets',
|
||||
component: Packets,
|
||||
} as ComponentMeta<typeof Packets>;
|
||||
|
||||
const Transfer: ComponentStory<typeof Packets> = (args) => (
|
||||
<Box width="500px">
|
||||
<Packets {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const HighTransfer = Transfer.bind({});
|
||||
HighTransfer.args = {
|
||||
sent: '100',
|
||||
received: '80',
|
||||
};
|
||||
|
||||
export const LowTransfer = Transfer.bind({});
|
||||
LowTransfer.args = {
|
||||
sent: '100',
|
||||
received: '50',
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Path } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Test my node / Node path',
|
||||
component: Path,
|
||||
} as ComponentMeta<typeof Path>;
|
||||
|
||||
const Template: ComponentStory<typeof Path> = (args) => (
|
||||
<Box display="flex">
|
||||
<Path {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Gateway = Template.bind({});
|
||||
Gateway.args = {
|
||||
layer: 'gateway',
|
||||
};
|
||||
|
||||
export const LayerOne = Template.bind({});
|
||||
LayerOne.args = {
|
||||
layer: '1',
|
||||
};
|
||||
|
||||
export const LayerTwo = Template.bind({});
|
||||
LayerTwo.args = {
|
||||
layer: '2',
|
||||
};
|
||||
|
||||
export const LayerThree = Template.bind({});
|
||||
LayerThree.args = {
|
||||
layer: '3',
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { NodeSpeed, Results } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Test my node / Results',
|
||||
component: Results,
|
||||
} as ComponentMeta<typeof Results>;
|
||||
|
||||
const Template: ComponentStory<typeof Results> = (args) => <Results {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
layer: '1',
|
||||
packetsSent: '1000',
|
||||
packetsReceived: '5000',
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { NodeSpeed } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Test my node / Node speed',
|
||||
component: NodeSpeed,
|
||||
} as ComponentMeta<typeof NodeSpeed>;
|
||||
|
||||
const Template: ComponentStory<typeof NodeSpeed> = (args) => (
|
||||
<Box display="flex" alignContent="center">
|
||||
<NodeSpeed {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const FastNode = Template.bind({});
|
||||
FastNode.args = {
|
||||
Mbps: 500,
|
||||
performance: 'good',
|
||||
};
|
||||
|
||||
export const FairNode = Template.bind({});
|
||||
FairNode.args = {
|
||||
Mbps: 100,
|
||||
performance: 'fair',
|
||||
};
|
||||
|
||||
export const SlowNode = Template.bind({});
|
||||
SlowNode.args = {
|
||||
Mbps: 10,
|
||||
performance: 'poor',
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { ArrowForward, CheckCircleOutline, Description, Download } from '@mui/icons-material';
|
||||
import { Box, Button, Card, Chip, CircularProgress, Divider, Grid, Stack, Typography } from '@mui/material';
|
||||
import format from 'date-fns/format';
|
||||
import { NodePath } from 'src/svg-icons/node-path';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import { ResultsCard } from '../components/ResultsCard';
|
||||
import { ResultsCardDetail } from '../components/ResultsCardDetail';
|
||||
|
||||
export type Layer = '1' | '2' | '3' | 'gateway';
|
||||
|
||||
const getLayerDescription = (layer: Layer) => {
|
||||
if (layer === 'gateway') return 'Your node was in the Gateway layer';
|
||||
return `Your node was in layer ${layer}`;
|
||||
};
|
||||
|
||||
export const NodeSpeed = ({ Mbps, performance }: { Mbps: number; performance: 'poor' | 'fair' | 'good' }) => (
|
||||
<ResultsCard
|
||||
label="Node speed"
|
||||
detail={`${performance === 'good' ? 'Fast' : performance === 'poor' ? 'Slow' : 'Fair'} node`}
|
||||
isOk={performance === 'good'}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
width: 250,
|
||||
height: 250,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mx: 'auto',
|
||||
mt: 4,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={performance === 'poor' ? 12.5 : performance === 'good' ? 85 : 65}
|
||||
size={250}
|
||||
sx={{ position: 'absolute', top: 0, left: 0 }}
|
||||
color={performance === 'poor' ? 'error' : performance === 'good' ? 'success' : 'warning'}
|
||||
/>
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Typography fontWeight="bold" variant="h4">
|
||||
{Mbps}
|
||||
</Typography>
|
||||
<Typography>Mbps</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ResultsCard>
|
||||
);
|
||||
|
||||
export const Packets = ({ sent, received }: { sent: string; received: string }) => {
|
||||
const percentage = Math.round((+received / +sent) * 100);
|
||||
return (
|
||||
<ResultsCard label="Packets" detail={`${percentage}% packets`} isOk={percentage > 75}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<ResultsCardDetail label="Packets sent" detail={sent} />
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<ResultsCardDetail label="Packets received" detail={received} />
|
||||
</ResultsCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const Path = ({ layer }: { layer: Layer }) => (
|
||||
<ResultsCard label="Path" detail={getLayerDescription(layer)} isOk>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<NodePath layer={layer} />
|
||||
</Box>
|
||||
</ResultsCard>
|
||||
);
|
||||
|
||||
export const Results = ({
|
||||
packetsSent,
|
||||
packetsReceived,
|
||||
layer,
|
||||
}: {
|
||||
packetsSent: string;
|
||||
packetsReceived: string;
|
||||
layer: '1' | '2' | '3' | 'gateway';
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const handleSaveToPdf = useReactToPrint({ documentTitle: 'Test results', content: () => ref.current });
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
|
||||
<Box display="flex" gap={1}>
|
||||
<Typography fontWeight="bold" component="span">
|
||||
Test date
|
||||
</Typography>
|
||||
<Typography>{format(new Date(), 'dd/MM/yyyy HH:mm')}</Typography>
|
||||
</Box>
|
||||
<Button onClick={handleSaveToPdf} startIcon={<Download />}>
|
||||
Save to PDF
|
||||
</Button>
|
||||
</Stack>
|
||||
<Grid container spacing={2} ref={ref}>
|
||||
<Grid item md={5}>
|
||||
<NodeSpeed Mbps={150.01} performance="good" />
|
||||
</Grid>
|
||||
<Grid item container direction="column" md={7}>
|
||||
<Stack spacing={2}>
|
||||
<Packets sent={packetsSent} received={packetsReceived} />
|
||||
<Path layer={layer} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { TestProgress } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Test my node / Test progress',
|
||||
component: TestProgress,
|
||||
} as ComponentMeta<typeof TestProgress>;
|
||||
|
||||
const Template: ComponentStory<typeof TestProgress> = ({ packetsSent, totalPackets }) => {
|
||||
const [sent, setSent] = React.useState(packetsSent);
|
||||
|
||||
const mockPacketTransfer = (sent: number) => {
|
||||
if (sent - 1 < totalPackets) {
|
||||
setSent(sent);
|
||||
setTimeout(() => {
|
||||
mockPacketTransfer(sent + 1);
|
||||
}, 25);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
mockPacketTransfer(0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box display="flex" alignContent="center">
|
||||
<TestProgress packetsSent={sent} totalPackets={totalPackets} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
packetsSent: 0,
|
||||
totalPackets: 100,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Stack, Typography } from '@mui/material';
|
||||
|
||||
export const TestProgress = ({ totalPackets, packetsSent }: { totalPackets: number; packetsSent: number }) => {
|
||||
const percentage = Math.round((packetsSent / totalPackets) * 100);
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={3}>
|
||||
<Typography sx={{ textTransform: 'uppercase' }}>Test in progress</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
width: 250,
|
||||
height: 250,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
value={percentage}
|
||||
size={250}
|
||||
sx={{ position: 'absolute', top: 0, left: 0 }}
|
||||
/>
|
||||
<Typography fontWeight="bold" variant="h4">
|
||||
{percentage}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography>Sending packets...</Typography>
|
||||
<Typography>{`${packetsSent} / ${totalPackets}`}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export const OverviewDescription = ({ title, description }: { title: string; description: string }) => (
|
||||
<Box>
|
||||
<Typography fontWeight="bold" sx={{ mb: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography fontSize="small" sx={{ color: 'grey.700', mb: 2 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Chip } from '@mui/material';
|
||||
|
||||
export const PathChip = ({ label, highlight }: { label: string; highlight: boolean }) => (
|
||||
<Chip label={label} size="medium" color={highlight ? 'primary' : 'default'} />
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CheckCircleOutline } from '@mui/icons-material';
|
||||
import { Card } from '@mui/material';
|
||||
import { ResultsCardDetail } from './ResultsCardDetail';
|
||||
|
||||
export const ResultsCard: React.FC<{ label: string; detail: string; isOk: boolean }> = ({
|
||||
label,
|
||||
detail,
|
||||
isOk,
|
||||
children,
|
||||
}) => (
|
||||
<Card variant="outlined" sx={{ p: 3 }}>
|
||||
<ResultsCardDetail
|
||||
label={label}
|
||||
detail={detail}
|
||||
boldLabel
|
||||
DescriptionIcon={isOk && <CheckCircleOutline sx={{ color: 'success.light' }} />}
|
||||
/>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
|
||||
export const ResultsCardDetail = ({
|
||||
label,
|
||||
detail,
|
||||
DescriptionIcon,
|
||||
boldLabel,
|
||||
}: {
|
||||
label: string;
|
||||
detail: string;
|
||||
DescriptionIcon?: React.ReactNode;
|
||||
boldLabel?: boolean;
|
||||
}) => (
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={boldLabel ? 'bold' : 'regular'}>{label}</Typography>
|
||||
<Box display="flex" gap={1} alignItems="center">
|
||||
<Typography>{detail}</Typography>
|
||||
{DescriptionIcon}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
vestingBondMixNode,
|
||||
vestingUnbondGateway,
|
||||
vestingUnbondMixnode,
|
||||
getPendingEpochEvents,
|
||||
updateMixnodeCostParams as updateMixnodeCostParamsRequest,
|
||||
vestingUpdateMixnodeCostParams as updateMixnodeVestingCostParamsRequest,
|
||||
getNodeDescription as getNodeDescriptionRequest,
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
getMixnodeAvgUptime,
|
||||
getMixnodeRewardEstimation,
|
||||
getGatewayReport,
|
||||
getMixnodeUptime,
|
||||
} from '../requests';
|
||||
import { useCheckOwnership } from '../hooks/useCheckOwnership';
|
||||
import { AppContext } from './main';
|
||||
@@ -71,10 +71,12 @@ export type TBondedMixnode = {
|
||||
verlocPort: number;
|
||||
version: string;
|
||||
isUnbonding: boolean;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export interface TBondedGateway {
|
||||
name: string;
|
||||
name?: string;
|
||||
id: number;
|
||||
identityKey: string;
|
||||
ip: string;
|
||||
bond: DecCoin;
|
||||
@@ -154,14 +156,18 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
status: MixnodeStatus;
|
||||
stakeSaturation: string;
|
||||
estimatedRewards?: DecCoin;
|
||||
uptime: number;
|
||||
} = {
|
||||
status: 'not_found',
|
||||
stakeSaturation: '0',
|
||||
uptime: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const statusResponse = await getMixnodeStatus(mixId);
|
||||
const uptime = await getMixnodeUptime(mixId);
|
||||
additionalDetails.status = statusResponse.status;
|
||||
additionalDetails.uptime = uptime;
|
||||
} catch (e) {
|
||||
Console.log('getMixnodeStatus fails', e);
|
||||
}
|
||||
@@ -259,7 +265,8 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
rewarding_details,
|
||||
bond_information: { mix_id },
|
||||
} = data;
|
||||
const { status, stakeSaturation, estimatedRewards } = await getAdditionalMixnodeDetails(mix_id);
|
||||
|
||||
const { status, stakeSaturation, estimatedRewards, uptime } = await getAdditionalMixnodeDetails(mix_id);
|
||||
const setProbabilities = await getSetProbabilities(mix_id);
|
||||
const nodeDescription = await getNodeDescription(
|
||||
bond_information.mix_node.host,
|
||||
@@ -267,6 +274,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
);
|
||||
const routingScore = await getAvgUptime();
|
||||
setBondedNode({
|
||||
id: data.bond_information.mix_id,
|
||||
name: nodeDescription?.name,
|
||||
mixId: mix_id,
|
||||
identityKey: bond_information.mix_node.identity_key,
|
||||
@@ -279,6 +287,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
|
||||
delegators: rewarding_details.unique_delegations,
|
||||
proxy: bond_information.proxy,
|
||||
operatorRewards,
|
||||
uptime,
|
||||
status,
|
||||
stakeSaturation,
|
||||
operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost),
|
||||
|
||||
@@ -7,8 +7,8 @@ import { mockSleep } from './utils';
|
||||
const SLEEP_MS = 1000;
|
||||
|
||||
const bondedMixnodeMock: TBondedMixnode = {
|
||||
name: 'Monster node',
|
||||
mixId: 1,
|
||||
name: 'Monster node',
|
||||
identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
|
||||
stake: { denom: 'nym', amount: '1234' },
|
||||
bond: { denom: 'nym', amount: '1234' },
|
||||
@@ -28,9 +28,11 @@ const bondedMixnodeMock: TBondedMixnode = {
|
||||
verlocPort: 1790,
|
||||
version: '1.0.2',
|
||||
isUnbonding: false,
|
||||
uptime: 1,
|
||||
};
|
||||
|
||||
const bondedGatewayMock: TBondedGateway = {
|
||||
id: 1,
|
||||
name: 'Monster node',
|
||||
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
|
||||
ip: '112.43.234.57',
|
||||
|
||||
@@ -5,18 +5,20 @@ import { Box, Typography, Stack, IconButton, Divider } from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
|
||||
import { TestNode } from 'src/pages/bonding/node-settings/test-my-node';
|
||||
import { Node as NodeIcon } from 'src/svg-icons/node';
|
||||
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||
import { NymCard } from 'src/components';
|
||||
import { PageLayout } from 'src/layouts';
|
||||
import { Tabs } from 'src/components/Tabs';
|
||||
import { useBondingContext, BondingContextProvider } from 'src/context';
|
||||
import { useBondingContext, BondingContextProvider, TBondedMixnode } from 'src/context';
|
||||
import { AppContext, urls } from 'src/context/main';
|
||||
|
||||
import { NodeGeneralSettings } from './settings-pages/general-settings';
|
||||
import { NodeUnbondPage } from './settings-pages/NodeUnbondPage';
|
||||
import { createNavItems } from './node-settings.constant';
|
||||
import { isMixnode } from 'src/types';
|
||||
import { ApyPlayground } from './apy-playground';
|
||||
|
||||
export const NodeSettings = () => {
|
||||
const theme = useTheme();
|
||||
@@ -123,6 +125,7 @@ export const NodeSettings = () => {
|
||||
{value === 'Unbond' && bondedNode && (
|
||||
<NodeUnbondPage bondedNode={bondedNode} onConfirm={handleUnbond} onError={handleError} />
|
||||
)}
|
||||
{value === 'Playground' && bondedNode && <ApyPlayground bondedNode={bondedNode as TBondedMixnode} />}
|
||||
{confirmationDetails && confirmationDetails.status === 'success' && (
|
||||
<ConfirmationDetailsModal
|
||||
title={confirmationDetails.title}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Box, Card, CardContent, CardHeader, Grid, Typography } from '@mui/material';
|
||||
import { decimalToPercentage, percentToDecimal } from '@nymproject/types';
|
||||
import { ResultsTable } from 'src/components/RewardsPlayground/ResultsTable';
|
||||
import { getDelegationSummary } from 'src/requests';
|
||||
import { NodeDetails } from 'src/components/RewardsPlayground/NodeDetail';
|
||||
import { Inputs, CalculateArgs } from 'src/components/RewardsPlayground/Inputs';
|
||||
import { AppContext, TBondedMixnode } from 'src/context';
|
||||
import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||
|
||||
export type DefaultInputValues = {
|
||||
profitMargin: string;
|
||||
uptime: string;
|
||||
bond: string;
|
||||
delegations: string;
|
||||
operatorCost: string;
|
||||
};
|
||||
|
||||
export const ApyPlayground = ({ bondedNode }: { bondedNode: TBondedMixnode }) => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [results, setResults] = useState({
|
||||
total: { daily: '-', monthly: '-', yearly: '-' },
|
||||
operator: { daily: '-', monthly: '-', yearly: '-' },
|
||||
delegator: { daily: '-', monthly: '-', yearly: '-' },
|
||||
});
|
||||
|
||||
const [defaultInputValues, setDefaultInputValues] = useState<DefaultInputValues>();
|
||||
const [stakeSaturation, setStakeSaturation] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const initialise = async (node: TBondedMixnode) => {
|
||||
try {
|
||||
const delegations = await getDelegationSummary();
|
||||
|
||||
const { estimation } = await computeEstimate({
|
||||
mixId: node.mixId,
|
||||
uptime: node.uptime.toString(),
|
||||
profitMargin: node.profitMargin,
|
||||
operatorCost: node.operatorCost.amount,
|
||||
totalDelegation: delegations.total_delegations.amount,
|
||||
pledgeAmount: node.bond.amount,
|
||||
});
|
||||
|
||||
setResults(
|
||||
handleCalculatePeriodRewards({
|
||||
estimatedOperatorReward: estimation.operator,
|
||||
estimatedDelegatorsReward: estimation.delegates,
|
||||
}),
|
||||
);
|
||||
|
||||
setStakeSaturation(node.stakeSaturation);
|
||||
|
||||
setDefaultInputValues({
|
||||
profitMargin: node.profitMargin,
|
||||
uptime: (node.uptime || 0).toString(),
|
||||
bond: node.bond.amount || '',
|
||||
delegations: delegations.total_delegations.amount,
|
||||
operatorCost: node.operatorCost.amount,
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
enqueueSnackbar(e as string, { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (bondedNode) {
|
||||
initialise(bondedNode);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <LoadingModal />;
|
||||
|
||||
const handleCalculateEstimate = async ({ bond, delegations, uptime, profitMargin, operatorCost }: CalculateArgs) => {
|
||||
try {
|
||||
const { estimation, reward_params } = await computeEstimate({
|
||||
mixId: bondedNode.mixId,
|
||||
uptime: uptime,
|
||||
profitMargin: profitMargin,
|
||||
operatorCost: operatorCost,
|
||||
totalDelegation: delegations,
|
||||
pledgeAmount: bond,
|
||||
});
|
||||
|
||||
const estimationResult = handleCalculatePeriodRewards({
|
||||
estimatedOperatorReward: estimation.operator,
|
||||
estimatedDelegatorsReward: estimation.delegates,
|
||||
});
|
||||
|
||||
const computedStakeSaturation = computeStakeSaturation(
|
||||
bond,
|
||||
delegations,
|
||||
reward_params.interval.stake_saturation_point,
|
||||
);
|
||||
|
||||
setStakeSaturation(computedStakeSaturation);
|
||||
setResults(estimationResult);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography fontWeight="medium" sx={{ mb: 1 }}>
|
||||
Playground
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'grey.600', mb: 2 }}>
|
||||
This is your parameters playground - change the parameters below to see the node specific estimations in the
|
||||
table
|
||||
</Typography>
|
||||
{defaultInputValues && (
|
||||
<Card variant="outlined" sx={{ p: 1, mb: 3 }}>
|
||||
<CardHeader
|
||||
title={
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
Estimation calculator
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Inputs onCalculate={handleCalculateEstimate} defaultValues={defaultInputValues} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<ResultsTable results={results} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<NodeDetails saturation={stakeSaturation} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { decimalToPercentage, percentToDecimal } from '@nymproject/types';
|
||||
import { computeMixnodeRewardEstimation } from 'src/requests';
|
||||
|
||||
const SCALE_FACTOR = 1_000_000;
|
||||
|
||||
export const computeStakeSaturation = (bond: string, delegations: string, stakeSaturationPoint: string) => {
|
||||
const res = ((+bond + +delegations) * SCALE_FACTOR) / +stakeSaturationPoint;
|
||||
return decimalToPercentage(res.toFixed(18).toString());
|
||||
};
|
||||
|
||||
export const computeEstimate = async ({
|
||||
mixId,
|
||||
uptime,
|
||||
pledgeAmount,
|
||||
totalDelegation,
|
||||
profitMargin,
|
||||
operatorCost,
|
||||
}: {
|
||||
mixId: number;
|
||||
uptime: string;
|
||||
pledgeAmount: string;
|
||||
totalDelegation: string;
|
||||
profitMargin: string;
|
||||
operatorCost: string;
|
||||
}) => {
|
||||
const computedEstimate = await computeMixnodeRewardEstimation({
|
||||
mixId: mixId,
|
||||
performance: percentToDecimal(uptime),
|
||||
pledgeAmount: Math.round(+pledgeAmount * SCALE_FACTOR),
|
||||
totalDelegation: Math.round(+totalDelegation * SCALE_FACTOR),
|
||||
profitMarginPercent: percentToDecimal(profitMargin),
|
||||
intervalOperatingCost: { denom: 'unym', amount: Math.round(+operatorCost * SCALE_FACTOR).toString() },
|
||||
});
|
||||
|
||||
return computedEstimate;
|
||||
};
|
||||
|
||||
export const handleCalculatePeriodRewards = ({
|
||||
estimatedOperatorReward,
|
||||
estimatedDelegatorsReward,
|
||||
}: {
|
||||
estimatedOperatorReward: string;
|
||||
estimatedDelegatorsReward: string;
|
||||
}) => {
|
||||
const dailyOperatorReward = (+estimatedOperatorReward / SCALE_FACTOR) * 24; // epoch_reward * 1 epoch_per_hour * 24 hours
|
||||
const dailyDelegatorReward = (+estimatedDelegatorsReward / SCALE_FACTOR) * 24;
|
||||
const dailyTotal = dailyOperatorReward + dailyDelegatorReward;
|
||||
|
||||
return {
|
||||
total: {
|
||||
daily: dailyTotal.toFixed(3).toString(),
|
||||
monthly: (dailyTotal * 30).toFixed(3).toString(),
|
||||
yearly: (dailyTotal * 365).toFixed(3).toString(),
|
||||
},
|
||||
operator: {
|
||||
daily: dailyOperatorReward.toFixed(3).toString(),
|
||||
monthly: (dailyOperatorReward * 30).toFixed(3).toString(),
|
||||
yearly: (dailyOperatorReward * 365).toFixed(3).toString(),
|
||||
},
|
||||
delegator: {
|
||||
daily: dailyDelegatorReward.toFixed(3).toString(),
|
||||
monthly: (dailyDelegatorReward * 30).toFixed(3).toString(),
|
||||
yearly: (dailyDelegatorReward * 365).toFixed(3).toString(),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export const createNavItems = (isMixnode: boolean) => {
|
||||
const navItems = ['Unbond'];
|
||||
if (isMixnode) return ['General', ...navItems];
|
||||
if (isMixnode) return ['General', 'Playground', ...navItems];
|
||||
return navItems;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
import { NymCard } from 'src/components';
|
||||
import { Overview } from 'src/components/TestMyNode/Overview';
|
||||
import { Results } from 'src/components/TestMyNode/Results';
|
||||
import { TestProgress } from 'src/components/TestMyNode/TestProgress';
|
||||
|
||||
export const TestNode = () => {
|
||||
const [view, setView] = useState('overview');
|
||||
const [packetsSent, setPacketsSent] = useState(0);
|
||||
const totalPackets = 500;
|
||||
|
||||
const mockPacketTransfer = (sent: number) => {
|
||||
if (sent - 1 < totalPackets) {
|
||||
setPacketsSent(sent);
|
||||
setTimeout(() => {
|
||||
mockPacketTransfer(sent + 1);
|
||||
}, 12.5);
|
||||
}
|
||||
if (sent === totalPackets) {
|
||||
setView('results');
|
||||
}
|
||||
};
|
||||
|
||||
const startTest = () => {
|
||||
setView('start-test');
|
||||
mockPacketTransfer(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<NymCard title="Test Node">
|
||||
{view === 'overview' && <Overview onStartTest={startTest} />}
|
||||
{view === 'start-test' && <TestProgress totalPackets={totalPackets} packetsSent={packetsSent} />}
|
||||
{view === 'results' && <Results layer="1" packetsSent="5000" packetsReceived="1000" />}
|
||||
</NymCard>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RewardEstimationResponse,
|
||||
WrappedDelegationEvent,
|
||||
PendingIntervalEvent,
|
||||
Coin,
|
||||
} from '@nymproject/types';
|
||||
import { Interval, TGatewayReport, TNodeDescription } from 'src/types';
|
||||
import { invokeWrapper } from './wrapper';
|
||||
@@ -51,3 +52,17 @@ export const getPendingIntervalEvents = async () =>
|
||||
|
||||
export const getGatewayReport = async (identity: string) =>
|
||||
invokeWrapper<TGatewayReport>('gateway_report', { identity });
|
||||
|
||||
export const computeMixnodeRewardEstimation = async (args: {
|
||||
mixId: number;
|
||||
performance: string;
|
||||
pledgeAmount: number;
|
||||
totalDelegation: number;
|
||||
profitMarginPercent: string;
|
||||
intervalOperatingCost: { denom: 'unym'; amount: string };
|
||||
}) => {
|
||||
console.log(args);
|
||||
|
||||
return invokeWrapper<RewardEstimationResponse>('compute_mixnode_reward_estimation', args);
|
||||
};
|
||||
export const getMixnodeUptime = async (mixId: number) => invokeWrapper<number>('get_mixnode_uptime', { mixId });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fee, FeeDetails, TransactionExecuteResult } from '@nymproject/types';
|
||||
import { Fee, FeeDetails, RewardingParams, TransactionExecuteResult } from '@nymproject/types';
|
||||
import { invokeWrapper } from './wrapper';
|
||||
|
||||
export const claimOperatorReward = async (fee?: Fee) =>
|
||||
@@ -9,3 +9,6 @@ export const claimDelegatorRewards = async (mixId: number, fee?: FeeDetails) =>
|
||||
mixId,
|
||||
fee: fee?.fee,
|
||||
});
|
||||
|
||||
export const getCurrentRewardingParameter = async () =>
|
||||
invokeWrapper<RewardingParams>('get_current_rewarding_parameters', {});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,5 @@
|
||||
|
||||
export interface AppEnv { ADMIN_ADDRESS: string | null, SHOW_TERMINAL: string | null, ENABLE_QA_MODE: string | null, }
|
||||
export interface AppEnv {
|
||||
ADMIN_ADDRESS: string | null;
|
||||
SHOW_TERMINAL: string | null;
|
||||
ENABLE_QA_MODE: string | null;
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
|
||||
export type Network = "QA" | "SANDBOX" | "MAINNET";
|
||||
export type Network = 'QA' | 'SANDBOX' | 'MAINNET';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { DecCoin } from "../../../../ts-packages/types/src/types/rust/DecCoin";
|
||||
import type { DecCoin } from '../../../../ts-packages/types/src/types/rust/DecCoin';
|
||||
|
||||
export interface TauriContractStateParams { minimum_mixnode_pledge: DecCoin, minimum_gateway_pledge: DecCoin, minimum_mixnode_delegation: DecCoin | null, }
|
||||
export interface TauriContractStateParams {
|
||||
minimum_mixnode_pledge: DecCoin;
|
||||
minimum_gateway_pledge: DecCoin;
|
||||
minimum_mixnode_delegation: DecCoin | null;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
|
||||
export interface ValidatorUrl { url: string, name: string | null, }
|
||||
export interface ValidatorUrl {
|
||||
url: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ValidatorUrl } from "./ValidatorUrl";
|
||||
import type { ValidatorUrl } from './ValidatorUrl';
|
||||
|
||||
export interface ValidatorUrls { urls: Array<ValidatorUrl>, }
|
||||
export interface ValidatorUrls {
|
||||
urls: Array<ValidatorUrl>;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
|
||||
].join(','),
|
||||
fontSize: 14,
|
||||
fontWeightRegular: 500,
|
||||
fontWeightMedium: 600,
|
||||
button: {
|
||||
textTransform: 'none',
|
||||
fontWeight: '600',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CurrencyDenom } from './CurrencyDenom';
|
||||
|
||||
export interface Account {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface AccountEntry {
|
||||
id: string;
|
||||
address: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Account } from './Account';
|
||||
|
||||
export interface AccountWithMnemonic {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface Balance {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface Coin {
|
||||
denom: string;
|
||||
amount: bigint;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Coin } from './Coin';
|
||||
|
||||
export interface CosmosFee {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CurrencyDenom = 'unknown' | 'nym' | 'nymt' | 'nyx' | 'nyxt';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CurrencyDenom } from './CurrencyDenom';
|
||||
|
||||
export type DecCoin = { denom: CurrencyDenom; amount: string };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface Delegation {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { DelegationEventKind } from './DelegationEventKind';
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DelegationEventKind = 'Delegate' | 'Undelegate';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface DelegationResult {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { DelegationWithEverything } from './DelegationWithEverything';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { DelegationEvent } from './DelegationEvent';
|
||||
import type { MixNodeCostParams } from './MixNodeCostParams';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CosmosFee } from './CosmosFee';
|
||||
|
||||
export type Fee = { Manual: CosmosFee } | { Auto: number | null };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { Fee } from './Fee';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface Gas {
|
||||
gas_units: bigint;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Gas } from './Gas';
|
||||
|
||||
export interface GasInfo {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface Gateway {
|
||||
host: string;
|
||||
mix_port: number;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { Gateway } from './Gateway';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SelectionChance } from './SelectionChance';
|
||||
|
||||
export interface InclusionProbabilityResponse {
|
||||
|
||||
@@ -3,6 +3,6 @@ export interface Interval {
|
||||
epochs_in_interval: number;
|
||||
current_epoch_start: string;
|
||||
current_epoch_id: number;
|
||||
epoch_length: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
total_elapsed_epochs: number;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { MixNode } from './Mixnode';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface MixNode {
|
||||
host: string;
|
||||
mix_port: number;
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type MixnodeStatus = 'active' | 'standby' | 'inactive' | 'not_found';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { MixnodeStatus } from './MixnodeStatus';
|
||||
|
||||
export interface MixnodeStatusResponse {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface OriginalVestingResponse {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Period = 'Before' | { In: number } | 'After';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface PledgeData {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RewardedSetNodeStatus = 'Active' | 'Standby';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { Gas } from './Gas';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { Gas } from './Gas';
|
||||
import type { TransactionDetails } from './TransactionDetails';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface StakeSaturationResponse {
|
||||
saturation: string;
|
||||
uncapped_saturation: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
|
||||
export interface TransactionDetails {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { GasInfo } from './GasInfo';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DecCoin } from './DecCoin';
|
||||
import type { VestingPeriod } from './VestingPeriod';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface VestingPeriod {
|
||||
start_time: bigint;
|
||||
period_seconds: bigint;
|
||||
|
||||
@@ -10,5 +10,8 @@ export const stringToDecimal = (raw: string): Decimal => Decimal.fromUserInput(r
|
||||
export const decimalToPercentage = (raw: string) =>
|
||||
Math.round(Decimal.fromUserInput(raw, 18).toFloatApproximation() * 100).toString();
|
||||
|
||||
export const percentToDecimal = (raw: string) =>
|
||||
(Decimal.fromUserInput(raw, 18).toFloatApproximation() / 100).toString();
|
||||
|
||||
export const decimalToFloatApproximation = (raw: string): number =>
|
||||
Decimal.fromUserInput(raw, 18).toFloatApproximation();
|
||||
|
||||
@@ -15817,6 +15817,13 @@ react-syntax-highlighter@^15.4.5:
|
||||
prismjs "^1.27.0"
|
||||
refractor "^3.6.0"
|
||||
|
||||
react-to-print@^2.14.7:
|
||||
version "2.14.7"
|
||||
resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.14.7.tgz#d288de0e573e6e63e203a48d4bad7a1759d18688"
|
||||
integrity sha512-lWVVAs9Co25uyE0toxcWeFsmaZObwUozXrJD9WMpDPclpBgk+WIzxlt3Q3omL/BCBG/cpf0XNvhayUWa+99YGw==
|
||||
dependencies:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-tooltip@*, react-tooltip@^4.2.21:
|
||||
version "4.2.21"
|
||||
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f"
|
||||
|
||||
Reference in New Issue
Block a user