Merge the nym-wallet-web repo into a nym directory (#684)
A snapshot of https://github.com/nymtech/nym-wallet-web/tree/60b36db17d994e597b91981be5d60b0488618019 is put into a wallet-web folder.
This commit is contained in:
committed by
GitHub
parent
86034bd955
commit
c09846bb36
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
.idea
|
||||
@@ -0,0 +1,24 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import ValidatorClient, {
|
||||
nativeCoinToDisplay,
|
||||
nymGasLimits,
|
||||
nymGasPrice,
|
||||
printableCoin
|
||||
} from "@nymproject/nym-validator-client";
|
||||
import { ADDRESS_LENGTH, DENOM, KEY_LENGTH, UDENOM } from "../pages/_app";
|
||||
import { buildFeeTable } from "@cosmjs/launchpad";
|
||||
import bs58 from "bs58";
|
||||
|
||||
export const makeBasicStyle = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
root: {
|
||||
textAlign: 'center',
|
||||
paddingTop: theme.spacing(4),
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 650,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
stepper: {
|
||||
padding: theme.spacing(3, 0, 5),
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
list: {
|
||||
width: 250,
|
||||
},
|
||||
wrapper: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(3),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
type NodeOwnership = {
|
||||
ownsMixnode: boolean,
|
||||
ownsGateway: boolean
|
||||
}
|
||||
|
||||
export async function checkNodesOwnership(client: ValidatorClient): Promise<NodeOwnership> {
|
||||
const ownsMixnodePromise = client.ownsMixNode();
|
||||
const ownsGatewayPromise = client.ownsGateway();
|
||||
|
||||
let ownsMixnode = false;
|
||||
let ownsGateway = false;
|
||||
|
||||
await Promise.allSettled([ownsMixnodePromise, ownsGatewayPromise]).then((results) => {
|
||||
if (results[0].status === "fulfilled") {
|
||||
ownsMixnode = results[0].value
|
||||
} else {
|
||||
console.error("failed to check for mixnode ownership")
|
||||
}
|
||||
if (results[1].status === "fulfilled") {
|
||||
ownsGateway = results[1].value
|
||||
} else {
|
||||
console.error("failed to check for gateway ownership")
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
ownsMixnode,
|
||||
ownsGateway
|
||||
}
|
||||
}
|
||||
|
||||
export const validateClientAddress = (address: string): boolean => {
|
||||
return address.length === ADDRESS_LENGTH && address.startsWith(DENOM)
|
||||
}
|
||||
|
||||
export const validateIdentityKey = (key: string): boolean => {
|
||||
try {
|
||||
const bytes = bs58.decode(key);
|
||||
// of length 32
|
||||
return bytes.length === KEY_LENGTH;
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const validateRawPort = (rawPort: string): boolean => {
|
||||
// first of all it must be an integer
|
||||
const port = parseInt(rawPort)
|
||||
if (port == null) {
|
||||
return false
|
||||
}
|
||||
// and it must be a non-zero 16 bit unsigned integer
|
||||
return port >= 1 && port <= 65535
|
||||
}
|
||||
|
||||
export const basicRawCoinValueValidation = (rawAmount: string): boolean => {
|
||||
let amountFloat = parseFloat(rawAmount)
|
||||
if (isNaN(amountFloat)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it cannot have more than 6 decimal places
|
||||
if ((amountFloat) != parseFloat(amountFloat.toFixed(6))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it cannot be larger than the total supply
|
||||
if (amountFloat > 1_000_000_000_000_000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it can't be lower than one micro coin
|
||||
return amountFloat >= 0.000001;
|
||||
}
|
||||
|
||||
export const getDisplayExecGasFee = (): string => {
|
||||
const table = buildFeeTable(nymGasPrice(UDENOM), nymGasLimits, nymGasLimits)
|
||||
return printableCoin(table.exec.amount[0])
|
||||
}
|
||||
|
||||
export const getDisplaySendGasFee = (): string => {
|
||||
const table = buildFeeTable(nymGasPrice(UDENOM), nymGasLimits, nymGasLimits)
|
||||
return printableCoin(table.send.amount[0])
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum NodeType {
|
||||
Mixnode = "Mixnode",
|
||||
Gateway = "Gateway",
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import {CircularProgress} from "@material-ui/core";
|
||||
import {Alert, AlertTitle} from '@material-ui/lab';
|
||||
|
||||
type ConfirmationProps = {
|
||||
finished: boolean,
|
||||
progressMessage: string,
|
||||
successMessage: string,
|
||||
failureMessage: string,
|
||||
error: Error,
|
||||
}
|
||||
|
||||
export default function Confirmation(props: ConfirmationProps) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!props.finished ? (
|
||||
<React.Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{props.progressMessage}
|
||||
</Typography>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<CircularProgress/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{props.error === null ? (
|
||||
<Alert severity="success">{props.successMessage}</Alert>
|
||||
) : (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>{props.error.name}</AlertTitle>
|
||||
<strong>{props.failureMessage}</strong> - {props.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Alert} from '@material-ui/lab';
|
||||
import { getDisplayExecGasFee } from "../common/helpers";
|
||||
|
||||
type ExecFeeNoticeProps = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const ExecFeeNotice = (props: ExecFeeNoticeProps) => {
|
||||
return (
|
||||
<Alert severity="info">
|
||||
The gas fee for
|
||||
<strong> {props.name} </strong>
|
||||
{`is ${getDisplayExecGasFee()}`}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExecFeeNotice
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from "next/router";
|
||||
import NextLink, { LinkProps as NextLinkProps } from 'next/link';
|
||||
import MuiLink, { LinkProps as MuiLinkProps } from '@material-ui/core/Link';
|
||||
|
||||
type NextComposedProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & NextLinkProps;
|
||||
|
||||
const NextComposed = React.forwardRef(function NextComposed(props: NextComposedProps, ref: React.Ref<HTMLAnchorElement>) {
|
||||
const { as, href, ...other } = props;
|
||||
|
||||
return (
|
||||
<NextLink href={href} as={as}>
|
||||
<a ref={ref} {...other} />
|
||||
</NextLink>
|
||||
);
|
||||
});
|
||||
|
||||
interface LinkPropsBase {
|
||||
activeClassName?: string;
|
||||
innerRef?: React.Ref<HTMLAnchorElement>;
|
||||
naked?: boolean;
|
||||
}
|
||||
|
||||
type LinkProps = LinkPropsBase & NextComposedProps & Omit<MuiLinkProps, 'ref'>;
|
||||
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
href,
|
||||
activeClassName = 'active',
|
||||
className: classNameProps,
|
||||
innerRef,
|
||||
naked,
|
||||
...other
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = typeof href === 'string' ? href : href.pathname;
|
||||
const className = clsx(classNameProps, {
|
||||
[activeClassName]: router.pathname === pathname && activeClassName,
|
||||
});
|
||||
|
||||
if (naked) {
|
||||
return <NextComposed className={className} ref={innerRef} href={href} {...other} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiLink component={NextComposed} className={className} ref={innerRef} href={href} {...other} />
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => <Link {...props} innerRef={ref} />);
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
AppBar,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
Toolbar,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import React, { useContext } from "react";
|
||||
import Link from 'next/link'
|
||||
import VpnKeyIcon from "@material-ui/icons/VpnKey"
|
||||
import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet';
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
import { ADMIN_ADDRESS } from "../pages/_app";
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import MonetizationOnIcon from '@material-ui/icons/MonetizationOn';
|
||||
import AttachMoneyIcon from '@material-ui/icons/AttachMoney';
|
||||
import MoneyOffIcon from '@material-ui/icons/MoneyOff';
|
||||
import HowToVoteIcon from '@material-ui/icons/HowToVote';
|
||||
import CancelIcon from '@material-ui/icons/Cancel';
|
||||
import PageviewIcon from '@material-ui/icons/Pageview';
|
||||
import { makeBasicStyle } from "../common/helpers";
|
||||
import { theme } from "../lib/theme";
|
||||
|
||||
export default function MainNav() {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
const { client } = useContext(ValidatorClientContext)
|
||||
|
||||
let adminPageDisplayed = false
|
||||
|
||||
if (client !== null && client.address === ADMIN_ADDRESS) {
|
||||
adminPageDisplayed = true
|
||||
}
|
||||
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AppBar position="absolute" color="default" className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu" onClick={toggleDrawer}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Drawer anchor={'left'} open={open} onClose={closeDrawer}>
|
||||
<div
|
||||
className={classes.list}
|
||||
role="presentation"
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="list-header"
|
||||
subheader={
|
||||
<ListSubheader id="list-header">
|
||||
Nym Wallet
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<Divider />
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<AccountBalanceWalletIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/balanceCheck">
|
||||
<ListItemText primary="Check Balance" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<MonetizationOnIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/send">
|
||||
<ListItemText primary="Send coins" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/*<ListItem>*/}
|
||||
{/* <ListItemText primary="Node management" secondary="bottom text"/>*/}
|
||||
{/*</ListItem>*/}
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<AttachMoneyIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/bond">
|
||||
<ListItemText primary="Bond node" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<MoneyOffIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/unbond">
|
||||
<ListItemText primary="Unbond node" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<HowToVoteIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/delegateStake">
|
||||
<ListItemText primary="Delegate stake" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<PageviewIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/checkDelegation">
|
||||
<ListItemText primary="Check current delegation" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<CancelIcon />
|
||||
</ListItemIcon>
|
||||
<Link href="/undelegateStake">
|
||||
<ListItemText primary="Undelegate stake" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
{adminPageDisplayed &&
|
||||
<React.Fragment>
|
||||
<Divider />
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<VpnKeyIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<Link href="/admin">
|
||||
<ListItemText primary="Admin" />
|
||||
</Link>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
|
||||
<Typography variant="h6" color="inherit" noWrap>
|
||||
Nym
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {Alert, AlertTitle} from "@material-ui/lab";
|
||||
import React from "react";
|
||||
import Link from "./Link";
|
||||
|
||||
export default function NoClientError () {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>No client detected</AlertTitle>
|
||||
Have you signed in? Try to go back to <Link href = "/">the main page</Link> and try again
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import { Button } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { makeBasicStyle, validateIdentityKey } from "../common/helpers";
|
||||
import { theme } from "../lib/theme";
|
||||
|
||||
type NodeIdentityFormProps = {
|
||||
onSubmit: (event: any) => void
|
||||
buttonText: string
|
||||
}
|
||||
|
||||
const NodeIdentityForm = (props: NodeIdentityFormProps) => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
const [validIdentity, setValidIdentity] = React.useState(true)
|
||||
|
||||
const validateForm = (event: any): boolean => {
|
||||
let validIdentity = validateIdentityKey(event.target.identity.value);
|
||||
setValidIdentity(validIdentity)
|
||||
|
||||
return validIdentity
|
||||
}
|
||||
|
||||
const submitForm = (event: any) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (validateForm(event)) {
|
||||
return props.onSubmit(event)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<form onSubmit={submitForm}>
|
||||
<TextField
|
||||
required
|
||||
id="identity"
|
||||
name="identity"
|
||||
label="Node identity"
|
||||
error={!validIdentity}
|
||||
helperText={validIdentity ? "" : "Please enter a valid identity like '824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z'"}
|
||||
fullWidth
|
||||
/>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
{props.buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeIdentityForm
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { FormControl, FormControlLabel, FormLabel, RadioGroup } from "@material-ui/core";
|
||||
import { NodeType } from "../common/node";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
type NodeTypeChooserProps = {
|
||||
nodeType: NodeType,
|
||||
setNodeType: Dispatch<SetStateAction<NodeType>>
|
||||
}
|
||||
|
||||
const NodeTypeChooser = (props: NodeTypeChooserProps) => {
|
||||
const handleNodeTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let eventValue = (event.target as HTMLInputElement).value
|
||||
let type = NodeType[eventValue as keyof typeof NodeType]
|
||||
props.setNodeType(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Select node type</FormLabel>
|
||||
<RadioGroup aria-label="nodeType" name="nodeTypeRadio" value={props.nodeType} onChange={handleNodeTypeChange}>
|
||||
<FormControlLabel value={NodeType.Mixnode} control={<Radio />} label="Mixnode" />
|
||||
<FormControlLabel value={NodeType.Gateway} control={<Radio />} label="Gateway" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeTypeChooser
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Container from '@material-ui/core/Container';
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
import { useRouter } from "next/router";
|
||||
import ValidatorClient from "@nymproject/nym-validator-client";
|
||||
import { BONDING_CONTRACT_ADDRESS, DENOM, VALIDATOR_URLS } from "../pages/_app";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import Link from "./Link";
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
paper: {
|
||||
marginTop: theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
form: {
|
||||
width: '100%', // Fix IE 11 issue.
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SignIn() {
|
||||
const classes = useStyles();
|
||||
const router = useRouter()
|
||||
|
||||
const { client, setClient } = useContext(ValidatorClientContext)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [clientError, setClientError] = useState(null)
|
||||
|
||||
console.log("context client is", client);
|
||||
|
||||
const makeClient = (mnemonic: string): Promise<boolean> => {
|
||||
return ValidatorClient.connect(
|
||||
BONDING_CONTRACT_ADDRESS,
|
||||
mnemonic,
|
||||
VALIDATOR_URLS,
|
||||
DENOM,
|
||||
).then((client) => {
|
||||
setClient(client);
|
||||
console.log(`connected to validator, our address is ${client.address}`);
|
||||
console.log("connected to validator", client.urls[0])
|
||||
return true
|
||||
}).catch((err) => {
|
||||
setClientError(err)
|
||||
throw new Error("failed to create the client");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const failedClient = (err: Error) => {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>Could not create the client</AlertTitle>
|
||||
{err.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setClientError(null)
|
||||
let mnemonic = event.target.mnemonic.value
|
||||
makeClient(mnemonic).then(async () => {
|
||||
// only push `/send` if we managed to create the client!
|
||||
await router.push("/bond")
|
||||
}
|
||||
).catch((_err) => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container component="main" maxWidth="xs">
|
||||
<CssBaseline />
|
||||
<div className={classes.paper}>
|
||||
{/* <Avatar className={classes.avatar}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar> */}
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign in
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="mnemonic"
|
||||
label="BIP-39 Mnemonic"
|
||||
name="mnemonic"
|
||||
autoComplete="mnemonic"
|
||||
autoFocus
|
||||
/>
|
||||
{clientError !== null && failedClient(clientError)}
|
||||
|
||||
{loading && <LinearProgress />}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.submit}
|
||||
disabled={loading}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<Link href="/createAccount" variant="body2">
|
||||
{"Don't have an account? Create one"}
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Button, Grid, InputAdornment } from "@material-ui/core";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import React from "react";
|
||||
import { nativeToPrintable, StateParams } from "@nymproject/nym-validator-client";
|
||||
import { DENOM } from "../../pages/_app";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { makeBasicStyle } from "../../common/helpers";
|
||||
|
||||
type AdminFormProps = {
|
||||
onSubmit: (event: any) => void
|
||||
currentParams: StateParams,
|
||||
}
|
||||
|
||||
export default function AdminForm(props: AdminFormProps) {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
return (
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="mix_bond"
|
||||
name="mix_bond"
|
||||
label="Minimum Mixnode BondMixnode"
|
||||
defaultValue={nativeToPrintable(props.currentParams.minimum_mixnode_bond)}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="gateway_bond"
|
||||
name="gateway_bond"
|
||||
label="Minimum Gateway BondMixnode"
|
||||
defaultValue={nativeToPrintable(props.currentParams.minimum_gateway_bond)}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="mix_reward"
|
||||
name="mix_reward"
|
||||
label="Mixnode BondMixnode Reward Rate"
|
||||
defaultValue={props.currentParams.mixnode_bond_reward_rate}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="gateway_reward"
|
||||
name="gateway_reward"
|
||||
label="Gateway BondMixnode Reward Rate"
|
||||
defaultValue={props.currentParams.gateway_bond_reward_rate}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="epoch_length"
|
||||
name="epoch_length"
|
||||
label="Epoch length (in hours)"
|
||||
defaultValue={props.currentParams.epoch_length}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">hours</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="active_set"
|
||||
name="active_set"
|
||||
label="Mixnode Active Set Size"
|
||||
defaultValue={props.currentParams.mixnode_active_set_size}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
Update Contract
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import React from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import { Button, Checkbox, FormControlLabel, InputAdornment } from "@material-ui/core";
|
||||
import bs58 from "bs58";
|
||||
import semver from "semver"
|
||||
import { NodeType } from "../../common/node";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { basicRawCoinValueValidation, makeBasicStyle, validateRawPort } from "../../common/helpers";
|
||||
import { Coin, printableCoin } from "@nymproject/nym-validator-client";
|
||||
import { DENOM } from "../../pages/_app";
|
||||
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
|
||||
import { BondingInformation } from "./NodeBond";
|
||||
|
||||
const DEFAULT_MIX_PORT = 1789
|
||||
const DEFAULT_VERLOC_PORT = 1790
|
||||
const DEFAULT_HTTP_API_PORT = 8000
|
||||
const DEFAULT_CLIENTS_PORT = 9000
|
||||
|
||||
type BondNodeFormProps = {
|
||||
type: NodeType
|
||||
minimumMixnodeBond: Coin,
|
||||
minimumGatewayBond: Coin,
|
||||
onSubmit: (event: any) => void
|
||||
}
|
||||
|
||||
export default function BondNodeForm(props: BondNodeFormProps) {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
const [validity, setValidity] = React.useState({
|
||||
validAmount: true,
|
||||
validSphinxKey: true,
|
||||
validIdentityKey: true,
|
||||
validHost: true,
|
||||
validVersion: true,
|
||||
validLocation: true,
|
||||
validMixPort: true,
|
||||
|
||||
// this should have probably be somehow split to be subclasses of the validity matrix
|
||||
// the above is more true now as more fields are added. This looks kinda disgusting...
|
||||
// mixnode-specific:
|
||||
validVerlocPort: true,
|
||||
validHttpApiPort: true,
|
||||
|
||||
// gateway-specific:
|
||||
validClientsPort: true,
|
||||
})
|
||||
|
||||
const [advancedShown, setAdvancedShown] = React.useState(false)
|
||||
|
||||
const handleCheckboxToggle = () => {
|
||||
setAdvancedShown((prevSet) => !prevSet);
|
||||
}
|
||||
|
||||
|
||||
const validateForm = (event: any): boolean => {
|
||||
let validAmount = validateAmount(event.target.amount.value);
|
||||
let validSphinxKey = validateKey(event.target.sphinxkey.value);
|
||||
let validIdentityKey = validateKey(event.target.identity.value);
|
||||
let validHost = validateHost(event.target.host.value);
|
||||
let validVersion = validateVersion(event.target.version.value);
|
||||
|
||||
let validLocation = (props.type == NodeType.Gateway) ? validateLocation(event.target.location.value) : true;
|
||||
|
||||
let newValidity = {
|
||||
validAmount: validAmount,
|
||||
validSphinxKey: validSphinxKey,
|
||||
validIdentityKey: validIdentityKey,
|
||||
validHost: validHost,
|
||||
validVersion: validVersion,
|
||||
|
||||
validLocation: validLocation,
|
||||
}
|
||||
|
||||
if (advancedShown) {
|
||||
let validMixPort = validateRawPort(event.target.mixPort.value)
|
||||
let validVerlocPort = (props.type == NodeType.Mixnode) ? validateRawPort(event.target.verlocPort.value) : true;
|
||||
let validHttpApiPort = (props.type == NodeType.Mixnode) ? validateRawPort(event.target.httpApiPort.value) : true;
|
||||
let validClientsPort = (props.type == NodeType.Gateway) ? validateRawPort(event.target.clientsPort.value) : true;
|
||||
|
||||
newValidity = {
|
||||
...newValidity, ...{
|
||||
validMixPort: validMixPort,
|
||||
validVerlocPort: validVerlocPort,
|
||||
validHttpApiPort: validHttpApiPort,
|
||||
validClientsPort: validClientsPort,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setValidity((previousState) => {
|
||||
return {...previousState, ...newValidity}
|
||||
});
|
||||
|
||||
// just AND everything together
|
||||
const reducer = (acc, current) => acc && current;
|
||||
return Object.entries(newValidity).map((entry) => entry[1]).reduce(reducer, true)
|
||||
}
|
||||
|
||||
const validateAmount = (rawValue: string): boolean => {
|
||||
// tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc
|
||||
if (!basicRawCoinValueValidation(rawValue)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// this conversion seems really iffy but I'm not sure how to better approach it
|
||||
let nativeValueString = printableBalanceToNative(rawValue)
|
||||
let nativeValue = parseInt(nativeValueString)
|
||||
if (props.type == NodeType.Mixnode) {
|
||||
return nativeValue >= parseInt(props.minimumMixnodeBond.amount)
|
||||
} else {
|
||||
return nativeValue >= parseInt(props.minimumGatewayBond.amount)
|
||||
}
|
||||
}
|
||||
|
||||
const validateKey = (key: string): boolean => {
|
||||
// it must be a valid base58 key
|
||||
try {
|
||||
const bytes = bs58.decode(key);
|
||||
// of length 32
|
||||
return bytes.length === 32
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const validateHost = (host: string): boolean => {
|
||||
// I don't think that proper checks are in scope of the change here
|
||||
// what would need to be checked is whether one of the following is true:
|
||||
// - host is an ipv4 address
|
||||
// - host is an ipv6 address
|
||||
// - host is a valid hostname
|
||||
|
||||
// so at least perform the dumbest possible checks
|
||||
// ipv4 needs 4 dot-separated octets
|
||||
// ipv6 can have multiple possible representations, but it needs to contain at least two colons
|
||||
// a hostname (in this case) needs to have a top level domain present
|
||||
|
||||
const dot_occurrences = host.split('.').length - 1
|
||||
const colon_occurrences = host.split(':').length - 1
|
||||
|
||||
if (dot_occurrences == 3) {
|
||||
// possible ipv4
|
||||
// make sure it has no ports attached!
|
||||
return colon_occurrences == 0
|
||||
} else if (colon_occurrences >= 2) {
|
||||
// possible ipv6
|
||||
return true
|
||||
} else if (dot_occurrences >= 1) {
|
||||
// possible hostname
|
||||
// make sure it has no ports attached!
|
||||
return colon_occurrences == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const validateVersion = (version: string): boolean => {
|
||||
// check if its a valid semver
|
||||
return semver.valid(version)
|
||||
}
|
||||
|
||||
const validateLocation = (location: string): boolean => {
|
||||
// right now only perform the stupid check of whether the user copy-pasted the tooltip... (with or without brackets)
|
||||
return !location.trim().includes("physical location of your node")
|
||||
}
|
||||
|
||||
const constructMixnodeBondingInfo = (event: any): BondingInformation => {
|
||||
return {
|
||||
amount: event.target.amount.value,
|
||||
nodeDetails: {
|
||||
host: event.target.host.value,
|
||||
http_api_port: advancedShown ? parseInt(event.target.httpApiPort.value) : DEFAULT_HTTP_API_PORT,
|
||||
mix_port: advancedShown ? parseInt(event.target.mixPort.value) : DEFAULT_MIX_PORT,
|
||||
verloc_port: advancedShown ? parseInt(event.target.verlocPort.value) : DEFAULT_VERLOC_PORT,
|
||||
sphinx_key: event.target.sphinxkey.value,
|
||||
identity_key: event.target.identity.value,
|
||||
version: event.target.version.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const constructGatewayBondingInfo = (event: any): BondingInformation => {
|
||||
return {
|
||||
amount: event.target.amount.value,
|
||||
nodeDetails: {
|
||||
host: event.target.host.value,
|
||||
mix_port: advancedShown ? parseInt(event.target.mixPort.value) : DEFAULT_MIX_PORT,
|
||||
clients_port: advancedShown ? parseInt(event.target.clientsPort.value) : DEFAULT_CLIENTS_PORT,
|
||||
sphinx_key: event.target.sphinxkey.value,
|
||||
identity_key: event.target.identity.value,
|
||||
version: event.target.version.value,
|
||||
location: event.target.location.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = (event: any) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (validateForm(event)) {
|
||||
if (props.type == NodeType.Mixnode) {
|
||||
return props.onSubmit(constructMixnodeBondingInfo(event))
|
||||
} else {
|
||||
return props.onSubmit(constructGatewayBondingInfo(event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let minimumBond = props.minimumMixnodeBond;
|
||||
if (props.type == NodeType.Gateway) {
|
||||
minimumBond = props.minimumGatewayBond
|
||||
}
|
||||
|
||||
// if this whole interface wasn't to be completely redone in a month time, I would have definitely redone the form
|
||||
// but I guess it's fine for time being
|
||||
return (
|
||||
<form onSubmit={submitForm}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
required
|
||||
id="amount"
|
||||
name="amount"
|
||||
label={`Amount to bond (minimum ${printableCoin(minimumBond)})`}
|
||||
error={!validity.validAmount}
|
||||
{...(!validity.validAmount ? {helperText: `Enter a valid bond amount (minimum ${printableCoin(minimumBond)})`} : {})}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
error={!validity.validIdentityKey}
|
||||
required
|
||||
id="identity"
|
||||
name="identity"
|
||||
label="Identity key"
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
error={!validity.validSphinxKey}
|
||||
required
|
||||
id="sphinxkey"
|
||||
name="sphinxkey"
|
||||
label="Sphinx key"
|
||||
fullWidth
|
||||
{...(!validity.validSphinxKey ? {helperText: "Enter a valid sphinx key"} : {})}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={!validity.validHost}
|
||||
required
|
||||
id="host"
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
{...(!validity.validHost ? {helperText: "Enter a valid IP or a hostname (without port)"} : {})}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* if it's a gateway - get location */}
|
||||
<Grid item xs={12} sm={6}>{
|
||||
props.type === NodeType.Gateway &&
|
||||
<TextField
|
||||
error={!validity.validLocation}
|
||||
required
|
||||
id="location"
|
||||
name="location"
|
||||
label="Location"
|
||||
fullWidth
|
||||
{...(!validity.validLocation ? {helperText: "Enter a valid location of your node"} : {})}
|
||||
/>
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={!validity.validVersion}
|
||||
required
|
||||
id="version"
|
||||
name="version"
|
||||
label="Version"
|
||||
fullWidth
|
||||
{...(!validity.validVersion ? {helperText: "Enter a valid version, like 0.10.0"} : {})}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={advancedShown}
|
||||
onChange={handleCheckboxToggle}
|
||||
|
||||
/>
|
||||
}
|
||||
label="Show advanced options"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{advancedShown &&
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validMixPort}
|
||||
variant="outlined"
|
||||
id="mixPort"
|
||||
name="mixPort"
|
||||
label="Mix Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_MIX_PORT}
|
||||
{...(!validity.validMixPort ? {helperText: "Enter a valid version, like 0.10.0"} : {})}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/*yes, I also hate so many layers of indentation here*/}
|
||||
{props.type === NodeType.Mixnode ? (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validVerlocPort}
|
||||
variant="outlined"
|
||||
id="verlocPort"
|
||||
name="verlocPort"
|
||||
label="Verloc Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_VERLOC_PORT}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validHttpApiPort}
|
||||
variant="outlined"
|
||||
id="httpApiPort"
|
||||
name="httpApiPort"
|
||||
label="HTTP API Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_HTTP_API_PORT}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validClientsPort}
|
||||
variant="outlined"
|
||||
id="clientsPort"
|
||||
name="clientsPort"
|
||||
label="client WS API Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_CLIENTS_PORT}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
Bond
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Grid, LinearProgress, Paper } from '@material-ui/core';
|
||||
import { Gateway, MixNode } from '@nymproject/nym-validator-client/dist/types';
|
||||
import Confirmation from "../Confirmation";
|
||||
import { ValidatorClientContext } from "../../contexts/ValidatorClient";
|
||||
import NoClientError from "../NoClientError";
|
||||
import { useRouter } from "next/router";
|
||||
import BondNodeForm from "./BondNodeForm";
|
||||
import { NodeType } from "../../common/node";
|
||||
import Link from "../Link";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { checkNodesOwnership, makeBasicStyle } from "../../common/helpers";
|
||||
import NodeTypeChooser from "../NodeTypeChooser";
|
||||
import ExecFeeNotice from "../ExecFeeNotice";
|
||||
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
|
||||
import { coin } from "@nymproject/nym-validator-client";
|
||||
import { UDENOM } from "../../pages/_app";
|
||||
|
||||
export type BondingInformation = {
|
||||
amount: string,
|
||||
nodeDetails: MixNode | Gateway;
|
||||
}
|
||||
|
||||
const BondNode = () => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
const router = useRouter()
|
||||
const {client} = useContext(ValidatorClientContext)
|
||||
|
||||
const [bondingStarted, setBondingStarted] = React.useState(false)
|
||||
const [bondingFinished, setBondingFinished] = React.useState(false)
|
||||
const [bondingError, setBondingError] = React.useState(null)
|
||||
|
||||
const [checkedOwnership, setCheckedOwnership] = React.useState(false)
|
||||
const [ownsMixnode, setOwnsMixnode] = React.useState(false)
|
||||
const [ownsGateway, setOwnsGateway] = React.useState(false)
|
||||
|
||||
const [minimumMixnodeBond, setMinimumMixnodeBond] = React.useState(null)
|
||||
const [minimumGatewayBond, setMinimumGatewayBond] = React.useState(null)
|
||||
|
||||
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
|
||||
|
||||
useEffect(() => {
|
||||
const getInitialData = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
} else {
|
||||
const nodeOwnership = await checkNodesOwnership(client)
|
||||
setOwnsMixnode(nodeOwnership.ownsMixnode)
|
||||
setOwnsGateway(nodeOwnership.ownsGateway)
|
||||
|
||||
const minimumMixnodeBond = await client.minimumMixnodeBond()
|
||||
setMinimumMixnodeBond(minimumMixnodeBond)
|
||||
const minimumGatewayBond = await client.minimumGatewayBond()
|
||||
setMinimumGatewayBond(minimumGatewayBond)
|
||||
|
||||
setCheckedOwnership(true)
|
||||
}
|
||||
}
|
||||
getInitialData()
|
||||
}, [client])
|
||||
|
||||
const bondNode = async (bondingInformation: BondingInformation) => {
|
||||
setBondingStarted(true)
|
||||
// event.preventDefault();
|
||||
console.log(`BOND button pressed`);
|
||||
|
||||
console.log(bondingInformation)
|
||||
let amountValue = parseInt(printableBalanceToNative(bondingInformation.amount))
|
||||
let amount = coin(amountValue, UDENOM)
|
||||
|
||||
console.log(bondingInformation.nodeDetails)
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
let mixnode = (bondingInformation.nodeDetails as MixNode)
|
||||
client.bondMixnode(mixnode, amount).then((value => {
|
||||
console.log("bonded mixnode!", value)
|
||||
})).catch(setBondingError).finally(() => setBondingFinished(true))
|
||||
} else {
|
||||
let gateway = (bondingInformation.nodeDetails as Gateway)
|
||||
client.bondGateway(gateway, amount).then((value => {
|
||||
console.log("bonded gateway!", value)
|
||||
})).catch(setBondingError).finally(() => setBondingFinished(true))
|
||||
}
|
||||
}
|
||||
|
||||
const getBondContent = () => {
|
||||
// we're not signed in
|
||||
if (client === null) {
|
||||
return (<NoClientError/>)
|
||||
}
|
||||
|
||||
// we haven't checked whether we actually already own a node
|
||||
if (!checkedOwnership) {
|
||||
return (<LinearProgress/>)
|
||||
}
|
||||
|
||||
// we already own a mixnode
|
||||
if (ownsMixnode) {
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography gutterBottom>
|
||||
You have already have a bonded mixnode. If you wish to bond a different one, you need to
|
||||
first <Link href="/unbond">unbond the existing one</Link>.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// we already own a gateway
|
||||
if (ownsGateway) {
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography gutterBottom>
|
||||
You have already have a bonded gateway. If you wish to bond a different one, you need to
|
||||
first <Link href="/unbond">unbond the existing one</Link>.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// we haven't clicked bond button yet
|
||||
if (!bondingStarted) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType}/>
|
||||
<BondNodeForm onSubmit={bondNode} type={nodeType} minimumGatewayBond={minimumGatewayBond}
|
||||
minimumMixnodeBond={minimumMixnodeBond}/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// We started bonding
|
||||
return (
|
||||
<Confirmation
|
||||
finished={bondingFinished}
|
||||
error={bondingError}
|
||||
progressMessage={`${nodeType} bonding is in progress...`}
|
||||
successMessage={`${nodeType} bonding was successful!`}
|
||||
failureMessage={`Failed to bond the ${nodeType}!`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<ExecFeeNotice name={"bonding"}/>
|
||||
<Typography component="h1" variant="h4" align="center" className={classes.wrapper}>
|
||||
Bond a {nodeType}
|
||||
</Typography>
|
||||
{getBondContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default BondNode;
|
||||
@@ -0,0 +1,121 @@
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import React from "react";
|
||||
import { Button, InputAdornment } from "@material-ui/core";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import { DENOM } from "../../pages/_app";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { basicRawCoinValueValidation, makeBasicStyle, validateIdentityKey } from "../../common/helpers";
|
||||
|
||||
|
||||
type DelegateFormProps = {
|
||||
onSubmit: (event: any) => void
|
||||
}
|
||||
|
||||
export default function DelegateForm(props: DelegateFormProps) {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
const [validAmount, setValidAmount] = React.useState(true)
|
||||
const [validIdentity, setValidIdentity] = React.useState(true)
|
||||
// const [checkboxSet, setCheckboxSet] = React.useState(false)
|
||||
|
||||
// const handleCheckboxToggle = () => {
|
||||
// setCheckboxSet((prevSet) => !prevSet);
|
||||
// }
|
||||
|
||||
const handleAmountChange = (event: any) => {
|
||||
let nonZeroAmount = event.target.value.length > 0
|
||||
if (nonZeroAmount) {
|
||||
// don't ask me about that. javascript works in mysterious ways
|
||||
// and this is apparently a good way of checking if string
|
||||
// is purely made of numeric characters
|
||||
let parsed = +event.target.value
|
||||
if (isNaN(parsed)) {
|
||||
setValidAmount(false)
|
||||
} else {
|
||||
setValidAmount(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = (event: any): boolean => {
|
||||
let validIdentity = validateIdentityKey(event.target.identity.value);
|
||||
let validAmount = validateAmount(event.target.amount.value);
|
||||
|
||||
setValidIdentity(validIdentity)
|
||||
setValidAmount(validAmount)
|
||||
|
||||
return validIdentity && validAmount
|
||||
}
|
||||
|
||||
const validateAmount = (rawAmount: string): boolean => {
|
||||
return basicRawCoinValueValidation(rawAmount)
|
||||
}
|
||||
|
||||
const submitForm = (event: any) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (validateForm(event)) {
|
||||
return props.onSubmit(event)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={submitForm}>
|
||||
<Grid container spacing={3}>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="identity"
|
||||
name="identity"
|
||||
label="Node identity"
|
||||
error={!validIdentity}
|
||||
helperText={validIdentity ? "" : "Please enter a valid identity like '824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z'"}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
required
|
||||
id="amount"
|
||||
name="amount"
|
||||
label="Amount to delegate"
|
||||
error={!validAmount}
|
||||
helperText={validAmount ? "" : "Please enter a valid amount"}
|
||||
onChange={handleAmountChange}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/*<Grid item xs={12}>*/}
|
||||
{/* <FormControlLabel*/}
|
||||
{/* control={*/}
|
||||
{/* <Checkbox*/}
|
||||
{/* checked={checkboxSet}*/}
|
||||
{/* onChange={handleCheckboxToggle}*/}
|
||||
|
||||
{/* />*/}
|
||||
{/* }*/}
|
||||
{/* label="checkbox text"*/}
|
||||
{/* />*/}
|
||||
{/*</Grid>*/}
|
||||
|
||||
</Grid>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
>
|
||||
Delegate stake
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Paper } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { useRouter } from "next/router";
|
||||
import { ValidatorClientContext } from "../../contexts/ValidatorClient";
|
||||
import { NodeType } from "../../common/node";
|
||||
import NoClientError from "../NoClientError";
|
||||
import Confirmation from "../Confirmation";
|
||||
import DelegateForm from "./DelegateForm";
|
||||
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
|
||||
import { Coin } from "@cosmjs/launchpad";
|
||||
import { coin, printableCoin } from "@nymproject/nym-validator-client";
|
||||
import { UDENOM } from "../../pages/_app";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { makeBasicStyle } from "../../common/helpers";
|
||||
import NodeTypeChooser from "../NodeTypeChooser";
|
||||
import ExecFeeNotice from "../ExecFeeNotice";
|
||||
|
||||
const DelegateToNode = () => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
const router = useRouter()
|
||||
const {client} = useContext(ValidatorClientContext)
|
||||
|
||||
const [delegationStarted, setDelegationStarted] = React.useState(false)
|
||||
const [delegationFinished, setDelegationFinished] = React.useState(false)
|
||||
const [delegationError, setDelegationError] = React.useState(null)
|
||||
|
||||
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
|
||||
const [stakeValue, setStakeValue] = React.useState("0 HAL")
|
||||
const [nodeIdentity, setNodeIdentity] = React.useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const checkClient = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
}
|
||||
}
|
||||
checkClient()
|
||||
}, [client])
|
||||
|
||||
const getDelegationValue = (raw: string): Coin => {
|
||||
let native = printableBalanceToNative(raw)
|
||||
return coin(parseInt(native), UDENOM)
|
||||
}
|
||||
|
||||
const delegateToNode = async (event) => {
|
||||
event.preventDefault();
|
||||
console.log(`DELEGATE button pressed`);
|
||||
|
||||
const nodeIdentity = event.target.identity.value;
|
||||
const delegationValue = getDelegationValue(event.target.amount.value)
|
||||
|
||||
setNodeIdentity(nodeIdentity)
|
||||
setStakeValue(printableCoin(delegationValue))
|
||||
setDelegationStarted(true)
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
client.delegateToMixnode(nodeIdentity, delegationValue).then((value => {
|
||||
console.log("delegated to mixnode!", value)
|
||||
})).catch(setDelegationError).finally(() => setDelegationFinished(true))
|
||||
} else {
|
||||
client.delegateToGateway(nodeIdentity, delegationValue).then((value => {
|
||||
console.log("delegated to gateway!", value)
|
||||
})).catch(setDelegationError).finally(() => setDelegationFinished(true))
|
||||
}
|
||||
}
|
||||
|
||||
const getDelegationContent = () => {
|
||||
// we're not signed in
|
||||
if (client === null) {
|
||||
return (<NoClientError/>)
|
||||
}
|
||||
|
||||
// we haven't clicked delegate button yet
|
||||
if (!delegationStarted) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType}/>
|
||||
<DelegateForm onSubmit={delegateToNode}/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// We started delegation
|
||||
return (
|
||||
<Confirmation
|
||||
finished={delegationFinished}
|
||||
error={delegationError}
|
||||
progressMessage={`Delegating stake on ${nodeType} ${nodeIdentity} is in progress...`}
|
||||
successMessage={`Successfully delegated ${stakeValue} stake on ${nodeType} ${nodeIdentity}`}
|
||||
failureMessage={`Failed to delegate to a ${nodeType}!`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<ExecFeeNotice name={"delegating stake"}/>
|
||||
<Typography component="h1" variant="h4" align="center" className={classes.wrapper}>
|
||||
Delegate to {nodeType}
|
||||
</Typography>
|
||||
{getDelegationContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default DelegateToNode;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Button, Paper } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { useRouter } from "next/router";
|
||||
import { ValidatorClientContext } from "../../contexts/ValidatorClient";
|
||||
import { NodeType } from "../../common/node";
|
||||
import NoClientError from "../NoClientError";
|
||||
import Confirmation from "../Confirmation";
|
||||
import NodeTypeChooser from "../NodeTypeChooser";
|
||||
import { printableCoin } from "@nymproject/nym-validator-client";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { makeBasicStyle } from "../../common/helpers";
|
||||
import NodeIdentityForm from "../NodeIdentityForm";
|
||||
|
||||
|
||||
const DelegationCheck = () => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
const router = useRouter()
|
||||
const {client} = useContext(ValidatorClientContext)
|
||||
|
||||
const [checkStarted, setCheckStarted] = React.useState(false)
|
||||
const [checkFinished, setCheckFinished] = React.useState(false)
|
||||
const [checkError, setCheckError] = React.useState(null)
|
||||
|
||||
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
|
||||
const [stakeValue, setStakeValue] = React.useState("0")
|
||||
const [nodeIdentity, setNodeIdentity] = React.useState("")
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const checkClient = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
}
|
||||
}
|
||||
checkClient()
|
||||
}, [client])
|
||||
|
||||
|
||||
// eh, crude, but I guess does the trick
|
||||
const handleDelegationCheckError = (err: Error) => {
|
||||
if (err.message.includes("Could not find any delegation information associated with")) {
|
||||
setStakeValue("0 HAL")
|
||||
} else {
|
||||
setCheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const checkDelegation = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log(`CHECK DELEGATION button pressed`);
|
||||
|
||||
let identity = event.target.identity.value
|
||||
setNodeIdentity(identity)
|
||||
setCheckStarted(true)
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
client.getMixDelegation(identity, client.address).then((value => {
|
||||
setStakeValue(printableCoin(value.amount))
|
||||
})).catch(handleDelegationCheckError).finally(() => setCheckFinished(true))
|
||||
} else {
|
||||
client.getGatewayDelegation(identity, client.address).then((value => {
|
||||
setStakeValue(printableCoin(value.amount))
|
||||
})).catch(handleDelegationCheckError).finally(() => setCheckFinished(true))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const getDelegationCheckContent = () => {
|
||||
// we're not signed in
|
||||
if (client === null) {
|
||||
return (<NoClientError/>)
|
||||
}
|
||||
|
||||
// we haven't clicked delegate button yet
|
||||
if (!checkStarted) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType}/>
|
||||
<NodeIdentityForm onSubmit={checkDelegation} buttonText="Check stake value"/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// We started the check
|
||||
const stakeMessage = `Current stake on ${nodeType} ${nodeIdentity} is ${stakeValue}`
|
||||
return (
|
||||
<Confirmation
|
||||
finished={checkFinished}
|
||||
error={checkError}
|
||||
progressMessage={`${nodeType} (${nodeIdentity}) stake check is in progress...`}
|
||||
successMessage={stakeMessage}
|
||||
failureMessage={`Failed to check stake value on ${nodeType} ${nodeIdentity}!`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center" className={classes.wrapper}>
|
||||
Check your stake on a {nodeType}
|
||||
</Typography>
|
||||
{getDelegationCheckContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default DelegationCheck;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { SendFundsMsg } from '../../pages/send';
|
||||
import { printableCoin } from "@nymproject/nym-validator-client";
|
||||
import { getDisplaySendGasFee } from "../../common/helpers";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
listItem: {
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
total: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
title: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export const Review = (parentTrans: SendFundsMsg) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="body1" gutterBottom className={classes.title}>
|
||||
You are about to send
|
||||
</Typography>
|
||||
<Typography variant="h6">{printableCoin(parentTrans.coin)}</Typography>
|
||||
<Typography>to</Typography>
|
||||
<Typography variant="h6" gutterBottom>{parentTrans.recipient}</Typography>
|
||||
<Typography>With additional gas fee of </Typography>
|
||||
<Typography variant="h6">{getDisplaySendGasFee()}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import { DENOM } from '../../pages/_app';
|
||||
import { InputAdornment } from "@material-ui/core";
|
||||
|
||||
type SendNymFormProps = {
|
||||
address: string,
|
||||
setFormStatus: (nonEmpty: boolean) => void,
|
||||
}
|
||||
|
||||
export default function SendNymForm({ address, setFormStatus }: SendNymFormProps) {
|
||||
const [recipientHasValue, setRecipientHasValue] = React.useState(false)
|
||||
const [amountHasValue, setAmountHasValue] = React.useState(false)
|
||||
const [validAmount, setValidAmount] = React.useState(true)
|
||||
|
||||
const handleInputData = (element) => (event) => {
|
||||
if (element === "recipient") {
|
||||
let nonZeroRecipient = event.target.value.length > 0
|
||||
setRecipientHasValue(nonZeroRecipient)
|
||||
setFormStatus(nonZeroRecipient && amountHasValue)
|
||||
} else if (element === "amount") {
|
||||
let nonZeroAmount = event.target.value.length > 0
|
||||
setAmountHasValue(nonZeroAmount)
|
||||
setFormStatus(recipientHasValue && nonZeroAmount)
|
||||
if (nonZeroAmount) {
|
||||
// don't ask me about that. javascript works in mysterious ways
|
||||
// and this is apparently a good way of checking if string
|
||||
// is purely made of numeric characters
|
||||
let parsed = +event.target.value
|
||||
if (isNaN(parsed)) {
|
||||
setValidAmount(false)
|
||||
} else {
|
||||
setValidAmount(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Enter recipient address and the amount
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Sending from {address}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
required
|
||||
id="recipient"
|
||||
name="recipient"
|
||||
label="Recipient address"
|
||||
onChange={handleInputData("recipient")}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
required
|
||||
id="amount"
|
||||
name="amount"
|
||||
label="Amount"
|
||||
error={!validAmount}
|
||||
helperText={validAmount ? "" : "Please enter a valid amount"}
|
||||
onChange={handleInputData("amount")}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { NodeType } from "../../common/node";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { ValidatorClientContext } from "../../contexts/ValidatorClient";
|
||||
import NoClientError from "../NoClientError";
|
||||
import { Grid, LinearProgress, Paper } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import UnbondNotice from "./UnbondNotice";
|
||||
import Confirmation from "../Confirmation";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { checkNodesOwnership, makeBasicStyle } from "../../common/helpers";
|
||||
import ExecFeeNotice from "../ExecFeeNotice";
|
||||
|
||||
const UnbondNode = () => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
const router = useRouter()
|
||||
const {client} = useContext(ValidatorClientContext)
|
||||
|
||||
const [unbondingStarted, setUnbondingStarted] = React.useState(false)
|
||||
const [unbondingFinished, setUnbondingFinished] = React.useState(false)
|
||||
const [unbondingError, setUnbondingError] = React.useState(null)
|
||||
|
||||
const [checkedOwnership, setCheckedOwnership] = React.useState(false)
|
||||
const [ownsMixnode, setOwnsMixnode] = React.useState(false)
|
||||
const [ownsGateway, setOwnsGateway] = React.useState(false)
|
||||
|
||||
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
|
||||
|
||||
useEffect(() => {
|
||||
const checkOwnership = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
} else {
|
||||
const nodeOwnership = await checkNodesOwnership(client).finally(() => setCheckedOwnership(true));
|
||||
setOwnsMixnode(nodeOwnership.ownsMixnode)
|
||||
setOwnsGateway(nodeOwnership.ownsGateway)
|
||||
if (nodeOwnership.ownsGateway) {
|
||||
setNodeType(NodeType.Gateway)
|
||||
}
|
||||
}
|
||||
}
|
||||
checkOwnership()
|
||||
}, [client])
|
||||
|
||||
const unbondNode = async (event) => {
|
||||
setUnbondingStarted(true)
|
||||
event.preventDefault();
|
||||
console.log(`UNBOND button pressed`);
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
client.unbondMixnode()
|
||||
.then(value => console.log("unbonded mixnode!", value))
|
||||
.catch(err => setUnbondingError(err))
|
||||
.finally(() => setUnbondingFinished(true))
|
||||
} else {
|
||||
client.unbondGateway()
|
||||
.then(value => console.log("unbonded gateway!", value))
|
||||
.catch(err => setUnbondingError(err))
|
||||
.finally(() => setUnbondingFinished(true))
|
||||
}
|
||||
}
|
||||
|
||||
const getUnbondContent = () => {
|
||||
// we're not signed in
|
||||
if (client === null) {
|
||||
return (<NoClientError />)
|
||||
}
|
||||
|
||||
// we haven't checked whether we actually own a node to unbond
|
||||
if (!checkedOwnership) {
|
||||
return (<LinearProgress />)
|
||||
}
|
||||
|
||||
// somehow this address has both a mixnode and a gateway bonded - this is super undesirable
|
||||
// if that happens it means the user must have sent transactions outside the wallet before the contract update
|
||||
// so they can send transactions outside the wallet to fix themselves up
|
||||
if (ownsMixnode && ownsGateway) {
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography gutterBottom>
|
||||
You seem to have both a mixnode and a gateway bonded - how the hell did you manage to do that?
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// we don't own anything
|
||||
if (!ownsMixnode && !ownsGateway) {
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography gutterBottom>
|
||||
You do not currently have a mixnode or a gateway bonded.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// we haven't clicked unbond button yet
|
||||
if (!unbondingStarted) {
|
||||
return (
|
||||
<UnbondNotice onClick={unbondNode}/>
|
||||
)
|
||||
}
|
||||
|
||||
// We started unbonding
|
||||
return (
|
||||
<Confirmation
|
||||
finished={unbondingFinished}
|
||||
error={unbondingError}
|
||||
progressMessage={`${nodeType} unbonding is in progress...`}
|
||||
successMessage={`${nodeType} unbonding was successful!`}
|
||||
failureMessage={`Failed to unbond the ${nodeType}!`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
let headerText = "Node"
|
||||
if (ownsGateway || ownsGateway) {
|
||||
headerText = nodeType
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<ExecFeeNotice name={"unbonding"}/>
|
||||
<Typography component="h1" variant="h4" align="center" className={classes.wrapper}>
|
||||
Unbond a {headerText}
|
||||
</Typography>
|
||||
{getUnbondContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnbondNode
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Button, Grid} from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import React from "react";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { makeBasicStyle } from "../../common/helpers";
|
||||
|
||||
type unbondNoticeProps = {
|
||||
onClick: (event: any) => void
|
||||
}
|
||||
|
||||
export default function UnbondNotice(props: unbondNoticeProps) {
|
||||
const classes = makeBasicStyle(theme);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Typography gutterBottom>
|
||||
You can only have 1 mixnode or gateway per account. Unbond it by pressing the button below.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
Unbond
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Paper } from "@material-ui/core";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { useRouter } from "next/router";
|
||||
import { ValidatorClientContext } from "../../contexts/ValidatorClient";
|
||||
import { NodeType } from "../../common/node";
|
||||
import NoClientError from "../NoClientError";
|
||||
import Confirmation from "../Confirmation";
|
||||
import { theme } from "../../lib/theme";
|
||||
import { makeBasicStyle } from "../../common/helpers";
|
||||
import NodeTypeChooser from "../NodeTypeChooser";
|
||||
import NodeIdentityForm from "../NodeIdentityForm";
|
||||
import ExecFeeNotice from "../ExecFeeNotice";
|
||||
|
||||
|
||||
const UndelegateFromNode = () => {
|
||||
const classes = makeBasicStyle(theme);
|
||||
const router = useRouter()
|
||||
const { client } = useContext(ValidatorClientContext)
|
||||
|
||||
const [undelegationStarted, setUndelegationStarted] = React.useState(false)
|
||||
const [undelegationFinished, setUndelegationFinished] = React.useState(false)
|
||||
const [undelegationError, setUndelegationError] = React.useState(null)
|
||||
|
||||
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
|
||||
|
||||
useEffect(() => {
|
||||
const checkClient = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
}
|
||||
}
|
||||
checkClient()
|
||||
}, [client])
|
||||
|
||||
|
||||
const undelegateFromNode = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log(`UNDELEGATE button pressed`);
|
||||
|
||||
let address = event.target.identity.value
|
||||
setUndelegationStarted(true)
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
client.removeMixnodeDelegation(address).then((value => {
|
||||
console.log("undelegated from mixnode!", value)
|
||||
})).catch(setUndelegationError).finally(() => setUndelegationFinished(true))
|
||||
} else {
|
||||
client.removeGatewayDelegation(address).then((value => {
|
||||
console.log("undelegated from gateway!", value)
|
||||
})).catch(setUndelegationError).finally(() => setUndelegationFinished(true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getUndelegationContent = () => {
|
||||
// we're not signed in
|
||||
if (client === null) {
|
||||
return (<NoClientError />)
|
||||
}
|
||||
|
||||
// we haven't clicked undelegate button yet
|
||||
if (!undelegationStarted) {
|
||||
return (
|
||||
<React.Fragment >
|
||||
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
|
||||
<NodeIdentityForm onSubmit={undelegateFromNode} buttonText={"Remove delegation"}/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// We started delegation
|
||||
return (
|
||||
<Confirmation
|
||||
finished={undelegationFinished}
|
||||
error={undelegationError}
|
||||
progressMessage={`${nodeType} undelegation is in progress...`}
|
||||
successMessage={`${nodeType} undelegation was successful!`}
|
||||
failureMessage={`Failed to undelegate from a ${nodeType}!`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<ExecFeeNotice name={"undelegating stake"}/>
|
||||
<Typography component="h1" variant="h4" align="center" className={classes.wrapper}>
|
||||
Undelegate stake from {nodeType}
|
||||
</Typography>
|
||||
{getUndelegationContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UndelegateFromNode;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ValidatorClient from "@nymproject/nym-validator-client";
|
||||
|
||||
type ClientContext = {
|
||||
client: ValidatorClient,
|
||||
setClient: (client: ValidatorClient) => void
|
||||
}
|
||||
|
||||
const defaultValue: ClientContext = {
|
||||
client: null,
|
||||
setClient: () => { },
|
||||
}
|
||||
|
||||
export const ValidatorClientContext = React.createContext(defaultValue);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import red from '@material-ui/core/colors/red';
|
||||
|
||||
export const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#556cd6',
|
||||
},
|
||||
secondary: {
|
||||
main: '#19857b',
|
||||
},
|
||||
error: {
|
||||
main: red.A400,
|
||||
},
|
||||
background: {
|
||||
default: '#fff',
|
||||
},
|
||||
},
|
||||
});
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
@@ -0,0 +1,7 @@
|
||||
// according to https://nextjs.org/docs/messages/webpack5 this should
|
||||
// improve speed of subsequent `next build` calls due to better caching
|
||||
module.exports = {
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "nym-wallet-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Bond, unbond, and delegate stake on Nym mixnodes, transfer Nym coins, and run Nym privacy clients.",
|
||||
"author": "Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build && next export",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@nymproject/nym-validator-client": "0.16.0",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"bs58": "^4.0.1",
|
||||
"next": "10.1.3",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.6.4",
|
||||
"@types/react": "^16.9.49",
|
||||
"typescript": "^4.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { theme } from '../lib/theme';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
|
||||
// TODO: should it perhaps be pulled from some config or also user provided?
|
||||
export const BONDING_CONTRACT_ADDRESS: string = "punk10pyejy66429refv3g35g2t7am0was7yalwrzen";
|
||||
export const VALIDATOR_URLS: string[] = [
|
||||
"https://testnet-milhon-validator1.nymtech.net",
|
||||
"https://testnet-milhon-validator2.nymtech.net",
|
||||
];
|
||||
export const ADDRESS_LENGTH: number = 43;
|
||||
export const ADMIN_ADDRESS: string = "punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0"
|
||||
export const DENOM: string = "punk"; // used everywhere else
|
||||
export const KEY_LENGTH: number = 32;
|
||||
export const UDENOM: string = "upunk"; // required for client and coin construction
|
||||
|
||||
|
||||
export default function Application(props: AppProps) {
|
||||
const { Component, pageProps } = props;
|
||||
|
||||
const [client, setClient] = useState(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
const jssStyles = document.querySelector('#jss-server-side');
|
||||
if (jssStyles) {
|
||||
jssStyles.parentElement.removeChild(jssStyles);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||
<title>Nym</title>
|
||||
</Head>
|
||||
<ValidatorClientContext.Provider value={{ client, setClient }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ValidatorClientContext.Provider>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { ServerStyleSheets } from '@material-ui/styles';
|
||||
import { theme } from '../lib/theme';
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en" dir="ltr">
|
||||
<Head>
|
||||
<meta name="theme-color" content={theme.palette.primary.main} />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyDocument.getInitialProps = async ctx => {
|
||||
const sheets = new ServerStyleSheets();
|
||||
const originalRenderPage = ctx.renderPage;
|
||||
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
|
||||
});
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: [
|
||||
...React.Children.toArray(initialProps.styles),
|
||||
sheets.getStyleElement(),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
import MainNav from "../components/MainNav";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import NoClientError from "../components/NoClientError";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { ADMIN_ADDRESS } from "./_app";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import AdminForm from "../components/admin/AdminForm";
|
||||
import { StateParams } from "@nymproject/nym-validator-client";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Admin() {
|
||||
const classes = useStyles();
|
||||
const router = useRouter()
|
||||
|
||||
const {client} = useContext(ValidatorClientContext)
|
||||
|
||||
const [currentStateParams, setCurrentStateParams] = useState(null)
|
||||
const [updateError, setUpdateError] = useState(null)
|
||||
const [updatingState, setUpdatingState] = useState(false)
|
||||
|
||||
// naive way of preventing non-tech-savvy people from accessing this page by manually changing
|
||||
// page to /admin and thinking they're "l33t h4x0rs".
|
||||
// However, even if somebody does access this page by telling the page their address is
|
||||
// the same as that of the current admin,
|
||||
// nothing is going to happen as they will be unable to sign a transaction to the contract without having
|
||||
// access to the account itself
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (client === null || client.address !== ADMIN_ADDRESS) {
|
||||
await router.push("404")
|
||||
} else {
|
||||
let params = await client.getStateParams()
|
||||
setCurrentStateParams(params)
|
||||
}
|
||||
})()
|
||||
}, [client])
|
||||
|
||||
const updateStateParams = async (event) => {
|
||||
event.preventDefault()
|
||||
let newState: StateParams = {
|
||||
// need to convert those to native coin (i.e. hal -> uhal)
|
||||
minimum_mixnode_bond: printableBalanceToNative(event.target.mix_bond.value),
|
||||
minimum_gateway_bond: printableBalanceToNative(event.target.gateway_bond.value),
|
||||
mixnode_bond_reward_rate: event.target.mix_reward.value,
|
||||
gateway_bond_reward_rate: event.target.gateway_reward.value,
|
||||
epoch_length: parseInt(event.target.epoch_length.value),
|
||||
mixnode_active_set_size: parseInt(event.target.active_set.value),
|
||||
};
|
||||
setUpdatingState(true)
|
||||
await client.updateStateParams(newState)
|
||||
.then((_) => setCurrentStateParams(newState))
|
||||
.catch((err) => setUpdateError(err))
|
||||
.finally(() => setUpdatingState(false))
|
||||
}
|
||||
|
||||
const getAdminContent = () => {
|
||||
// we're not signed in (I guess it should get displayed super briefly before getting forwarded
|
||||
// to a 404)
|
||||
if (client === null) {
|
||||
return (<NoClientError/>)
|
||||
}
|
||||
|
||||
// we encountered error while trying to update state
|
||||
if (updateError !== null) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
<AlertTitle>{updateError.name}</AlertTitle>
|
||||
<strong>Failed to update contract state</strong> - {updateError.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
// We're getting current state params
|
||||
if (currentStateParams === null) {
|
||||
return (<LinearProgress/>)
|
||||
}
|
||||
|
||||
if (updatingState) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
<Typography component="h1" variant="h5" align="center">
|
||||
Updating contract state...
|
||||
</Typography>
|
||||
<LinearProgress/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (<AdminForm onSubmit={updateStateParams} currentParams={currentStateParams}/>)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav/>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Contract control
|
||||
</Typography>
|
||||
|
||||
{getAdminContent()}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Confirmation from "../components/Confirmation";
|
||||
import RefreshIcon from "@material-ui/icons/Refresh"
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
import NoClientError from "../components/NoClientError";
|
||||
import { useRouter } from "next/router";
|
||||
import { printableCoin } from "@nymproject/nym-validator-client";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CheckBalance() {
|
||||
const classes = useStyles();
|
||||
const router = useRouter()
|
||||
|
||||
const { client } = useContext(ValidatorClientContext)
|
||||
|
||||
useEffect(() => {
|
||||
const updateBalance = async () => {
|
||||
if (client === null) {
|
||||
await router.push("/")
|
||||
} else {
|
||||
await getBalance()
|
||||
}
|
||||
}
|
||||
updateBalance()
|
||||
}, [client])
|
||||
|
||||
const [balanceCheckStarted, setBalanceCheckStarted] = React.useState(false)
|
||||
const [balanceCheckFinished, setBalanceCheckFinished] = React.useState(false)
|
||||
const [balanceCheckError, setBalanceCheckError] = React.useState(null)
|
||||
const [accountBalance, setAccountBalance] = React.useState("")
|
||||
|
||||
const getBalance = async () => {
|
||||
setBalanceCheckFinished(false)
|
||||
setBalanceCheckStarted(true)
|
||||
|
||||
console.log(`using the context client, our address is ${client.address}`);
|
||||
|
||||
client.getBalance(client.address).then(value => {
|
||||
setAccountBalance(printableCoin(value))
|
||||
setBalanceCheckFinished(true)
|
||||
}).catch(err => {
|
||||
setBalanceCheckError(err)
|
||||
setBalanceCheckFinished(true)
|
||||
})
|
||||
}
|
||||
|
||||
const balanceMessage = `Current account balance is ${accountBalance}`
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Check Balance
|
||||
</Typography>
|
||||
|
||||
{client === null ?
|
||||
(
|
||||
<NoClientError />
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Confirmation
|
||||
finished={balanceCheckFinished}
|
||||
error={balanceCheckError}
|
||||
progressMessage="Checking balance..."
|
||||
successMessage={balanceMessage}
|
||||
failureMessage="Failed to check the account balance!"
|
||||
/>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={getBalance}
|
||||
disabled={!balanceCheckFinished}
|
||||
className={classes.button}
|
||||
startIcon={<RefreshIcon />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment >
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import BondNode from "../components/bond/NodeBond";
|
||||
|
||||
const Bond = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<BondNode/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Bond
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import DelegationCheck from "../components/delegation-check/DelegationCheck";
|
||||
|
||||
const CheckDelegation = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<DelegationCheck />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckDelegation
|
||||
@@ -0,0 +1,148 @@
|
||||
import React, { useState } from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import ValidatorClient from "@nymproject/nym-validator-client";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
type AccountDetailsProps = {
|
||||
mnemonic: String,
|
||||
address: String
|
||||
}
|
||||
|
||||
function AccountDetails(props: AccountDetailsProps) {
|
||||
if (props.mnemonic === "" || props.address === "") {
|
||||
return <React.Fragment />
|
||||
}
|
||||
|
||||
return (
|
||||
// Yeah, I probably should have done it properly with CSS but inserting `<br />` was way quicker to achieve
|
||||
// the same result
|
||||
<React.Fragment>
|
||||
<br />
|
||||
|
||||
<Typography variant="h6" align="center">
|
||||
Mnemonic
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{props.mnemonic}
|
||||
</Typography>
|
||||
<br />
|
||||
|
||||
<Typography variant="h6" align="center">
|
||||
Account Address
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{props.address}
|
||||
</Typography>
|
||||
<br />
|
||||
|
||||
<Typography variant="body2">
|
||||
Save your mnemonic in a safe space as it's your only way to recover your account!
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CreateAccount() {
|
||||
const classes = useStyles();
|
||||
const router = useRouter()
|
||||
|
||||
const [mnemonic, setMnemonic] = useState("");
|
||||
const [address, setAddress] = useState("")
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [created, setCreated] = useState(false)
|
||||
|
||||
const createAccount = () => {
|
||||
let mnemonic = ValidatorClient.randomMnemonic();
|
||||
setMnemonic(mnemonic)
|
||||
|
||||
ValidatorClient.mnemonicToAddress(mnemonic, "punk").then((address) => {
|
||||
setAddress(address)
|
||||
setCreated(true)
|
||||
})
|
||||
}
|
||||
|
||||
const handleBack = async (event) => {
|
||||
setLoading(true)
|
||||
event.preventDefault()
|
||||
await router.push("/")
|
||||
// no need to set loading to false as at this point the component is unmounted
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Create new account
|
||||
</Typography>
|
||||
<AccountDetails mnemonic={mnemonic} address={address} />
|
||||
{loading && <LinearProgress />}
|
||||
<div className={classes.buttons}>
|
||||
{!created ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={createAccount}
|
||||
className={classes.button}
|
||||
>
|
||||
Create new
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleBack}
|
||||
className={classes.button}
|
||||
>
|
||||
Go back to sign in
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment >
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import NodeDelegation from "../components/delegate/NodeDelegation";
|
||||
|
||||
const DelegateStake = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<NodeDelegation />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default DelegateStake
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import { Theme, makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
textAlign: 'center',
|
||||
paddingTop: theme.spacing(4),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const Home = () => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Head>
|
||||
<title>Nym Wallet</title>
|
||||
</Head>
|
||||
<div className={classes.root}>
|
||||
<SignIn />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Stepper from '@material-ui/core/Stepper';
|
||||
import Step from '@material-ui/core/Step';
|
||||
import StepLabel from '@material-ui/core/StepLabel';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Review } from '../components/send-funds/Review';
|
||||
import SendNymForm from '../components/send-funds/SendNymForm';
|
||||
import { coin, Coin, printableCoin } from '@nymproject/nym-validator-client';
|
||||
import Confirmation from '../components/Confirmation';
|
||||
import MainNav from '../components/MainNav';
|
||||
import { ValidatorClientContext } from "../contexts/ValidatorClient";
|
||||
import NoClientError from "../components/NoClientError";
|
||||
import { UDENOM } from './_app';
|
||||
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
padding: theme.spacing(2),
|
||||
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(6),
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
},
|
||||
stepper: {
|
||||
padding: theme.spacing(3, 0, 5),
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const steps = ['Enter addresses', 'Review & send', 'Await confirmation'];
|
||||
|
||||
export interface SendFundsMsg {
|
||||
sender: string,
|
||||
recipient: string,
|
||||
coin?: Coin,
|
||||
}
|
||||
|
||||
export default function SendFunds() {
|
||||
const getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <SendNymForm address={client.address || ""} setFormStatus={setFormStatus} />;
|
||||
case 1:
|
||||
return <Review {...transaction}/>;
|
||||
case 2:
|
||||
const successMessage = `Funds transfer was complete! - sent ${printableCoin(transaction.coin)} to ${transaction.recipient}`
|
||||
return <Confirmation
|
||||
finished={sendingFinished}
|
||||
progressMessage="Funds transfer is in progress..."
|
||||
successMessage={successMessage}
|
||||
failureMessage="Failed to complete the transfer"
|
||||
error={sendingError}
|
||||
/>
|
||||
default:
|
||||
throw new Error('Unknown step');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const classes = useStyles();
|
||||
const { client } = useContext(ValidatorClientContext)
|
||||
|
||||
console.log("client in send", client)
|
||||
|
||||
|
||||
// Here's the React state
|
||||
const [activeStep, setActiveStep] = React.useState(0);
|
||||
const send: SendFundsMsg = { sender: "", recipient: "", coin: null };
|
||||
const [transaction, setTransaction] = React.useState(send);
|
||||
const [formFilled, setFormFilled] = React.useState(false)
|
||||
|
||||
const [sendingStarted, setSendingStarted] = React.useState(false)
|
||||
const [sendingFinished, setSendingFinished] = React.useState(false)
|
||||
const [sendingError, setSendingError] = React.useState(null)
|
||||
|
||||
|
||||
const setFormStatus = (nonEmpty: boolean) => {
|
||||
setFormFilled(nonEmpty)
|
||||
}
|
||||
|
||||
const handleNext = (event) => {
|
||||
event.preventDefault();
|
||||
if (activeStep == 0) {
|
||||
console.log("activeStep is 0, handling form")
|
||||
try {
|
||||
handleForm(event);
|
||||
setActiveStep(activeStep + 1);
|
||||
} catch (e) {
|
||||
// right now just don't do anything.
|
||||
// this error can be thrown when a value with more than 6 fractionalDigits is entered
|
||||
// ideally it should show an error to the user, but it'd involve some additional
|
||||
// work to correctly wire it to the form and I'm not sure it's worth it at this current
|
||||
// time
|
||||
}
|
||||
} else if (activeStep == 1) {
|
||||
console.log("activeStep is 1, sending funds")
|
||||
setActiveStep(activeStep + 1);
|
||||
setSendingStarted(true)
|
||||
console.log("starting funds transfer")
|
||||
sendFunds(transaction).then(() => {
|
||||
console.log("funds transfer is finished!")
|
||||
setSendingStarted(false)
|
||||
setSendingFinished(true)
|
||||
}).catch(err => {
|
||||
setSendingError(err)
|
||||
setSendingStarted(false)
|
||||
setSendingFinished(true)
|
||||
});
|
||||
} else {
|
||||
console.log("resetting the progress")
|
||||
setSendingStarted(false)
|
||||
setSendingFinished(false)
|
||||
setSendingError(null)
|
||||
setActiveStep(0)
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep(activeStep - 1);
|
||||
};
|
||||
|
||||
const getCoinValue = (raw: string): number => {
|
||||
let native = printableBalanceToNative(raw)
|
||||
return parseInt(native)
|
||||
}
|
||||
|
||||
const handleForm = (event) => {
|
||||
event.preventDefault();
|
||||
let coinValue = getCoinValue(event.target.amount.value)
|
||||
|
||||
const send: SendFundsMsg = {
|
||||
sender: client.address,
|
||||
recipient: event.target.recipient.value,
|
||||
coin: coin(coinValue, UDENOM)
|
||||
};
|
||||
console.log("Setting transaction", send);
|
||||
setTransaction(send);
|
||||
}
|
||||
|
||||
const sendFunds = async (transaction: SendFundsMsg) => {
|
||||
console.log(`using the context client, our address is ${client.address}`);
|
||||
await client.send(client.address, transaction.recipient, [transaction.coin]);
|
||||
}
|
||||
|
||||
const checkButtonDisabled = (): boolean => {
|
||||
if (activeStep === 0) {
|
||||
return !formFilled
|
||||
// the form must be filled
|
||||
} else if (activeStep === 1) {
|
||||
// it should always be enabled
|
||||
return false
|
||||
} else if (activeStep === 2) {
|
||||
// transfer must be completed
|
||||
return sendingStarted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const getStepperContent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Stepper activeStep={activeStep} className={classes.stepper}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<React.Fragment>
|
||||
{activeStep === steps.length ? (
|
||||
<React.Fragment>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Payment complete.
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
You (<b>{transaction.sender}</b>)
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
have sent <b>{printableCoin(transaction.coin)}</b>
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
to <b>{transaction.recipient}</b>.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<form onSubmit={handleNext}>
|
||||
{getStepContent(activeStep)}
|
||||
<div className={classes.buttons}>
|
||||
{activeStep !== 0 && (
|
||||
<Button onClick={handleBack} className={classes.button}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={checkButtonDisabled()}
|
||||
className={classes.button}
|
||||
>
|
||||
{activeStep === 1 ? 'Send' : (activeStep === steps.length - 1 ? 'Send again' : 'Next')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Send Nym
|
||||
</Typography>
|
||||
|
||||
{client === null ? (
|
||||
<NoClientError />
|
||||
) : (
|
||||
getStepperContent()
|
||||
)}
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import UnbondNode from "../components/unbond/UnbondNode";
|
||||
|
||||
const Unbond = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav/>
|
||||
<UnbondNode />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Unbond
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import MainNav from "../components/MainNav";
|
||||
import NodeUndelegation from "../components/undelegate/NodeUndelegation";
|
||||
|
||||
const UndelegateStake = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainNav />
|
||||
<NodeUndelegation />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default UndelegateStake
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,122 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-basis: 45%;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user