Compare commits

...

1 Commits

Author SHA1 Message Date
Mark Sinclair c4601c51df ts-packages: APY playground 2022-07-20 14:06:52 +01:00
24 changed files with 15990 additions and 31 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
+98
View File
@@ -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',
};
+85
View File
@@ -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"
}
}
+51
View File
@@ -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;
}
+14
View File
@@ -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>
+5
View File
@@ -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();
});
});
+37
View File
@@ -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);
};
+16
View File
@@ -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} />
</>
)}