From da95e4e903432084f6898d970535f90ed6c717dd Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Fri, 14 Oct 2022 10:40:53 +0200 Subject: [PATCH] Wallet - bonding, new node stats component (#1535) * feature(wallet-bonding): add bond more skeleton * fix(wallet-bonding): post godzilla merge fix: post rebase feature(wallet-bonding): wip * feat(wallet-bonding): add node stats component feat(wallet-bonding): add node stats component * feat(wallet-bonding): fix tooltip icon size * fix(wallet-bonding): node stats fix responsiveness fix(wallet-bonding): post godzilla merge * feat(wallet): fetch active set probabilities * feat(wallet): operation mixnode avg uptime * fix: typo * fix(wallet): theme colors fix(wallet): theme colors * fix(wallet): destructuring * feat(wallet): request total reward data * refactor(wallet): clean code * fix(wallet): typo, better error logging * fix(wallet): some nym decimal values * fix(wallet): deal with unym nym values * fix(wallet): routing score chart * feat(wallet): new node stats design * feat(wallet): new node stats design * fix(wallet): increase component width * fix(wallet): some vertical alignements --- nym-wallet/package.json | 3 + nym-wallet/src-tauri/src/main.rs | 1 + .../src-tauri/src/operations/mixnet/bond.rs | 26 ++++ .../src/components/Bonding/BondedMixnode.tsx | 97 ++++++------ .../Bonding/BondedMixnodeActions.tsx | 14 +- .../src/components/Bonding/NodeStats.tsx | 142 ++++++++++++++++++ .../src/components/Bonding/NodeTable.tsx | 2 +- nym-wallet/src/components/InfoToolTip.tsx | 4 +- nym-wallet/src/context/bonding.tsx | 107 ++++++++++--- nym-wallet/src/context/mocks/bonding.tsx | 6 +- nym-wallet/src/requests/queries.ts | 1 + nym-wallet/src/theme/mui-theme.d.ts | 13 +- nym-wallet/src/theme/theme.tsx | 2 + nym-wallet/src/utils/index.ts | 36 +++++ yarn.lock | 124 ++++++++++++++- 15 files changed, 495 insertions(+), 83 deletions(-) create mode 100644 nym-wallet/src/components/Bonding/NodeStats.tsx diff --git a/nym-wallet/package.json b/nym-wallet/package.json index d1493c43b0..2f8830b620 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -34,6 +34,7 @@ "@storybook/react": "^6.5.8", "@tauri-apps/api": "^1.0.2", "@tauri-apps/tauri-forage": "^1.0.0-beta.2", + "big.js": "^6.2.1", "bs58": "^4.0.1", "clsx": "^1.1.1", "date-fns": "^2.28.0", @@ -46,6 +47,7 @@ "react-error-boundary": "^3.1.3", "react-hook-form": "^7.14.2", "react-router-dom": "6", + "recharts": "^2.1.13", "semver": "^6.3.0", "string-to-color": "^2.2.2", "use-clipboard-copy": "^0.2.0", @@ -65,6 +67,7 @@ "@tauri-apps/cli": "^1.0.5", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", + "@types/big.js": "^6.1.6", "@types/bs58": "^4.0.1", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index d40bedc777..db6795ff49 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -64,6 +64,7 @@ fn main() { mixnet::bond::update_mixnode_config, mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, + mixnet::bond::get_mixnode_avg_uptime, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 08f9dc1daa..2004e1d51e 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -170,6 +170,32 @@ pub async fn update_mixnode_config( )?) } +#[tauri::command] +pub async fn get_mixnode_avg_uptime( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get mixnode bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let res = client.nymd.get_owned_mixnode(client.nymd.address()).await?; + + match res.mixnode_details { + Some(details) => { + let id = details.mix_id(); + log::trace!(" >>> Get average uptime percentage: mix_id = {}", id); + let avg_uptime_percent = client + .validator_api + .get_mixnode_avg_uptime(id) + .await + .ok() + .map(|r| r.avg_uptime); + log::trace!(" <<< {:?}", avg_uptime_percent); + Ok(avg_uptime_percent) + } + None => Ok(None), + } +} + #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 27ecf34df3..9cefa40331 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -1,16 +1,16 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box, Button, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; -import { isMixnode } from 'src/types'; +import { isMixnode, Network } from 'src/types'; import { TBondedMixnode, urls } from 'src/context'; import { NymCard } from 'src/components'; -import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; import { NodeStatus } from 'src/components/NodeStatus'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions'; +import { NodeStats } from './NodeStats'; const headers: Header[] = [ { @@ -74,6 +74,7 @@ export const BondedMixnode = ({ delegators, status, identityKey, + host, } = mixnode; const cells: Cell[] = [ { @@ -93,7 +94,7 @@ export const BondedMixnode = ({ id: 'pm-cell', }, { - cell: operatorCost ? `${operatorCost} USD` : '-', + cell: operatorCost ? `${operatorCost} NYM` : '-', id: 'operator-cost-cell', }, { @@ -109,6 +110,7 @@ export const BondedMixnode = ({ ), id: 'actions-cell', @@ -117,46 +119,51 @@ export const BondedMixnode = ({ ]; return ( - - - - Mix node - - - - {name && ( - - {name} - - )} - - - } - Action={ - isMixnode(mixnode) && ( - - ) - } - > - - {network && ( - - Check more stats of your node on the{' '} - - explorer - - - )} - + + + + + Mix node + + + + {name && ( + + + {name} + + + )} + + + } + Action={ + isMixnode(mixnode) && ( + + ) + } + > + + {network && ( + + Check more stats of your node on the{' '} + + explorer + + + )} + + + ); }; diff --git a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx index 7cbb352526..37b52452c7 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx @@ -8,9 +8,11 @@ export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 're export const BondedMixnodeActions = ({ onActionSelect, disabledRedeemAndCompound, + disabledBondMore, }: { onActionSelect: (action: TBondedMixnodeActions) => void; disabledRedeemAndCompound: boolean; + disabledBondMore?: boolean; }) => { const [isOpen, setIsOpen] = useState(false); @@ -24,11 +26,13 @@ export const BondedMixnodeActions = ({ return ( - } - onClick={() => handleActionClick('bondMore')} - /> + {!disabledBondMore && ( + } + onClick={() => handleActionClick('bondMore')} + /> + )} R} diff --git a/nym-wallet/src/components/Bonding/NodeStats.tsx b/nym-wallet/src/components/Bonding/NodeStats.tsx new file mode 100644 index 0000000000..65e6f7436e --- /dev/null +++ b/nym-wallet/src/components/Bonding/NodeStats.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material'; +import { TBondedMixnode } from 'src/context'; +import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts'; +import { SelectionChance } from '@nymproject/types'; +import { NymCard } from '../NymCard'; +import { InfoTooltip } from '../InfoToolTip'; + +const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { + const { value } = props; + return ( + + + {`${Math.round(value)}%`} + + + + + + ); +}; + +const StatRow = ({ + label, + tooltipText, + value, + textColor, + progressValue, +}: { + label: string; + tooltipText?: string; + value: string | number; + textColor?: string; + progressValue?: number; +}) => ( + + t.palette.nym.text.muted }}> + {tooltipText && } + {label} + + {typeof progressValue === 'number' ? ( + + ) : ( + {value} + )} + +); + +export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { + const { activeSetProbability, routingScore } = mixnode; + const theme = useTheme(); + + // clamp routing score to [0-100] + const score = Math.min(Math.max(routingScore, 0), 100); + + const data = [ + { key: 'routingScore', value: score }, + { key: 'rest', value: 100 - score }, + ]; + const colors = [theme.palette.success.main, theme.palette.nym.nymWallet.chart.grey]; + + const getSetProbabilityLabel = (chance?: SelectionChance): { value: string; color?: string } => { + switch (chance) { + case 'High': + return { value: 'High', color: theme.palette.nym.success }; + case 'Good': + return { value: 'Good' }; + case 'Low': + return { value: 'Low', color: theme.palette.nym.red }; + default: + return { value: 'Unknown' }; + } + }; + + const renderLegend = () => ( + + + {routingScore}% + + + ); + + const activeSetProb = getSetProbabilityLabel(activeSetProbability); + + return ( + + + + Node stats + + } + > + + + + + {data.map(({ key }, index) => ( + + ))} + + + + + Routing score + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/NodeTable.tsx b/nym-wallet/src/components/Bonding/NodeTable.tsx index d1e620a1ce..9fd1692eba 100644 --- a/nym-wallet/src/components/Bonding/NodeTable.tsx +++ b/nym-wallet/src/components/Bonding/NodeTable.tsx @@ -28,7 +28,7 @@ export const NodeTable = ({ headers, cells }: TableProps) => ( {headers.map(({ header, id, tooltipText }) => ( - + {tooltipText && } {header} diff --git a/nym-wallet/src/components/InfoToolTip.tsx b/nym-wallet/src/components/InfoToolTip.tsx index 43bacff3bc..6b88f8fddd 100644 --- a/nym-wallet/src/components/InfoToolTip.tsx +++ b/nym-wallet/src/components/InfoToolTip.tsx @@ -6,12 +6,12 @@ export const InfoTooltip = ({ title, tooltipPlacement = 'bottom', light, - size = 'small', + size = 'inherit', }: { title: string; tooltipPlacement?: TooltipProps['placement']; light?: boolean; - size?: 'small' | 'medium' | 'large'; + size?: 'small' | 'medium' | 'large' | 'inherit'; }) => ( diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 7c2930de92..ed4f753171 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -3,10 +3,11 @@ import { DecCoin, MixnodeStatus, TransactionExecuteResult, - decimalToFloatApproximation, decimalToPercentage, + SelectionChance, } from '@nymproject/types'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import Big from 'big.js'; import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; import { Console } from 'src/utils/console'; import { @@ -24,17 +25,24 @@ import { vestingUnbondMixnode, updateMixnodeCostParams as updateMixnodeCostParamsRequest, vestingUpdateMixnodeCostParams as updateMixnodeVestingCostParamsRequest, - getNodeDescription as getNodeDescriptioRequest, + getNodeDescription as getNodeDescriptionRequest, getMixnodeStatus, getPendingOperatorRewards, getMixnodeStakeSaturation, vestingClaimOperatorReward, + getInclusionProbability, + getMixnodeAvgUptime, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; -import { attachDefaultOperatingCost, toPercentFloatString, toPercentIntegerString } from '../utils'; +import { + attachDefaultOperatingCost, + toDisplay, + toPercentFloatString, + toPercentIntegerString, + unymToNym, +} from '../utils'; -// TODO add relevant data export type TBondedMixnode = { name?: string; identityKey: string; @@ -46,12 +54,16 @@ export type TBondedMixnode = { delegators: number; status: MixnodeStatus; proxy?: string; + operatorCost?: string; host: string; + estimatedRewards?: DecCoin; + activeSetProbability?: SelectionChance; + standbySetProbability?: SelectionChance; + routingScore: number; httpApiPort: number; mixPort: number; verlocPort: number; version: string; - operatorCost?: string; }; export interface TBondedGateway { @@ -84,9 +96,6 @@ export type TBondingContext = { checkOwnership: () => Promise; }; -const calculateStake = (pledge: string, delegations: string): number => - decimalToFloatApproximation(pledge) + decimalToFloatApproximation(delegations); - export const BondingContext = createContext({ isLoading: true, refresh: async () => undefined, @@ -129,7 +138,12 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod }; const getAdditionalMixnodeDetails = async (mixId: number) => { - const additionalDetails: { status: MixnodeStatus; stakeSaturation: string; operatorCost?: string } = { + const additionalDetails: { + status: MixnodeStatus; + stakeSaturation: string; + operatorCost?: string; + estimatedRewards?: DecCoin; + } = { status: 'not_found', stakeSaturation: '0', }; @@ -138,31 +152,69 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod const statusResponse = await getMixnodeStatus(mixId); additionalDetails.status = statusResponse.status; } catch (e) { - Console.log(e); + Console.log('getMixnodeStatus fails', e); } try { const stakeSaturationResponse = await getMixnodeStakeSaturation(mixId); additionalDetails.stakeSaturation = decimalToPercentage(stakeSaturationResponse.saturation); } catch (e) { - Console.log(e); + Console.log('getMixnodeStakeSaturation fails', e); } try { const rewardEstimation = await getMixnodeRewardEstimation(mixId); - additionalDetails.operatorCost = rewardEstimation.estimation.operating_cost; + additionalDetails.operatorCost = unymToNym(rewardEstimation.estimation.operating_cost); + const estimatedRewards = unymToNym(rewardEstimation.estimation.total_node_reward); + if (estimatedRewards) { + additionalDetails.estimatedRewards = { + amount: estimatedRewards, + denom: 'nym', + }; + } } catch (e) { - Console.log(e); + Console.log('getMixnodeRewardEstimation fails', e); } return additionalDetails; }; const getNodeDescription = async (host: string, port: number) => { + let result; try { - return await getNodeDescriptioRequest(host, port); + result = await getNodeDescriptionRequest(host, port); } catch (e) { - Console.log(e); + Console.log('getNodeDescriptionRequest fails', e); } - return undefined; + return result; + }; + + const getSetProbabilities = async (mixId: number) => { + let result; + try { + result = await getInclusionProbability(mixId); + } catch (e: any) { + Console.log('getInclusionProbability fails', e); + } + return result; + }; + + const getAvgUptime = async () => { + let result; + try { + result = await getMixnodeAvgUptime(); + } catch (e: any) { + Console.log('getMixnodeAvgUptime fails', e); + } + return result; + }; + + const calculateStake = (pledge: string, delegations: string) => { + let stake; + try { + stake = unymToNym(Big(pledge).plus(delegations)); + } catch (e: any) { + Console.warn(`not a valid decimal number: ${e}`); + } + return stake; }; const refresh = useCallback(async () => { @@ -174,22 +226,33 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod let operatorRewards; try { operatorRewards = await getPendingOperatorRewards(clientDetails?.client_address); + const opRewards = toDisplay(operatorRewards.amount); + if (opRewards) { + operatorRewards.amount = opRewards; + } } catch (e) { Console.warn(`get_operator_rewards request failed: ${e}`); } if (data) { - const { bond_information, rewarding_details } = data; - const { status, stakeSaturation, operatorCost } = await getAdditionalMixnodeDetails(data.bond_information.id); + const { + bond_information, + rewarding_details, + bond_information: { id }, + } = data; + + const { status, stakeSaturation, operatorCost, estimatedRewards } = await getAdditionalMixnodeDetails(id); + const setProbabilities = await getSetProbabilities(id); const nodeDescription = await getNodeDescription( bond_information.mix_node.host, bond_information.mix_node.http_api_port, ); + const routingScore = await getAvgUptime(); setBondedNode({ name: nodeDescription?.name, identityKey: bond_information.mix_node.identity_key, ip: bond_information.id, stake: { - amount: calculateStake(rewarding_details.operator, data.rewarding_details.delegates).toString(), + amount: calculateStake(rewarding_details.operator, rewarding_details.delegates), denom: bond_information.original_pledge.denom, }, bond: bond_information.original_pledge, @@ -199,12 +262,16 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod operatorRewards, status, stakeSaturation, + operatorCost, host: bond_information.mix_node.host.replace(/\s/g, ''), + routingScore, + activeSetProbability: setProbabilities?.in_active, + standbySetProbability: setProbabilities?.in_reserve, + estimatedRewards, httpApiPort: bond_information.mix_node.http_api_port, mixPort: bond_information.mix_node.mix_port, verlocPort: bond_information.mix_node.verloc_port, version: bond_information.mix_node.version, - operatorCost, } as TBondedMixnode); } } catch (e: any) { diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index ff282f15e5..88f4df6530 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -16,8 +16,12 @@ const bondedMixnodeMock: TBondedMixnode = { operatorRewards: { denom: 'nym', amount: '1234' }, delegators: 5423, status: 'active', - operatorCost: '2', + operatorCost: '0.22', host: '1.2.3.4', + routingScore: 75, + activeSetProbability: 'High', + standbySetProbability: 'Low', + estimatedRewards: { denom: 'nym', amount: '2' }, httpApiPort: 8000, mixPort: 1789, verlocPort: 1790, diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index 9857b088b4..00a28aa03b 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -16,6 +16,7 @@ export const getAllPendingDelegations = async () => export const getMixnodeBondDetails = async () => invokeWrapper('mixnode_bond_details'); export const getGatewayBondDetails = async () => invokeWrapper('gateway_bond_details'); +export const getMixnodeAvgUptime = async () => invokeWrapper('get_mixnode_avg_uptime'); export const getPendingOperatorRewards = async (address: string) => invokeWrapper('get_pending_operator_rewards', { address }); diff --git a/nym-wallet/src/theme/mui-theme.d.ts b/nym-wallet/src/theme/mui-theme.d.ts index 2b703db05a..e1e98ca7c0 100644 --- a/nym-wallet/src/theme/mui-theme.d.ts +++ b/nym-wallet/src/theme/mui-theme.d.ts @@ -73,6 +73,9 @@ declare module '@mui/material/styles' { modal: { border: string; }; + chart: { + grey: string; + }; } /** @@ -91,7 +94,7 @@ declare module '@mui/material/styles' { /** * Add anything not palette related to the theme here */ - interface NymTheme { } + interface NymTheme {} /** * This augments the definitions of the MUI Theme with the Nym theme, as well as @@ -99,8 +102,8 @@ declare module '@mui/material/styles' { * * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below */ - interface Theme extends NymTheme { } - interface ThemeOptions extends Partial { } - interface Palette extends NymPaletteAndNymWalletPalette { } - interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions { } + interface Theme extends NymTheme {} + interface ThemeOptions extends Partial {} + interface Palette extends NymPaletteAndNymWalletPalette {} + interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions {} } diff --git a/nym-wallet/src/theme/theme.tsx b/nym-wallet/src/theme/theme.tsx index 569ad028b2..c473dacaf9 100644 --- a/nym-wallet/src/theme/theme.tsx +++ b/nym-wallet/src/theme/theme.tsx @@ -67,6 +67,7 @@ const darkMode: NymPaletteVariant = { modal: { border: '#484d53', }, + chart: { grey: '#3D4249' }, }; const lightMode: NymPaletteVariant = { @@ -98,6 +99,7 @@ const lightMode: NymPaletteVariant = { modal: { border: 'transparent', }, + chart: { grey: '#E6E6E6' }, }; /** diff --git a/nym-wallet/src/utils/index.ts b/nym-wallet/src/utils/index.ts index c0361c902d..2ed8074216 100644 --- a/nym-wallet/src/utils/index.ts +++ b/nym-wallet/src/utils/index.ts @@ -1,5 +1,6 @@ import { appWindow } from '@tauri-apps/api/window'; import bs58 from 'bs58'; +import Big from 'big.js'; import { valid } from 'semver'; import { isValidRawCoin, DecCoin, MixNodeCostParams } from '@nymproject/types'; import { TPoolOption } from 'src/components'; @@ -142,3 +143,38 @@ export const toPercentFloatString = (value: string) => (Number(value) / 100).toS * @returns A stringified integer */ export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); + +/** + * Converts a decimal number of NYM to a pretty representation + * with fixed decimal places. + * + * @param nym - a decimal number of NYM + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns A prettyfied decimal number + */ +export const toDisplay = (nym: string | number | Big, dp = 4) => { + let displayValue; + try { + displayValue = Big(nym).toFixed(dp); + } catch (e: any) { + Console.warn(`${displayValue} not a valid decimal number: ${e}`); + } + return displayValue; +}; + +/** + * Converts a decimal number of μNYM (micro NYM) to NYM. + * + * @param unym - string representation of a decimal number of μNYM + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns The corresponding decimal number in NYM + */ +export const unymToNym = (unym: string | Big, dp = 4) => { + let nym; + try { + nym = Big(unym).div(1_000_000).toFixed(dp); + } catch (e: any) { + Console.warn(`${unym} not a valid decimal number: ${e}`); + } + return nym; +}; diff --git a/yarn.lock b/yarn.lock index 378fc69ea2..1ec7760a2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5054,6 +5054,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/big.js@^6.1.6": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.6.tgz#3d417e758483d55345a03a087f7e0c87137ca444" + integrity sha512-0r9J+Zz9rYm2hOTwiMAVkm3XFQ4u5uTK37xrQMhc9bysn/sf/okzovWMYYIBMFTn/yrEZ11pusgLEaoarTlQbA== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -6975,6 +6980,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +big.js@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" + integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== + bignumber.js@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8" @@ -7667,6 +7677,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" @@ -8406,6 +8421,11 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" +css-unit-converter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" + integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== + css-vendor@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" @@ -8599,14 +8619,19 @@ d3-geo@^2.0.1: dependencies: d3-color "1 - 2" -"d3-interpolate@1.2.0 - 3": +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" -d3-scale@^4.0.0: +"d3-path@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" + integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== + +d3-scale@^4.0.0, d3-scale@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== @@ -8622,6 +8647,13 @@ d3-selection@2, d3-selection@^2.0.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== +d3-shape@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" + integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ== + dependencies: + d3-path "1 - 3" + "d3-time-format@2 - 4": version "4.1.0" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" @@ -8750,6 +8782,11 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -9082,6 +9119,13 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -9834,7 +9878,7 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -10033,6 +10077,11 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -15521,6 +15570,11 @@ postcss-unique-selectors@^5.1.0: dependencies: postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -16129,7 +16183,7 @@ react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -16139,6 +16193,11 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-load-script@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/react-load-script/-/react-load-script-0.0.6.tgz#db6851236aaa25bb622677a2eb51dad4f8d2c258" @@ -16164,6 +16223,13 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-resize-detector@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c" + integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw== + dependencies: + lodash "^4.17.21" + react-router-dom@6: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" @@ -16233,6 +16299,14 @@ react-simple-maps@^2.3.0: d3-zoom "^2.0.0" topojson-client "^3.1.0" +react-smooth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.1.tgz#74c7309916d6ccca182c4b30c8992f179e6c5a05" + integrity sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA== + dependencies: + fast-equals "^2.0.0" + react-transition-group "2.9.0" + react-syntax-highlighter@^15.4.5: version "15.5.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" @@ -16252,6 +16326,16 @@ react-tooltip@*, react-tooltip@^4.2.21: prop-types "^15.7.2" uuid "^7.0.3" +react-transition-group@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react-transition-group@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" @@ -16440,6 +16524,30 @@ readonly-date@^1.0.0: resolved "https://registry.yarnpkg.com/readonly-date/-/readonly-date-1.0.0.tgz#5af785464d8c7d7c40b9d738cbde8c646f97dcd9" integrity sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ== +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.1.13: + version "2.1.13" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.1.13.tgz#61801acf3e13896b07dc6a8b38cbdd648480d0b7" + integrity sha512-9VWu2nzExmfiMFDHKqRFhYlJVmjzQGVKH5rBetXR4EuyEXuu3Y6cVxQuNEdusHhbm4SoPPrVDCwlBdREL3sQPA== + dependencies: + classnames "^2.2.5" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^7.1.2" + react-smooth "^2.0.1" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -16463,6 +16571,14 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + refractor@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"