Compare commits

...

19 Commits

Author SHA1 Message Date
fmtabbara fda9bc65fc [ci skip] Generate TS types 2022-03-10 19:15:26 +00:00
fmtabbara 3c476560d2 fix ts and es errors 2022-03-10 19:14:00 +00:00
fmtabbara bcd5531f40 [ci skip] Generate TS types 2022-03-10 15:36:02 +00:00
fmtabbara fc747503cf Merge branch 'feature/update-wallet-dependencies' of https://github.com/nymtech/nym into feature/update-wallet-dependencies 2022-03-10 15:34:36 +00:00
fmtabbara f3b7f7874e ts and es fixes 2022-03-10 15:34:28 +00:00
fmtabbara 36ec0e180e [ci skip] Generate TS types 2022-03-10 15:26:04 +00:00
fmtabbara b2ce0a8300 fix ts errors 2022-03-10 15:24:43 +00:00
Mark Sinclair 727a8305a3 Fix up typings for image and json modules
Add tsconfig for eslint to process webpack config
2022-03-10 14:30:54 +00:00
mmsinclair 2978d4ea6b [ci skip] Generate TS types 2022-03-10 13:28:46 +00:00
fmtabbara c31235b3f9 fix breaking change 2022-03-10 13:24:00 +00:00
fmtabbara 5c264d79bf fix sign in pages 2022-03-10 12:35:16 +00:00
Mark Sinclair 6bda9ae5f7 lint fixes 2022-03-10 12:20:26 +00:00
fmtabbara 9e932defb4 lint fixes 2022-03-10 11:59:07 +00:00
Mark Sinclair f249e7b497 linting fixes 2022-03-10 11:48:57 +00:00
fmtabbara 05a9c24437 linting fixes 2022-03-10 11:48:26 +00:00
Mark Sinclair eb6ecc7241 Formatting: fix linting errors for dependencies 2022-03-10 11:08:57 +00:00
Mark Sinclair f841242ce2 Formatting: run eslint --fix on all files 2022-03-10 11:05:52 +00:00
Mark Sinclair c3ba7e5ff2 Add eslint rules 2022-03-10 11:04:10 +00:00
Mark Sinclair 15ab890932 Use shared ts-packages in wallet 2022-03-10 10:55:21 +00:00
123 changed files with 2431 additions and 9216 deletions
-109
View File
@@ -1,109 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2019,
"sourceType": "module"
},
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
"extends": [
"plugin:react/recommended",
"airbnb",
"prettier",
"plugin:jest/recommended",
"plugin:jest/style"
],
"rules": {
"jest/prefer-strict-equal": "error",
"jest/prefer-to-have-length": "warn",
"prettier/prettier": "error",
"import/prefer-default-export": "off",
"react/prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-props-no-spreading": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.[jt]s",
"**/*.spec.[jt]s",
"**/*.test.[jt]sx",
"**/*.spec.[jt]sx"
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
]
},
"overrides": [
{
"files": "**/*.+(ts|tsx)",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint/eslint-plugin"],
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-use-before-define": [0],
"@typescript-eslint/no-use-before-define": [1],
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx"
]
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
}
}
],
"settings": {
"import/resolver": {
"root-import": {
"rootPathPrefix": "@",
"rootPathSuffix": "src",
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
+1 -2
View File
@@ -2,6 +2,5 @@
"trailingComma": "all", "trailingComma": "all",
"singleQuote": true, "singleQuote": true,
"printWidth": 120, "printWidth": 120,
"tabWidth": 2, "tabWidth": 2
"semi": false
} }
+45 -4
View File
@@ -1,5 +1,5 @@
{ {
"name": "tauri-app", "name": "@nymproject/nym-wallet-app",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
@@ -8,8 +8,11 @@
"webpack:prod": "yarn webpack --progress --config webpack.prod.js", "webpack:prod": "yarn webpack --progress --config webpack.prod.js",
"tauri:dev": "yarn tauri dev", "tauri:dev": "yarn tauri dev",
"tauri:build": "yarn tauri build", "tauri:build": "yarn tauri build",
"tsc": "tsc --noEmit true",
"dev": "yarn run webpack:dev & yarn run tauri:dev", "dev": "yarn run webpack:dev & yarn run tauri:dev",
"build": "run-s webpack:prod tauri:build" "build": "run-s webpack:prod tauri:build",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}, },
"dependencies": { "dependencies": {
"@babel/preset-typescript": "^7.15.0", "@babel/preset-typescript": "^7.15.0",
@@ -19,7 +22,9 @@
"@mui/icons-material": "^5.2.0", "@mui/icons-material": "^5.2.0",
"@mui/material": "^5.2.2", "@mui/material": "^5.2.2",
"@mui/styles": "^5.2.2", "@mui/styles": "^5.2.2",
"@types/react-dom": "^17.0.9", "@nymproject/mui-theme": "^1.0.0",
"@nymproject/react": "^1.0.0",
"@tauri-apps/api": "^1.0.0-rc.1",
"bs58": "^4.0.1", "bs58": "^4.0.1",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
@@ -38,26 +43,62 @@
"@babel/plugin-transform-async-to-generator": "^7.14.5", "@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0", "@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1", "@svgr/webpack": "^6.1.1",
"@tauri-apps/api": "^1.0.0-rc.1",
"@tauri-apps/cli": "^1.0.0-rc.5", "@tauri-apps/cli": "^1.0.0-rc.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/bs58": "^4.0.1", "@types/bs58": "^4.0.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.9",
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",
"@types/semver": "^7.3.8", "@types/semver": "^7.3.8",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-root-import": "^6.6.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0", "css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3", "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",
"eslint-plugin-storybook": "^0.5.7",
"favicons": "^6.2.2", "favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2", "favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "2.3.2",
"react-refresh": "^0.10.0",
"react-refresh-typescript": "^2.0.2",
"style-loader": "^3.2.1", "style-loader": "^3.2.1",
"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", "url-loader": "^4.1.1",
"webpack": "^5.64.3", "webpack": "^5.64.3",
"webpack-cli": "^4.8.0", "webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0", "webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0" "webpack-merge": "^5.8.0"
} }
} }
+9 -9
View File
@@ -1,12 +1,12 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar, Typography } from '@mui/material' import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { Logout } from '@mui/icons-material' import { Logout } from '@mui/icons-material';
import { ClientContext } from '../context/main' import { ClientContext } from '../context/main';
import { NetworkSelector } from '.' import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node' import { Node as NodeIcon } from '../svg-icons/node';
export const AppBar = () => { export const AppBar = () => {
const { showSettings, logOut, handleShowSettings } = useContext(ClientContext) const { showSettings, logOut, handleShowSettings } = useContext(ClientContext);
return ( return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}> <MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
@@ -34,5 +34,5 @@ export const AppBar = () => {
</Grid> </Grid>
</Toolbar> </Toolbar>
</MuiAppBar> </MuiAppBar>
) );
} };
+8 -9
View File
@@ -1,12 +1,11 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Typography } from '@mui/material' import { Box, Typography } from '@mui/material';
import { Box } from '@mui/system' import { ClientContext } from '../context/main';
import { ClientContext } from '../context/main' import { CopyToClipboard } from './CopyToClipboard';
import { CopyToClipboard } from '../components' import { splice } from '../utils';
import { splice } from '../utils'
export const ClientAddress = ({ withCopy }: { withCopy?: boolean }) => { export const ClientAddress = ({ withCopy }: { withCopy?: boolean }) => {
const { clientDetails } = useContext(ClientContext) const { clientDetails } = useContext(ClientContext);
return ( return (
<Box> <Box>
<Typography variant="body2" component="span" sx={{ color: 'grey.600' }}> <Typography variant="body2" component="span" sx={{ color: 'grey.600' }}>
@@ -17,5 +16,5 @@ export const ClientAddress = ({ withCopy }: { withCopy?: boolean }) => {
</Typography> </Typography>
{withCopy && <CopyToClipboard text={clientDetails?.client_address} iconButton />} {withCopy && <CopyToClipboard text={clientDetails?.client_address} iconButton />}
</Box> </Box>
) );
} };
+26 -43
View File
@@ -1,37 +1,37 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react';
import { Button, IconButton, Tooltip } from '@mui/material' import { Button, IconButton, Tooltip } from '@mui/material';
import { Check, ContentCopy } from '@mui/icons-material' import { Check, ContentCopy } from '@mui/icons-material';
import { clipboard } from '@tauri-apps/api' import { clipboard } from '@tauri-apps/api';
export const CopyToClipboard = ({ export const CopyToClipboard = ({
text = '', text = '',
light, light,
iconButton, iconButton,
}: { }: {
text?: string text?: string;
light?: boolean light?: boolean;
iconButton?: boolean iconButton?: boolean;
}) => { }) => {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false);
const handleCopy = async (text: string) => { const handleCopy = async (_text: string) => {
try { try {
await clipboard.writeText(text) await clipboard.writeText(_text);
setCopied(true) setCopied(true);
} catch (e) { } catch (e) {
console.log('failed to copy: ' + e) console.log(`failed to copy: ${e}`);
}
} }
};
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout let timer: NodeJS.Timeout;
if (copied) { if (copied) {
timer = setTimeout(() => { timer = setTimeout(() => {
setCopied(false) setCopied(false);
}, 2000) }, 2000);
} }
return () => clearTimeout(timer) return () => clearTimeout(timer);
}, [copied]) }, [copied]);
if (iconButton) if (iconButton)
return ( return (
@@ -40,43 +40,26 @@ export const CopyToClipboard = ({
onClick={() => handleCopy(text)} onClick={() => handleCopy(text)}
size="small" size="small"
sx={{ sx={{
color: (theme) => color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
light
? theme.palette.common.white
: theme.palette.nym.background.dark,
}} }}
> >
{!copied ? ( {!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
<ContentCopy fontSize="small" />
) : (
<Check color="success" />
)}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
) );
return ( return (
<Button <Button
variant="outlined" variant="outlined"
color="inherit" color="inherit"
sx={{ sx={{
color: (theme) => color: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
light borderColor: (theme) => (light ? theme.palette.common.white : theme.palette.nym.background.dark),
? theme.palette.common.white
: theme.palette.nym.background.dark,
borderColor: (theme) =>
light
? theme.palette.common.white
: theme.palette.nym.background.dark,
}} }}
onClick={() => handleCopy(text)} onClick={() => handleCopy(text)}
endIcon={ endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
copied && (
<Check sx={{ color: (theme) => theme.palette.success.light }} />
)
}
> >
{!copied ? 'Copy' : 'Copied'} {!copied ? 'Copy' : 'Copied'}
</Button> </Button>
) );
} };
+5 -7
View File
@@ -1,9 +1,8 @@
import React from 'react' import React from 'react';
import { FallbackProps } from 'react-error-boundary' import { FallbackProps } from 'react-error-boundary';
import { Alert, AlertTitle, Button } from '@mui/material' import { Alert, AlertTitle, Button } from '@mui/material';
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
return (
<div> <div>
<Alert severity="error" data-testid="error-message"> <Alert severity="error" data-testid="error-message">
<AlertTitle>{error.name}</AlertTitle> <AlertTitle>{error.name}</AlertTitle>
@@ -15,5 +14,4 @@ export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
</Alert> </Alert>
<Button onClick={resetErrorBoundary}>Back to safety</Button> <Button onClick={resetErrorBoundary}>Back to safety</Button>
</div> </div>
) );
}
+15 -15
View File
@@ -1,29 +1,29 @@
import React, { useState, useEffect, useContext } from 'react' import React, { useState, useEffect, useContext } from 'react';
import { Typography } from '@mui/material' import { Typography } from '@mui/material';
import { Operation } from '../types' import { Operation } from '../types';
import { getGasFee } from '../requests' import { getGasFee } from '../requests';
import { ClientContext } from '../context/main' import { ClientContext } from '../context/main';
export const Fee = ({ feeType }: { feeType: Operation }) => { export const Fee = ({ feeType }: { feeType: Operation }) => {
const [fee, setFee] = useState<string>() const [fee, setFee] = useState<string>();
const {currency} = useContext(ClientContext) const { currency } = useContext(ClientContext);
const getFee = async () => { const getFee = async () => {
const fee = await getGasFee(feeType) const res = await getGasFee(feeType);
setFee(fee.amount) setFee(res.amount);
} };
useEffect(() => { useEffect(() => {
getFee() getFee();
}, []) }, []);
if (fee) { if (fee) {
return ( return (
<Typography sx={{ color: 'nym.fee', fontWeight: 600 }}> <Typography sx={{ color: 'nym.fee', fontWeight: 600 }}>
Fee for this transaction: {`${fee} ${currency?.major}`}{' '} Fee for this transaction: {`${fee} ${currency?.major}`}{' '}
</Typography> </Typography>
) );
} }
return null return null;
} };
+8 -8
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react';
import { InfoOutlined } from '@mui/icons-material' import { InfoOutlined } from '@mui/icons-material';
import { Tooltip, TooltipProps } from '@mui/material' import { Tooltip, TooltipProps } from '@mui/material';
export const InfoTooltip = ({ export const InfoTooltip = ({
title, title,
@@ -8,12 +8,12 @@ export const InfoTooltip = ({
light, light,
size = 'small', size = 'small',
}: { }: {
title: string title: string;
tooltipPlacement?: TooltipProps['placement'] tooltipPlacement?: TooltipProps['placement'];
light?: boolean light?: boolean;
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large';
}) => ( }) => (
<Tooltip title={title} arrow placement={tooltipPlacement}> <Tooltip title={title} arrow placement={tooltipPlacement}>
<InfoOutlined fontSize={size} sx={{ color: light ? 'grey.500' : undefined }} /> <InfoOutlined fontSize={size} sx={{ color: light ? 'grey.500' : undefined }} />
</Tooltip> </Tooltip>
) );
+16 -16
View File
@@ -1,11 +1,11 @@
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material' import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material' import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { ADMIN_ADDRESS, ClientContext } from '../context/main' import { ADMIN_ADDRESS, ClientContext } from '../context/main';
import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons' import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons';
let routesSchema = [ const routesSchema = [
{ {
label: 'Balance', label: 'Balance',
route: '/balance', route: '/balance',
@@ -41,11 +41,11 @@ let routesSchema = [
route: '/undelegate', route: '/undelegate',
Icon: Undelegate, Icon: Undelegate,
}, },
] ];
export const Nav = () => { export const Nav = () => {
const { clientDetails, handleShowAdmin } = useContext(ClientContext) const { clientDetails, handleShowAdmin } = useContext(ClientContext);
const location = useLocation() const location = useLocation();
useEffect(() => { useEffect(() => {
if (clientDetails?.client_address === ADMIN_ADDRESS) { if (clientDetails?.client_address === ADMIN_ADDRESS) {
@@ -53,9 +53,9 @@ export const Nav = () => {
label: 'Docs', label: 'Docs',
route: '/docs', route: '/docs',
Icon: Description, Icon: Description,
}) });
} }
}, []) }, []);
return ( return (
<div <div
@@ -66,8 +66,8 @@ export const Nav = () => {
}} }}
> >
<List disablePadding> <List disablePadding>
{routesSchema.map(({ Icon, route, label }, i) => ( {routesSchema.map(({ Icon, route, label }) => (
<ListItem disableGutters component={Link} to={route} key={i}> <ListItem disableGutters component={Link} to={route} key={label}>
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: 30, minWidth: 30,
@@ -98,5 +98,5 @@ export const Nav = () => {
)} )}
</List> </List>
</div> </div>
) );
} };
+29 -29
View File
@@ -1,27 +1,38 @@
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react';
import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material' import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material';
import { ArrowDropDown, CheckSharp } from '@mui/icons-material' import { ArrowDropDown, CheckSharp } from '@mui/icons-material';
import { ClientContext, IS_DEV_MODE } from '../context/main' import { ClientContext, IS_DEV_MODE } from '../context/main';
import { Network } from '../types' import { Network } from '../types';
const networks: { networkName: Network; name: string }[] = [ const networks: { networkName: Network; name: string }[] = [
{ networkName: 'MAINNET', name: 'Nym Mainnet' }, { networkName: 'MAINNET', name: 'Nym Mainnet' },
{ networkName: 'SANDBOX', name: 'Testnet Sandbox' }, { networkName: 'SANDBOX', name: 'Testnet Sandbox' },
{ networkName: 'QA', name: 'QA' }, { networkName: 'QA', name: 'QA' },
] ];
export function NetworkSelector() { const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () => void }> = ({
const { network, switchNetwork } = useContext(ClientContext) title,
isSelected,
onSelect,
}) => (
<ListItem button onClick={onSelect}>
<ListItemIcon>{isSelected && <CheckSharp color="success" />}</ListItemIcon>
<ListItemText>{title}</ListItemText>
</ListItem>
);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null) export const NetworkSelector = () => {
const { network, switchNetwork } = useContext(ClientContext);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget);
} };
const handleClose = () => { const handleClose = () => {
setAnchorEl(null) setAnchorEl(null);
} };
return ( return (
<> <>
@@ -47,31 +58,20 @@ export function NetworkSelector() {
<List> <List>
<ListSubheader>Network selection</ListSubheader> <ListSubheader>Network selection</ListSubheader>
{networks {networks
.filter((network) => !(!IS_DEV_MODE && network.networkName === 'QA')) .filter(({ networkName }) => !(!IS_DEV_MODE && networkName === 'QA'))
.map(({ name, networkName }) => ( .map(({ name, networkName }) => (
<NetworkItem <NetworkItem
key={networkName} key={networkName}
title={name} title={name}
isSelected={networkName === network} isSelected={networkName === network}
onSelect={() => { onSelect={() => {
handleClose() handleClose();
switchNetwork(networkName) switchNetwork(networkName);
}} }}
/> />
))} ))}
</List> </List>
</Popover> </Popover>
</> </>
) );
} };
const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () => void }> = ({
title,
isSelected,
onSelect,
}) => (
<ListItem button onClick={onSelect}>
<ListItemIcon>{isSelected && <CheckSharp color="success" />}</ListItemIcon>
<ListItemText>{title}</ListItemText>
</ListItem>
)
+6 -9
View File
@@ -1,13 +1,10 @@
import React from 'react' import React from 'react';
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom';
import { Alert, AlertTitle } from '@mui/material' import { Alert, AlertTitle } from '@mui/material';
export const NoClientError = () => { export const NoClientError = () => (
return (
<Alert severity="error"> <Alert severity="error">
<AlertTitle data-testid="client-error">No client detected</AlertTitle> <AlertTitle data-testid="client-error">No client detected</AlertTitle>
Have you signed in? Try to go back to{' '} Have you signed in? Try to go back to <Link to="/signin">the main page</Link> and try again
<Link to="/signin">the main page</Link> and try again
</Alert> </Alert>
) );
}
+21 -21
View File
@@ -1,36 +1,36 @@
import React from 'react' import React from 'react';
import { Typography } from '@mui/material' import { Typography } from '@mui/material';
import { CircleOutlined, PauseCircleOutlined, CheckCircleOutline } from '@mui/icons-material' import { CircleOutlined, PauseCircleOutlined, CheckCircleOutline } from '@mui/icons-material';
import { MixnodeStatus } from '../types' import { MixnodeStatus } from '../types';
export const NodeStatus = ({ status }: { status: MixnodeStatus }) => {
switch (status) {
case 'active':
return <Active />
case 'inactive':
return <Inactive />
case 'standby':
return <Standby />
default:
null
}
return null
}
const Active = () => ( const Active = () => (
<Typography sx={{ color: 'success.main', display: 'flex', alignItems: 'center' }}> <Typography sx={{ color: 'success.main', display: 'flex', alignItems: 'center' }}>
<CheckCircleOutline fontSize="small" color="success" sx={{ mr: 1 }} /> Active <CheckCircleOutline fontSize="small" color="success" sx={{ mr: 1 }} /> Active
</Typography> </Typography>
) );
const Inactive = () => ( const Inactive = () => (
<Typography sx={{ color: 'nym.text.dark', display: 'flex', alignItems: 'center' }}> <Typography sx={{ color: 'nym.text.dark', display: 'flex', alignItems: 'center' }}>
<CircleOutlined fontSize="small" sx={{ color: 'nym.text.dark', mr: 1 }} /> Inactive <CircleOutlined fontSize="small" sx={{ color: 'nym.text.dark', mr: 1 }} /> Inactive
</Typography> </Typography>
) );
const Standby = () => ( const Standby = () => (
<Typography sx={{ color: 'info.main', display: 'flex', alignItems: 'center' }}> <Typography sx={{ color: 'info.main', display: 'flex', alignItems: 'center' }}>
<PauseCircleOutlined fontSize="small" color="info" sx={{ mr: 1 }} /> Standby <PauseCircleOutlined fontSize="small" color="info" sx={{ mr: 1 }} /> Standby
</Typography> </Typography>
) );
export const NodeStatus = ({ status }: { status: MixnodeStatus }) => {
switch (status) {
case 'active':
return <Active />;
case 'inactive':
return <Inactive />;
case 'standby':
return <Standby />;
default:
return null;
}
return null;
};
@@ -1,17 +1,17 @@
import React from 'react' import React from 'react';
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material' import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
import { EnumNodeType } from '../types/global' import { EnumNodeType } from '../types/global';
export const NodeTypeSelector = ({ export const NodeTypeSelector = ({
disabled, disabled,
nodeType, nodeType,
setNodeType, setNodeType,
}: { }: {
disabled: boolean disabled: boolean;
nodeType: EnumNodeType nodeType: EnumNodeType;
setNodeType: (nodeType: EnumNodeType) => void setNodeType: (nodeType: EnumNodeType) => void;
}) => { }) => {
const handleNodeTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => setNodeType(e.target.value as EnumNodeType) const handleNodeTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => setNodeType(e.target.value as EnumNodeType);
return ( return (
<FormControl component="fieldset"> <FormControl component="fieldset">
@@ -39,5 +39,5 @@ export const NodeTypeSelector = ({
/> />
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
) );
} };
+18 -20
View File
@@ -1,16 +1,22 @@
import React from 'react' import React from 'react';
import { Box, Card, CardContent, CardHeader } from '@mui/material' import { Box, Card, CardContent, CardHeader } from '@mui/material';
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles';
import { Title } from './Title' import { Title } from './Title';
const CardContentNoPadding = styled(CardContent)(() => ({
padding: 0,
'&:last-child': {
paddingBottom: 0,
},
}));
export const NymCard: React.FC<{ export const NymCard: React.FC<{
title: string | React.ReactElement title: string | React.ReactElement;
subheader?: string subheader?: string;
Action?: React.ReactNode Action?: React.ReactNode;
Icon?: any Icon?: any;
noPadding?: boolean noPadding?: boolean;
}> = ({ title, subheader, Action, Icon, noPadding, children }) => { }> = ({ title, subheader, Action, Icon, noPadding, children }) => (
return (
<Card variant="outlined" sx={{ overflow: 'auto' }}> <Card variant="outlined" sx={{ overflow: 'auto' }}>
<CardHeader <CardHeader
sx={{ p: 3, color: 'nym.background.dark' }} sx={{ p: 3, color: 'nym.background.dark' }}
@@ -26,12 +32,4 @@ export const NymCard: React.FC<{
<CardContent sx={{ p: 3 }}>{children}</CardContent> <CardContent sx={{ p: 3 }}>{children}</CardContent>
)} )}
</Card> </Card>
) );
}
const CardContentNoPadding = styled(CardContent)(({ theme }) => ({
padding: 0,
'&:last-child': {
paddingBottom: 0,
},
}))
+7 -7
View File
@@ -1,10 +1,10 @@
import React from 'react' import React from 'react';
import Logo from '../images/logo-background.svg' import Logo from '../images/logo-background.svg';
const imgSize = { const imgSize = {
['small']: 40, small: 40,
['medium']: 80, medium: 80,
['large']: 120, large: 120,
} };
export const NymLogo = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => <Logo width={imgSize[size]} /> export const NymLogo = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => <Logo width={imgSize[size]} />;
+7 -9
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react';
import { CircularProgress, Box } from '@mui/material' import { CircularProgress, Box } from '@mui/material';
export enum EnumRequestStatus { export enum EnumRequestStatus {
initial = 'initial', initial = 'initial',
@@ -13,11 +13,10 @@ export const RequestStatus = ({
Success, Success,
Error, Error,
}: { }: {
status: EnumRequestStatus status: EnumRequestStatus;
Success: React.ReactNode Success: React.ReactNode;
Error: React.ReactNode Error: React.ReactNode;
}) => { }) => (
return (
<Box sx={{ padding: [3, 5] }}> <Box sx={{ padding: [3, 5] }}>
{status === EnumRequestStatus.loading && ( {status === EnumRequestStatus.loading && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
@@ -27,5 +26,4 @@ export const RequestStatus = ({
{status === EnumRequestStatus.success && Success} {status === EnumRequestStatus.success && Success}
{status === EnumRequestStatus.error && Error} {status === EnumRequestStatus.error && Error}
</Box> </Box>
) );
}
@@ -1,12 +1,11 @@
import React from 'react' import React from 'react';
import { Stack, Typography } from '@mui/material' import { Stack, Typography } from '@mui/material';
export const SuccessReponse: React.FC<{ export const SuccessReponse: React.FC<{
title: string title: string;
subtitle: string | React.ReactNode subtitle: string | React.ReactNode;
caption: string | React.ReactNode caption: string | React.ReactNode;
}> = ({ title, subtitle, caption }) => { }> = ({ title, subtitle, caption }) => (
return (
<Stack spacing={3} alignItems="center" sx={{ mb: 5 }}> <Stack spacing={3} alignItems="center" sx={{ mb: 5 }}>
<Typography variant="h5" fontWeight="600" data-testid="transaction-complete" color="success.main"> <Typography variant="h5" fontWeight="600" data-testid="transaction-complete" color="success.main">
{title} {title}
@@ -14,5 +13,4 @@ export const SuccessReponse: React.FC<{
<Typography fontWeight="600">{subtitle}</Typography> <Typography fontWeight="600">{subtitle}</Typography>
<Typography>{caption}</Typography> <Typography>{caption}</Typography>
</Stack> </Stack>
) );
}
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react';
import { Box, Typography } from '@mui/material' import { Box, Typography } from '@mui/material';
export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => ( export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => (
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
@@ -8,4 +8,4 @@ export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = (
{title} {title}
</Typography> </Typography>
</Box> </Box>
) );
@@ -1,18 +1,16 @@
import { ListItem, ListItemText, Select } from '@mui/material' import { ListItem, ListItemText, Select } from '@mui/material';
import React, { useState } from 'react' import React, { useState } from 'react';
type TPool = 'balance' | 'locked' type TPool = 'balance' | 'locked';
export const TokenPoolSelector: React.FC<{ onSelect: (pool: TPool) => void }> = ({ onSelect }) => { export const TokenPoolSelector: React.FC = () => {
const [value, setValue] = useState<TPool>() const [value] = useState<TPool>();
return ( return (
<>
<Select label="Token Pool" value={value}> <Select label="Token Pool" value={value}>
<ListItem> <ListItem>
<ListItemText primary="Balance" secondary="123 nymt" /> <ListItemText primary="Balance" secondary="123 nymt" />
</ListItem> </ListItem>
</Select> </Select>
</> );
) };
}
@@ -1,14 +1,12 @@
import React from 'react' import React from 'react';
import { Card, Grid, Typography } from '@mui/material' import { Card, Grid, Typography } from '@mui/material';
export type TTransactionDetails = Array<{ primary: string; secondary: string }> export type TTransactionDetails = Array<{ primary: string; secondary: string }>;
export const TransactionDetails: React.FC<{ details: TTransactionDetails }> = ({ details }) => { export const TransactionDetails: React.FC<{ details: TTransactionDetails }> = ({ details }) => (
return (
<Card variant="outlined" sx={{ width: '100%', p: 2 }}> <Card variant="outlined" sx={{ width: '100%', p: 2 }}>
{details.map(({ primary, secondary }, i) => { {details.map(({ primary, secondary }, i) => (
return ( <Grid container sx={{ mt: i !== 0 ? 1 : 0 }} key={primary}>
<Grid container sx={{ mt: i !== 0 ? 1 : 0 }} key={i}>
<Grid item sm={4} md={3} lg={2}> <Grid item sm={4} md={3} lg={2}>
<Typography sx={{ color: (theme) => theme.palette.grey[600] }}>{primary}</Typography> <Typography sx={{ color: (theme) => theme.palette.grey[600] }}>{primary}</Typography>
</Grid> </Grid>
@@ -16,8 +14,6 @@ export const TransactionDetails: React.FC<{ details: TTransactionDetails }> = ({
<Typography data-testid="to-address">{secondary}</Typography> <Typography data-testid="to-address">{secondary}</Typography>
</Grid> </Grid>
</Grid> </Grid>
) ))}
})}
</Card> </Card>
) );
}
+16 -17
View File
@@ -1,17 +1,16 @@
export * from './Error' export * from './Error';
export * from './CopyToClipboard' export * from './CopyToClipboard';
export * from './NymCard' export * from './NymCard';
export * from './Nav' export * from './Nav';
export * from './NodeTypeSelector' export * from './NodeTypeSelector';
export * from './RequestStatus' export * from './RequestStatus';
export * from './NoClientError' export * from './NoClientError';
export * from './SuccessResponse' export * from './SuccessResponse';
export * from './TransactionDetails' export * from './TransactionDetails';
export * from './NymLogo' export * from './NymLogo';
export * from './Fee' export * from './Fee';
export * from './AppBar' export * from './AppBar';
export * from './NetworkSelector' export * from './NetworkSelector';
export * from './ClientAddress' export * from './ClientAddress';
export * from './InfoToolTip' export * from './InfoToolTip';
export * from './Nav' export * from './Title';
export * from './Title'
+95 -96
View File
@@ -1,120 +1,119 @@
import React, { createContext, useEffect, useState } from 'react' import React, { createContext, useEffect, useMemo, useState } from 'react';
import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types' import { useHistory } from 'react-router-dom';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance' import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types';
import { config } from '../../config' import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signOut } from '../requests' import { config } from '../../config';
import { currencyMap } from '../utils' import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signOut } from '../requests';
import { useHistory } from 'react-router-dom' import { currencyMap } from '../utils';
export const { ADMIN_ADDRESS, IS_DEV_MODE } = config export const { ADMIN_ADDRESS, IS_DEV_MODE } = config;
export const urls = (network?: Network) => export const urls = (networkName?: Network) =>
network === 'MAINNET' networkName === 'MAINNET'
? { ? {
blockExplorer: 'https://blocks.nymtech.net', blockExplorer: 'https://blocks.nymtech.net',
networkExplorer: 'https://explorer.nymtech.net', networkExplorer: 'https://explorer.nymtech.net',
} }
: { : {
blockExplorer: `https://${network}-blocks.nymtech.net`, blockExplorer: `https://${networkName}-blocks.nymtech.net`,
networkExplorer: `https://${network}-explorer.nymtech.net`, networkExplorer: `https://${networkName}-explorer.nymtech.net`,
} };
type TClientContext = { type TClientContext = {
mode: 'light' | 'dark' mode: 'light' | 'dark';
clientDetails?: Account clientDetails?: Account;
mixnodeDetails?: TMixnodeBondDetails | null mixnodeDetails?: TMixnodeBondDetails | null;
userBalance: TUseuserBalance userBalance: TUseuserBalance;
showAdmin: boolean showAdmin: boolean;
showSettings: boolean showSettings: boolean;
network?: Network network?: Network;
currency?: TCurrency currency?: TCurrency;
isLoading: boolean isLoading: boolean;
error?: string error?: string;
switchNetwork: (network: Network) => void switchNetwork: (network: Network) => void;
getBondDetails: () => Promise<void> getBondDetails: () => Promise<void>;
handleShowSettings: () => void handleShowSettings: () => void;
handleShowAdmin: () => void handleShowAdmin: () => void;
logIn: (mnemonic: string) => void logIn: (mnemonic: string) => void;
logOut: () => void logOut: () => void;
} };
export const ClientContext = createContext({} as TClientContext) export const ClientContext = createContext({} as TClientContext);
export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => { export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => {
const [clientDetails, setClientDetails] = useState<Account>() const [clientDetails, setClientDetails] = useState<Account>();
const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>() const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>();
const [network, setNetwork] = useState<Network | undefined>() const [network, setNetwork] = useState<Network | undefined>();
const [currency, setCurrency] = useState<TCurrency>() const [currency, setCurrency] = useState<TCurrency>();
const [showAdmin, setShowAdmin] = useState(false) const [showAdmin, setShowAdmin] = useState(false);
const [showSettings, setShowSettings] = useState(false) const [showSettings, setShowSettings] = useState(false);
const [mode, setMode] = useState<'light' | 'dark'>('light') const [mode] = useState<'light' | 'dark'>('light');
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const userBalance = useGetBalance(clientDetails?.client_address) const userBalance = useGetBalance(clientDetails?.client_address);
const history = useHistory() const history = useHistory();
const loadAccount = async (n: Network) => {
try {
const client = await selectNetwork(n);
setClientDetails(client);
} catch (e) {
console.error(e);
} finally {
setCurrency(currencyMap(n));
}
};
const getBondDetails = async () => {
setMixnodeDetails(undefined);
try {
const mixnode = await getMixnodeBondDetails();
setMixnodeDetails(mixnode);
} catch (e) {
console.log(e);
}
};
useEffect(() => { useEffect(() => {
const refreshAccount = async () => { const refreshAccount = async () => {
if (network) { if (network) {
await loadAccount(network) await loadAccount(network);
await getBondDetails() await getBondDetails();
userBalance.fetchBalance() userBalance.fetchBalance();
} }
} };
refreshAccount() refreshAccount();
}, [network]) }, [network]);
const logIn = async (mnemonic: string) => { const logIn = async (mnemonic: string) => {
try { try {
setIsLoading(true) setIsLoading(true);
await signInWithMnemonic(mnemonic || '') await signInWithMnemonic(mnemonic || '');
await getBondDetails() await getBondDetails();
setNetwork('MAINNET') setNetwork('MAINNET');
history.push('/balance') history.push('/balance');
} catch (e) { } catch (e) {
setIsLoading(false) setIsLoading(false);
setError(e as string) setError(e as string);
}
}
const loadAccount = async (network: Network) => {
try {
const clientDetails = await selectNetwork(network)
setClientDetails(clientDetails)
} catch (e) {
} finally {
setCurrency(currencyMap(network))
}
} }
};
const logOut = async () => { const logOut = async () => {
setClientDetails(undefined) setClientDetails(undefined);
setNetwork(undefined) setNetwork(undefined);
setError(undefined) setError(undefined);
setIsLoading(false) setIsLoading(false);
userBalance.clearAll() userBalance.clearAll();
await signOut() await signOut();
} };
const handleShowAdmin = () => setShowAdmin((show) => !show) const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowSettings = () => setShowSettings((show) => !show) const handleShowSettings = () => setShowSettings((show) => !show);
const switchNetwork = (_network: Network) => setNetwork(_network);
const getBondDetails = async () => { const memoizedValue = useMemo(
setMixnodeDetails(undefined) () => ({
try {
const mixnodeDetails = await getMixnodeBondDetails()
setMixnodeDetails(mixnodeDetails)
} catch (e) {
console.log(e)
}
}
const switchNetwork = (network: Network) => setNetwork(network)
return (
<ClientContext.Provider
value={{
mode, mode,
isLoading, isLoading,
error, error,
@@ -131,9 +130,9 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
handleShowAdmin, handleShowAdmin,
logIn, logIn,
logOut, logOut,
}} }),
> [mode, isLoading, error, clientDetails, mixnodeDetails, userBalance, showAdmin, showSettings, network, currency],
{children} );
</ClientContext.Provider>
) return <ClientContext.Provider value={memoizedValue}>{children}</ClientContext.Provider>;
} };
+26 -26
View File
@@ -1,49 +1,49 @@
import { useCallback, useContext, useEffect, useState } from 'react' import { useCallback, useContext, useEffect, useState } from 'react';
import { ClientContext } from '../context/main' import { ClientContext } from '../context/main';
import { checkGatewayOwnership, checkMixnodeOwnership } from '../requests' import { checkGatewayOwnership, checkMixnodeOwnership } from '../requests';
import { EnumNodeType, TNodeOwnership } from '../types' import { EnumNodeType, TNodeOwnership } from '../types';
const initial = { const initial = {
hasOwnership: false, hasOwnership: false,
nodeType: undefined, nodeType: undefined,
} };
export const useCheckOwnership = () => { export const useCheckOwnership = () => {
const { clientDetails } = useContext(ClientContext) const { clientDetails } = useContext(ClientContext);
const [ownership, setOwnership] = useState<TNodeOwnership>(initial) const [ownership, setOwnership] = useState<TNodeOwnership>(initial);
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const checkOwnership = useCallback(async () => { const checkOwnership = useCallback(async () => {
const status = {} as TNodeOwnership const status = {} as TNodeOwnership;
setIsLoading(true) setIsLoading(true);
try { try {
const ownsMixnode = await checkMixnodeOwnership() const ownsMixnode = await checkMixnodeOwnership();
const ownsGateway = await checkGatewayOwnership() const ownsGateway = await checkGatewayOwnership();
if (ownsMixnode) { if (ownsMixnode) {
status.hasOwnership = true status.hasOwnership = true;
status.nodeType = EnumNodeType.mixnode status.nodeType = EnumNodeType.mixnode;
} }
if (ownsGateway) { if (ownsGateway) {
status.hasOwnership = true status.hasOwnership = true;
status.nodeType = EnumNodeType.gateway status.nodeType = EnumNodeType.gateway;
} }
setOwnership(status) setOwnership(status);
} catch (e) { } catch (e) {
setError(e as string) setError(e as string);
setIsLoading(false) setIsLoading(false);
setOwnership(initial) setOwnership(initial);
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
checkOwnership() checkOwnership();
}, [clientDetails]) }, [clientDetails]);
return { isLoading, error, ownership, checkOwnership } return { isLoading, error, ownership, checkOwnership };
} };
+72 -72
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { Balance, Coin, OriginalVestingResponse, Period } from '../types' import { VestingAccountInfo } from 'src/types/rust/vestingaccountinfo';
import { Balance, Coin, OriginalVestingResponse, Period } from '../types';
import { import {
getVestingCoins, getVestingCoins,
getVestedCoins, getVestedCoins,
@@ -9,53 +10,41 @@ import {
getOriginalVesting, getOriginalVesting,
getCurrentVestingPeriod, getCurrentVestingPeriod,
getVestingAccountInfo, getVestingAccountInfo,
} from '../requests' } from '../requests';
import { VestingAccountInfo } from 'src/types/rust/vestingaccountinfo'
type TTokenAllocation = { type TTokenAllocation = {
[key in 'vesting' | 'vested' | 'locked' | 'spendable']: Coin['amount'] [key in 'vesting' | 'vested' | 'locked' | 'spendable']: Coin['amount'];
} };
export type TUseuserBalance = { export type TUseuserBalance = {
error?: string error?: string;
balance?: Balance balance?: Balance;
tokenAllocation?: TTokenAllocation tokenAllocation?: TTokenAllocation;
originalVesting?: OriginalVestingResponse originalVesting?: OriginalVestingResponse;
currentVestingPeriod?: Period currentVestingPeriod?: Period;
vestingAccountInfo?: VestingAccountInfo vestingAccountInfo?: VestingAccountInfo;
isLoading: boolean isLoading: boolean;
fetchBalance: () => void fetchBalance: () => void;
clearBalance: () => void clearBalance: () => void;
clearAll: () => void clearAll: () => void;
fetchTokenAllocation: () => void fetchTokenAllocation: () => void;
} };
export const useGetBalance = (address?: string): TUseuserBalance => { export const useGetBalance = (address?: string): TUseuserBalance => {
const [balance, setBalance] = useState<Balance>() const [balance, setBalance] = useState<Balance>();
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const [tokenAllocation, setTokenAllocation] = useState<TTokenAllocation>() const [tokenAllocation, setTokenAllocation] = useState<TTokenAllocation>();
const [originalVesting, setOriginalVesting] = useState<OriginalVestingResponse>() const [originalVesting, setOriginalVesting] = useState<OriginalVestingResponse>();
const [currentVestingPeriod, setCurrentVestingPeriod] = useState<Period>() const [currentVestingPeriod, setCurrentVestingPeriod] = useState<Period>();
const [vestingAccountInfo, setVestingAccountInfo] = useState<VestingAccountInfo>() const [vestingAccountInfo, setVestingAccountInfo] = useState<VestingAccountInfo>();
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const fetchBalance = useCallback(async () => { const clearBalance = () => setBalance(undefined);
setIsLoading(true) const clearTokenAllocation = () => setTokenAllocation(undefined);
setError(undefined) const clearOriginalVesting = () => setOriginalVesting(undefined);
try {
const balance = await invoke('get_balance')
setBalance(balance as Balance)
} catch (error) {
setError(error as string)
} finally {
setTimeout(() => {
setIsLoading(false)
}, 1000)
}
}, [])
const fetchTokenAllocation = async () => { const fetchTokenAllocation = async () => {
setIsLoading(true) setIsLoading(true);
if (address) { if (address) {
try { try {
const [ const [
@@ -64,8 +53,8 @@ export const useGetBalance = (address?: string): TUseuserBalance => {
vestedCoins, vestedCoins,
lockedCoins, lockedCoins,
spendableCoins, spendableCoins,
currentVestingPeriod, currentVestingPer,
vestingAccountInfo, vestingAccountDetail,
] = await Promise.all([ ] = await Promise.all([
getOriginalVesting(address), getOriginalVesting(address),
getVestingCoins(address), getVestingCoins(address),
@@ -74,47 +63,58 @@ export const useGetBalance = (address?: string): TUseuserBalance => {
getSpendableCoins(address), getSpendableCoins(address),
getCurrentVestingPeriod(address), getCurrentVestingPeriod(address),
getVestingAccountInfo(address), getVestingAccountInfo(address),
]) ]);
setOriginalVesting(originalVestingValue) setOriginalVesting(originalVestingValue);
setCurrentVestingPeriod(currentVestingPeriod) setCurrentVestingPeriod(currentVestingPer);
setTokenAllocation({ setTokenAllocation({
vesting: vestingCoins.amount, vesting: vestingCoins.amount,
vested: vestedCoins.amount, vested: vestedCoins.amount,
locked: lockedCoins.amount, locked: lockedCoins.amount,
spendable: spendableCoins.amount, spendable: spendableCoins.amount,
}) });
setVestingAccountInfo(vestingAccountInfo) setVestingAccountInfo(vestingAccountDetail);
} catch (e) { } catch (e) {
clearTokenAllocation() clearTokenAllocation();
clearOriginalVesting() clearOriginalVesting();
console.error(e) console.error(e);
} }
} }
setIsLoading(false) setIsLoading(false);
} };
const clearBalance = () => setBalance(undefined) const fetchBalance = useCallback(async () => {
const clearTokenAllocation = () => setTokenAllocation(undefined) setIsLoading(true);
const clearOriginalVesting = () => setOriginalVesting(undefined) setError(undefined);
try {
const bal = await invoke('get_balance');
setBalance(bal as Balance);
} catch (err) {
setError(err as string);
} finally {
setTimeout(() => {
setIsLoading(false);
}, 1000);
}
}, []);
const clearAll = () => { const clearAll = () => {
clearBalance() clearBalance();
clearTokenAllocation() clearTokenAllocation();
clearOriginalVesting() clearOriginalVesting();
};
const handleRefresh = (addr?: string) => {
if (addr) {
fetchBalance();
fetchTokenAllocation();
} else {
clearAll();
} }
};
useEffect(() => { useEffect(() => {
handleRefresh(address) handleRefresh(address);
}, [address]) }, [address]);
const handleRefresh = (address?: string) => {
if (address) {
fetchBalance()
fetchTokenAllocation()
} else {
clearAll()
}
}
return { return {
error, error,
@@ -128,5 +128,5 @@ export const useGetBalance = (address?: string): TUseuserBalance => {
clearBalance, clearBalance,
clearAll, clearAll,
fetchTokenAllocation, fetchTokenAllocation,
} };
} };
-3
View File
@@ -1,3 +0,0 @@
declare module '*.jpg'
declare module '*.png'
declare module '*.svg'
+21 -24
View File
@@ -1,23 +1,22 @@
import React, { useContext, useEffect, useLayoutEffect } from 'react' import React, { useContext, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary' import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter as Router } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom';
import { SnackbarProvider } from 'notistack' import { SnackbarProvider } from 'notistack';
import { Routes } from './routes' import { Routes } from './routes';
import { ClientContext, ClientContextProvider } from './context/main' import { ClientContext, ClientContextProvider } from './context/main';
import { ApplicationLayout } from './layouts' import { ApplicationLayout } from './layouts';
import { Admin, Welcome } from './pages' import { Admin, Welcome, Settings } from './pages';
import { ErrorFallback } from './components' import { ErrorFallback } from './components';
import { NymWalletTheme, WelcomeTheme } from './theme' import { NymWalletTheme, WelcomeTheme } from './theme';
import { Settings } from './pages' import { maximizeWindow } from './utils';
import { maximizeWindow } from './utils'
const App = () => { const App = () => {
const { clientDetails } = useContext(ClientContext) const { clientDetails } = useContext(ClientContext);
useLayoutEffect(() => { useLayoutEffect(() => {
maximizeWindow() maximizeWindow();
}, []) }, []);
return !clientDetails ? ( return !clientDetails ? (
<WelcomeTheme> <WelcomeTheme>
@@ -31,11 +30,10 @@ const App = () => {
<Routes /> <Routes />
</ApplicationLayout> </ApplicationLayout>
</NymWalletTheme> </NymWalletTheme>
) );
} };
const AppWrapper = () => { const AppWrapper = () => (
return (
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<Router> <Router>
<SnackbarProvider <SnackbarProvider
@@ -50,9 +48,8 @@ const AppWrapper = () => {
</SnackbarProvider> </SnackbarProvider>
</Router> </Router>
</ErrorBoundary> </ErrorBoundary>
) );
}
const root = document.getElementById('root') const root = document.getElementById('root');
ReactDOM.render(<AppWrapper />, root) ReactDOM.render(<AppWrapper />, root);
+6 -9
View File
@@ -1,11 +1,9 @@
import React from 'react' import React from 'react';
import { Box, Container } from '@mui/material' import { Box, Container } from '@mui/material';
import Logo from '../images/logo-background.svg' import Logo from '../images/logo-background.svg';
import { AppBar, Nav } from '../components' import { AppBar, Nav } from '../components';
import { PageLayout } from '.'
export const ApplicationLayout: React.FC = ({ children }) => { export const ApplicationLayout: React.FC = ({ children }) => (
return (
<Box <Box
sx={{ sx={{
height: '100vh', height: '100vh',
@@ -39,5 +37,4 @@ export const ApplicationLayout: React.FC = ({ children }) => {
{children} {children}
</Container> </Container>
</Box> </Box>
) );
}
+4 -6
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react';
import { Box } from '@mui/material' import { Box } from '@mui/material';
export const PageLayout: React.FC<{ position?: 'flex-start' | 'flex-end' }> = ({ position, children }) => { export const PageLayout: React.FC<{ position?: 'flex-start' | 'flex-end' }> = ({ position, children }) => (
return (
<Box <Box
sx={{ sx={{
height: 'calc(100% - 65px)', height: 'calc(100% - 65px)',
@@ -15,5 +14,4 @@ export const PageLayout: React.FC<{ position?: 'flex-start' | 'flex-end' }> = ({
{children} {children}
</Box> </Box>
</Box> </Box>
) );
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './AppLayout' export * from './AppLayout';
export * from './PageLayout' export * from './PageLayout';
+63 -128
View File
@@ -1,68 +1,27 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form';
import { Backdrop, Box, Button, CircularProgress, FormControl, Grid, Paper, Slide, TextField } from '@mui/material' import { Backdrop, Box, Button, CircularProgress, FormControl, Grid, Paper, Slide, TextField } from '@mui/material';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { getContractParams, setContractParams } from '../../requests' import { getContractParams, setContractParams } from '../../requests';
import { TauriContractStateParams } from '../../types' import { TauriContractStateParams } from '../../types';
export const Admin: React.FC = () => {
const { showAdmin, handleShowAdmin } = useContext(ClientContext)
const [isLoading, setIsLoading] = useState(false)
const [params, setParams] = useState<TauriContractStateParams>()
const onCancel = () => {
setParams(undefined)
setIsLoading(false)
handleShowAdmin()
}
useEffect(() => {
const requestContractParams = async () => {
if (showAdmin) {
setIsLoading(true)
const params = await getContractParams()
setParams(params)
setIsLoading(false)
}
}
requestContractParams()
}, [showAdmin])
return (
<Backdrop open={showAdmin} style={{ zIndex: 2, overflow: 'auto' }}>
<Slide in={showAdmin}>
<Paper style={{ margin: 'auto' }}>
<NymCard title="Admin" subheader="Contract administration" noPadding>
{isLoading && (
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress size={48} />
</Box>
)}
{!isLoading && params && <AdminForm onCancel={onCancel} params={params} />}
</NymCard>
</Paper>
</Slide>
</Backdrop>
)
}
const AdminForm: React.FC<{ const AdminForm: React.FC<{
params: TauriContractStateParams params: TauriContractStateParams;
onCancel: () => void onCancel: () => void;
}> = ({ params, onCancel }) => { }> = ({ params, onCancel }) => {
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm({ defaultValues: { ...params } }) } = useForm({ defaultValues: { ...params } });
const onSubmit = async (data: TauriContractStateParams) => { const onSubmit = async (data: TauriContractStateParams) => {
await setContractParams(data) await setContractParams(data);
console.log(data) console.log(data);
onCancel() onCancel();
} };
return ( return (
<FormControl fullWidth> <FormControl fullWidth>
@@ -70,93 +29,28 @@ const AdminForm: React.FC<{
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<TextField <TextField
{...register('minimum_mixnode_bond')} {...register('minimum_mixnode_pledge')}
required required
variant="outlined" variant="outlined"
id="minimum_mixnode_bond" id="minimum_mixnode_bond"
name="minimum_mixnode_bond" name="minimum_mixnode_bond"
label="Minumum mixnode bond" label="Minumum mixnode bond"
fullWidth fullWidth
error={!!errors.minimum_mixnode_bond} error={!!errors.minimum_mixnode_pledge}
helperText={errors?.minimum_mixnode_bond?.message} helperText={errors?.minimum_mixnode_pledge?.message}
/> />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<TextField <TextField
{...register('minimum_gateway_bond')} {...register('minimum_gateway_pledge')}
required required
variant="outlined" variant="outlined"
id="minimum_gateway_bond" id="minimum_gateway_bond"
name="minimum_gateway_bond" name="minimum_gateway_bond"
label="Minumum gateway bond" label="Minumum gateway bond"
fullWidth fullWidth
error={!!errors.minimum_gateway_bond} error={!!errors.minimum_gateway_pledge}
helperText={errors?.minimum_gateway_bond?.message} helperText={errors?.minimum_gateway_pledge?.message}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('mixnode_bond_reward_rate')}
required
variant="outlined"
id="mixnode_bond_reward_rate"
name="mixnode_bond_reward_rate"
label="Mixnode bond reward rate"
fullWidth
error={!!errors.mixnode_bond_reward_rate}
helperText={errors?.mixnode_bond_reward_rate?.message}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('gateway_bond_reward_rate')}
required
variant="outlined"
id="gateway_bond_reward_rate"
name="gateway_bond_reward_rate"
label="Gateway bond reward rate"
fullWidth
error={!!errors.gateway_bond_reward_rate}
helperText={errors?.gateway_bond_reward_rate?.message}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('mixnode_delegation_reward_rate')}
required
variant="outlined"
id="mixnode_delegation_reward_rate"
name="mixnode_delegation_reward_rate"
label="Mixnode Delegation Reward Rate"
fullWidth
error={!!errors.mixnode_delegation_reward_rate}
helperText={errors?.mixnode_delegation_reward_rate?.message}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('gateway_delegation_reward_rate')}
required
variant="outlined"
id="gateway_delegation_reward_rate"
name="gateway_delegation_reward_rate"
label="Gateway Delegation Reward Rate"
fullWidth
error={!!errors.gateway_delegation_reward_rate}
helperText={errors?.gateway_delegation_reward_rate?.message}
/>
</Grid>
<Grid item xs={12}>
<TextField
{...register('epoch_length')}
required
variant="outlined"
id="epochLength"
name="epochLength"
label="Epoch length (hours)"
fullWidth
error={!!errors.epoch_length}
helperText={errors?.epoch_length?.message}
/> />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
@@ -202,5 +96,46 @@ const AdminForm: React.FC<{
</Grid> </Grid>
</Grid> </Grid>
</FormControl> </FormControl>
) );
} };
export const Admin: React.FC = () => {
const { showAdmin, handleShowAdmin } = useContext(ClientContext);
const [isLoading, setIsLoading] = useState(false);
const [params, setParams] = useState<TauriContractStateParams>();
const onCancel = () => {
setParams(undefined);
setIsLoading(false);
handleShowAdmin();
};
useEffect(() => {
const requestContractParams = async () => {
if (showAdmin) {
setIsLoading(true);
const prms = await getContractParams();
setParams(prms);
setIsLoading(false);
}
};
requestContractParams();
}, [showAdmin]);
return (
<Backdrop open={showAdmin} style={{ zIndex: 2, overflow: 'auto' }}>
<Slide in={showAdmin}>
<Paper style={{ margin: 'auto' }}>
<NymCard title="Admin" subheader="Contract administration" noPadding>
{isLoading && (
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress size={48} />
</Box>
)}
{!isLoading && params && <AdminForm onCancel={onCancel} params={params} />}
</NymCard>
</Paper>
</Slide>
</Backdrop>
);
};
+10 -10
View File
@@ -1,15 +1,15 @@
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react';
import { Alert, Button, Grid, Link, Typography } from '@mui/material' import { Alert, Button, Grid, Link, Typography } from '@mui/material';
import { OpenInNew } from '@mui/icons-material' import { OpenInNew } from '@mui/icons-material';
import { NymCard, ClientAddress } from '../../components' import { NymCard, ClientAddress } from '../../components';
import { ClientContext, urls } from '../../context/main' import { ClientContext, urls } from '../../context/main';
export const BalanceCard = () => { export const BalanceCard = () => {
const { userBalance, clientDetails, network } = useContext(ClientContext) const { userBalance, clientDetails, network } = useContext(ClientContext);
useEffect(() => { useEffect(() => {
userBalance.fetchBalance() userBalance.fetchBalance();
}, []) }, []);
return ( return (
<NymCard title="Balance" data-testid="check-balance" Action={<ClientAddress withCopy />}> <NymCard title="Balance" data-testid="check-balance" Action={<ClientAddress withCopy />}>
@@ -40,5 +40,5 @@ export const BalanceCard = () => {
)} )}
</Grid> </Grid>
</NymCard> </NymCard>
) );
} };
@@ -1,19 +1,30 @@
import React, { useContext } from 'react' /* eslint-disable react/no-array-index-key */
import { Box, Tooltip, Typography } from '@mui/material' import React, { useContext } from 'react';
import { format } from 'date-fns' import { Box, Tooltip, Typography } from '@mui/material';
import { ClientContext } from '../../../context/main' import { format } from 'date-fns';
import { ClientContext } from '../../../context/main';
const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index;
const Marker: React.FC<{ tooltipText: string; color: string; position: string }> = ({
tooltipText,
color,
position,
}) => (
<Tooltip title={tooltipText}>
<rect x={position} width="4" height="12" rx="1" fill={color} style={{ cursor: 'pointer' }} />
</Tooltip>
);
export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ percentageComplete }) => { export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ percentageComplete }) => {
const { const {
userBalance: { currentVestingPeriod, vestingAccountInfo }, userBalance: { currentVestingPeriod, vestingAccountInfo },
} = useContext(ClientContext) } = useContext(ClientContext);
const nextPeriod = const nextPeriod =
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time) ? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
: undefined : undefined;
return ( return (
<Box display="flex" flexDirection="column" gap={1} position="relative" width="100%"> <Box display="flex" flexDirection="column" gap={1} position="relative" width="100%">
@@ -40,15 +51,5 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
</Typography> </Typography>
)} )}
</Box> </Box>
) );
} };
const Marker: React.FC<{ tooltipText: string; color: string; position: string }> = ({
tooltipText,
color,
position,
}) => (
<Tooltip title={tooltipText}>
<rect x={position} width="4" height="12" rx="1" fill={color} style={{ cursor: 'pointer' }} />
</Tooltip>
)
+12 -11
View File
@@ -1,15 +1,16 @@
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react';
import { Box } from '@mui/material' import { Box } from '@mui/material';
import { BalanceCard } from './balance' import { BalanceCard } from './balance';
import { VestingCard } from './vesting' import { VestingCard } from './vesting';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Balance = () => { export const Balance = () => {
const { userBalance } = useContext(ClientContext) const { userBalance } = useContext(ClientContext);
useEffect(() => { useEffect(() => {
userBalance.fetchBalance() userBalance.fetchBalance();
}, []) }, []);
return ( return (
<PageLayout> <PageLayout>
@@ -18,5 +19,5 @@ export const Balance = () => {
{userBalance.originalVesting && <VestingCard />} {userBalance.originalVesting && <VestingCard />}
</Box> </Box>
</PageLayout> </PageLayout>
) );
} };
+113 -117
View File
@@ -1,120 +1,59 @@
import React, { useEffect, useContext, useState } from 'react' import React, { useCallback, useContext, useEffect, useState } from 'react';
import { import {
IconButton, Box,
Button,
CircularProgress, CircularProgress,
LinearProgress, Grid,
IconButton,
Table, Table,
TableCell, TableCell,
TableCellProps,
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
Typography, Typography,
Box, } from '@mui/material';
Button, import { InfoOutlined, Refresh } from '@mui/icons-material';
TableCellProps, import { useSnackbar } from 'notistack';
Grid, import { Fee, InfoTooltip, NymCard, Title } from '../../components';
} from '@mui/material' import { ClientContext } from '../../context/main';
import { InfoOutlined, Refresh } from '@mui/icons-material' import { withdrawVestedCoins } from '../../requests';
import { useSnackbar } from 'notistack' import { Period } from '../../types';
import { NymCard, InfoTooltip, Title, Fee } from '../../components' import { VestingTimeline } from './components/vesting-timeline';
import { ClientContext } from '../../context/main'
import { withdrawVestedCoins } from '../../requests'
import { Period } from '../../types'
import { VestingTimeline } from './components/vesting-timeline'
export const VestingCard = () => {
const { userBalance } = useContext(ClientContext)
const [isLoading, setIsLoading] = useState(false)
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const refreshBalances = async () => {
await userBalance.fetchBalance()
await userBalance.fetchTokenAllocation()
}
useEffect(() => {
return () => closeSnackbar()
}, [])
return (
<NymCard
title="Vesting Schedule"
data-testid="check-unvested-tokens"
Icon={InfoOutlined}
Action={
<IconButton
onClick={async () => {
await refreshBalances()
enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true })
}}
>
<Refresh />
</IconButton>
}
>
<VestingSchedule />
<TokenTransfer />
<Box display="flex" justifyContent="space-between" alignItems="center">
{userBalance.tokenAllocation?.spendable !== '0' ? <Fee feeType="Send" /> : <div />}
<Button
size="large"
variant="contained"
onClick={async () => {
setIsLoading(true)
try {
await withdrawVestedCoins(userBalance.tokenAllocation?.spendable!)
await refreshBalances()
enqueueSnackbar('Token transfer succeeded', {
variant: 'success',
preventDuplicate: true,
})
} catch (e) {
console.log(e)
enqueueSnackbar('Token transfer failed. You may not have any transferable tokens at this time', {
variant: 'error',
preventDuplicate: true,
})
} finally {
setIsLoading(false)
}
}}
endIcon={isLoading && <CircularProgress size={16} color="inherit" />}
disabled={isLoading || userBalance.tokenAllocation?.spendable === '0'}
disableElevation
>
Transfer
</Button>
</Box>
</NymCard>
)
}
const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = [ const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = [
{ title: 'Locked', align: 'left' }, { title: 'Locked', align: 'left' },
{ title: 'Period', align: 'left' }, { title: 'Period', align: 'left' },
{ title: 'Percentage Vested', align: 'left' }, { title: 'Percentage Vested', align: 'left' },
{ title: 'Unlocked', align: 'right' }, { title: 'Unlocked', align: 'right' },
] ];
const vestingPeriod = (current?: Period, original?: number) => {
if (current === 'After') return 'Complete';
if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`;
return 'N/A';
};
const VestingSchedule = () => { const VestingSchedule = () => {
const { userBalance, currency } = useContext(ClientContext) const { userBalance, currency } = useContext(ClientContext);
const [vestedPercentage, setVestedPercentage] = useState(0) const [vestedPercentage, setVestedPercentage] = useState(0);
const calculatePercentage = () => { const calculatePercentage = () => {
const { tokenAllocation, originalVesting } = userBalance const { tokenAllocation, originalVesting } = userBalance;
if (tokenAllocation?.vesting && tokenAllocation.vested && tokenAllocation.vested !== '0' && originalVesting) { if (tokenAllocation?.vesting && tokenAllocation.vested && tokenAllocation.vested !== '0' && originalVesting) {
const percentage = (+tokenAllocation.vested / +originalVesting?.amount.amount) * 100 const percentage = (+tokenAllocation.vested / +originalVesting.amount.amount) * 100;
const rounded = percentage.toFixed(2) const rounded = percentage.toFixed(2);
setVestedPercentage(+rounded) setVestedPercentage(+rounded);
} else { } else {
setVestedPercentage(0) setVestedPercentage(0);
}
} }
};
useEffect(() => { useEffect(() => {
calculatePercentage() calculatePercentage();
}, [userBalance.tokenAllocation, calculatePercentage]) }, [userBalance.tokenAllocation, calculatePercentage]);
return ( return (
<TableContainer> <TableContainer>
@@ -149,32 +88,23 @@ const VestingSchedule = () => {
</TableHead> </TableHead>
</Table> </Table>
</TableContainer> </TableContainer>
) );
} };
const vestingPeriod = (current?: Period, original?: number) => {
if (current === 'After') return 'Complete'
if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`
return 'N/A'
}
const TokenTransfer = () => { const TokenTransfer = () => {
const { userBalance, currency } = useContext(ClientContext) const { userBalance, currency } = useContext(ClientContext);
return ( const icon = useCallback(
<Grid container sx={{ my: 2 }} direction="column" spacing={2}> () => (
<Grid item>
<Title
title="Transfer unlocked tokens"
Icon={() => {
return (
<Box sx={{ display: 'flex', mr: 1 }}> <Box sx={{ display: 'flex', mr: 1 }}>
<InfoTooltip title="Unlocked tokens that are available to transfer to your balance" size="medium" /> <InfoTooltip title="Unlocked tokens that are available to transfer to your balance" size="medium" />
</Box> </Box>
) ),
}} [],
/> );
return (
<Grid container sx={{ my: 2 }} direction="column" spacing={2}>
<Grid item>
<Title title="Transfer unlocked tokens" Icon={icon} />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle2" sx={{ color: 'grey.500', mt: 2 }}> <Typography variant="subtitle2" sx={{ color: 'grey.500', mt: 2 }}>
@@ -186,5 +116,71 @@ const TokenTransfer = () => {
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
) );
} };
export const VestingCard = () => {
const { userBalance } = useContext(ClientContext);
const [isLoading, setIsLoading] = useState(false);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const refreshBalances = async () => {
await userBalance.fetchBalance();
await userBalance.fetchTokenAllocation();
};
useEffect(() => () => closeSnackbar(), []);
return (
<NymCard
title="Vesting Schedule"
data-testid="check-unvested-tokens"
Icon={InfoOutlined}
Action={
<IconButton
onClick={async () => {
await refreshBalances();
enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true });
}}
>
<Refresh />
</IconButton>
}
>
<VestingSchedule />
<TokenTransfer />
<Box display="flex" justifyContent="space-between" alignItems="center">
{userBalance.tokenAllocation?.spendable !== '0' ? <Fee feeType="Send" /> : <div />}
<Button
size="large"
variant="contained"
onClick={async () => {
setIsLoading(true);
try {
await withdrawVestedCoins(userBalance.tokenAllocation?.spendable!);
await refreshBalances();
enqueueSnackbar('Token transfer succeeded', {
variant: 'success',
preventDuplicate: true,
});
} catch (e) {
console.log(e);
enqueueSnackbar('Token transfer failed. You may not have any transferable tokens at this time', {
variant: 'error',
preventDuplicate: true,
});
} finally {
setIsLoading(false);
}
}}
endIcon={isLoading && <CircularProgress size={16} color="inherit" />}
disabled={isLoading || userBalance.tokenAllocation?.spendable === '0'}
disableElevation
>
Transfer
</Button>
</Box>
</NymCard>
);
};
+60 -63
View File
@@ -1,4 +1,4 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { import {
Box, Box,
Button, Button,
@@ -9,34 +9,33 @@ import {
Grid, Grid,
InputAdornment, InputAdornment,
TextField, TextField,
Typography, } from '@mui/material';
} from '@mui/material' import { yupResolver } from '@hookform/resolvers/yup';
import { yupResolver } from '@hookform/resolvers/yup' import { useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form' import { EnumNodeType } from '../../types/global';
import { EnumNodeType } from '../../types/global' import { NodeTypeSelector } from '../../components/NodeTypeSelector';
import { NodeTypeSelector } from '../../components/NodeTypeSelector' import { bond, majorToMinor } from '../../requests';
import { bond, majorToMinor } from '../../requests' import { validationSchema } from './validationSchema';
import { validationSchema } from './validationSchema' import { Gateway, MixNode } from '../../types';
import { Gateway, MixNode } from '../../types' import { ClientContext } from '../../context/main';
import { ClientContext } from '../../context/main' import { Fee } from '../../components';
import { Fee } from '../../components'
type TBondFormFields = { type TBondFormFields = {
withAdvancedOptions: boolean withAdvancedOptions: boolean;
nodeType: EnumNodeType nodeType: EnumNodeType;
ownerSignature: string ownerSignature: string;
identityKey: string identityKey: string;
sphinxKey: string sphinxKey: string;
profitMarginPercent: number profitMarginPercent: number;
amount: string amount: string;
host: string host: string;
version: string version: string;
location?: string location?: string;
mixPort: number mixPort: number;
verlocPort: number verlocPort: number;
clientsPort: number clientsPort: number;
httpApiPort: number httpApiPort: number;
} };
const defaultValues = { const defaultValues = {
withAdvancedOptions: false, withAdvancedOptions: false,
@@ -53,7 +52,7 @@ const defaultValues = {
verlocPort: 1790, verlocPort: 1790,
httpApiPort: 8000, httpApiPort: 8000,
clientsPort: 9000, clientsPort: 9000,
} };
const formatData = (data: TBondFormFields) => { const formatData = (data: TBondFormFields) => {
const payload: { [key: string]: any } = { const payload: { [key: string]: any } = {
@@ -63,27 +62,26 @@ const formatData = (data: TBondFormFields) => {
version: data.version, version: data.version,
mix_port: data.mixPort, mix_port: data.mixPort,
profit_margin_percent: data.profitMarginPercent, profit_margin_percent: data.profitMarginPercent,
} };
if (data.nodeType === EnumNodeType.mixnode) { if (data.nodeType === EnumNodeType.mixnode) {
payload.verloc_port = data.verlocPort payload.verloc_port = data.verlocPort;
payload.http_api_port = data.httpApiPort payload.http_api_port = data.httpApiPort;
return payload as MixNode return payload as MixNode;
} else {
payload.clients_port = data.clientsPort
payload.location = data.location
return payload as Gateway
} }
} payload.clients_port = data.clientsPort;
payload.location = data.location;
return payload as Gateway;
};
export const BondForm = ({ export const BondForm = ({
disabled, disabled,
onError, onError,
onSuccess, onSuccess,
}: { }: {
disabled: boolean disabled: boolean;
onError: (message?: string) => void onError: (message?: string) => void;
onSuccess: (details: { address: string; amount: string }) => void onSuccess: (details: { address: string; amount: string }) => void;
}) => { }) => {
const { const {
register, register,
@@ -94,27 +92,27 @@ export const BondForm = ({
} = useForm<TBondFormFields>({ } = useForm<TBondFormFields>({
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
defaultValues, defaultValues,
}) });
const { userBalance, currency, getBondDetails } = useContext(ClientContext) const { userBalance, currency, getBondDetails } = useContext(ClientContext);
const watchNodeType = watch('nodeType', defaultValues.nodeType) const watchNodeType = watch('nodeType', defaultValues.nodeType);
const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions) const watchAdvancedOptions = watch('withAdvancedOptions', defaultValues.withAdvancedOptions);
const onSubmit = async (data: TBondFormFields) => { const onSubmit = async (data: TBondFormFields) => {
const formattedData = formatData(data) const formattedData = formatData(data);
const pledge = await majorToMinor(data.amount) const pledge = await majorToMinor(data.amount);
await bond({ type: data.nodeType, ownerSignature: data.ownerSignature, data: formattedData, pledge }) await bond({ type: data.nodeType, ownerSignature: data.ownerSignature, data: formattedData, pledge })
.then(async () => { .then(async () => {
await getBondDetails() await getBondDetails();
userBalance.fetchBalance() userBalance.fetchBalance();
onSuccess({ address: data.identityKey, amount: data.amount }) onSuccess({ address: data.identityKey, amount: data.amount });
}) })
.catch((e) => { .catch((e) => {
onError(e) onError(e);
}) });
} };
return ( return (
<FormControl fullWidth> <FormControl fullWidth>
@@ -125,8 +123,8 @@ export const BondForm = ({
<NodeTypeSelector <NodeTypeSelector
nodeType={watchNodeType} nodeType={watchNodeType}
setNodeType={(nodeType) => { setNodeType={(nodeType) => {
setValue('nodeType', nodeType) setValue('nodeType', nodeType);
if (nodeType === EnumNodeType.mixnode) setValue('location', undefined) if (nodeType === EnumNodeType.mixnode) setValue('location', undefined);
}} }}
disabled={disabled} disabled={disabled}
/> />
@@ -268,20 +266,19 @@ export const BondForm = ({
if (watchAdvancedOptions) { if (watchAdvancedOptions) {
setValue('mixPort', defaultValues.mixPort, { setValue('mixPort', defaultValues.mixPort, {
shouldValidate: true, shouldValidate: true,
}) });
setValue('clientsPort', defaultValues.clientsPort, { setValue('clientsPort', defaultValues.clientsPort, {
shouldValidate: true, shouldValidate: true,
}) });
setValue('verlocPort', defaultValues.verlocPort, { setValue('verlocPort', defaultValues.verlocPort, {
shouldValidate: true, shouldValidate: true,
}) });
setValue('httpApiPort', defaultValues.httpApiPort, { setValue('httpApiPort', defaultValues.httpApiPort, {
shouldValidate: true, shouldValidate: true,
}) });
setValue('withAdvancedOptions', false) setValue('withAdvancedOptions', false);
resizeTo
} else { } else {
setValue('withAdvancedOptions', true) setValue('withAdvancedOptions', true);
} }
}} }}
/> />
@@ -380,5 +377,5 @@ export const BondForm = ({
</Button> </Button>
</Box> </Box>
</FormControl> </FormControl>
) );
} };
+7 -7
View File
@@ -1,10 +1,10 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Box } from '@mui/system' import { Box } from '@mui/material';
import { SuccessReponse, TransactionDetails } from '../../components' import { SuccessReponse, TransactionDetails } from '../../components';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => { export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
const { userBalance, currency } = useContext(ClientContext) const { userBalance, currency } = useContext(ClientContext);
return ( return (
<> <>
<SuccessReponse <SuccessReponse
@@ -23,5 +23,5 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
</Box> </Box>
)} )}
</> </>
) );
} };
+33 -33
View File
@@ -1,31 +1,31 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Button, CircularProgress } from '@mui/material' import { Alert, Box, Button, CircularProgress } from '@mui/material';
import { BondForm } from './BondForm' import { BondForm } from './BondForm';
import { SuccessView } from './SuccessView' import { SuccessView } from './SuccessView';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus' import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus';
import { unbond } from '../../requests' import { unbond } from '../../requests';
import { useCheckOwnership } from '../../hooks/useCheckOwnership' import { useCheckOwnership } from '../../hooks/useCheckOwnership';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Bond = () => { export const Bond = () => {
const [status, setStatus] = useState(EnumRequestStatus.initial) const [status, setStatus] = useState(EnumRequestStatus.initial);
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>() const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>();
const { checkOwnership, ownership } = useCheckOwnership() const { checkOwnership, ownership } = useCheckOwnership();
const { userBalance, getBondDetails } = useContext(ClientContext) const { userBalance, getBondDetails } = useContext(ClientContext);
useEffect(() => { useEffect(() => {
if (status === EnumRequestStatus.initial) { if (status === EnumRequestStatus.initial) {
const initialiseForm = async () => { const initialiseForm = async () => {
await checkOwnership() await checkOwnership();
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
};
initialiseForm();
} }
initialiseForm() }, [status]);
}
}, [status])
return ( return (
<PageLayout> <PageLayout>
@@ -43,11 +43,11 @@ export const Bond = () => {
<Button <Button
disabled={status === EnumRequestStatus.loading} disabled={status === EnumRequestStatus.loading}
onClick={async () => { onClick={async () => {
setStatus(EnumRequestStatus.loading) setStatus(EnumRequestStatus.loading);
await unbond(ownership.nodeType!) await unbond(ownership.nodeType!);
await getBondDetails() await getBondDetails();
await userBalance.fetchBalance() await userBalance.fetchBalance();
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
}} }}
data-testid="unBond" data-testid="unBond"
color="inherit" color="inherit"
@@ -74,12 +74,12 @@ export const Bond = () => {
{status === EnumRequestStatus.initial && ( {status === EnumRequestStatus.initial && (
<BondForm <BondForm
onError={(e?: string) => { onError={(e?: string) => {
setError(e) setError(e);
setStatus(EnumRequestStatus.error) setStatus(EnumRequestStatus.error);
}} }}
onSuccess={(details) => { onSuccess={(details) => {
setSuccessDetails(details) setSuccessDetails(details);
setStatus(EnumRequestStatus.success) setStatus(EnumRequestStatus.success);
}} }}
disabled={ownership?.hasOwnership} disabled={ownership?.hasOwnership}
/> />
@@ -106,8 +106,8 @@ export const Bond = () => {
> >
<Button <Button
onClick={() => { onClick={() => {
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
checkOwnership() checkOwnership();
}} }}
> >
{status === EnumRequestStatus.error ? 'Again?' : 'Finish'} {status === EnumRequestStatus.error ? 'Again?' : 'Finish'}
@@ -117,5 +117,5 @@ export const Bond = () => {
)} )}
</NymCard> </NymCard>
</PageLayout> </PageLayout>
) );
} };
+26 -44
View File
@@ -1,4 +1,4 @@
import * as Yup from 'yup' import * as Yup from 'yup';
import { import {
checkHasEnoughFunds, checkHasEnoughFunds,
isValidHostname, isValidHostname,
@@ -7,78 +7,60 @@ import {
validateLocation, validateLocation,
validateRawPort, validateRawPort,
validateVersion, validateVersion,
} from '../../utils' } from '../../utils';
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
identityKey: Yup.string() identityKey: Yup.string()
.required('An indentity key is required') .required('An indentity key is required')
.test('valid-id-key', 'A valid identity key is required', function (value) { .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
return validateKey(value || '', 32)
}),
sphinxKey: Yup.string() sphinxKey: Yup.string()
.required('A sphinx key is required') .required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', function (value) { .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
return validateKey(value || '', 32)
}),
ownerSignature: Yup.string() ownerSignature: Yup.string()
.required('Signature is required') .required('Signature is required')
.test('valid-signature', 'A valid signature is required', function (value) { .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
return validateKey(value || '', 64)
}),
profitMarginPercent: Yup.number().required('Profit Percentage is required').min(0).max(100), profitMarginPercent: Yup.number().required('Profit Percentage is required').min(0).max(100),
amount: Yup.string() amount: Yup.string()
.required('An amount is required') .required('An amount is required')
.test('valid-amount', `Pledge error`, async function (value) { .test('valid-amount', 'Pledge error', async function (value) {
const isValid = await validateAmount(value || '', '100000000') const isValid = await validateAmount(value || '', '100000000');
if (!isValid) { if (!isValid) {
return this.createError({ message: `A valid amount is required (min 100)` }) return this.createError({ message: 'A valid amount is required (min 100)' });
} else { }
const hasEnough = await checkHasEnoughFunds(value || '') const hasEnough = await checkHasEnoughFunds(value || '');
if (!hasEnough) { if (!hasEnough) {
return this.createError({ message: 'Not enough funds in wallet' }) return this.createError({ message: 'Not enough funds in wallet' });
} }
}
return true return true;
}), }),
host: Yup.string() host: Yup.string()
.required('A host is required') .required('A host is required')
.test('valid-host', 'A valid host is required', function (value) { .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
return !!value ? isValidHostname(value) : false
}),
version: Yup.string() version: Yup.string()
.required('A version is required') .required('A version is required')
.test('valid-version', 'A valid version is required', function (value) { .test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
return !!value ? validateVersion(value) : false location: Yup.lazy((locationValue) => {
}), if (locationValue) {
location: Yup.lazy((value) => {
if (!!value) {
return Yup.string() return Yup.string()
.required('A location is required') .required('A location is required')
.test('valid-location', 'A valid version is required', function (value) { .test('valid-location', 'A valid version is required', (locationValueTest) =>
return !!value ? validateLocation(value) : false locationValueTest ? validateLocation(locationValueTest) : false,
}) );
} }
return Yup.mixed().notRequired() return Yup.mixed().notRequired();
}), }),
mixPort: Yup.number() mixPort: Yup.number()
.required('A mixport is required') .required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', function (value) { .test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
return !!value ? validateRawPort(value) : false
}),
verlocPort: Yup.number() verlocPort: Yup.number()
.required('A verloc port is required') .required('A verloc port is required')
.test('valid-verloc', 'A valid verloc port is required', function (value) { .test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
return !!value ? validateRawPort(value) : false
}),
httpApiPort: Yup.number() httpApiPort: Yup.number()
.required('A http-api port is required') .required('A http-api port is required')
.test('valid-http', 'A valid http-api port is required', function (value) { .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
return !!value ? validateRawPort(value) : false
}),
clientsPort: Yup.number() clientsPort: Yup.number()
.required('A clients port is required') .required('A clients port is required')
.test('valid-clients', 'A valid clients port is required', function (value) { .test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
return !!value ? validateRawPort(value) : false });
}),
})
+32 -34
View File
@@ -1,57 +1,55 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Box, Button, CircularProgress, FormControl, Grid, InputAdornment, TextField, Typography } from '@mui/material' import { Box, Button, CircularProgress, FormControl, Grid, InputAdornment, TextField } from '@mui/material';
import { yupResolver } from '@hookform/resolvers/yup' import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form';
import { EnumNodeType } from '../../types' import { EnumNodeType } from '../../types';
import { validationSchema } from './validationSchema' import { validationSchema } from './validationSchema';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { delegate, majorToMinor } from '../../requests' import { delegate, majorToMinor } from '../../requests';
import { checkHasEnoughFunds } from '../../utils' import { checkHasEnoughFunds } from '../../utils';
import { Fee } from '../../components' import { Fee } from '../../components';
type TDelegateForm = { type TDelegateForm = {
nodeType: EnumNodeType nodeType: EnumNodeType;
identity: string identity: string;
amount: string amount: string;
} };
const defaultValues: TDelegateForm = { const defaultValues: TDelegateForm = {
nodeType: EnumNodeType.mixnode, nodeType: EnumNodeType.mixnode,
identity: '', identity: '',
amount: '', amount: '',
} };
export const DelegateForm = ({ export const DelegateForm = ({
onError, onError,
onSuccess, onSuccess,
}: { }: {
onError: (message?: string) => void onError: (message?: string) => void;
onSuccess: (details: { amount: string; address: string }) => void onSuccess: (details: { amount: string; address: string }) => void;
}) => { }) => {
const { const {
register, register,
watch,
handleSubmit, handleSubmit,
setError, setError,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<TDelegateForm>({ } = useForm<TDelegateForm>({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}) });
const watchNodeType = watch('nodeType', defaultValues.nodeType) const { userBalance, currency } = useContext(ClientContext);
const { userBalance, currency } = useContext(ClientContext)
const onSubmit = async (data: TDelegateForm) => { const onSubmit = async (data: TDelegateForm) => {
const hasEnoughFunds = await checkHasEnoughFunds(data.amount) const hasEnoughFunds = await checkHasEnoughFunds(data.amount);
if (!hasEnoughFunds) { if (!hasEnoughFunds) {
return setError('amount', { setError('amount', {
message: 'Not enough funds in wallet', message: 'Not enough funds in wallet',
}) });
return;
} }
const amount = await majorToMinor(data.amount) const amount = await majorToMinor(data.amount);
await delegate({ await delegate({
type: data.nodeType, type: data.nodeType,
@@ -59,14 +57,14 @@ export const DelegateForm = ({
amount, amount,
}) })
.then((res) => { .then((res) => {
onSuccess({ amount: data.amount, address: res.target_address }) onSuccess({ amount: data.amount, address: res.target_address });
userBalance.fetchBalance() userBalance.fetchBalance();
}) })
.catch((e) => { .catch((e) => {
console.log(e) console.log(e);
onError(e) onError(e);
}) });
} };
return ( return (
<FormControl fullWidth> <FormControl fullWidth>
@@ -131,5 +129,5 @@ export const DelegateForm = ({
</Button> </Button>
</Box> </Box>
</FormControl> </FormControl>
) );
} };
@@ -1,10 +1,10 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Box } from '@mui/system' import { Box } from '@mui/material';
import { SuccessReponse, TransactionDetails } from '../../components' import { SuccessReponse, TransactionDetails } from '../../components';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => { export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
const { userBalance, currency } = useContext(ClientContext) const { userBalance, currency } = useContext(ClientContext);
return ( return (
<> <>
<SuccessReponse <SuccessReponse
@@ -23,5 +23,5 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
</Box> </Box>
)} )}
</> </>
) );
} };
+19 -19
View File
@@ -1,18 +1,18 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react';
import { Alert, AlertTitle, Box, Button, Link, Typography } from '@mui/material' import { Alert, AlertTitle, Box, Button, Link, Typography } from '@mui/material';
import { DelegateForm } from './DelegateForm' import { DelegateForm } from './DelegateForm';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus' import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus';
import { SuccessView } from './SuccessView' import { SuccessView } from './SuccessView';
import { urls, ClientContext } from '../../context/main' import { urls, ClientContext } from '../../context/main';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Delegate = () => { export const Delegate = () => {
const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial) const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial);
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>() const [successDetails, setSuccessDetails] = useState<{ amount: string; address: string }>();
const { network } = useContext(ClientContext) const { network } = useContext(ClientContext);
return ( return (
<PageLayout> <PageLayout>
@@ -26,12 +26,12 @@ export const Delegate = () => {
{status === EnumRequestStatus.initial && ( {status === EnumRequestStatus.initial && (
<DelegateForm <DelegateForm
onError={(message?: string) => { onError={(message?: string) => {
setStatus(EnumRequestStatus.error) setStatus(EnumRequestStatus.error);
setError(message) setError(message);
}} }}
onSuccess={(details) => { onSuccess={(details) => {
setStatus(EnumRequestStatus.success) setStatus(EnumRequestStatus.success);
setSuccessDetails(details) setSuccessDetails(details);
}} }}
/> />
)} )}
@@ -62,7 +62,7 @@ export const Delegate = () => {
<Button <Button
data-testid="finish-button" data-testid="finish-button"
onClick={() => { onClick={() => {
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
}} }}
> >
Finish Finish
@@ -80,5 +80,5 @@ export const Delegate = () => {
for uptime and performances to help make delegation decisions for uptime and performances to help make delegation decisions
</Typography> </Typography>
</PageLayout> </PageLayout>
) );
} };
@@ -1,5 +1,5 @@
import * as Yup from 'yup' import * as Yup from 'yup';
import { validateAmount, validateKey } from '../../utils' import { validateAmount, validateKey } from '../../utils';
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
identity: Yup.string() identity: Yup.string()
@@ -7,9 +7,9 @@ export const validationSchema = Yup.object().shape({
.test( .test(
'valid-id-key', 'valid-id-key',
'A valid identity key is required e.g. 824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z', 'A valid identity key is required e.g. 824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z',
(value) => (!!value ? validateKey(value, 32) : false), (value) => (value ? validateKey(value, 32) : false),
), ),
amount: Yup.string() amount: Yup.string()
.required() .required()
.test('valid-amount-key', 'A valid amount is required', (value) => (!!value ? validateAmount(value, '0') : false)), .test('valid-amount-key', 'A valid amount is required', (value) => (value ? validateAmount(value, '0') : false)),
}) });
+11 -11
View File
@@ -1,11 +1,11 @@
export * from './Admin' export * from './Admin';
export * from './balance' export * from './balance';
export * from './bond' export * from './bond';
export * from './delegate' export * from './delegate';
export * from './internal-docs' export * from './internal-docs';
export * from './receive' export * from './receive';
export * from './send' export * from './send';
export * from './welcome' export * from './welcome';
export * from './settings' export * from './settings';
export * from './unbond' export * from './unbond';
export * from './undelegate' export * from './undelegate';
@@ -1,9 +1,8 @@
import React from 'react' import React from 'react';
import { List, ListItem } from '@mui/material' import { List, ListItem } from '@mui/material';
import { DocEntry } from './DocEntry' import { DocEntry } from './DocEntry';
export const ApiList = () => { export const ApiList = () => (
return (
<List> <List>
<ListItem> <ListItem>
<DocEntry <DocEntry
@@ -161,5 +160,4 @@ export const ApiList = () => {
/> />
</ListItem> </ListItem>
</List> </List>
) );
}
+26 -43
View File
@@ -1,56 +1,50 @@
import React from 'react' /* eslint-disable react/destructuring-assignment */
import { Button, Card, CardContent, TextField } from '@mui/material' import React from 'react';
import { invoke } from '@tauri-apps/api' import { Button, Card, CardContent, TextField } from '@mui/material';
import { invoke } from '@tauri-apps/api';
interface DocEntryProps { interface DocEntryProps {
function: FunctionDef function: FunctionDef;
} }
interface FunctionDef { interface FunctionDef {
name: string name: string;
args: ArgDef[] args: ArgDef[];
} }
interface ArgDef { interface ArgDef {
name: string name: string;
type: string type: string;
} }
const argKey = (functionName: string, arg: string) => `${functionName}_${arg}` const argKey = (functionName: string, arg: string) => `${functionName}_${arg}`;
function collectArgs(functionName: string, args: ArgDef[]) { function collectArgs(functionName: string, args: ArgDef[]) {
let invokeArgs: { [key: string]: string } = {} const invokeArgs: { [key: string]: string } = {};
args.forEach((arg) => { args.forEach((arg) => {
let elem: HTMLElement | null = document.getElementById( const elem: HTMLElement | null = document.getElementById(argKey(functionName, arg.name));
argKey(functionName, arg.name),
)
if (arg.type === 'object') { if (arg.type === 'object') {
console.log(arg) invokeArgs[arg.name] = JSON.parse((elem as HTMLInputElement).value);
invokeArgs[arg.name] = JSON.parse((elem as HTMLInputElement).value)
} else { } else {
invokeArgs[arg.name] = (elem as HTMLInputElement).value || '' invokeArgs[arg.name] = (elem as HTMLInputElement).value || '';
} }
}) });
console.log(invokeArgs) return invokeArgs;
return invokeArgs
} }
export const DocEntry = (props: DocEntryProps) => { export const DocEntry: React.FC<DocEntryProps> = (props) => {
const [card, setCard] = React.useState(<Card />) const [card, setCard] = React.useState(<Card />);
const onClick = () => { const onClick = () => {
invoke( invoke(props.function.name, collectArgs(props.function.name, props.function.args))
props.function.name,
collectArgs(props.function.name, props.function.args),
)
.then((result) => { .then((result) => {
setCard( setCard(
<Card> <Card>
<CardContent>{JSON.stringify(result, null, 4)}</CardContent> <CardContent>{JSON.stringify(result, null, 4)}</CardContent>
</Card>, </Card>,
) );
}) })
.catch((e) => .catch((e) =>
setCard( setCard(
@@ -58,26 +52,15 @@ export const DocEntry = (props: DocEntryProps) => {
<CardContent>{e}</CardContent> <CardContent>{e}</CardContent>
</Card>, </Card>,
), ),
) );
} };
return ( return (
<div> <div>
<Button <Button variant="contained" color="primary" size="small" disableElevation onClick={onClick}>
variant="contained"
color="primary"
size="small"
disableElevation
onClick={onClick}
>
{props.function.name} {props.function.name}
</Button> </Button>
<Button <Button variant="contained" size="small" disableElevation onClick={() => setCard(<Card />)}>
variant="contained"
size="small"
disableElevation
onClick={() => setCard(<Card />)}
>
X X
</Button> </Button>
<div> <div>
@@ -92,5 +75,5 @@ export const DocEntry = (props: DocEntryProps) => {
<br /> <br />
{card} {card}
</div> </div>
) );
} };
+8 -8
View File
@@ -1,18 +1,18 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { ApiList } from './ApiList' import { ApiList } from './ApiList';
import { ADMIN_ADDRESS, ClientContext } from '../../context/main' import { ADMIN_ADDRESS, ClientContext } from '../../context/main';
export const InternalDocs = () => { export const InternalDocs = () => {
const { clientDetails } = useContext(ClientContext) const { clientDetails } = useContext(ClientContext);
if (clientDetails?.client_address === ADMIN_ADDRESS) { if (clientDetails?.client_address === ADMIN_ADDRESS) {
return ( return (
<NymCard title="Docs" subheader="Internal API docs"> <NymCard title="Docs" subheader="Internal API docs">
<ApiList /> <ApiList />
</NymCard> </NymCard>
) );
} }
return null return null;
} };
+9 -9
View File
@@ -1,12 +1,12 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react';
import { Alert, Box, Stack } from '@mui/material' import { Alert, Box, Stack } from '@mui/material';
import { ClientAddress, NymCard } from '../../components' import { ClientAddress, NymCard } from '../../components';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Receive = () => { export const Receive = () => {
const { clientDetails, currency } = useContext(ClientContext) const { clientDetails, currency } = useContext(ClientContext);
return ( return (
<PageLayout> <PageLayout>
@@ -22,5 +22,5 @@ export const Receive = () => {
</Stack> </Stack>
</NymCard> </NymCard>
</PageLayout> </PageLayout>
) );
} };
+13 -13
View File
@@ -1,21 +1,21 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Box, CircularProgress, Link, Typography } from '@mui/material' import { Box, CircularProgress, Link, Typography } from '@mui/material';
import { SendError } from './SendError' import { SendError } from './SendError';
import { ClientContext, urls } from '../../context/main' import { ClientContext, urls } from '../../context/main';
import { SuccessReponse } from '../../components' import { SuccessReponse } from '../../components';
import { TransactionDetails } from '../../components/TransactionDetails' import { TransactionDetails } from '../../components/TransactionDetails';
import { TransactionDetails as TTransactionDetails } from '../../types' import { TransactionDetails as TTransactionDetails } from '../../types';
export const SendConfirmation = ({ export const SendConfirmation = ({
data, data,
error, error,
isLoading, isLoading,
}: { }: {
data?: TTransactionDetails & { tx_hash: string } data?: TTransactionDetails & { tx_hash: string };
error?: string error?: string;
isLoading: boolean isLoading: boolean;
}) => { }) => {
const { userBalance, currency, network } = useContext(ClientContext) const { userBalance, currency, network } = useContext(ClientContext);
return ( return (
<Box <Box
sx={{ sx={{
@@ -55,5 +55,5 @@ export const SendConfirmation = ({
</> </>
)} )}
</Box> </Box>
) );
} };
+5 -7
View File
@@ -1,9 +1,8 @@
import React from 'react' import React from 'react';
import { Alert, Box, Card, Typography } from '@mui/material' import { Alert, Box, Card, Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material' import { ErrorOutline } from '@mui/icons-material';
export const SendError = ({ message }: { message?: string }) => { export const SendError = ({ message }: { message?: string }) => (
return (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -34,5 +33,4 @@ export const SendError = ({ message }: { message?: string }) => {
</Card> </Card>
</> </>
</Box> </Box>
) );
}
+9 -9
View File
@@ -1,15 +1,15 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Grid, InputAdornment, TextField } from '@mui/material' import { Grid, InputAdornment, TextField } from '@mui/material';
import { useFormContext } from 'react-hook-form' import { useFormContext } from 'react-hook-form';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { Fee, ClientAddress } from '../../components' import { Fee, ClientAddress } from '../../components';
export const SendForm = () => { export const SendForm = () => {
const { const {
register, register,
formState: { errors }, formState: { errors },
} = useFormContext() } = useFormContext();
const { currency } = useContext(ClientContext) const { currency } = useContext(ClientContext);
return ( return (
<Grid container spacing={3}> <Grid container spacing={3}>
@@ -51,5 +51,5 @@ export const SendForm = () => {
<Fee feeType="Send" /> <Fee feeType="Send" />
</Grid> </Grid>
</Grid> </Grid>
) );
} };
+18 -20
View File
@@ -1,13 +1,22 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Card, Divider, Grid, Typography } from '@mui/material' import { Card, Divider, Grid, Typography } from '@mui/material';
import { useFormContext } from 'react-hook-form' import { useFormContext } from 'react-hook-form';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
const SendReviewField = ({ title, subtitle, info }: { title: string; subtitle?: string; info?: boolean }) => (
<>
<Typography sx={{ color: info ? 'nym.fee' : '' }}>{title}</Typography>
<Typography data-testid={title} sx={{ color: info ? 'nym.fee' : '', wordBreak: 'break-all' }}>
{subtitle}
</Typography>
</>
);
export const SendReview = ({ transferFee }: { transferFee?: string }) => { export const SendReview = ({ transferFee }: { transferFee?: string }) => {
const { getValues } = useFormContext() const { getValues } = useFormContext();
const { clientDetails, currency } = useContext(ClientContext) const { clientDetails, currency } = useContext(ClientContext);
const values = getValues() const values = getValues();
return ( return (
<Card <Card
@@ -44,16 +53,5 @@ export const SendReview = ({ transferFee }: { transferFee?: string }) => {
</Grid> </Grid>
</Grid> </Grid>
</Card> </Card>
) );
} };
export const SendReviewField = ({ title, subtitle, info }: { title: string; subtitle?: string; info?: boolean }) => {
return (
<>
<Typography sx={{ color: info ? 'nym.fee' : '' }}>{title}</Typography>
<Typography data-testid={title} sx={{ color: info ? 'nym.fee' : '', wordBreak: 'break-all' }}>
{subtitle}
</Typography>
</>
)
}
+63 -62
View File
@@ -1,79 +1,80 @@
import React, { useEffect, useContext, useState } from 'react' import React, { useEffect, useContext, useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form' import { useForm, FormProvider } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup' import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Button, Step, StepLabel, Stepper } from '@mui/material' import { Box, Button, Step, StepLabel, Stepper } from '@mui/material';
import { SendForm } from './SendForm' import { SendForm } from './SendForm';
import { SendReview } from './SendReview' import { SendReview } from './SendReview';
import { SendConfirmation } from './SendConfirmation' import { SendConfirmation } from './SendConfirmation';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { validationSchema } from './validationSchema' import { validationSchema } from './validationSchema';
import { TauriTxResult, TransactionDetails } from '../../types' import { TauriTxResult, TransactionDetails } from '../../types';
import { getGasFee, majorToMinor, send } from '../../requests' import { getGasFee, majorToMinor, send } from '../../requests';
import { checkHasEnoughFunds } from '../../utils' import { checkHasEnoughFunds } from '../../utils';
const defaultValues = { const defaultValues = {
amount: '', amount: '',
memo: '', memo: '',
to: '', to: '',
} };
export type TFormData = { export type TFormData = {
amount: string amount: string;
memo: string memo: string;
to: string to: string;
} };
export const SendWizard = () => { export const SendWizard = () => {
const [activeStep, setActiveStep] = useState(0) const [activeStep, setActiveStep] = useState(0);
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const [requestError, setRequestError] = useState<string>() const [requestError, setRequestError] = useState<string>();
const [transferFee, setTransferFee] = useState<string>() const [transferFee, setTransferFee] = useState<string>();
const [confirmedData, setConfirmedData] = useState<TransactionDetails & { tx_hash: string }>() const [confirmedData, setConfirmedData] = useState<TransactionDetails & { tx_hash: string }>();
const { userBalance } = useContext(ClientContext) const { userBalance } = useContext(ClientContext);
useEffect(() => { useEffect(() => {
const getFee = async () => { const getFee = async () => {
const fee = await getGasFee('Send') const fee = await getGasFee('Send');
setTransferFee(fee.amount) setTransferFee(fee.amount);
} };
getFee() getFee();
}, []) }, []);
const steps = ['Enter address', 'Review and send', 'Await confirmation'] const steps = ['Enter address', 'Review and send', 'Await confirmation'];
const methods = useForm<TFormData>({ const methods = useForm<TFormData>({
defaultValues: { defaultValues: {
...defaultValues, ...defaultValues,
}, },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}) });
const handleNextStep = methods.handleSubmit(() => setActiveStep((s) => s + 1)) const handleNextStep = methods.handleSubmit(() => setActiveStep((s) => s + 1));
const handlePreviousStep = () => setActiveStep((s) => s - 1) const handlePreviousStep = () => setActiveStep((s) => s - 1);
const handleFinish = () => { const handleFinish = () => {
methods.reset() methods.reset();
setIsLoading(false) setIsLoading(false);
setRequestError(undefined) setRequestError(undefined);
setConfirmedData(undefined) setConfirmedData(undefined);
setActiveStep(0) setActiveStep(0);
} };
const handleSend = async () => { const handleSend = async () => {
const formState = methods.getValues() const formState = methods.getValues();
const hasEnoughFunds = await checkHasEnoughFunds(formState.amount) const hasEnoughFunds = await checkHasEnoughFunds(formState.amount);
if (!hasEnoughFunds) { if (!hasEnoughFunds) {
methods.setError('amount', { methods.setError('amount', {
message: 'Not enough funds in wallet', message: 'Not enough funds in wallet',
}) });
return handlePreviousStep() handlePreviousStep();
} else { return;
setIsLoading(true) }
setActiveStep((s) => s + 1) setIsLoading(true);
const amount = await majorToMinor(formState.amount) setActiveStep((s) => s + 1);
const amount = await majorToMinor(formState.amount);
send({ send({
amount, amount,
@@ -81,24 +82,24 @@ export const SendWizard = () => {
memo: formState.memo, memo: formState.memo,
}) })
.then((res: TauriTxResult) => { .then((res: TauriTxResult) => {
const { details, tx_hash } = res // eslint-disable-next-line @typescript-eslint/naming-convention
const { details, tx_hash } = res;
setActiveStep((s) => s + 1) setActiveStep((s) => s + 1);
setConfirmedData({ setConfirmedData({
...details, ...details,
amount: { denom: 'Major', amount: formState.amount }, amount: { denom: 'Major', amount: formState.amount },
tx_hash, tx_hash,
}) });
setIsLoading(false) setIsLoading(false);
userBalance.fetchBalance() userBalance.fetchBalance();
}) })
.catch((e) => { .catch((e) => {
setRequestError(e) setRequestError(e);
setIsLoading(false) setIsLoading(false);
console.log(e) console.error(e);
}) });
} };
}
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
@@ -109,8 +110,8 @@ export const SendWizard = () => {
p: 2, p: 2,
}} }}
> >
{steps.map((s, i) => ( {steps.map((s) => (
<Step key={i}> <Step key={s}>
<StepLabel>{s}</StepLabel> <StepLabel>{s}</StepLabel>
</Step> </Step>
))} ))}
@@ -126,7 +127,7 @@ export const SendWizard = () => {
}} }}
> >
{activeStep === 0 ? ( {activeStep === 0 ? (
<SendForm transferFee={transferFee} /> <SendForm />
) : activeStep === 1 ? ( ) : activeStep === 1 ? (
<SendReview transferFee={transferFee} /> <SendReview transferFee={transferFee} />
) : ( ) : (
@@ -160,5 +161,5 @@ export const SendWizard = () => {
</Box> </Box>
</Box> </Box>
</FormProvider> </FormProvider>
) );
} };
+8 -8
View File
@@ -1,16 +1,16 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { SendWizard } from './SendWizard' import { SendWizard } from './SendWizard';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Send = () => { export const Send = () => {
const { currency } = useContext(ClientContext) const { currency } = useContext(ClientContext);
return ( return (
<PageLayout> <PageLayout>
<NymCard title={`Send ${currency?.major}`} noPadding> <NymCard title={`Send ${currency?.major}`} noPadding>
<SendWizard /> <SendWizard />
</NymCard> </NymCard>
</PageLayout> </PageLayout>
) );
} };
@@ -1,11 +1,9 @@
import * as Yup from 'yup' import * as Yup from 'yup';
import { validateAmount } from '../../utils' import { validateAmount } from '../../utils';
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
to: Yup.string().strict().trim('Cannot have leading space').required(), to: Yup.string().strict().trim('Cannot have leading space').required(),
amount: Yup.string() amount: Yup.string()
.required() .required()
.test('valid-amount', 'A valid amount is required', (amount) => { .test('valid-amount', 'A valid amount is required', (amount) => validateAmount(amount || '0', '0')),
return validateAmount(amount || '0', '0') });
}),
})
+20 -20
View File
@@ -1,27 +1,27 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react';
import { Alert, Box, Dialog } from '@mui/material' import { Alert, Box, Dialog } from '@mui/material';
import { NymCard } from '../../components' import { NymCard } from '../../components';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { Tabs } from './tabs' import { Tabs } from './tabs';
import { Profile } from './profile' import { Profile } from './profile';
import { SystemVariables } from './system-variables' import { SystemVariables } from './system-variables';
import { NodeStats } from './node-stats' import { NodeStats } from './node-stats';
import { useSettingsState } from './useSettingsState' import { useSettingsState } from './useSettingsState';
import { NodeStatus } from '../../components/NodeStatus' import { NodeStatus } from '../../components/NodeStatus';
import { Node as NodeIcon } from '../../svg-icons/node' import { Node as NodeIcon } from '../../svg-icons/node';
const tabs = ['Profile', 'System variables', 'Node stats'] const tabs = ['Profile', 'System variables', 'Node stats'];
export const Settings = () => { export const Settings = () => {
const [selectedTab, setSelectedTab] = useState(0) const [selectedTab, setSelectedTab] = useState(0);
const { mixnodeDetails, showSettings, handleShowSettings, getBondDetails } = useContext(ClientContext) const { mixnodeDetails, showSettings, handleShowSettings, getBondDetails } = useContext(ClientContext);
const { status, saturation, rewardEstimation, inclusionProbability } = useSettingsState(showSettings) const { status, saturation, rewardEstimation, inclusionProbability } = useSettingsState(showSettings);
const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab) const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab);
return showSettings ? ( return showSettings ? (
<Dialog open={true} onClose={handleShowSettings} maxWidth="md" fullWidth> <Dialog open onClose={handleShowSettings} maxWidth="md" fullWidth>
<NymCard <NymCard
title={ title={
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
@@ -36,7 +36,7 @@ export const Settings = () => {
<Tabs tabs={tabs} selectedTab={selectedTab} onChange={handleTabChange} disabled={!mixnodeDetails} /> <Tabs tabs={tabs} selectedTab={selectedTab} onChange={handleTabChange} disabled={!mixnodeDetails} />
{!mixnodeDetails && ( {!mixnodeDetails && (
<Alert severity="info" sx={{ m: 4 }}> <Alert severity="info" sx={{ m: 4 }}>
You don't currently have a node running You do not currently have a node running
</Alert> </Alert>
)} )}
{selectedTab === 0 && mixnodeDetails && <Profile />} {selectedTab === 0 && mixnodeDetails && <Profile />}
@@ -53,5 +53,5 @@ export const Settings = () => {
</> </>
</NymCard> </NymCard>
</Dialog> </Dialog>
) : null ) : null;
} };
+7 -7
View File
@@ -1,10 +1,10 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { OpenInNew } from '@mui/icons-material' import { OpenInNew } from '@mui/icons-material';
import { Button, Link, Stack, Typography } from '@mui/material' import { Button, Link, Stack, Typography } from '@mui/material';
import { urls, ClientContext} from '../../context/main' import { urls, ClientContext } from '../../context/main';
export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => { export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => {
const {network} = useContext(ClientContext) const { network } = useContext(ClientContext);
return ( return (
<Stack spacing={2} sx={{ p: 4 }}> <Stack spacing={2} sx={{ p: 4 }}>
<Typography>All your node stats are available on the link below</Typography> <Typography>All your node stats are available on the link below</Typography>
@@ -12,5 +12,5 @@ export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => {
<Button endIcon={<OpenInNew />}>Network Explorer</Button> <Button endIcon={<OpenInNew />}>Network Explorer</Button>
</Link> </Link>
</Stack> </Stack>
) );
} };
+6 -7
View File
@@ -1,10 +1,9 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { Button, Divider, Stack, TextField, Typography } from '@mui/material' import { Box, Button, Divider, Stack, TextField, Typography } from '@mui/material';
import { Box } from '@mui/system' import { ClientContext } from '../../context/main';
import { ClientContext } from '../../context/main'
export const Profile = () => { export const Profile = () => {
const { mixnodeDetails } = useContext(ClientContext) const { mixnodeDetails } = useContext(ClientContext);
return ( return (
<> <>
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
@@ -31,5 +30,5 @@ export const Profile = () => {
</Button> </Button>
</Box> </Box>
</> </>
) );
} };
@@ -1,18 +1,52 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react';
import { Box, Button, CircularProgress, Grid, LinearProgress, Stack, TextField, Typography } from '@mui/material' import { Box, Button, CircularProgress, Grid, LinearProgress, Stack, TextField, Typography } from '@mui/material';
import { PercentOutlined } from '@mui/icons-material' import { PercentOutlined } from '@mui/icons-material';
import { yupResolver } from '@hookform/resolvers/yup' import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form';
import { InfoTooltip } from '../../components/InfoToolTip' import { Fee, InfoTooltip } from '../../components';
import { InclusionProbabilityResponse, TMixnodeBondDetails } from '../../types' import { InclusionProbabilityResponse, TMixnodeBondDetails } from '../../types';
import { validationSchema } from './validationSchema' import { validationSchema } from './validationSchema';
import { updateMixnode } from '../../requests' import { updateMixnode } from '../../requests';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { Fee } from '../../components'
type TFormData = { type TFormData = {
profitMarginPercent: number profitMarginPercent: string;
} };
const DataField = ({ title, info, Indicator }: { title: string; info: string; Indicator: React.ReactElement }) => (
<Grid container justifyContent="space-between">
<Grid item xs={12} md={6}>
<Box display="flex" alignItems="center">
<InfoTooltip title={info} tooltipPlacement="right" />
<Typography sx={{ ml: 1 }}>{title}</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box display="flex" justifyContent="flex-end">
{Indicator}
</Box>
</Grid>
</Grid>
);
const PercentIndicator = ({ value, warning }: { value: number; warning?: boolean }) => (
<Grid container alignItems="center">
<Grid item xs={2}>
<Typography component="span" sx={{ color: warning ? 'error.main' : 'nym.fee', fontWeight: 600 }}>
{value}%
</Typography>
</Grid>
<Grid item xs={10}>
<LinearProgress
color="inherit"
sx={{ color: warning ? 'error.main' : 'nym.fee' }}
variant="determinate"
value={value < 100 ? value : 100}
/>
</Grid>
</Grid>
);
export const SystemVariables = ({ export const SystemVariables = ({
mixnodeDetails, mixnodeDetails,
@@ -21,13 +55,13 @@ export const SystemVariables = ({
inclusionProbability, inclusionProbability,
onUpdate, onUpdate,
}: { }: {
mixnodeDetails: TMixnodeBondDetails['mix_node'] mixnodeDetails: TMixnodeBondDetails['mix_node'];
saturation: number saturation: number;
rewardEstimation: number rewardEstimation: number;
inclusionProbability: InclusionProbabilityResponse inclusionProbability: InclusionProbabilityResponse;
onUpdate: () => void onUpdate: () => void;
}) => { }) => {
const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>() const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>();
const { const {
register, register,
@@ -36,21 +70,21 @@ export const SystemVariables = ({
} = useForm({ } = useForm({
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() }, defaultValues: { profitMarginPercent: mixnodeDetails.profit_margin_percent.toString() },
}) });
const { userBalance, currency } = useContext(ClientContext) const { userBalance, currency } = useContext(ClientContext);
const onSubmit = async (data: TFormData) => { const onSubmit = async (data: TFormData) => {
try { try {
await updateMixnode({ profitMarginPercent: data.profitMarginPercent }) await updateMixnode({ profitMarginPercent: +data.profitMarginPercent });
await userBalance.fetchBalance() await userBalance.fetchBalance();
onUpdate() onUpdate();
setNodeUpdateResponse('success') setNodeUpdateResponse('success');
} catch (e) { } catch (e) {
setNodeUpdateResponse('failed') setNodeUpdateResponse('failed');
console.log(e) console.error(e);
}
} }
};
return ( return (
<> <>
@@ -60,7 +94,7 @@ export const SystemVariables = ({
{...register('profitMarginPercent', { valueAsNumber: true })} {...register('profitMarginPercent', { valueAsNumber: true })}
label="Profit margin" label="Profit margin"
helperText={ helperText={
!!errors.profitMarginPercent errors.profitMarginPercent
? errors.profitMarginPercent.message ? errors.profitMarginPercent.message
: "The percentage of your delegators' rewards that you as the node operator will take" : "The percentage of your delegators' rewards that you as the node operator will take"
} }
@@ -125,42 +159,5 @@ export const SystemVariables = ({
</Button> </Button>
</Box> </Box>
</> </>
) );
} };
const DataField = ({ title, info, Indicator }: { title: string; info: string; Indicator: React.ReactElement }) => (
<Grid container justifyContent="space-between">
<Grid item xs={12} md={6}>
<Box display="flex" alignItems="center">
<InfoTooltip title={info} tooltipPlacement="right" />
<Typography sx={{ ml: 1 }}>{title}</Typography>
</Box>
</Grid>
<Grid item xs={12} md={6}>
<Box display="flex" justifyContent="flex-end">
{Indicator}
</Box>
</Grid>
</Grid>
)
const PercentIndicator = ({ value, warning }: { value: number; warning?: boolean }) => {
return (
<Grid container alignItems="center">
<Grid item xs={2}>
<Typography component="span" sx={{ color: warning ? 'error.main' : 'nym.fee', fontWeight: 600 }}>
{value}%
</Typography>
</Grid>
<Grid item xs={10}>
<LinearProgress
color="inherit"
sx={{ color: warning ? 'error.main' : 'nym.fee' }}
variant="determinate"
value={value < 100 ? value : 100}
/>
</Grid>
</Grid>
)
}
+10 -11
View File
@@ -1,12 +1,11 @@
import React from 'react' import React from 'react';
import { Box } from '@mui/system' import { Tab, Tabs as MuiTabs, Box } from '@mui/material';
import { Tab, Tabs as MuiTabs } from '@mui/material'
export const Tabs: React.FC<{ export const Tabs: React.FC<{
tabs: string[] tabs: string[];
selectedTab: number selectedTab: number;
disabled: boolean disabled: boolean;
onChange: (event: React.SyntheticEvent, tab: number) => void onChange: (event: React.SyntheticEvent, tab: number) => void;
}> = ({ tabs, selectedTab, disabled, onChange }) => ( }> = ({ tabs, selectedTab, disabled, onChange }) => (
<MuiTabs <MuiTabs
value={selectedTab} value={selectedTab}
@@ -14,10 +13,10 @@ export const Tabs: React.FC<{
sx={{ bgcolor: 'grey.200', borderTop: '1px solid', borderBottom: '1px solid', borderColor: 'grey.300' }} sx={{ bgcolor: 'grey.200', borderTop: '1px solid', borderBottom: '1px solid', borderColor: 'grey.300' }}
textColor="inherit" textColor="inherit"
> >
{tabs.map((tabName, index) => ( {tabs.map((tabName) => (
<Tab key={index} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} /> <Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} />
))} ))}
</MuiTabs> </MuiTabs>
) );
export const TabPanel: React.FC = ({ children }) => <Box sx={{ p: 4 }}>{children}</Box> export const TabPanel: React.FC = ({ children }) => <Box sx={{ p: 4 }}>{children}</Box>;
@@ -1,77 +1,79 @@
import { useContext, useEffect, useState } from 'react' import { useContext, useEffect, useState } from 'react';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { import {
getMixnodeRewardEstimation, getMixnodeRewardEstimation,
getMixnodeStakeSaturation, getMixnodeStakeSaturation,
getMixnodeStatus, getMixnodeStatus,
minorToMajor, minorToMajor,
getInclusionProbability, getInclusionProbability,
} from '../../requests' } from '../../requests';
import { MixnodeStatus, InclusionProbabilityResponse } from '../../types' import { MixnodeStatus, InclusionProbabilityResponse } from '../../types';
export const useSettingsState = (shouldUpdate: boolean) => { export const useSettingsState = (shouldUpdate: boolean) => {
const [status, setStatus] = useState<MixnodeStatus>('not_found') const [status, setStatus] = useState<MixnodeStatus>('not_found');
const [saturation, setSaturation] = useState<number>(0) const [saturation, setSaturation] = useState<number>(0);
const [rewardEstimation, setRewardEstimation] = useState<number>(0) const [rewardEstimation, setRewardEstimation] = useState<number>(0);
const [inclusionProbability, setInclusionProbability] = useState<InclusionProbabilityResponse>({ const [inclusionProbability, setInclusionProbability] = useState<InclusionProbabilityResponse>({
in_active: 0, in_active: 0,
in_reserve: 0, in_reserve: 0,
}) });
const { mixnodeDetails } = useContext(ClientContext) const { mixnodeDetails } = useContext(ClientContext);
const getStatus = async (mixnodeKey: string) => { const getStatus = async (mixnodeKey: string) => {
const status = await getMixnodeStatus(mixnodeKey) const newStatus = await getMixnodeStatus(mixnodeKey);
setStatus(status.status) setStatus(newStatus.status);
} };
const getStakeSaturation = async (mixnodeKey: string) => { const getStakeSaturation = async (mixnodeKey: string) => {
const saturation = await getMixnodeStakeSaturation(mixnodeKey) const newSaturation = await getMixnodeStakeSaturation(mixnodeKey);
if (saturation) { if (newSaturation) {
setSaturation(Math.round(saturation.saturation * 100)) setSaturation(Math.round(newSaturation.saturation * 100));
}
} }
};
const getRewardEstimation = async (mixnodeKey: string) => { const getRewardEstimation = async (mixnodeKey: string) => {
const rewardEstimation = await getMixnodeRewardEstimation(mixnodeKey) const newRewardEstimation = await getMixnodeRewardEstimation(mixnodeKey);
if (rewardEstimation) { if (newRewardEstimation) {
const toMajor = await minorToMajor(rewardEstimation.estimated_total_node_reward.toString()) const toMajor = await minorToMajor(newRewardEstimation.estimated_total_node_reward.toString());
setRewardEstimation(parseInt(toMajor.amount)) setRewardEstimation(parseInt(toMajor.amount, Number(10)));
}
} }
};
const getMixnodeInclusionProbability = async (mixnodeKey: string) => { const getMixnodeInclusionProbability = async (mixnodeKey: string) => {
const probability = await getInclusionProbability(mixnodeKey) const probability = await getInclusionProbability(mixnodeKey);
if (probability) { if (probability) {
const in_active = Math.round(probability.in_active * 100) // eslint-disable-next-line @typescript-eslint/naming-convention
const in_reserve = Math.round(probability.in_reserve * 100) const in_active = Math.round(probability.in_active * 100);
setInclusionProbability({ in_active, in_reserve }) // eslint-disable-next-line @typescript-eslint/naming-convention
} const in_reserve = Math.round(probability.in_reserve * 100);
setInclusionProbability({ in_active, in_reserve });
} }
};
const reset = () => { const reset = () => {
setStatus('not_found') setStatus('not_found');
setSaturation(0) setSaturation(0);
setRewardEstimation(0) setRewardEstimation(0);
setInclusionProbability({ in_active: 0, in_reserve: 0 }) setInclusionProbability({ in_active: 0, in_reserve: 0 });
} };
useEffect(() => { useEffect(() => {
if (shouldUpdate && mixnodeDetails?.mix_node.identity_key) { if (shouldUpdate && mixnodeDetails?.mix_node.identity_key) {
getStatus(mixnodeDetails?.mix_node.identity_key) getStatus(mixnodeDetails?.mix_node.identity_key);
getStakeSaturation(mixnodeDetails?.mix_node.identity_key) getStakeSaturation(mixnodeDetails?.mix_node.identity_key);
getRewardEstimation(mixnodeDetails?.mix_node.identity_key) getRewardEstimation(mixnodeDetails?.mix_node.identity_key);
getMixnodeInclusionProbability(mixnodeDetails?.mix_node.identity_key) getMixnodeInclusionProbability(mixnodeDetails?.mix_node.identity_key);
} else { } else {
reset() reset();
} }
}, [shouldUpdate]) }, [shouldUpdate]);
return { return {
status, status,
saturation, saturation,
rewardEstimation, rewardEstimation,
inclusionProbability, inclusionProbability,
} };
} };
@@ -1,5 +1,5 @@
import * as Yup from 'yup' import * as Yup from 'yup';
export const validationSchema = Yup.object({ export const validationSchema = Yup.object({
profitMarginPercent: Yup.number().typeError('profit margin percent must be a number').min(0).max(100).required(), profitMarginPercent: Yup.number().typeError('profit margin percent must be a number').min(0).max(100).required(),
}) });
+23 -23
View File
@@ -1,22 +1,22 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Button, CircularProgress } from '@mui/material' import { Alert, Box, Button, CircularProgress } from '@mui/material';
import { Fee, NymCard } from '../../components' import { Fee, NymCard } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership' import { useCheckOwnership } from '../../hooks/useCheckOwnership';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { unbond } from '../../requests' import { unbond } from '../../requests';
import { PageLayout } from '../../layouts' import { PageLayout } from '../../layouts';
export const Unbond = () => { export const Unbond = () => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const { checkOwnership, ownership } = useCheckOwnership() const { checkOwnership, ownership } = useCheckOwnership();
const { userBalance, getBondDetails } = useContext(ClientContext) const { userBalance, getBondDetails } = useContext(ClientContext);
useEffect(() => { useEffect(() => {
const initialiseForm = async () => { const initialiseForm = async () => {
await checkOwnership() await checkOwnership();
} };
initialiseForm() initialiseForm();
}, [ownership.hasOwnership, checkOwnership]) }, [ownership.hasOwnership, checkOwnership]);
return ( return (
<PageLayout> <PageLayout>
@@ -31,12 +31,12 @@ export const Unbond = () => {
data-testid="un-bond" data-testid="un-bond"
disabled={isLoading} disabled={isLoading}
onClick={async () => { onClick={async () => {
setIsLoading(true) setIsLoading(true);
await unbond(ownership.nodeType!) await unbond(ownership.nodeType!);
await userBalance.fetchBalance() await userBalance.fetchBalance();
await getBondDetails() await getBondDetails();
await checkOwnership() await checkOwnership();
setIsLoading(false) setIsLoading(false);
}} }}
color="inherit" color="inherit"
> >
@@ -54,7 +54,7 @@ export const Unbond = () => {
</> </>
) : ( ) : (
<Alert severity="info" sx={{ m: 3 }} data-testid="no-bond"> <Alert severity="info" sx={{ m: 3 }} data-testid="no-bond">
You don't currently have a bonded node You do not currently have a bonded node
</Alert> </Alert>
)} )}
{isLoading && ( {isLoading && (
@@ -71,5 +71,5 @@ export const Unbond = () => {
)} )}
</NymCard> </NymCard>
</PageLayout> </PageLayout>
) );
} };
@@ -1,33 +1,32 @@
import React, { useContext, useEffect } from 'react' import React, { useContext, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form';
import { Box, Autocomplete, Button, CircularProgress, FormControl, Grid, TextField, Typography } from '@mui/material' import { Autocomplete, Box, Button, CircularProgress, FormControl, Grid, TextField } from '@mui/material';
import { yupResolver } from '@hookform/resolvers/yup' import { yupResolver } from '@hookform/resolvers/yup';
import { validationSchema } from './validationSchema' import { validationSchema } from './validationSchema';
import { EnumNodeType, TDelegation, TFee } from '../../types' import { EnumNodeType, TDelegation, TFee } from '../../types';
import { ClientContext } from '../../context/main' import { ClientContext } from '../../context/main';
import { undelegate } from '../../requests' import { undelegate } from '../../requests';
import { Fee } from '../../components' import { Fee } from '../../components';
type TFormData = { type TFormData = {
nodeType: EnumNodeType nodeType: EnumNodeType;
identity: string identity: string;
} };
const defaultValues = { const defaultValues = {
nodeType: EnumNodeType.mixnode, nodeType: EnumNodeType.mixnode,
identity: '', identity: '',
} };
export const UndelegateForm = ({ export const UndelegateForm = ({
fees,
delegations, delegations,
onError, onError,
onSuccess, onSuccess,
}: { }: {
fees: TFee fees: TFee;
delegations?: TDelegation[] delegations?: TDelegation[];
onError: (message?: string) => void onError: (message?: string) => void;
onSuccess: (message?: string) => void onSuccess: (message?: string) => void;
}) => { }) => {
const { const {
control, control,
@@ -38,14 +37,14 @@ export const UndelegateForm = ({
} = useForm<TFormData>({ } = useForm<TFormData>({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}) });
const watchNodeType = watch('nodeType') const watchNodeType = watch('nodeType');
useEffect(() => { useEffect(() => {
setValue('identity', '') setValue('identity', '');
}, [watchNodeType]) }, [watchNodeType]);
const { userBalance } = useContext(ClientContext) const { userBalance } = useContext(ClientContext);
const onSubmit = async (data: TFormData) => { const onSubmit = async (data: TFormData) => {
await undelegate({ await undelegate({
@@ -53,11 +52,11 @@ export const UndelegateForm = ({
identity: data.identity, identity: data.identity,
}) })
.then(async (res) => { .then(async (res) => {
onSuccess(`Successfully undelegated from ${res.target_address}`) onSuccess(`Successfully undelegated from ${res.target_address}`);
userBalance.fetchBalance() userBalance.fetchBalance();
}) })
.catch((e) => onError(e)) .catch((e) => onError(e));
} };
return ( return (
<FormControl fullWidth> <FormControl fullWidth>
@@ -118,5 +117,5 @@ export const UndelegateForm = ({
</Button> </Button>
</Box> </Box>
</FormControl> </FormControl>
) );
} };
+37 -38
View File
@@ -1,48 +1,47 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react';
import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material' import { Alert, AlertTitle, Box, Button, CircularProgress } from '@mui/material';
import { NymCard } from '../../components' import { EnumRequestStatus, NymCard, RequestStatus } from '../../components';
import { UndelegateForm } from './UndelegateForm' import { UndelegateForm } from './UndelegateForm';
import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus' import { getGasFee, getReverseMixDelegations } from '../../requests';
import { getGasFee, getReverseMixDelegations } from '../../requests' import { TFee, TPagedDelegations } from '../../types';
import { TFee, TPagedDelegations } from '../../types' import { ClientContext } from '../../context/main';
import { ClientContext } from '../../context/main' import { PageLayout } from '../../layouts';
import { PageLayout } from '../../layouts'
export const Undelegate = () => { export const Undelegate = () => {
const [message, setMessage] = useState<string>() const [message, setMessage] = useState<string>();
const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial) const [status, setStatus] = useState<EnumRequestStatus>(EnumRequestStatus.initial);
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true);
const [fees, setFees] = useState<TFee>() const [fees, setFees] = useState<TFee>();
const [pagedDelegations, setPagesDelegations] = useState<TPagedDelegations>() const [pagedDelegations, setPagesDelegations] = useState<TPagedDelegations>();
const { clientDetails } = useContext(ClientContext) const { clientDetails } = useContext(ClientContext);
useEffect(() => {
initialize()
}, [clientDetails])
const initialize = async () => { const initialize = async () => {
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
setIsLoading(true) setIsLoading(true);
try { try {
const [mixnodeFee, mixnodeDelegations] = await Promise.all([ const [mixnodeFee, mixnodeDelegations] = await Promise.all([
getGasFee('UndelegateFromMixnode'), getGasFee('UndelegateFromMixnode'),
getReverseMixDelegations(), getReverseMixDelegations(),
]) ]);
setFees({ setFees({
mixnode: mixnodeFee, mixnode: mixnodeFee,
}) });
setPagesDelegations(mixnodeDelegations) setPagesDelegations(mixnodeDelegations);
} catch (e) { } catch (e) {
setStatus(EnumRequestStatus.error) setStatus(EnumRequestStatus.error);
setMessage(e as string) setMessage(e as string);
} }
setIsLoading(false) setIsLoading(false);
} };
useEffect(() => {
initialize();
}, [clientDetails]);
return ( return (
<PageLayout> <PageLayout>
@@ -63,13 +62,13 @@ export const Undelegate = () => {
<UndelegateForm <UndelegateForm
fees={fees} fees={fees}
delegations={pagedDelegations?.delegations} delegations={pagedDelegations?.delegations}
onError={(message) => { onError={(m) => {
setMessage(message) setMessage(m);
setStatus(EnumRequestStatus.error) setStatus(EnumRequestStatus.error);
}} }}
onSuccess={(message) => { onSuccess={(m) => {
setMessage(message) setMessage(m);
setStatus(EnumRequestStatus.success) setStatus(EnumRequestStatus.success);
}} }}
/> />
)} )}
@@ -104,8 +103,8 @@ export const Undelegate = () => {
variant="contained" variant="contained"
disableElevation disableElevation
onClick={() => { onClick={() => {
setStatus(EnumRequestStatus.initial) setStatus(EnumRequestStatus.initial);
initialize() initialize();
}} }}
size="large" size="large"
> >
@@ -117,5 +116,5 @@ export const Undelegate = () => {
</> </>
</NymCard> </NymCard>
</PageLayout> </PageLayout>
) );
} };
@@ -1,10 +1,8 @@
import * as Yup from 'yup' import * as Yup from 'yup';
import { validateKey } from '../../utils' import { validateKey } from '../../utils';
export const validationSchema = Yup.object().shape({ export const validationSchema = Yup.object().shape({
identity: Yup.string() identity: Yup.string()
.required() .required()
.test('valid-id-key', 'A valid identity key is required', function (value) { .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
return validateKey(value || '', 32) });
}),
})
@@ -1,28 +1,28 @@
import React, { useEffect, useState } from 'react' /* eslint-disable react/no-unused-prop-types */
import { Alert, Button, Card, CardActions, CardContent, CardHeader, Stack, Typography } from '@mui/material' import React, { useEffect, useState } from 'react';
import { createAccount } from '../../requests' import { Alert, Button, Card, CardActions, CardContent, CardHeader, Stack, Typography } from '@mui/material';
import { TCreateAccount } from '../../types' import { createAccount } from '../../requests';
import { CopyToClipboard } from '../../components' import { TCreateAccount } from '../../types';
import { CopyToClipboard } from '../../components';
import { TPages } from './types';
export const CreateAccountContent: React.FC<{ page: 'legacy create account'; showSignIn: () => void }> = ({ export const CreateAccountContent: React.FC<{ page: TPages; showSignIn: () => void }> = ({ page, showSignIn }) => {
showSignIn, const [accountDetails, setAccountDetails] = useState<TCreateAccount>();
}) => { const [error, setError] = useState<Error>();
const [accountDetails, setAccountDetails] = useState<TCreateAccount>()
const [error, setError] = useState<Error>()
const handleCreateAccount = async () => { const handleCreateAccount = async () => {
setError(undefined) setError(undefined);
try { try {
const account = await createAccount() const account = await createAccount();
setAccountDetails(account) setAccountDetails(account);
} catch (e: any) { } catch (e: any) {
setError(e) setError(e);
}
} }
};
useEffect(() => { useEffect(() => {
handleCreateAccount() handleCreateAccount();
}, []) }, []);
return ( return (
<Stack spacing={4} alignItems="center" sx={{ width: 700 }}> <Stack spacing={4} alignItems="center" sx={{ width: 700 }}>
@@ -33,7 +33,7 @@ export const CreateAccountContent: React.FC<{ page: 'legacy create account'; sho
Account setup complete! Account setup complete!
</Typography> </Typography>
<Alert severity="info" variant="outlined" sx={{ color: 'info.light' }} data-testid="mnemonic-warning"> <Alert severity="info" variant="outlined" sx={{ color: 'info.light' }} data-testid="mnemonic-warning">
<Typography>Please store your mnemonic in a safe place. You'll need it to access your account!</Typography> <Typography>Please store your mnemonic in a safe place. You will need it to access your account!</Typography>
</Alert> </Alert>
<Card variant="outlined" sx={{ bgcolor: 'transparent', p: 2, borderColor: 'common.white' }}> <Card variant="outlined" sx={{ bgcolor: 'transparent', p: 2, borderColor: 'common.white' }}>
<CardHeader sx={{ color: 'common.white' }} title="Mnemonic" /> <CardHeader sx={{ color: 'common.white' }} title="Mnemonic" />
@@ -53,5 +53,5 @@ export const CreateAccountContent: React.FC<{ page: 'legacy create account'; sho
Sign in Sign in
</Button> </Button>
</Stack> </Stack>
) );
} };
@@ -1,32 +1,29 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react';
import { Button, CircularProgress, Grid, Stack, TextField, Typography, Alert } from '@mui/material' import { Alert, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
import { styled } from '@mui/material/styles' import { ClientContext } from '../../context/main';
import { signInWithMnemonic } from '../../requests' import { NymLogo } from '../../components';
import { ClientContext } from '../../context/main'
import { NymLogo } from '../../components'
export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ showCreateAccount }) => { export const SignInContent: React.FC = () => {
const [mnemonic, setMnemonic] = useState<string>('') const [mnemonic] = useState<string>('');
const [inputError, setInputError] = useState<string>() const [inputError, setInputError] = useState<string>();
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const { logIn } = useContext(ClientContext) const { logIn } = useContext(ClientContext);
const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => { const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault() e.preventDefault();
setIsLoading(true) setIsLoading(true);
setInputError(undefined) setInputError(undefined);
try { try {
await signInWithMnemonic(mnemonic || '') await logIn(mnemonic || '');
setIsLoading(false) setIsLoading(false);
logIn() } catch (error: any) {
} catch (e: any) { setIsLoading(false);
setIsLoading(false) setInputError(error);
setInputError(e)
}
} }
};
return ( return (
<Stack spacing={3} alignItems="center" sx={{ width: '80%' }}> <Stack spacing={3} alignItems="center" sx={{ width: '80%' }}>
@@ -65,25 +62,5 @@ export const SignInContent: React.FC<{ showCreateAccount: () => void }> = ({ sho
)} )}
</Grid> </Grid>
</Stack> </Stack>
) );
} };
const StyledInput = styled((props) => <TextField {...props} />)(({ theme }) => ({
'& input': {
color: theme.palette.nym.text.light,
},
'& label': {
color: theme.palette.nym.text.light,
},
'& label.Mui-focused': {
color: theme.palette.primary.main,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: theme.palette.common.white,
},
'&:hover fieldset': {
borderColor: theme.palette.primary.main,
},
},
}))
@@ -1,16 +1,16 @@
import React from 'react' import React from 'react';
import { Typography } from '@mui/material' import { Typography } from '@mui/material';
export const Title = ({ title }: { title: string }) => ( export const Title = ({ title }: { title: string }) => (
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>{title}</Typography> <Typography sx={{ color: 'common.white', fontWeight: 600 }}>{title}</Typography>
) );
export const Subtitle = ({ subtitle }: { subtitle: string }) => ( export const Subtitle = ({ subtitle }: { subtitle: string }) => (
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 400 }}>{subtitle}</Typography> <Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 400 }}>{subtitle}</Typography>
) );
export const SubtitleSlick = ({ subtitle }: { subtitle: string }) => ( export const SubtitleSlick = ({ subtitle }: { subtitle: string }) => (
<Typography variant="caption" sx={{ color: 'grey.600', textTransform: 'uppercase', letterSpacing: 4 }}> <Typography variant="caption" sx={{ color: 'grey.600', textTransform: 'uppercase', letterSpacing: 4 }}>
{subtitle} {subtitle}
</Typography> </Typography>
) );
@@ -1,4 +1,4 @@
export * from './heading' export * from './heading';
export * from './word-tiles' export * from './word-tiles';
export * from './render-page' export * from './render-page';
export * from './password-strength' export * from './password-strength';
@@ -1,64 +1,65 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react';
import { LockOutlined } from '@mui/icons-material' import { LockOutlined } from '@mui/icons-material';
import { LinearProgress, Stack, Typography } from '@mui/material' import { LinearProgress, Stack, Typography, Box } from '@mui/material';
import { Box } from '@mui/system'
type TStrength = 'weak' | 'medium' | 'strong' | 'init' type TStrength = 'weak' | 'medium' | 'strong' | 'init';
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/ const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/ const medium = /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/;
const colorMap = { const colorMap = {
init: "inherit" as "inherit", init: 'inherit' as 'inherit',
weak: 'error' as 'error', weak: 'error' as 'error',
medium: 'warning' as 'warning', medium: 'warning' as 'warning',
strong: 'success' as 'success', strong: 'success' as 'success',
} };
const getText = (strength: TStrength) => { const getText = (strength: TStrength) => {
switch (strength) { switch (strength) {
case 'strong': case 'strong':
return 'Strong password' return 'Strong password';
case 'medium': case 'medium':
return 'Medium strength password' return 'Medium strength password';
case 'weak': case 'weak':
return 'Weak password' return 'Weak password';
default: default:
return 'Password strength' return 'Password strength';
} }
} };
const getTextColor = (strength: TStrength) => { const getTextColor = (strength: TStrength) => {
switch (strength) { switch (strength) {
case 'strong': case 'strong':
return 'success.main' return 'success.main';
case 'medium': case 'medium':
return 'warning.main' return 'warning.main';
case 'weak': case 'weak':
return 'error.main' return 'error.main';
default: default:
return 'grey.500' return 'grey.500';
} }
} };
export const PasswordStrength = ({ password }: { password: string }) => { export const PasswordStrength = ({ password }: { password: string }) => {
const [strength, setStrength] = useState<TStrength>('init') const [strength, setStrength] = useState<TStrength>('init');
useEffect(() => { useEffect(() => {
if (password.length === 0) { if (password.length === 0) {
return setStrength('init') setStrength('init');
return;
} }
if (password.match(strong)) { if (password.match(strong)) {
return setStrength('strong') setStrength('strong');
return;
} }
if (password.match(medium)) { if (password.match(medium)) {
return setStrength('medium') setStrength('medium');
return;
} }
setStrength('weak') setStrength('weak');
}, [password]) }, [password]);
return ( return (
<Stack spacing={0.5}> <Stack spacing={0.5}>
@@ -74,5 +75,5 @@ export const PasswordStrength = ({ password }: { password: string }) => {
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
) );
} };
@@ -1,11 +1,11 @@
import React from 'react' import React from 'react';
import { TPages } from '../types' import { TPages } from '../types';
export const RenderPage = ({ children, page }: { children: React.ReactElement[]; page: TPages }) => ( export const RenderPage = ({ children, page }: { children: React.ReactElement[]; page: TPages }) => (
<> <>
{React.Children.map(children, (Child) => { {React.Children.map(children, (Child) => {
if (page === Child?.props.page) return Child if (page === Child?.props.page) return Child;
return null return null;
})} })}
</> </>
) );
@@ -1,30 +1,6 @@
import React from 'react' import React from 'react';
import { Box, Card, CardHeader, Grid, Typography, Stack, Fade } from '@mui/material' import { Box, Card, CardHeader, Grid, Typography, Stack, Fade } from '@mui/material';
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWords } from '../types' import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWords } from '../types';
export const WordTiles = ({
mnemonicWords,
showIndex,
onClick,
}: {
mnemonicWords?: TMnemonicWords
showIndex?: boolean
onClick?: ({ name, index }: { name: string; index: number }) => void
}) => {
if (mnemonicWords) {
return (
<Grid container spacing={3} justifyContent="center">
{mnemonicWords.map(({ name, index, disabled }) => (
<Grid item xs={2} key={index} onClick={() => onClick?.({ name, index })}>
<WordTile mnemonicWord={name} index={showIndex ? index : undefined} onClick={!!onClick} disabled={disabled}/>
</Grid>
))}
</Grid>
)
}
return null
}
export const WordTile = ({ export const WordTile = ({
mnemonicWord, mnemonicWord,
@@ -32,10 +8,10 @@ export const WordTile = ({
disabled, disabled,
onClick, onClick,
}: { }: {
mnemonicWord: string mnemonicWord: string;
index?: number index?: number;
disabled?: boolean disabled?: boolean;
onClick?: boolean onClick?: boolean;
}) => ( }) => (
<Card <Card
variant="outlined" variant="outlined"
@@ -51,14 +27,56 @@ export const WordTile = ({
titleTypographyProps={{ sx: { fontWeight: 700 }, variant: 'body1', textAlign: index ? 'left' : 'center' }} titleTypographyProps={{ sx: { fontWeight: 700 }, variant: 'body1', textAlign: index ? 'left' : 'center' }}
avatar={ avatar={
index && ( index && (
<Typography variant="caption" color={'#3A4053'}> <Typography variant="caption" color="#3A4053">
{index} {index}
</Typography> </Typography>
) )
} }
/> />
</Card> </Card>
) );
export const WordTiles = ({
mnemonicWords,
showIndex,
onClick,
}: {
mnemonicWords?: TMnemonicWords;
showIndex?: boolean;
onClick?: ({ name, index }: { name: string; index: number }) => void;
}) => {
if (mnemonicWords) {
return (
<Grid container spacing={3} justifyContent="center">
{mnemonicWords.map(({ name, index, disabled }) => (
<Grid item xs={2} key={index} onClick={() => onClick?.({ name, index })}>
<WordTile
mnemonicWord={name}
index={showIndex ? index : undefined}
onClick={!!onClick}
disabled={disabled}
/>
</Grid>
))}
</Grid>
);
}
return null;
};
const HiddenWord = ({ mnemonicWord }: { mnemonicWord: THiddenMnemonicWord }) => (
<Stack spacing={2} alignItems="center">
<Box borderBottom="1px solid #3A4053" sx={{ p: 2, width: '100%' }}>
<Fade in={!mnemonicWord.hidden}>
<Box>
<WordTile mnemonicWord={mnemonicWord.name} />
</Box>
</Fade>
</Box>
<Typography>{mnemonicWord.index}.</Typography>
</Stack>
);
export const HiddenWords = ({ mnemonicWords }: { mnemonicWords?: THiddenMnemonicWords }) => { export const HiddenWords = ({ mnemonicWords }: { mnemonicWords?: THiddenMnemonicWords }) => {
if (mnemonicWords) { if (mnemonicWords) {
@@ -70,22 +88,7 @@ export const HiddenWords = ({ mnemonicWords }: { mnemonicWords?: THiddenMnemonic
</Grid> </Grid>
))} ))}
</Grid> </Grid>
) );
} }
return null return null;
} };
const HiddenWord = ({ mnemonicWord }: { mnemonicWord: THiddenMnemonicWord }) => {
return (
<Stack spacing={2} alignItems="center">
<Box borderBottom="1px solid #3A4053" sx={{ p: 2, width: '100%' }}>
<Fade in={!mnemonicWord.hidden}>
<Box>
<WordTile mnemonicWord={mnemonicWord.name} />
</Box>
</Fade>
</Box>
<Typography>{mnemonicWord.index}.</Typography>
</Stack>
)
}
+21 -22
View File
@@ -1,26 +1,25 @@
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react';
import { Box } from '@mui/system' import { CircularProgress, Stack, Box } from '@mui/material';
import { CircularProgress, Stack } from '@mui/material' import { ExistingAccount, WelcomeContent } from './pages';
import { WelcomeContent, VerifyMnemonic, MnemonicWords, CreatePassword, ExistingAccount } from './pages' import { NymLogo } from '../../components';
import { NymLogo } from '../../components' import { TPages } from './types';
import { TMnemonicWords, TPages } from './types' import { RenderPage } from './components';
import { RenderPage } from './components' import { CreateAccountContent } from './_legacy_create-account';
import { CreateAccountContent } from './_legacy_create-account' import { ClientContext } from '../../context/main';
import { ClientContext } from '../../context/main'
const testMnemonic = // const testMnemonic =
'futuristic big receptive caption saw hug odd spoon internal dime bike rake helpless left distribution gusty eyes beg enormous word influence trashy pets curl' // 'futuristic big receptive caption saw hug odd spoon internal dime bike rake helpless left distribution gusty eyes beg enormous word influence trashy pets curl';
//
const mnemonicToArray = (mnemonic: string): TMnemonicWords => // const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
mnemonic // mnemonic
.split(' ') // .split(' ')
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords) // .reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const Welcome = () => { export const Welcome = () => {
const [page, setPage] = useState<TPages>('welcome') const [page, setPage] = useState<TPages>('welcome');
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>() // const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>();
const { isLoading } = useContext(ClientContext) const { isLoading } = useContext(ClientContext);
// useEffect(() => { // useEffect(() => {
// const mnemonicArray = mnemonicToArray(testMnemonic) // const mnemonicArray = mnemonicToArray(testMnemonic)
@@ -72,11 +71,11 @@ export const Welcome = () => {
page="verify mnemonic" page="verify mnemonic"
/> />
<CreatePassword page="create password" /> */} <CreatePassword page="create password" /> */}
<ExistingAccount page="existing account" onPrev={() => setPage('welcome')} /> <ExistingAccount onPrev={() => setPage('welcome')} page="existing account" />
</RenderPage> </RenderPage>
</Stack> </Stack>
)} )}
</Box> </Box>
</Box> </Box>
) );
} };
@@ -1,13 +1,13 @@
import React, { useState } from 'react' import React, { useState } from 'react';
import { Button, FormControl, Grid, IconButton, Stack, TextField } from '@mui/material' import { Button, FormControl, Grid, IconButton, Stack, TextField } from '@mui/material';
import { VisibilityOff, Visibility } from '@mui/icons-material' import { VisibilityOff, Visibility } from '@mui/icons-material';
import { Subtitle, Title, PasswordStrength } from '../components' import { Subtitle, Title, PasswordStrength } from '../components';
export const CreatePassword = ({}: { page: 'create password' }) => { export const CreatePassword = () => {
const [password, setPassword] = useState<string>('') const [password, setPassword] = useState<string>('');
const [confirmedPassword, setConfirmedPassword] = useState<string>() const [confirmedPassword, setConfirmedPassword] = useState<string>();
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false);
const [showConfirmedPassword, setShowConfirmedPassword] = useState(false) const [showConfirmedPassword, setShowConfirmedPassword] = useState(false);
return ( return (
<> <>
@@ -56,5 +56,5 @@ export const CreatePassword = ({}: { page: 'create password' }) => {
</Grid> </Grid>
</Grid> </Grid>
</> </>
) );
} };
@@ -1,16 +1,18 @@
import React, { useContext, useState } from 'react' /* eslint-disable react/no-unused-prop-types */
import { Alert, Button, Stack, TextField } from '@mui/material' import React, { useContext, useState } from 'react';
import { Subtitle } from '../components' import { Alert, Button, Stack, TextField } from '@mui/material';
import { ClientContext } from '../../../context/main' import { Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
import { TPages } from '../types';
export const ExistingAccount: React.FC<{ page: 'existing account'; onPrev: () => void }> = ({ onPrev }) => { export const ExistingAccount: React.FC<{ page: TPages; onPrev: () => void }> = ({ onPrev }) => {
const [mnemonic, setMnemonic] = useState<string>('') const [mnemonic, setMnemonic] = useState<string>('');
const { logIn, error } = useContext(ClientContext) const { logIn, error } = useContext(ClientContext);
const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => { const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault() e.preventDefault();
logIn(mnemonic) logIn(mnemonic);
} };
return ( return (
<Stack spacing={2} sx={{ width: 400 }} alignItems="center"> <Stack spacing={2} sx={{ width: 400 }} alignItems="center">
@@ -36,5 +38,5 @@ export const ExistingAccount: React.FC<{ page: 'existing account'; onPrev: () =>
Back Back
</Button> </Button>
</Stack> </Stack>
) );
} };
+5 -5
View File
@@ -1,5 +1,5 @@
export * from './welcome-content' export * from './welcome-content';
export * from './mnemonic-words' export * from './mnemonic-words';
export * from './verify-mnemonic' export * from './verify-mnemonic';
export * from './create-password' export * from './create-password';
export * from './existing-account' export * from './existing-account';
@@ -1,19 +1,17 @@
import React from 'react' import React from 'react';
import { Alert, Button, Typography } from '@mui/material' import { Alert, Button, Typography } from '@mui/material';
import { WordTiles } from '../components/word-tiles' import { WordTiles } from '../components';
import { TMnemonicWords } from '../types' import { TMnemonicWords } from '../types';
export const MnemonicWords = ({ export const MnemonicWords = ({
mnemonicWords, mnemonicWords,
onNext, onNext,
onPrev, onPrev,
}: { }: {
mnemonicWords?: TMnemonicWords mnemonicWords?: TMnemonicWords;
page: 'create account' onNext: () => void;
onNext: () => void onPrev: () => void;
onPrev: () => void }) => (
}) => {
return (
<> <>
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Write down your mnemonic</Typography> <Typography sx={{ color: 'common.white', fontWeight: 600 }}>Write down your mnemonic</Typography>
<Alert icon={false} severity="info" sx={{ bgcolor: '#18263B', color: '#50ABFF', width: 625 }}> <Alert icon={false} severity="info" sx={{ bgcolor: '#18263B', color: '#50ABFF', width: 625 }}>
@@ -33,5 +31,4 @@ export const MnemonicWords = ({
Back Back
</Button> </Button>
</> </>
) );
}
@@ -1,45 +1,43 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react';
import { Button } from '@mui/material' import { Button } from '@mui/material';
import { WordTiles, HiddenWords } from '../components/word-tiles' import { HiddenWords, Subtitle, Title, WordTiles } from '../components';
import { THiddenMnemonicWords, THiddenMnemonicWord, TMnemonicWord, TMnemonicWords } from '../types' import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWord, TMnemonicWords } from '../types';
import { randomNumberBetween } from '../../../utils' import { randomNumberBetween } from '../../../utils';
import { Title, Subtitle } from '../components'
const numberOfRandomWords = 4 const numberOfRandomWords = 4;
export const VerifyMnemonic = ({ export const VerifyMnemonic = ({
mnemonicWords, mnemonicWords,
onComplete, onComplete,
}: { }: {
page: 'verify mnemonic' mnemonicWords?: TMnemonicWords;
mnemonicWords?: TMnemonicWords onComplete: () => void;
onComplete: () => void
}) => { }) => {
const [randomWords, setRandomWords] = useState<TMnemonicWords>() const [randomWords, setRandomWords] = useState<TMnemonicWords>();
const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>() const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>();
const [currentSelection, setCurrentSelection] = useState(0) const [currentSelection, setCurrentSelection] = useState(0);
useEffect(() => { useEffect(() => {
if (mnemonicWords) { if (mnemonicWords) {
const randomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords) const newRandomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords);
const withHiddenProperty = randomWords.map((word) => ({ ...word, hidden: true })) const withHiddenProperty = newRandomWords.map((word) => ({ ...word, hidden: true }));
const shuffled = getRandomEntriesFromArray<THiddenMnemonicWord>(withHiddenProperty, numberOfRandomWords) const shuffled = getRandomEntriesFromArray<THiddenMnemonicWord>(withHiddenProperty, numberOfRandomWords);
setRandomWords(randomWords) setRandomWords(newRandomWords);
setHiddenRandomWords(shuffled) setHiddenRandomWords(shuffled);
} }
}, [mnemonicWords]) }, [mnemonicWords]);
const revealWord = ({ name }: { name: string }) => { const revealWord = ({ name }: { name: string }) => {
if (name === hiddenRandomWords![currentSelection].name) { if (name === hiddenRandomWords![currentSelection].name) {
setHiddenRandomWords((hiddenWords) => setHiddenRandomWords((hiddenWords) =>
hiddenWords?.map((word) => (word.name === name ? { ...word, hidden: false } : word)), hiddenWords?.map((word) => (word.name === name ? { ...word, hidden: false } : word)),
) );
setRandomWords((randomWords) => setRandomWords((argRandomWords) =>
randomWords?.map((word) => (word.name === name ? { ...word, disabled: true } : word)), argRandomWords?.map((word) => (word.name === name ? { ...word, disabled: true } : word)),
) );
setCurrentSelection((current) => current + 1) setCurrentSelection((current) => current + 1);
}
} }
};
if (randomWords && hiddenRandomWords) { if (randomWords && hiddenRandomWords) {
return ( return (
@@ -61,20 +59,20 @@ export const VerifyMnemonic = ({
Next Next
</Button> </Button>
</> </>
) );
} }
return null return null;
} };
function getRandomEntriesFromArray<T>(arr: T[], numberOfEntries: number) { function getRandomEntriesFromArray<T>(arr: T[], numberOfEntries: number) {
const init = [...arr] const init = [...arr];
let randomEntries: T[] = [] const randomEntries: T[] = [];
while (randomEntries.length !== numberOfEntries) { while (randomEntries.length !== numberOfEntries) {
const rand = randomNumberBetween(0, init.length - 1) const rand = randomNumberBetween(0, init.length - 1);
randomEntries.push(init[rand]) randomEntries.push(init[rand]);
init.splice(rand, 1) init.splice(rand, 1);
} }
return randomEntries return randomEntries;
} }
@@ -1,13 +1,14 @@
import React from 'react' /* eslint-disable react/no-unused-prop-types */
import { Button, Stack } from '@mui/material' import React from 'react';
import { SubtitleSlick, Title } from '../components' import { Button, Stack } from '@mui/material';
import { SubtitleSlick, Title } from '../components';
import { TPages } from '../types';
export const WelcomeContent: React.FC<{ export const WelcomeContent: React.FC<{
page: 'welcome' page: TPages;
onUseExisting: () => void onUseExisting: () => void;
onCreateAccountComplete: () => void onCreateAccountComplete: () => void;
}> = ({ onUseExisting, onCreateAccountComplete }) => { }> = ({ onUseExisting, onCreateAccountComplete }) => (
return (
<> <>
<Title title="Welcome to NYM" /> <Title title="Welcome to NYM" />
<SubtitleSlick subtitle="Next generation of privacy" /> <SubtitleSlick subtitle="Next generation of privacy" />
@@ -38,5 +39,4 @@ export const WelcomeContent: React.FC<{
</Button> </Button>
</Stack> </Stack>
</> </>
) );
}
+8 -8
View File
@@ -5,15 +5,15 @@ export type TPages =
| 'create password' | 'create password'
| 'existing account' | 'existing account'
| 'select network' | 'select network'
| 'legacy create account' | 'legacy create account';
export type TMnemonicWord = { export type TMnemonicWord = {
name: string name: string;
index: number index: number;
disabled: boolean disabled: boolean;
} };
export type TMnemonicWords = TMnemonicWord[] export type TMnemonicWords = TMnemonicWord[];
export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord;
export type THiddenMnemonicWords = THiddenMnemonicWord[] export type THiddenMnemonicWords = THiddenMnemonicWord[];
+5 -5
View File
@@ -1,9 +1,9 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { Account, TCreateAccount } from '../types' import { Account, TCreateAccount } from '../types';
export const createAccount = async (): Promise<TCreateAccount> => await invoke('create_new_account') export const createAccount = async (): Promise<TCreateAccount> => invoke('create_new_account');
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> => export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
await invoke('connect_with_mnemonic', { mnemonic }) invoke('connect_with_mnemonic', { mnemonic });
export const signOut = async () => await invoke('logout') export const signOut = async () => invoke('logout');
+17 -17
View File
@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { Coin, DelegationResult, EnumNodeType, Gateway, MixNode, TauriTxResult } from '../types' import { Coin, DelegationResult, EnumNodeType, Gateway, MixNode, TauriTxResult } from '../types';
export const bond = async ({ export const bond = async ({
type, type,
@@ -7,34 +7,34 @@ export const bond = async ({
pledge, pledge,
ownerSignature, ownerSignature,
}: { }: {
type: EnumNodeType type: EnumNodeType;
data: MixNode | Gateway data: MixNode | Gateway;
pledge: Coin pledge: Coin;
ownerSignature: string ownerSignature: string;
}): Promise<any> => await invoke(`bond_${type}`, { [type]: data, ownerSignature, pledge }) }): Promise<any> => invoke(`bond_${type}`, { [type]: data, ownerSignature, pledge });
export const unbond = async (type: EnumNodeType) => await invoke(`unbond_${type}`) export const unbond = async (type: EnumNodeType) => invoke(`unbond_${type}`);
export const delegate = async ({ export const delegate = async ({
type, type,
identity, identity,
amount, amount,
}: { }: {
type: EnumNodeType type: EnumNodeType;
identity: string identity: string;
amount: Coin amount: Coin;
}): Promise<DelegationResult> => await invoke(`delegate_to_${type}`, { identity, amount }) }): Promise<DelegationResult> => invoke(`delegate_to_${type}`, { identity, amount });
export const undelegate = async ({ export const undelegate = async ({
type, type,
identity, identity,
}: { }: {
type: EnumNodeType type: EnumNodeType;
identity: string identity: string;
}): Promise<DelegationResult> => await invoke(`undelegate_from_${type}`, { identity }) }): Promise<DelegationResult> => invoke(`undelegate_from_${type}`, { identity });
export const send = async (args: { amount: Coin; address: string; memo: string }): Promise<TauriTxResult> => export const send = async (args: { amount: Coin; address: string; memo: string }): Promise<TauriTxResult> =>
await invoke('send', args) invoke('send', args);
export const updateMixnode = async ({ profitMarginPercent }: { profitMarginPercent: number }) => export const updateMixnode = async ({ profitMarginPercent }: { profitMarginPercent: number }) =>
await invoke('update_mixnode', { profitMarginPercent }) invoke('update_mixnode', { profitMarginPercent });
+4 -4
View File
@@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { Coin } from '../types' import { Coin } from '../types';
export const minorToMajor = async (amount: string): Promise<Coin> => await invoke('minor_to_major', { amount }) export const minorToMajor = async (amount: string): Promise<Coin> => invoke('minor_to_major', { amount });
export const majorToMinor = async (amount: string): Promise<Coin> => await invoke('major_to_minor', { amount }) export const majorToMinor = async (amount: string): Promise<Coin> => invoke('major_to_minor', { amount });
+4 -4
View File
@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { TauriContractStateParams } from '../types' import { TauriContractStateParams } from '../types';
export const getContractParams = async (): Promise<TauriContractStateParams> => await invoke('get_contract_settings') export const getContractParams = async (): Promise<TauriContractStateParams> => invoke('get_contract_settings');
export const setContractParams = async (params: TauriContractStateParams): Promise<TauriContractStateParams> => export const setContractParams = async (params: TauriContractStateParams): Promise<TauriContractStateParams> =>
await invoke('update_contract_settings', { params }) invoke('update_contract_settings', { params });
+7 -7
View File
@@ -1,7 +1,7 @@
export * from './account' export * from './account';
export * from './actions' export * from './actions';
export * from './coin' export * from './coin';
export * from './contract' export * from './contract';
export * from './vesting' export * from './vesting';
export * from './network' export * from './network';
export * from './queries' export * from './queries';
+3 -3
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { Account, Network } from '../types' import { Account, Network } from '../types';
export const selectNetwork = async (network: Network): Promise<Account> => await invoke('switch_network', { network }) export const selectNetwork = async (network: Network): Promise<Account> => invoke('switch_network', { network });
+13 -14
View File
@@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { import {
Balance, Balance,
Coin, Coin,
@@ -9,36 +9,35 @@ import {
StakeSaturationResponse, StakeSaturationResponse,
TMixnodeBondDetails, TMixnodeBondDetails,
TPagedDelegations, TPagedDelegations,
} from '../types' } from '../types';
export const getReverseMixDelegations = async (): Promise<TPagedDelegations> => export const getReverseMixDelegations = async (): Promise<TPagedDelegations> =>
await invoke('get_reverse_mix_delegations_paged') invoke('get_reverse_mix_delegations_paged');
export const getReverseGatewayDelegations = async (): Promise<TPagedDelegations> => export const getReverseGatewayDelegations = async (): Promise<TPagedDelegations> =>
await invoke('get_reverse_gateway_delegations_paged') invoke('get_reverse_gateway_delegations_paged');
export const getMixnodeBondDetails = async (): Promise<TMixnodeBondDetails | null> => export const getMixnodeBondDetails = async (): Promise<TMixnodeBondDetails | null> => invoke('mixnode_bond_details');
await invoke('mixnode_bond_details')
export const getMixnodeStakeSaturation = async (identity: string): Promise<StakeSaturationResponse> => export const getMixnodeStakeSaturation = async (identity: string): Promise<StakeSaturationResponse> =>
await invoke('mixnode_stake_saturation', { identity }) invoke('mixnode_stake_saturation', { identity });
export const getMixnodeRewardEstimation = async (identity: string): Promise<RewardEstimationResponse> => export const getMixnodeRewardEstimation = async (identity: string): Promise<RewardEstimationResponse> =>
await invoke('mixnode_reward_estimation', { identity }) invoke('mixnode_reward_estimation', { identity });
export const getMixnodeStatus = async (identity: string): Promise<MixnodeStatusResponse> => export const getMixnodeStatus = async (identity: string): Promise<MixnodeStatusResponse> =>
await invoke('mixnode_status', { identity }) invoke('mixnode_status', { identity });
export const checkMixnodeOwnership = async (): Promise<boolean> => await invoke('owns_mixnode') export const checkMixnodeOwnership = async (): Promise<boolean> => invoke('owns_mixnode');
export const checkGatewayOwnership = async (): Promise<boolean> => await invoke('owns_gateway') export const checkGatewayOwnership = async (): Promise<boolean> => invoke('owns_gateway');
// NOTE: this uses OUTDATED defaults that might have no resemblance with the reality // NOTE: this uses OUTDATED defaults that might have no resemblance with the reality
// as for the actual transaction, the gas cost is being simulated beforehand // as for the actual transaction, the gas cost is being simulated beforehand
export const getGasFee = async (operation: Operation): Promise<Coin> => export const getGasFee = async (operation: Operation): Promise<Coin> =>
await invoke('outdated_get_approximate_fee', { operation }) invoke('outdated_get_approximate_fee', { operation });
export const getInclusionProbability = async (identity: string): Promise<InclusionProbabilityResponse> => export const getInclusionProbability = async (identity: string): Promise<InclusionProbabilityResponse> =>
await invoke('mixnode_inclusion_probability', { identity }) invoke('mixnode_inclusion_probability', { identity });
export const userBalance = async (): Promise<Balance> => await invoke('get_balance') export const userBalance = async (): Promise<Balance> => invoke('get_balance');
+29 -29
View File
@@ -1,51 +1,51 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { VestingAccountInfo } from 'src/types/rust/vestingaccountinfo' import { VestingAccountInfo } from 'src/types/rust/vestingaccountinfo';
import { majorToMinor, minorToMajor } from '.' import { majorToMinor, minorToMajor } from './coin';
import { Coin, DelegationResult, OriginalVestingResponse, Period } from '../types' import { Coin, DelegationResult, OriginalVestingResponse, Period } from '../types';
export const getLockedCoins = async (address: string): Promise<Coin> => { export const getLockedCoins = async (address: string): Promise<Coin> => {
const res: Coin = await invoke('locked_coins', { address }) const res: Coin = await invoke('locked_coins', { address });
return await minorToMajor(res.amount) return minorToMajor(res.amount);
} };
export const getSpendableCoins = async (vestingAccountAddress?: string): Promise<Coin> => { export const getSpendableCoins = async (vestingAccountAddress?: string): Promise<Coin> => {
const res: Coin = await invoke('spendable_coins', { vestingAccountAddress }) const res: Coin = await invoke('spendable_coins', { vestingAccountAddress });
return await minorToMajor(res.amount) return minorToMajor(res.amount);
} };
export const getVestingCoins = async (vestingAccountAddress: string): Promise<Coin> => { export const getVestingCoins = async (vestingAccountAddress: string): Promise<Coin> => {
const res: Coin = await invoke('vesting_coins', { vestingAccountAddress }) const res: Coin = await invoke('vesting_coins', { vestingAccountAddress });
return await minorToMajor(res.amount) return minorToMajor(res.amount);
} };
export const getVestedCoins = async (vestingAccountAddress: string): Promise<Coin> => { export const getVestedCoins = async (vestingAccountAddress: string): Promise<Coin> => {
const res: Coin = await invoke('vested_coins', { vestingAccountAddress }) const res: Coin = await invoke('vested_coins', { vestingAccountAddress });
return await minorToMajor(res.amount) return minorToMajor(res.amount);
} };
export const getOriginalVesting = async (vestingAccountAddress: string): Promise<OriginalVestingResponse> => { export const getOriginalVesting = async (vestingAccountAddress: string): Promise<OriginalVestingResponse> => {
const res: OriginalVestingResponse = await invoke('original_vesting', { vestingAccountAddress }) const res: OriginalVestingResponse = await invoke('original_vesting', { vestingAccountAddress });
const majorValue = await minorToMajor(res.amount.amount) const majorValue = await minorToMajor(res.amount.amount);
return { ...res, amount: majorValue } return { ...res, amount: majorValue };
} };
export const withdrawVestedCoins = async (amount: string) => { export const withdrawVestedCoins = async (amount: string) => {
const minor = await majorToMinor(amount) const minor = await majorToMinor(amount);
await invoke('withdraw_vested_coins', { amount: { amount: minor.amount, denom: 'Minor' } }) await invoke('withdraw_vested_coins', { amount: { amount: minor.amount, denom: 'Minor' } });
} };
export const getCurrentVestingPeriod = async (address: string): Promise<Period> => export const getCurrentVestingPeriod = async (address: string): Promise<Period> =>
await invoke('get_current_vesting_period', { address }) invoke('get_current_vesting_period', { address });
export const vestingDelegateToMixnode = async ({ export const vestingDelegateToMixnode = async ({
identity, identity,
amount, amount,
}: { }: {
identity: string identity: string;
amount: Coin amount: Coin;
}): Promise<DelegationResult> => await invoke('vesting_delegate_to_mixnode', { identity, amount }) }): Promise<DelegationResult> => invoke('vesting_delegate_to_mixnode', { identity, amount });
export const vestingUnelegateFromMixnode = async (identity: string): Promise<DelegationResult> => export const vestingUnelegateFromMixnode = async (identity: string): Promise<DelegationResult> =>
await invoke('vesting_delegate_to_mixnode', { identity }) invoke('vesting_delegate_to_mixnode', { identity });
export const getVestingAccountInfo = async (address: string): Promise<VestingAccountInfo> => export const getVestingAccountInfo = async (address: string): Promise<VestingAccountInfo> =>
await invoke('get_account_info', { address }) invoke('get_account_info', { address });
+5 -5
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react';
import { Switch, Route } from 'react-router-dom' import { Switch, Route } from 'react-router-dom';
import { Balance } from '../pages/balance' import { Balance } from '../pages/balance';
import { Bond, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages' import { Bond, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages';
export const Routes = () => ( export const Routes = () => (
<Switch> <Switch>
@@ -30,4 +30,4 @@ export const Routes = () => (
<InternalDocs /> <InternalDocs />
</Route> </Route>
</Switch> </Switch>
) );
+4 -6
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material' import { SvgIcon, SvgIconProps } from '@mui/material';
export const Bond = (props: SvgIconProps) => { export const Bond = (props: SvgIconProps) => (
return (
<SvgIcon {...props}> <SvgIcon {...props}>
<path <path
fillRule="evenodd" fillRule="evenodd"
@@ -20,5 +19,4 @@ export const Bond = (props: SvgIconProps) => {
d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z" d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z"
/> />
</SvgIcon> </SvgIcon>
) );
}
+4 -6
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material' import { SvgIcon, SvgIconProps } from '@mui/material';
export const Delegate = (props: SvgIconProps) => { export const Delegate = (props: SvgIconProps) => (
return (
<SvgIcon {...props}> <SvgIcon {...props}>
<path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" />
<path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" />
@@ -10,5 +9,4 @@ export const Delegate = (props: SvgIconProps) => {
<rect x="18" y="17" width="2" height="3" /> <rect x="18" y="17" width="2" height="3" />
<rect x="4" y="17" width="2" height="3" /> <rect x="4" y="17" width="2" height="3" />
</SvgIcon> </SvgIcon>
) );
}
+4 -4
View File
@@ -1,4 +1,4 @@
export * from './delegate' export * from './delegate';
export * from './undelegate' export * from './undelegate';
export * from './bond' export * from './bond';
export * from './unbond' export * from './unbond';
+5 -7
View File
@@ -1,15 +1,13 @@
import React from 'react' import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material' import { SvgIcon, SvgIconProps } from '@mui/material';
export const Node = (props: SvgIconProps) => { export const Node: React.FC<SvgIconProps> = ({ color, ...props }) => (
return (
<SvgIcon {...props}> <SvgIcon {...props}>
<path <path
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
d="M18.9536 6.99307L12 3.05788L5.04641 6.99304L11.5144 10.5864C11.8164 10.7542 12.1837 10.7542 12.4857 10.5864L18.9536 6.99307ZM19.9005 8.75493L13.457 12.3347C13.3093 12.4167 13.1564 12.4854 13 12.5407V20.3762L19.9005 16.4711V8.75493ZM11 12.5407C10.8436 12.4854 10.6908 12.4167 10.5431 12.3347L4.09946 8.75488V16.4711L11 20.3762V12.5407ZM13.0497 1.2757C12.4002 0.908099 11.5998 0.908099 10.9503 1.2757L3.04973 5.74676C2.40015 6.11437 2 6.79373 2 7.52894V16.4711C2 17.2063 2.40015 17.8856 3.04973 18.2532L10.9503 22.7243C11.5998 23.0919 12.4002 23.0919 13.0497 22.7243L20.9503 18.2532C21.5998 17.8856 22 17.2063 22 16.4711V7.52894C22 6.79373 21.5998 6.11437 20.9503 5.74676L13.0497 1.2757Z" d="M18.9536 6.99307L12 3.05788L5.04641 6.99304L11.5144 10.5864C11.8164 10.7542 12.1837 10.7542 12.4857 10.5864L18.9536 6.99307ZM19.9005 8.75493L13.457 12.3347C13.3093 12.4167 13.1564 12.4854 13 12.5407V20.3762L19.9005 16.4711V8.75493ZM11 12.5407C10.8436 12.4854 10.6908 12.4167 10.5431 12.3347L4.09946 8.75488V16.4711L11 20.3762V12.5407ZM13.0497 1.2757C12.4002 0.908099 11.5998 0.908099 10.9503 1.2757L3.04973 5.74676C2.40015 6.11437 2 6.79373 2 7.52894V16.4711C2 17.2063 2.40015 17.8856 3.04973 18.2532L10.9503 22.7243C11.5998 23.0919 12.4002 23.0919 13.0497 22.7243L20.9503 18.2532C21.5998 17.8856 22 17.2063 22 16.4711V7.52894C22 6.79373 21.5998 6.11437 20.9503 5.74676L13.0497 1.2757Z"
fill={props.color} fill={color}
/> />
</SvgIcon> </SvgIcon>
) );
}
+4 -6
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material' import { SvgIcon, SvgIconProps } from '@mui/material';
export const Unbond = (props: SvgIconProps) => { export const Unbond = (props: SvgIconProps) => (
return (
<SvgIcon {...props}> <SvgIcon {...props}>
<path <path
fillRule="evenodd" fillRule="evenodd"
@@ -15,5 +14,4 @@ export const Unbond = (props: SvgIconProps) => {
d="M2 17C0.89543 17 -1.00647e-07 16.1046 -2.24801e-07 15L-8.99206e-07 9C-1.02336e-06 7.89543 0.895429 7 2 7L8 7C9.10457 7 10 7.89543 10 9L10 15C10 16.1046 9.10457 17 8 17L2 17ZM2 9L2 15L8 15L8 9L2 9Z" d="M2 17C0.89543 17 -1.00647e-07 16.1046 -2.24801e-07 15L-8.99206e-07 9C-1.02336e-06 7.89543 0.895429 7 2 7L8 7C9.10457 7 10 7.89543 10 9L10 15C10 16.1046 9.10457 17 8 17L2 17ZM2 9L2 15L8 15L8 9L2 9Z"
/> />
</SvgIcon> </SvgIcon>
) );
}
+4 -6
View File
@@ -1,8 +1,7 @@
import React from 'react' import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material' import { SvgIcon, SvgIconProps } from '@mui/material';
export const Undelegate = (props: SvgIconProps) => { export const Undelegate = (props: SvgIconProps) => (
return (
<SvgIcon {...props}> <SvgIcon {...props}>
<path d="M4 12V15H6V12H4ZM4 17H20V15H4V17Z" /> <path d="M4 12V15H6V12H4ZM4 17H20V15H4V17Z" />
<path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" />
@@ -11,5 +10,4 @@ export const Undelegate = (props: SvgIconProps) => {
<rect x="4" y="17" width="2" height="3" /> <rect x="4" y="17" width="2" height="3" />
<path d="M9.41 7.41L8 6L12 2L16 6L14.61 7.39L13 5.81L13 11L11 11L11 5.83L9.41 7.41Z" /> <path d="M9.41 7.41L8 6L12 2L16 6L14.61 7.39L13 5.81L13 11L11 11L11 5.83L9.41 7.41Z" />
</SvgIcon> </SvgIcon>
) );
}
+12 -12
View File
@@ -1,29 +1,29 @@
import React, { useContext } from 'react' import React, { useContext } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles' import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material' import { CssBaseline } from '@mui/material';
import { getDesignTokens } from './theme' import { getDesignTokens } from './theme';
import { ClientContext } from '../context/main' import { ClientContext } from '../context/main';
/** /**
* Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context. * Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
*/ */
export const NymWalletTheme: React.FC = ({ children }) => { export const NymWalletTheme: React.FC = ({ children }) => {
const { mode } = useContext(ClientContext) const { mode } = useContext(ClientContext);
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]) const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{children} {children}
</ThemeProvider> </ThemeProvider>
) );
} };
export const WelcomeTheme: React.FC = ({ children }) => { export const WelcomeTheme: React.FC = ({ children }) => {
const theme = createTheme(getDesignTokens('dark')) const theme = createTheme(getDesignTokens('dark'));
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{children} {children}
</ThemeProvider> </ThemeProvider>
) );
} };
+21 -21
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */ /* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles' import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { PaletteMode } from '@mui/material' import { PaletteMode } from '@mui/material';
/** /**
* If you are unfamiliar with Material UI theming, please read the following first: * If you are unfamiliar with Material UI theming, please read the following first:
@@ -28,43 +28,43 @@ declare module '@mui/material/styles' {
* This interface defines a palette used across Nym for branding * This interface defines a palette used across Nym for branding
*/ */
interface NymPalette { interface NymPalette {
highlight: string highlight: string;
success: string success: string;
info: string info: string;
fee: string fee: string;
background: { light: string; dark: string } background: { light: string; dark: string };
text: { text: {
light: string light: string;
dark: string dark: string;
} };
} }
interface NymPaletteVariant { interface NymPaletteVariant {
mode: PaletteMode mode: PaletteMode;
background: { background: {
main: string main: string;
paper: string paper: string;
} };
text: { text: {
main: string main: string;
} };
topNav: { topNav: {
background: string background: string;
} };
} }
/** /**
* A palette definition only for the Nym Wallet that extends the Nym palette * A palette definition only for the Nym Wallet that extends the Nym palette
*/ */
interface NymWalletPalette { interface NymWalletPalette {
nymWallet: {} nymWallet: {};
} }
interface NymPaletteAndNymWalletPalette { interface NymPaletteAndNymWalletPalette {
nym: NymPalette & NymWalletPalette nym: NymPalette & NymWalletPalette;
} }
type NymPaletteAndNymWalletPaletteOptions = Partial<NymPaletteAndNymWalletPalette> type NymPaletteAndNymWalletPaletteOptions = Partial<NymPaletteAndNymWalletPalette>;
/** /**
* Add anything not palette related to the theme here * Add anything not palette related to the theme here
+13 -13
View File
@@ -1,4 +1,4 @@
import { PaletteMode } from '@mui/material' import { PaletteMode } from '@mui/material';
import { import {
PaletteOptions, PaletteOptions,
NymPalette, NymPalette,
@@ -6,7 +6,7 @@ import {
ThemeOptions, ThemeOptions,
createTheme, createTheme,
NymPaletteVariant, NymPaletteVariant,
} from '@mui/material/styles' } from '@mui/material/styles';
//----------------------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------------------
// Nym palette type definitions // Nym palette type definitions
@@ -29,7 +29,7 @@ const nymPalette: NymPalette = {
light: '#F2F2F2', light: '#F2F2F2',
dark: '#121726', dark: '#121726',
}, },
} };
const darkMode: NymPaletteVariant = { const darkMode: NymPaletteVariant = {
mode: 'dark', mode: 'dark',
@@ -43,7 +43,7 @@ const darkMode: NymPaletteVariant = {
topNav: { topNav: {
background: '#111826', background: '#111826',
}, },
} };
const lightMode: NymPaletteVariant = { const lightMode: NymPaletteVariant = {
mode: 'light', mode: 'light',
@@ -57,7 +57,7 @@ const lightMode: NymPaletteVariant = {
topNav: { topNav: {
background: '#111826', background: '#111826',
}, },
} };
/** /**
* Nym palette specific to the Nym Wallet * Nym palette specific to the Nym Wallet
@@ -65,9 +65,9 @@ const lightMode: NymPaletteVariant = {
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct * IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode. * colours for dark/light mode.
*/ */
const nymWalletPalette = (variant: NymPaletteVariant): NymWalletPalette => ({ const nymWalletPalette = (_variant: NymPaletteVariant): NymWalletPalette => ({
nymWallet: {}, nymWallet: {},
}) });
//----------------------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------------------
// Nym palettes for light and dark mode // Nym palettes for light and dark mode
@@ -94,7 +94,7 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
default: variant.background.main, default: variant.background.main,
paper: variant.background.paper, paper: variant.background.paper,
}, },
}) });
/** /**
* Returns the Network Explorer palette for light mode. * Returns the Network Explorer palette for light mode.
@@ -105,7 +105,7 @@ const createLightModePalette = (): PaletteOptions => ({
...nymWalletPalette(lightMode), ...nymWalletPalette(lightMode),
}, },
...variantToMUIPalette(lightMode), ...variantToMUIPalette(lightMode),
}) });
/** /**
* Returns the Network Explorer palette for dark mode. * Returns the Network Explorer palette for dark mode.
@@ -116,7 +116,7 @@ const createDarkModePalette = (): PaletteOptions => ({
...nymWalletPalette(darkMode), ...nymWalletPalette(darkMode),
}, },
...variantToMUIPalette(darkMode), ...variantToMUIPalette(darkMode),
}) });
/** /**
* IMPORANT: if you need to get the default MUI theme, use the following * IMPORANT: if you need to get the default MUI theme, use the following
@@ -157,7 +157,7 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
mode, mode,
...(mode === 'light' ? createLightModePalette() : createDarkModePalette()), ...(mode === 'light' ? createLightModePalette() : createDarkModePalette()),
}, },
}) });
// then customise theme and components // then customise theme and components
return { return {
@@ -217,5 +217,5 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
}, },
}, },
palette, palette,
} };
} };

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