Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4601c51df |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/env", "@babel/react"]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# Example with React + Typescript + Webpack 5 + MUI
|
||||
|
||||
An example of using default Webpack and Typescript settings with React and MUI, including theming.
|
||||
|
||||
You can use this example as a seed for a new project.
|
||||
|
||||
Remember to build the dependency packages from the root of this repo by running:
|
||||
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
If you need to make changes to the dependency packages, you can run `yarn watch` in that package to watch for chagnes and build them. This project will pick up the changes in the built package and hot-reload / recompile.
|
||||
|
||||
## Features
|
||||
|
||||
### Yarn workspaces
|
||||
|
||||
Packages from `ts-packages` are shared using Yarn workspaces. Make sure you add you new project to [package.json](../../package.json) to use the shared packages.
|
||||
|
||||
> ⚠️ **Warning**: Yarn workspaces will share all dependencies between projects and works by falling back to parent directories until a `node_modules` directory is found. So be careful when messing around with `node_modules` and resolution, because unexpected things could happen - for example, if you do not run `yarn` from the root and you have a `node_modules` in a directory that is a parent of the directory where you checkout out this repository, that `node_modules` will be used for resolving packages 🙀.
|
||||
|
||||
### Typescript
|
||||
|
||||
Shared Typescript config is in [tsconfig.json](./tsconfig.json), with specific production settings in [tsconfig.prod.json](./tsconfig.prod.json) that:
|
||||
|
||||
- exclude Storybook stories and Jest tests
|
||||
- do not output typing `*.d.ts` files
|
||||
|
||||
### Webpack
|
||||
|
||||
Inherit config for Webpack 5 with additional tweaks including:
|
||||
|
||||
- favicon generation from [favicon asset files](../../assets/favicon/favicon.png)
|
||||
- asset handling (svg, png, fonts, css, etc)
|
||||
- minification
|
||||
|
||||
The development settings include:
|
||||
|
||||
- `ts-loader` for quick transpilation
|
||||
- threaded type checking using `tsc`
|
||||
- hot reloading using `react-refresh`
|
||||
|
||||
### Storybook
|
||||
|
||||
Storybook is available in [@nymproject/react](../react-components/src/stories/Introduction.stories.mdx) and can be run using `yarn storybook`.
|
||||
|
||||
### MUI and theming
|
||||
|
||||
The [Nym theme](../mui-theme/src/theme/theme.ts) provides a theme provider that you can add as follows:
|
||||
|
||||
```typescript jsx
|
||||
export const App: React.FC = () => (
|
||||
<AppContextProvider>
|
||||
<AppTheme>
|
||||
<Content />
|
||||
</AppTheme>
|
||||
</AppContextProvider>
|
||||
);
|
||||
|
||||
export const AppTheme: React.FC = ({ children }) => {
|
||||
const { mode } = useAppContext();
|
||||
|
||||
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
|
||||
};
|
||||
|
||||
export const Content: React.FC = () => {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And augment typings for the Theme by adding [mui-theme.d.ts](./src/theme/mui-theme.d.ts):
|
||||
|
||||
```typescript
|
||||
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
|
||||
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme extends NymTheme {}
|
||||
interface ThemeOptions extends Partial<NymTheme> {}
|
||||
interface Palette extends NymPaletteWithExtensions {}
|
||||
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
|
||||
}
|
||||
```
|
||||
|
||||
Adding the above, means that any component now has the correct typings, for example, below the Nym palette interface is available for all MUI `Theme` instances with code completion for VSCode and IntelliJ:
|
||||
|
||||
```typescript jsx
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
...
|
||||
|
||||
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
|
||||
The quick brown fox jumps over the white fence
|
||||
</Typography>
|
||||
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "@nymproject/apy-playground",
|
||||
"description": "APY calculator and playground",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"@mui/material": "^5.0.1",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@mui/icons-material": "^5.5.0",
|
||||
"@mui/lab": "^5.0.0-alpha.72",
|
||||
"@nymproject/mui-theme" : "^1.0.0",
|
||||
"@nymproject/react" : "^1.0.0",
|
||||
"@cosmjs/math": "^0.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.14.5",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@nymproject/webpack": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^17.0.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-root-import": "^5.1.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"react-refresh-typescript": "^2.0.3",
|
||||
"style-loader": "^3.2.1",
|
||||
"thread-loader": "^3.0.4",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.64.3",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --progress --port 3000",
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
"build:dev": "webpack build --progress",
|
||||
"build:serve": "npx serve dist",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tsc": "tsc",
|
||||
"tsc:watch": "tsc --watch",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Container, Grid, Typography } from '@mui/material';
|
||||
import { NymLogo } from '@nymproject/react/logo/NymLogo';
|
||||
import { Playground } from '@nymproject/react/playground/Playground';
|
||||
import { useIsMounted } from '@nymproject/react/hooks/useIsMounted';
|
||||
import { NymThemeProvider } from '@nymproject/mui-theme';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { AppContextProvider, useAppContext } from './context';
|
||||
import { MixNodes } from './components/MixNodes';
|
||||
|
||||
export const AppTheme: React.FC = ({ children }) => {
|
||||
const { mode } = useAppContext();
|
||||
|
||||
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
|
||||
};
|
||||
|
||||
export const Content: React.FC = () => {
|
||||
const { mode } = useAppContext();
|
||||
const theme = useTheme();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
if (isMounted()) {
|
||||
console.log('Content is mounted');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ px: 4, py: 4 }}>
|
||||
<Box display="flex" justifyContent="space-between" pb={2}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<NymLogo height={50} />
|
||||
<Box ml={2}>
|
||||
<h1>APY Playground</h1>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<ThemeToggle />
|
||||
</Box>
|
||||
</Box>
|
||||
<MixNodes />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<AppContextProvider>
|
||||
<AppTheme>
|
||||
<Content />
|
||||
</AppTheme>
|
||||
</AppContextProvider>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import { useAppContext } from './context';
|
||||
|
||||
export const ThemeToggle: React.FC = () => {
|
||||
const { mode, toggleMode } = useAppContext();
|
||||
return (
|
||||
<Button variant="outlined" color="secondary" onClick={toggleMode} sx={{ display: 'flex', alignItems: 'centre' }}>
|
||||
{mode === 'dark' ? (
|
||||
<DarkModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
|
||||
) : (
|
||||
<LightModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
|
||||
)}
|
||||
<Typography ml={1} color={(theme) => theme.palette.primary.light}>
|
||||
Switch to {mode === 'dark' ? 'light mode' : 'dark mode'}
|
||||
</Typography>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,716 @@
|
||||
import * as React from 'react';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import {
|
||||
Checkbox,
|
||||
Stack,
|
||||
Box,
|
||||
IconButton,
|
||||
Paper,
|
||||
Slider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
tableCellClasses,
|
||||
TableContainer,
|
||||
Typography,
|
||||
Link,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
|
||||
import { Currency } from '@nymproject/react/currency/Currency';
|
||||
import { CurrencyAmountString } from '@nymproject/react/currency/CurrencyAmount';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
|
||||
import { Api, useAppContext } from '../context';
|
||||
import { toMajorCurrencyFromCoin } from '../utils/coin';
|
||||
import { round } from '../utils/round';
|
||||
import {
|
||||
MixNodeBondWithDetails,
|
||||
RewardEstimation,
|
||||
RewardEstimationParamsForSliders,
|
||||
RewardEstimationWithAPY,
|
||||
} from '../context/types';
|
||||
|
||||
const NETWORK_EXPLORER_BASE_URL = 'https://explorer.nymtech.net';
|
||||
const MAJOR_AMOUNT_FOR_CALCS = 1000;
|
||||
|
||||
const selectionChanceToProb = (value: string): number => {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'veryhigh':
|
||||
return 0.95;
|
||||
case 'high':
|
||||
return 0.8;
|
||||
case 'moderate':
|
||||
return 0.6;
|
||||
case 'low':
|
||||
return 0.25;
|
||||
default:
|
||||
return 0.05;
|
||||
}
|
||||
};
|
||||
|
||||
const MinorValue: React.FC<{
|
||||
value?: number;
|
||||
decimals?: number;
|
||||
}> = ({ value, decimals = 3 }) =>
|
||||
// <CurrencyAmountString
|
||||
// majorAmount={value ? round(value / 1_000_000, decimals).toString() : undefined}
|
||||
// sx={{ flexDirection: 'row-reverse' }}
|
||||
// />
|
||||
value ? <span>{round(value / 1_000_000, decimals)}</span> : <span>-</span>;
|
||||
|
||||
const TableCellValue: React.FC<{
|
||||
value?: number;
|
||||
decimals?: number;
|
||||
suffix?: string;
|
||||
}> = ({ value, suffix, decimals = 0 }) => (
|
||||
<TableCell align="right">
|
||||
{value ? round(value, decimals) : '-'}
|
||||
{suffix && ` ${suffix}`}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
const ResultValue: React.FC<{
|
||||
value?: number;
|
||||
decimals?: number;
|
||||
}> = ({ value, decimals = 0 }) => (
|
||||
<>
|
||||
<TableCell align="right">
|
||||
<MinorValue value={value ? value * 24 : undefined} decimals={decimals} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<MinorValue value={value ? value * 24 * 30 : undefined} decimals={decimals} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<MinorValue value={value ? value * 24 * 365 : undefined} decimals={decimals} />
|
||||
</TableCell>
|
||||
</>
|
||||
);
|
||||
|
||||
const SliderWithValue: React.FC<{
|
||||
label: string;
|
||||
value?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
scaleValue?: number;
|
||||
onChange: (value?: number) => void;
|
||||
onReset: () => void;
|
||||
display: React.ReactNode;
|
||||
}> = ({ label, value, min, max, onChange, onReset, display, scaleValue = 1 }) => {
|
||||
const minScaled = min !== undefined ? min * scaleValue : undefined;
|
||||
const maxScaled = max !== undefined ? max * scaleValue : undefined;
|
||||
const valueScaled = value !== undefined ? value * scaleValue : undefined;
|
||||
|
||||
console.log({ label, minScaled, maxScaled, valueScaled });
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell width="20%">{label}</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
<Stack spacing={2} direction="row">
|
||||
<Slider
|
||||
value={valueScaled}
|
||||
min={minScaled}
|
||||
max={maxScaled}
|
||||
onChange={(_event, newValue) => {
|
||||
const scaledNewValue = (newValue as number) / scaleValue;
|
||||
console.log({ label, minScaled, maxScaled, valueScaled, scaledNewValue });
|
||||
onChange(scaledNewValue);
|
||||
}}
|
||||
/>
|
||||
<IconButton>
|
||||
<RestartAltIcon opacity={0.15} onClick={onReset} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell width="50%">{display}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const InclusionProbabilityDisplay: React.FC<{
|
||||
isActive?: boolean;
|
||||
value: string;
|
||||
}> = ({ isActive, value }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
color={(theme) =>
|
||||
isActive
|
||||
? theme.palette.nym.networkExplorer.mixnodes.status.active
|
||||
: theme.palette.nym.networkExplorer.mixnodes.status.standby
|
||||
}
|
||||
>
|
||||
{isActive ? (
|
||||
<Box color="inherit">
|
||||
<CheckCircleOutlineIcon fontSize="small" color="inherit" />
|
||||
</Box>
|
||||
) : (
|
||||
<Box color="inherit">
|
||||
<PauseCircleOutlineIcon fontSize="small" color="inherit" />
|
||||
</Box>
|
||||
)}
|
||||
<Box color="inherit">{value}</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export const MixNodeRow: React.FC<{ index: number; mixnode: MixNodeBondWithDetails }> = ({ index, mixnode }) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const [showRaw, setShowRaw] = React.useState<boolean>(false);
|
||||
const ref = React.useRef<NodeJS.Timeout | null>(null);
|
||||
const [result, setResult] = React.useState<RewardEstimationWithAPY | undefined>();
|
||||
const [defaultResult, setDefaultResult] = React.useState<RewardEstimation | undefined>();
|
||||
|
||||
const defaultParams: RewardEstimationParamsForSliders = {
|
||||
pledge_amount: +(Number.parseFloat(mixnode.mixnode_bond.pledge_amount.amount) / 1_000_000),
|
||||
uptime: mixnode.uptime,
|
||||
total_delegation: +(Number.parseFloat(mixnode.mixnode_bond.total_delegation.amount) / 1_000_000),
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const [params, setParams] = React.useState<RewardEstimationParamsForSliders>(defaultParams);
|
||||
|
||||
const handleChange = (prop: string) => (value: any) => {
|
||||
setParams((prevState) => ({ ...prevState, [prop]: value }));
|
||||
};
|
||||
|
||||
const handleReset = (prop: string) => () =>
|
||||
setParams((prevState) => ({ ...prevState, [prop]: (defaultParams as any)[prop] }));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
clearTimeout(ref.current);
|
||||
}
|
||||
ref.current = setTimeout(() => calculate(), 250);
|
||||
}, [params.is_active, params.pledge_amount, params.uptime, params.total_delegation]);
|
||||
|
||||
const calculate = async () => {
|
||||
const res = await Api.computeRewardEstimation(mixnode.mixnode_bond.mix_node.identity_key, {
|
||||
...params,
|
||||
total_delegation: Math.floor(params.total_delegation * 1_000_000),
|
||||
pledge_amount: Math.floor(params.pledge_amount * 1_000_000),
|
||||
});
|
||||
const majorAmountToUseInCalcs = MAJOR_AMOUNT_FOR_CALCS;
|
||||
const operatorReward = (res.estimated_operator_reward / 1_000_000) * 24; // epoch_reward * 1 epoch_per_hour * 24 hours
|
||||
const delegatorsReward = (res.estimated_delegators_reward / 1_000_000) * 24;
|
||||
const totalPledge = Number.parseFloat(mixnode.mixnode_bond.pledge_amount.amount) / 1_000_000;
|
||||
// const totalDelegations = Number.parseFloat(mixnode.mixnode_bond.total_delegation.amount) / 1_000_000;
|
||||
|
||||
const operatorRewardScaled = majorAmountToUseInCalcs * (operatorReward / params.pledge_amount);
|
||||
const delegatorReward = majorAmountToUseInCalcs * (delegatorsReward / params.total_delegation);
|
||||
|
||||
const nodeApy = ((operatorReward + delegatorsReward) / (totalPledge + params.total_delegation)) * 365 * 100;
|
||||
|
||||
const res2: RewardEstimationWithAPY = {
|
||||
...res,
|
||||
estimates: {
|
||||
majorAmountToUseInCalcs,
|
||||
nodeApy,
|
||||
operator: {
|
||||
apy: (operatorRewardScaled / majorAmountToUseInCalcs) * 365 * 100,
|
||||
rewardMajorAmount: {
|
||||
daily: operatorRewardScaled,
|
||||
monthly: operatorRewardScaled * 30,
|
||||
yearly: operatorRewardScaled * 365,
|
||||
},
|
||||
},
|
||||
delegator: {
|
||||
apy: (delegatorReward / majorAmountToUseInCalcs) * 365 * 100,
|
||||
rewardMajorAmount: {
|
||||
daily: delegatorReward,
|
||||
monthly: delegatorReward * 30,
|
||||
yearly: delegatorReward * 365,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (!defaultResult) {
|
||||
setDefaultResult(res);
|
||||
} else {
|
||||
setResult(res2);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open && !result) {
|
||||
calculate();
|
||||
}
|
||||
}, [open, result]);
|
||||
|
||||
const bond = toMajorCurrencyFromCoin(mixnode.mixnode_bond.pledge_amount);
|
||||
const totalDelegation = toMajorCurrencyFromCoin(mixnode.mixnode_bond.total_delegation);
|
||||
const totalDelegationFloat = Number.parseFloat(totalDelegation?.amount || '1');
|
||||
let color;
|
||||
// eslint-disable-next-line default-case
|
||||
switch (mixnode.status) {
|
||||
case 'active':
|
||||
color = theme.palette.nym.networkExplorer.mixnodes.status.active;
|
||||
break;
|
||||
case 'standby':
|
||||
color = theme.palette.nym.networkExplorer.mixnodes.status.standby;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{open ? (
|
||||
<IconButton onClick={() => setOpen(false)}>
|
||||
<ArrowDropUpIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => setOpen(true)}>
|
||||
<ArrowDropDownIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Chip sx={{ ml: 1 }} label={`${index + 1}`} variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`${NETWORK_EXPLORER_BASE_URL}/network-components/mixnode/${mixnode.mixnode_bond.mix_node.identity_key}`}
|
||||
target="_blank"
|
||||
>
|
||||
{mixnode.mixnode_bond.mix_node.identity_key.slice(0, 6)}
|
||||
...
|
||||
{mixnode.mixnode_bond.mix_node.identity_key.slice(-6)}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency majorAmount={bond} showCoinMark coinMarkPrefix hideFractions sx={{ fontSize: 14 }} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency majorAmount={totalDelegation} showCoinMark coinMarkPrefix hideFractions sx={{ fontSize: 14 }} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography color={(theme) => (mixnode.stake_saturation > 1 ? theme.palette.warning.main : undefined)}>
|
||||
{round(mixnode.stake_saturation * 100, 1)}%
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography fontSize="inherit" color={color}>
|
||||
{mixnode.status}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{round(mixnode.uptime, 0)}%</TableCell>
|
||||
<TableCell>{mixnode.mixnode_bond.mix_node.profit_margin_percent}%</TableCell>
|
||||
<TableCell>
|
||||
{mixnode.inclusion_probability && (
|
||||
<InclusionProbabilityDisplay isActive value={mixnode.inclusion_probability.in_active} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{round(mixnode.estimated_operator_apy, 0)}%</TableCell>
|
||||
<TableCell>
|
||||
<Currency
|
||||
majorAmount={{
|
||||
amount: defaultResult
|
||||
? ((defaultResult.estimated_operator_reward / 1_000_000) * 24 * 365).toString()
|
||||
: '',
|
||||
denom: 'NYM',
|
||||
}}
|
||||
showCoinMark
|
||||
coinMarkPrefix
|
||||
hideFractions
|
||||
sx={{ fontSize: 14 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{round(mixnode.estimated_delegators_apy, 0)}%</TableCell>
|
||||
<TableCell>
|
||||
<Currency
|
||||
majorAmount={{
|
||||
amount: defaultResult
|
||||
? (
|
||||
(MAJOR_AMOUNT_FOR_CALCS * ((defaultResult.estimated_delegators_reward / 1_000_000) * 24 * 365)) /
|
||||
totalDelegationFloat
|
||||
).toString()
|
||||
: '',
|
||||
denom: 'NYM',
|
||||
}}
|
||||
showCoinMark
|
||||
coinMarkPrefix
|
||||
hideFractions
|
||||
sx={{ fontSize: 14 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{mixnode.inclusion_probability &&
|
||||
round(mixnode.estimated_delegators_apy * selectionChanceToProb(mixnode.inclusion_probability.in_active), 0)}
|
||||
%
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency
|
||||
majorAmount={{
|
||||
amount:
|
||||
defaultResult && mixnode.inclusion_probability
|
||||
? (
|
||||
((MAJOR_AMOUNT_FOR_CALCS * ((defaultResult.estimated_delegators_reward / 1_000_000) * 24 * 365)) /
|
||||
totalDelegationFloat) *
|
||||
selectionChanceToProb(mixnode.inclusion_probability.in_active)
|
||||
).toString()
|
||||
: '',
|
||||
denom: 'NYM',
|
||||
}}
|
||||
showCoinMark
|
||||
coinMarkPrefix
|
||||
hideFractions
|
||||
sx={{ fontSize: 14 }}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{open && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={12}>
|
||||
<Paper elevation={3} sx={{ px: 2, py: 2 }}>
|
||||
<Box>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
<SliderWithValue
|
||||
label="Pledge"
|
||||
value={params.pledge_amount}
|
||||
min={0}
|
||||
max={Math.max(1_000_000, (defaultParams.pledge_amount || 0) * 2)}
|
||||
// max={Math.max(1_000_000_000_000, (params.pledge_amount || 0) * 1.2)}
|
||||
onChange={handleChange('pledge_amount')}
|
||||
onReset={handleReset('pledge_amount')}
|
||||
display={
|
||||
<Stack direction="row" spacing={2}>
|
||||
<CurrencyAmountString majorAmount={params.pledge_amount?.toString()} hideFractions />
|
||||
<span>nym</span>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<SliderWithValue
|
||||
label="Total delegations"
|
||||
min={0}
|
||||
max={Math.max(1_000_000, (defaultParams.total_delegation || 0) * 2)}
|
||||
value={params.total_delegation}
|
||||
onChange={handleChange('total_delegation')}
|
||||
onReset={handleReset('total_delegation')}
|
||||
display={
|
||||
<Stack direction="row" spacing={2}>
|
||||
<CurrencyAmountString majorAmount={params.total_delegation?.toString()} hideFractions />
|
||||
<span>nym</span>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<SliderWithValue
|
||||
label="Uptime"
|
||||
min={0}
|
||||
max={100}
|
||||
value={params.uptime}
|
||||
onChange={handleChange('uptime')}
|
||||
onReset={handleReset('uptime')}
|
||||
display={<span>{params.uptime}%</span>}
|
||||
/>
|
||||
<TableRow>
|
||||
<TableCell width="20%">In active set?</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
<Checkbox
|
||||
checked={params.is_active === true}
|
||||
onChange={(_, checked) => {
|
||||
handleChange('is_active')(checked);
|
||||
}}
|
||||
/>
|
||||
<IconButton>
|
||||
<RestartAltIcon opacity={0.15} onClick={handleReset('is_active')} />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell width="50%">{params.is_active === undefined ? '-' : `${params.is_active}`}</TableCell>
|
||||
</TableRow>
|
||||
{result && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>
|
||||
<Box>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
'& .MuiTableRow-root:hover': {
|
||||
backgroundColor: 'grey.800',
|
||||
},
|
||||
[`& .${tableCellClasses.root}`]: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} />
|
||||
<TableCell colSpan={5} align="center">
|
||||
<strong>Total rewards</strong>
|
||||
</TableCell>
|
||||
<TableCell colSpan={4} align="center">
|
||||
<strong>
|
||||
When {result.estimates.majorAmountToUseInCalcs} NYM is staked,
|
||||
<br />
|
||||
estimated rewards in NYM are:
|
||||
</strong>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell align="right" sx={{ opacity: 0.2 }}>
|
||||
<strong>Current per day</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Est. per day</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Est. per month</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Est. per year</strong>
|
||||
</TableCell>
|
||||
<TableCell />
|
||||
<TableCell align="right">
|
||||
<strong>Daily</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Monthly</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>Annual</strong>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<strong>APY</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Total node reward</TableCell>
|
||||
<TableCell sx={{ opacity: 0.3 }} align="right">
|
||||
<MinorValue value={defaultResult?.estimated_total_node_reward} />
|
||||
</TableCell>
|
||||
<ResultValue value={result.estimated_total_node_reward} />
|
||||
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
|
||||
<TableCell />
|
||||
<TableCell />
|
||||
<TableCell />
|
||||
<TableCellValue value={result.estimates.nodeApy} decimals={0} suffix="%" />
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Operator reward</TableCell>
|
||||
<TableCell sx={{ opacity: 0.3 }} align="right">
|
||||
<MinorValue value={defaultResult?.estimated_operator_reward} />
|
||||
</TableCell>
|
||||
<ResultValue value={result.estimated_operator_reward} />
|
||||
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
|
||||
<TableCellValue
|
||||
value={result.estimates.operator.rewardMajorAmount.daily}
|
||||
decimals={3}
|
||||
/>
|
||||
<TableCellValue value={result.estimates.operator.rewardMajorAmount.monthly} />
|
||||
<TableCellValue value={result.estimates.operator.rewardMajorAmount.yearly} />
|
||||
<TableCellValue value={result.estimates.operator.apy} suffix="%" />
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>All delegators reward</TableCell>
|
||||
<TableCell sx={{ opacity: 0.3 }} align="right">
|
||||
<MinorValue value={defaultResult?.estimated_delegators_reward} />
|
||||
</TableCell>
|
||||
<ResultValue value={result.estimated_delegators_reward} />
|
||||
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
|
||||
<TableCellValue
|
||||
value={result.estimates.delegator.rewardMajorAmount.daily}
|
||||
decimals={3}
|
||||
/>
|
||||
<TableCellValue value={result.estimates.delegator.rewardMajorAmount.monthly} />
|
||||
<TableCellValue value={result.estimates.delegator.rewardMajorAmount.yearly} />
|
||||
<TableCellValue value={result.estimates.delegator.apy} suffix="%" />
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Node profit</TableCell>
|
||||
<TableCell sx={{ opacity: 0.3 }} align="right">
|
||||
<MinorValue value={defaultResult?.estimated_node_profit} />
|
||||
</TableCell>
|
||||
<ResultValue value={result.estimated_node_profit} />
|
||||
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Operator cost</TableCell>
|
||||
<TableCell sx={{ opacity: 0.3 }} align="right">
|
||||
<MinorValue value={defaultResult?.estimated_operator_cost} />
|
||||
</TableCell>
|
||||
<ResultValue value={result.estimated_operator_cost} />
|
||||
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
Raw values
|
||||
{showRaw ? (
|
||||
<IconButton onClick={() => setShowRaw(false)}>
|
||||
<ArrowDropUpIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => setShowRaw(true)}>
|
||||
<ArrowDropDownIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{showRaw && (
|
||||
<TableRow>
|
||||
<TableCell>Raw Result</TableCell>
|
||||
<TableCell>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
result,
|
||||
mixnode,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MixNodes: React.FC = () => {
|
||||
const { loading, mixnodes, rewardParams } = useAppContext();
|
||||
|
||||
if (loading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table
|
||||
sx={{
|
||||
'& .MuiTableRow-root:hover': {
|
||||
backgroundColor: 'grey.A700',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} />
|
||||
<TableCell colSpan={4} align="center" sx={{ background: (theme) => theme.palette.divider }}>
|
||||
Maximum achievable values
|
||||
<br />
|
||||
(when always in the active set)
|
||||
</TableCell>
|
||||
<TableCell colSpan={2} align="center">
|
||||
More realistic values
|
||||
<br />
|
||||
(scaled by selection probability)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>Identity</TableCell>
|
||||
<TableCell>Pledge</TableCell>
|
||||
<TableCell>Total delegations</TableCell>
|
||||
<TableCell>Saturation</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Uptime</TableCell>
|
||||
<TableCell>Profit Margin</TableCell>
|
||||
<TableCell>Selection Probability</TableCell>
|
||||
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Est. Operator APY</TableCell>
|
||||
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Annual operator rewards</TableCell>
|
||||
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Est. All Delegators APY</TableCell>
|
||||
<TableCell sx={{ background: (theme) => theme.palette.divider }}>
|
||||
Annual delegator rewards
|
||||
<br />
|
||||
for staking {MAJOR_AMOUNT_FOR_CALCS} NYM
|
||||
</TableCell>
|
||||
<TableCell>Est. All Delegators APY</TableCell>
|
||||
<TableCell>
|
||||
Annual delegator rewards
|
||||
<br />
|
||||
for staking {MAJOR_AMOUNT_FOR_CALCS} NYM
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(mixnodes || []).map((m, i) => (
|
||||
<MixNodeRow key={m.mixnode_bond.mix_node.identity_key} index={i} mixnode={m} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Box mt={6}>
|
||||
<h3>Reward Params (for epoch)</h3>
|
||||
</Box>
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableCell>Epoch reward pool</TableCell>
|
||||
<TableCell>
|
||||
<Currency
|
||||
coinMarkPrefix
|
||||
showCoinMark
|
||||
hideFractions
|
||||
majorAmount={
|
||||
rewardParams?.epoch_reward_pool
|
||||
? toMajorCurrencyFromCoin({
|
||||
amount: rewardParams.epoch_reward_pool,
|
||||
denom: 'unym',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Rewarded set size</TableCell>
|
||||
<TableCell>{rewardParams?.rewarded_set_size}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Active set size</TableCell>
|
||||
<TableCell>{rewardParams?.active_set_size}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Staking supply</TableCell>
|
||||
<TableCell>
|
||||
<Currency
|
||||
coinMarkPrefix
|
||||
showCoinMark
|
||||
hideFractions
|
||||
majorAmount={
|
||||
rewardParams?.staking_supply
|
||||
? toMajorCurrencyFromCoin({
|
||||
amount: rewardParams.staking_supply,
|
||||
denom: 'unym',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Sybil resistance percent</TableCell>
|
||||
<TableCell>{rewardParams?.sybil_resistance_percent}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Active set work factor</TableCell>
|
||||
<TableCell>{rewardParams?.active_set_work_factor}</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { PaletteMode } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
InclusionProbability,
|
||||
MixNodeBondWithDetails,
|
||||
RewardEstimation,
|
||||
RewardEstimationParams,
|
||||
RewardParams,
|
||||
} from './types';
|
||||
|
||||
// const API_BASE = 'https://qa-validator-api.nymtech.net/api';
|
||||
const API_BASE = 'https://validator-apy.dev.nymte.ch/api';
|
||||
|
||||
interface State {
|
||||
mode: PaletteMode;
|
||||
toggleMode: () => void;
|
||||
loading: boolean;
|
||||
mixnodes: MixNodeBondWithDetails[] | undefined;
|
||||
rewardParams: RewardParams | undefined;
|
||||
}
|
||||
|
||||
const AppContext = React.createContext<State | undefined>(undefined);
|
||||
|
||||
export const Api = {
|
||||
computeRewardEstimation: async (identityKey: string, params: RewardEstimationParams): Promise<RewardEstimation> => {
|
||||
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/compute-reward-estimation`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
getMixnodesDetailed: async (): Promise<MixNodeBondWithDetails[]> => {
|
||||
const response = await fetch(`${API_BASE}/v1/mixnodes/detailed`);
|
||||
const items = (await response.json()) as MixNodeBondWithDetails[];
|
||||
const page = items
|
||||
.sort((a, b) => {
|
||||
const amountA = Number.parseFloat(a.mixnode_bond.total_delegation.amount);
|
||||
const amountB = Number.parseFloat(b.mixnode_bond.total_delegation.amount);
|
||||
return amountB - amountA;
|
||||
})
|
||||
.slice(0, 100);
|
||||
await Promise.all(
|
||||
page.map(async (item) => {
|
||||
const status = await Api.getMixnodeStatus(item.mixnode_bond.mix_node.identity_key);
|
||||
const probability = await Api.getMixnodeInclusionProbability(item.mixnode_bond.mix_node.identity_key);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.status = status;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.inclusion_probability = probability;
|
||||
}),
|
||||
);
|
||||
return page;
|
||||
},
|
||||
getRewardParams: async (): Promise<RewardParams> => {
|
||||
const response = await fetch(`${API_BASE}/v1/epoch/reward_params`);
|
||||
const params = (await response.json()) as RewardParams;
|
||||
return params;
|
||||
},
|
||||
getMixnodeStatus: async (identityKey: string): Promise<string> => {
|
||||
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/status`);
|
||||
return (await response.json()).status;
|
||||
},
|
||||
getMixnodeInclusionProbability: async (identityKey: string): Promise<InclusionProbability> => {
|
||||
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/inclusion-probability`);
|
||||
return (await response.json()) as InclusionProbability;
|
||||
},
|
||||
};
|
||||
|
||||
export const useAppContext = (): State => {
|
||||
const context = React.useContext<State | undefined>(AppContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Please include a `import { AppContextProvider } from "./context"` before using this hook');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AppContextProvider: React.FC = ({ children }) => {
|
||||
// light/dark mode
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark');
|
||||
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [mixnodes, setMixnodes] = React.useState<MixNodeBondWithDetails[] | undefined>();
|
||||
const [rewardParams, setRewardParams] = React.useState<RewardParams | undefined>();
|
||||
|
||||
const refresh = async () => {
|
||||
setMixnodes(await Api.getMixnodesDetailed());
|
||||
setRewardParams(await Api.getRewardParams());
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
refresh().finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo<State>(
|
||||
() => ({
|
||||
mode,
|
||||
toggleMode: () => setMode((prevMode) => (prevMode !== 'light' ? 'light' : 'dark')),
|
||||
loading,
|
||||
mixnodes,
|
||||
rewardParams,
|
||||
}),
|
||||
[mode, mixnodes, loading, rewardParams],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
export interface RewardParams {
|
||||
epoch_reward_pool: string;
|
||||
rewarded_set_size: string;
|
||||
active_set_size: string;
|
||||
staking_supply: string;
|
||||
sybil_resistance_percent: number;
|
||||
active_set_work_factor: number;
|
||||
}
|
||||
|
||||
export interface RewardEstimationParams {
|
||||
uptime?: number;
|
||||
is_active?: boolean;
|
||||
pledge_amount?: number;
|
||||
total_delegation?: number;
|
||||
}
|
||||
|
||||
export interface RewardEstimationParamsForSliders {
|
||||
uptime: number;
|
||||
is_active: boolean;
|
||||
pledge_amount: number;
|
||||
total_delegation: number;
|
||||
}
|
||||
|
||||
export interface RewardEstimation {
|
||||
estimated_total_node_reward: number;
|
||||
estimated_operator_reward: number;
|
||||
estimated_delegators_reward: number;
|
||||
estimated_node_profit: number;
|
||||
estimated_operator_cost: number;
|
||||
reward_params: any;
|
||||
as_at: number;
|
||||
}
|
||||
|
||||
export interface RewardEstimationWithAPY {
|
||||
estimated_total_node_reward: number;
|
||||
estimated_operator_reward: number;
|
||||
estimated_delegators_reward: number;
|
||||
estimated_node_profit: number;
|
||||
estimated_operator_cost: number;
|
||||
reward_params: any;
|
||||
as_at: number;
|
||||
estimates: {
|
||||
majorAmountToUseInCalcs: number;
|
||||
nodeApy: number;
|
||||
operator: {
|
||||
apy: number;
|
||||
rewardMajorAmount: {
|
||||
daily: number;
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
};
|
||||
};
|
||||
delegator: {
|
||||
apy: number;
|
||||
rewardMajorAmount: {
|
||||
daily: number;
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface MixNodeBondWithDetails {
|
||||
mixnode_bond: {
|
||||
pledge_amount: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
total_delegation: {
|
||||
denom: string;
|
||||
amount: string;
|
||||
};
|
||||
owner: string;
|
||||
layer: number;
|
||||
block_height: number;
|
||||
mix_node: {
|
||||
host: string;
|
||||
mix_port: number;
|
||||
verloc_port: number;
|
||||
http_api_port: number;
|
||||
sphinx_key: string;
|
||||
identity_key: string;
|
||||
version: string;
|
||||
profit_margin_percent: number;
|
||||
};
|
||||
proxy: null;
|
||||
accumulated_rewards: string;
|
||||
};
|
||||
stake_saturation: number;
|
||||
uptime: number;
|
||||
estimated_operator_apy: number;
|
||||
estimated_delegators_apy: number;
|
||||
status?: string;
|
||||
inclusion_probability?: InclusionProbability;
|
||||
}
|
||||
|
||||
export interface InclusionProbability {
|
||||
in_active: string;
|
||||
in_reserve: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nym APY Playground</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { App } from '../App';
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
render(<App />);
|
||||
});
|
||||
it('should render without exploding', () => {
|
||||
const { container } = render(<App />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
|
||||
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
|
||||
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
|
||||
|
||||
/**
|
||||
* If you are unfamiliar with Material UI theming, please read the following first:
|
||||
* - https://mui.com/customization/theming/
|
||||
* - https://mui.com/customization/palette/
|
||||
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
|
||||
*
|
||||
* This file adds typings to the theme using Typescript's module augmentation.
|
||||
*
|
||||
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
|
||||
* look at the recommendations from Material UI docs for implementation:
|
||||
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
|
||||
* - https://mui.com/customization/palette/#adding-new-colors
|
||||
*
|
||||
*
|
||||
* IMPORTANT:
|
||||
*
|
||||
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
|
||||
* `type Foo = { ... }` - this is necessary to merge the definitions.
|
||||
*/
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
/**
|
||||
* This augments the definitions of the MUI Theme with the Nym theme, as well as
|
||||
* a partial `ThemeOptions` type used by `createTheme`
|
||||
*
|
||||
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
|
||||
*/
|
||||
interface Theme extends NymTheme {}
|
||||
interface ThemeOptions extends Partial<NymTheme> {}
|
||||
interface Palette extends NymPaletteWithExtensions {}
|
||||
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Decimal } from '@cosmjs/math';
|
||||
import { MajorCurrencyAmount } from '@nymproject/types';
|
||||
|
||||
export const toMajorCurrency = (amount: string, denom: string): MajorCurrencyAmount => {
|
||||
if (denom[0].toLowerCase() !== 'u') {
|
||||
return {
|
||||
amount,
|
||||
denom: denom as any,
|
||||
};
|
||||
}
|
||||
const decimal = Decimal.fromAtomics(amount, 6);
|
||||
return {
|
||||
amount: decimal.toString(),
|
||||
denom: denom.slice(1) as any,
|
||||
};
|
||||
};
|
||||
|
||||
export const toMajorCurrencyFromCoin = (coin?: { amount: string; denom: string }): MajorCurrencyAmount | undefined => {
|
||||
if (!coin) {
|
||||
return undefined;
|
||||
}
|
||||
return toMajorCurrency(coin.amount, coin.denom);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Reproduce the behaviour of Python's round method
|
||||
* @param value The floating point number to round
|
||||
* @param decimals The number of decimals to round to, e.g. 11.4999 to 2 decimals is 11.50
|
||||
*/
|
||||
export const round = (value: number, decimals: number = 0): number => {
|
||||
if (decimals === 0) {
|
||||
return Math.round(value);
|
||||
}
|
||||
const pow = 10 ** decimals;
|
||||
return +Math.round(value * pow) / pow;
|
||||
// return +(Math.round(Number.parseFloat(value + `e+${decimals}`)) + `e-${decimals}`)
|
||||
};
|
||||
|
||||
/**
|
||||
* Round returning 0 when value is undefined
|
||||
*/
|
||||
export const roundWithDefault = (value?: number, decimals: number = 0): number => {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
return round(value, decimals);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"declaration": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"dist",
|
||||
"**/*.stories.*",
|
||||
"**/*.test.*",
|
||||
"**/*.spec.*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const path = require('path');
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const { webpackCommon } = require('@nymproject/webpack');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(webpackCommon(__dirname), {
|
||||
entry: path.resolve(__dirname, 'src/index.tsx'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const webpack = require('webpack');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
const ReactRefreshTypeScript = require('react-refresh-typescript');
|
||||
const commonConfig = require('./webpack.common');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(commonConfig, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
getCustomTransformers: () => ({
|
||||
before: [ReactRefreshTypeScript()],
|
||||
}),
|
||||
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
|
||||
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
|
||||
// this can be included automatically by the dev server, however build mode fails if missing
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
|
||||
target: 'web',
|
||||
|
||||
devServer: {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
historyApiFallback: true,
|
||||
},
|
||||
|
||||
// recommended for faster rebuild
|
||||
optimization: {
|
||||
runtimeChunk: true,
|
||||
removeAvailableModules: false,
|
||||
removeEmptyChunks: false,
|
||||
splitChunks: false,
|
||||
},
|
||||
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
// restart on config change
|
||||
config: ['./webpack.config.js'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const commonConfig = require('./webpack.common');
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(commonConfig, {
|
||||
mode: 'production',
|
||||
|
||||
// TODO: no source maps, add back
|
||||
devtool: false,
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: ['...', new CssMinimizerPlugin()],
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash].css',
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
pathinfo: false,
|
||||
filename: '[name].[contenthash].js',
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,25 +8,28 @@ export const Currency: React.FC<{
|
||||
majorAmount?: DecCoin;
|
||||
showDenom?: boolean;
|
||||
showCoinMark?: boolean;
|
||||
hideFractions?: boolean;
|
||||
coinMarkPrefix?: boolean;
|
||||
sx?: SxProps;
|
||||
}> = ({ majorAmount, sx, showDenom = true, showCoinMark = false, coinMarkPrefix = false }) => {
|
||||
}> = ({ majorAmount, sx, showDenom = true, showCoinMark = false, coinMarkPrefix = false, hideFractions = false }) => {
|
||||
if (!majorAmount || !majorAmount.amount) {
|
||||
return (
|
||||
<Stack direction="row" sx={sx}>
|
||||
<Stack direction="row" sx={sx} fontSize="inherit">
|
||||
<span>-</span>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (!showDenom) {
|
||||
return <CurrencyAmount majorAmount={majorAmount} sx={sx} />;
|
||||
return <CurrencyAmount majorAmount={majorAmount} hideFractions={hideFractions} sx={sx} />;
|
||||
}
|
||||
if (showCoinMark) {
|
||||
return <CurrencyWithCoinMark majorAmount={majorAmount} prefix={coinMarkPrefix} sx={sx} />;
|
||||
return (
|
||||
<CurrencyWithCoinMark majorAmount={majorAmount} hideFractions={hideFractions} prefix={coinMarkPrefix} sx={sx} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="row" spacing={CURRENCY_AMOUNT_SPACING} sx={sx}>
|
||||
<CurrencyAmount majorAmount={majorAmount} />
|
||||
<Stack direction="row" spacing={CURRENCY_AMOUNT_SPACING} sx={sx} fontSize="inherit">
|
||||
<CurrencyAmount majorAmount={majorAmount} hideFractions={hideFractions} />
|
||||
<span>{majorAmount.denom}</span>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -39,18 +39,19 @@ const toChunks = (value: String, size: number = 3): Array<string> => {
|
||||
export const CurrencyAmountString: React.FC<{
|
||||
majorAmount?: string;
|
||||
showSeparators?: boolean;
|
||||
hideFractions?: boolean;
|
||||
sx?: SxProps;
|
||||
}> = ({ majorAmount, sx, showSeparators = true }) => {
|
||||
}> = ({ majorAmount, sx, showSeparators = true, hideFractions = false }) => {
|
||||
if (!majorAmount) {
|
||||
return (
|
||||
<Stack direction="row" sx={sx}>
|
||||
<Stack direction="row" sx={sx} fontSize="inherit">
|
||||
<span>-</span>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (!showSeparators) {
|
||||
return (
|
||||
<Stack direction="row" sx={sx}>
|
||||
<Stack direction="row" sx={sx} fontSize="inherit">
|
||||
<span>{majorAmount}</span>
|
||||
</Stack>
|
||||
);
|
||||
@@ -66,29 +67,30 @@ export const CurrencyAmountString: React.FC<{
|
||||
|
||||
const parts = majorAmount.split('.');
|
||||
if (parts.length !== 1 && parts.length !== 2) {
|
||||
return <Typography sx={sx}>Error</Typography>;
|
||||
return (
|
||||
<Typography sx={sx} fontSize="inherit">
|
||||
Error
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const wholePart = toReverseChunks(parts[0]);
|
||||
const fractionPart = parts[1] ? toChunks(parts[1]) : [];
|
||||
const wholePartFormatted = new Intl.NumberFormat('en-US', { style: 'decimal' })
|
||||
.format(Number.parseFloat(parts[0]))
|
||||
.replaceAll(',', ' ');
|
||||
|
||||
if (parts.length === 1 || hideFractions) {
|
||||
return (
|
||||
<Stack direction="row" sx={sx}>
|
||||
<span>{wholePartFormatted}</span>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="row" sx={sx}>
|
||||
<Stack direction="row" spacing={CURRENCY_AMOUNT_SPACING}>
|
||||
{wholePart.map((chunk, index) => (
|
||||
<span key={`${chunk}-${index}`}>{chunk}</span>
|
||||
))}
|
||||
</Stack>
|
||||
{parts[1] && (
|
||||
<>
|
||||
<span>.</span>
|
||||
<Stack direction="row" spacing={CURRENCY_AMOUNT_SPACING}>
|
||||
{fractionPart.map((chunk, index) => (
|
||||
<span key={`${chunk}-${index}`}>{chunk}</span>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<span>{wholePartFormatted}</span>
|
||||
<span>.</span>
|
||||
<span>{parts[1]}</span>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -96,5 +98,6 @@ export const CurrencyAmountString: React.FC<{
|
||||
export const CurrencyAmount: React.FC<{
|
||||
majorAmount?: DecCoin;
|
||||
showSeparators?: boolean;
|
||||
hideFractions?: boolean;
|
||||
sx?: SxProps;
|
||||
}> = ({ majorAmount, ...props }) => <CurrencyAmountString majorAmount={majorAmount?.amount} {...props} />;
|
||||
|
||||
@@ -11,8 +11,9 @@ export const CurrencyWithCoinMark: React.FC<{
|
||||
fontSize?: number;
|
||||
prefix?: boolean;
|
||||
showSeparators?: boolean;
|
||||
hideFractions?: boolean;
|
||||
sx?: SxProps;
|
||||
}> = ({ majorAmount, fontSize, prefix, showSeparators, sx }) => {
|
||||
}> = ({ majorAmount, fontSize, prefix, showSeparators, hideFractions, sx }) => {
|
||||
const theme = useTheme();
|
||||
const size = fontSize || theme.typography.htmlFontSize;
|
||||
if (!majorAmount) {
|
||||
@@ -20,15 +21,15 @@ export const CurrencyWithCoinMark: React.FC<{
|
||||
}
|
||||
const DenomMark = majorAmount.denom === 'nymt' ? CoinMarkTestnet : CoinMark;
|
||||
return (
|
||||
<Stack direction="row" fontSize={size} spacing={1} alignItems="center" sx={sx}>
|
||||
<Stack direction="row" fontSize={size || 'inherit'} spacing={1} alignItems="center" sx={sx}>
|
||||
{prefix ? (
|
||||
<>
|
||||
<DenomMark height={size} />
|
||||
<CurrencyAmount majorAmount={majorAmount} showSeparators={showSeparators} />
|
||||
<CurrencyAmount majorAmount={majorAmount} showSeparators={showSeparators} hideFractions={hideFractions} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CurrencyAmount majorAmount={majorAmount} showSeparators={showSeparators} />
|
||||
<CurrencyAmount majorAmount={majorAmount} showSeparators={showSeparators} hideFractions={hideFractions} />
|
||||
<DenomMark height={size} />
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user