Merge pull request #698 from nymtech/bond_and_delegation_alerts

Bond and delegation alerts
This commit is contained in:
Fouad
2021-07-26 13:48:35 +01:00
committed by GitHub
16 changed files with 10266 additions and 3420 deletions
+155 -116
View File
@@ -1,148 +1,187 @@
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
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";
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),
}
})
);
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
ownsMixnode: boolean
ownsGateway: boolean
}
export async function checkNodesOwnership(client: ValidatorClient): Promise<NodeOwnership> {
const ownsMixnodePromise = client.ownsMixNode();
const ownsGatewayPromise = client.ownsGateway();
export async function checkNodesOwnership(
client: ValidatorClient
): Promise<NodeOwnership> {
const ownsMixnodePromise = client.ownsMixNode()
const ownsGatewayPromise = client.ownsGateway()
let ownsMixnode = false;
let ownsGateway = false;
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
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)
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
}
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
// 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
}
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 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 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;
// 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])
const table = buildFeeTable(nymGasPrice(DENOM), 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])
const table = buildFeeTable(nymGasPrice(DENOM), nymGasLimits, nymGasLimits)
return printableCoin(table.send.amount[0])
}
// Check amount to bond or delegate is valid
export const checkAllocationSize = (
allocationValue: number,
walletValue: number,
transactionType: 'bond' | 'delegate'
) => {
const remaining = walletValue - allocationValue
const table = buildFeeTable(nymGasPrice(DENOM), nymGasLimits, nymGasLimits)
const threshold =
transactionType === 'bond'
? +table.exec.amount[0].amount * 2
: +table.exec.amount[0].amount
if (remaining < 0) {
return {
error: true,
message: 'The allocation size is greater than the value of your wallet',
}
}
if (walletValue > 0 && remaining < threshold) {
return {
error: false,
message: `You'll only have ${printableCoin({
amount: remaining.toString(),
denom: UDENOM,
})} after this transaction. You may want to keep some in order to un${transactionType} this mixnode at a later time.`,
}
}
return {
error: false,
message: undefined,
}
}
+38 -36
View File
@@ -1,41 +1,43 @@
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';
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,
isLoading: 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>
);
export default function Confirmation({
isLoading,
progressMessage,
successMessage,
failureMessage,
error,
}: ConfirmationProps) {
return isLoading ? (
<>
<Typography variant='h6' gutterBottom>
{progressMessage}
</Typography>
<Grid item xs={12} sm={6}>
<CircularProgress />
</Grid>
</>
) : (
<>
{error === null ? (
<Alert severity='success'>{successMessage}</Alert>
) : (
<Alert severity='error'>
<AlertTitle>{error.name}</AlertTitle>
<strong>{failureMessage}</strong> - {error.message}
</Alert>
)}
</>
)
}
+2 -5
View File
@@ -1,15 +1,12 @@
import {Alert} from '@material-ui/lab';
import { getDisplayExecGasFee } from "../common/helpers";
type ExecFeeNoticeProps = {
name: string
}
const ExecFeeNotice = (props: ExecFeeNoticeProps) => {
const ExecFeeNotice = ({name}: {name: string}) => {
return (
<Alert severity="info">
The gas fee for
<strong> {props.name} </strong>
<strong> {name} </strong>
{`is ${getDisplayExecGasFee()}`}
</Alert>
)
+472 -353
View File
@@ -1,380 +1,499 @@
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, nativeToPrintable } from "@nymproject/nym-validator-client";
import { DENOM } from "../../pages/_app";
import { printableBalanceToNative } from "@nymproject/nym-validator-client/dist/currency";
import { BondingInformation } from "./NodeBond";
import React, { useEffect, useState, ChangeEvent } from 'react'
import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency'
import { Coin, nativeToPrintable } from '@nymproject/nym-validator-client'
import { Alert } from '@material-ui/lab'
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,
checkAllocationSize,
makeBasicStyle,
validateRawPort,
} from '../../common/helpers'
import { DENOM } from '../../pages/_app'
import { BondingInformation } from './NodeBond'
import { useGetBalance } from '../../hooks/useGetBalance'
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
type TBondNodeFormProps = {
type: NodeType
minimumMixnodeBond: Coin
minimumGatewayBond: Coin
onSubmit: (event: any) => void
}
export default function BondNodeForm(props: BondNodeFormProps) {
const classes = makeBasicStyle(theme);
type TFormInput = {
amount: string
host: string
http_api_port: string
mixPort: string
verlocPort: string
sphinxKey: string
identityKey: string
version: string
location?: string
clientsPort?: string
httpApiPort?: string
}
const [validity, setValidity] = React.useState({
validAmount: true,
validSphinxKey: true,
validIdentityKey: true,
validHost: true,
validVersion: true,
validLocation: true,
validMixPort: true,
export default function BondNodeForm(props: TBondNodeFormProps) {
const classes = makeBasicStyle(theme)
// 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,
const [validity, setValidity] = React.useState({
validAmount: true,
validSphinxKey: true,
validIdentityKey: true,
validHost: true,
validVersion: true,
validLocation: true,
validMixPort: true,
// gateway-specific:
validClientsPort: 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 [allocationWarning, setAllocationWarning] = useState<string>()
const [isValidAmount, setIsValidAmount] = useState(true)
const { getBalance, accountBalance } = useGetBalance()
useEffect(() => {
getBalance()
}, [getBalance])
const handleCheckboxToggle = () => {
setAdvancedShown((prevSet) => !prevSet)
}
const handleAmountChange = (e: ChangeEvent<HTMLInputElement>) => {
const parsed = +e.target.value
const balance = +accountBalance.amount
if (isNaN(parsed)) {
setIsValidAmount(false)
} else {
try {
const allocationCheck = checkAllocationSize(
+printableBalanceToNative(e.target.value),
balance,
'bond'
)
if (allocationCheck.error) {
setAllocationWarning(allocationCheck.message)
setIsValidAmount(false)
} else {
setAllocationWarning(allocationCheck.message)
setIsValidAmount(true)
}
} catch {
setIsValidAmount(false)
}
}
}
const validateForm = ({
amount,
sphinxKey,
identityKey,
host,
version,
verlocPort,
location,
mixPort,
httpApiPort,
clientsPort,
}: TFormInput): boolean => {
let validAmount = validateAmount(amount)
let validSphinxKey = validateKey(sphinxKey)
let validIdentityKey = validateKey(identityKey)
let validHost = validateHost(host)
let validVersion = validateVersion(version)
let validLocation =
props.type == NodeType.Gateway ? validateLocation(location) : true
let newValidity = {
validAmount: validAmount,
validSphinxKey: validSphinxKey,
validIdentityKey: validIdentityKey,
validHost: validHost,
validVersion: validVersion,
validLocation: validLocation,
}
if (advancedShown) {
let validMixPort = validateRawPort(mixPort)
let validVerlocPort =
props.type == NodeType.Mixnode ? validateRawPort(verlocPort) : true
let validHttpApiPort =
props.type == NodeType.Mixnode ? validateRawPort(httpApiPort) : true
let validClientsPort =
props.type == NodeType.Gateway ? validateRawPort(clientsPort) : true
newValidity = {
...newValidity,
...{
validMixPort: validMixPort,
validVerlocPort: validVerlocPort,
validHttpApiPort: validHttpApiPort,
validClientsPort: validClientsPort,
},
}
}
setValidity((previousState) => {
return { ...previousState, ...newValidity }
})
const [advancedShown, setAdvancedShown] = React.useState(false)
// just AND everything together
const reducer = (acc, current) => acc && current
return Object.entries(newValidity)
.map((entry) => entry[1])
.reduce(reducer, true)
}
const handleCheckboxToggle = () => {
setAdvancedShown((prevSet) => !prevSet);
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 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);
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
}
}
let validLocation = (props.type == NodeType.Gateway) ? validateLocation(event.target.location.value) : true;
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
let newValidity = {
validAmount: validAmount,
validSphinxKey: validSphinxKey,
validIdentityKey: validIdentityKey,
validHost: validHost,
validVersion: validVersion,
// 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
validLocation: validLocation,
}
const dot_occurrences = host.trim().split('.').length - 1
const colon_occurrences = host.trim().split(':').length - 1
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;
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
}
newValidity = {
...newValidity, ...{
validMixPort: validMixPort,
validVerlocPort: validVerlocPort,
validHttpApiPort: validHttpApiPort,
validClientsPort: validClientsPort,
const validateVersion = (version: string): boolean => {
// check if its a valid semver
return semver.valid(version) && semver.minor(version) >= 11
}
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 = ({
amount,
host,
httpApiPort,
mixPort,
verlocPort,
sphinxKey,
identityKey,
version,
}: TFormInput): BondingInformation => {
return {
amount,
nodeDetails: {
host,
http_api_port: advancedShown
? parseInt(httpApiPort)
: DEFAULT_HTTP_API_PORT,
mix_port: advancedShown ? parseInt(mixPort) : DEFAULT_MIX_PORT,
verloc_port: advancedShown ? parseInt(verlocPort) : DEFAULT_VERLOC_PORT,
sphinx_key: sphinxKey,
identity_key: identityKey,
version,
},
}
}
const constructGatewayBondingInfo = (
data: TFormInput
): BondingInformation => {
return {
amount: data.amount,
nodeDetails: {
host: data.host,
mix_port: advancedShown ? parseInt(data.mixPort) : DEFAULT_MIX_PORT,
clients_port: advancedShown
? parseInt(data.clientsPort)
: DEFAULT_CLIENTS_PORT,
sphinx_key: data.sphinxKey,
identity_key: data.identityKey,
version: data.version,
location: data.location,
},
}
}
const submitForm = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const target = event.target as unknown as TFormInput
if (validateForm(target)) {
if (props.type == NodeType.Mixnode) {
return props.onSubmit(constructMixnodeBondingInfo(target))
} else {
return props.onSubmit(constructGatewayBondingInfo(target))
}
}
}
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 ${nativeToPrintable(
minimumBond.amount
)} ${minimumBond.denom})`}
error={!validateAmount}
helperText={
!validateAmount
? `Enter a valid bond amount (minimum ${nativeToPrintable(
minimumBond.amount
)})`
: ''
}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position='end'>{DENOM}</InputAdornment>
),
}}
onChange={handleAmountChange}
/>
</Grid>
{allocationWarning && (
<Grid item>
<Alert severity={!isValidAmount ? 'error' : 'info'}>
{allocationWarning}
</Alert>
</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 (min. 0.11.0), like 0.11.0',
}
: {})}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Checkbox
checked={advancedShown}
onChange={handleCheckboxToggle}
/>
}
}
label='Show advanced options'
/>
</Grid>
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) && semver.minor(version) >= 11
}
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 ${nativeToPrintable(minimumBond.amount)} ${minimumBond.denom})`}
error={!validity.validAmount}
{...(!validity.validAmount ? {helperText: `Enter a valid bond amount (minimum ${nativeToPrintable(minimumBond.amount)})`} : {})}
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 (min. 0.11.0), like 0.11.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>
}
{advancedShown && (
<>
<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>
<div className={classes.buttons}>
<Button
variant="contained"
color="primary"
type="submit"
className={classes.button}
>
Bond
</Button>
</div>
</form>
);
}
{/*yes, I also hate so many layers of indentation here*/}
{props.type === NodeType.Mixnode ? (
<>
<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>
</>
) : (
<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>
)}
</>
)}
</Grid>
<div className={classes.buttons}>
<Button
variant='contained'
color='primary'
type='submit'
className={classes.button}
disabled={!isValidAmount}
>
Bond
</Button>
</div>
</form>
)
}
+161 -142
View File
@@ -1,164 +1,183 @@
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";
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;
amount: string
nodeDetails: MixNode | Gateway
}
const BondNode = () => {
const classes = makeBasicStyle(theme);
const router = useRouter()
const {client} = useContext(ValidatorClientContext)
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 [isLoading, setIsLoading] = 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 [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 [minimumMixnodeBond, setMinimumMixnodeBond] = React.useState(null)
const [minimumGatewayBond, setMinimumGatewayBond] = React.useState(null)
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
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)
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)
const minimumMixnodeBond = await client.minimumMixnodeBond()
setMinimumMixnodeBond(minimumMixnodeBond)
const minimumGatewayBond = await client.minimumGatewayBond()
setMinimumGatewayBond(minimumGatewayBond)
setCheckedOwnership(true)
}
}
getInitialData()
}, [client])
setCheckedOwnership(true)
}
}
getInitialData()
}, [client])
const bondNode = async (bondingInformation: BondingInformation) => {
setBondingStarted(true)
// event.preventDefault();
console.log(`BOND button pressed`);
const bondNode = async (bondingInformation: BondingInformation) => {
setIsLoading(true)
console.log(`BOND button pressed`)
console.log(bondingInformation)
let amountValue = parseInt(printableBalanceToNative(bondingInformation.amount))
let amount = coin(amountValue, UDENOM)
console.log(bondingInformation)
let amountValue = parseInt(
printableBalanceToNative(bondingInformation.amount)
)
let amount = coin(amountValue, UDENOM)
console.log(bondingInformation.nodeDetails)
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))
}
if (nodeType == NodeType.Mixnode) {
let mixnode = bondingInformation.nodeDetails as MixNode
client
.bondMixnode(mixnode, amount)
.then((value) => {
console.log('bonded mixnode!', value)
})
.catch(setBondingError)
.finally(() => setIsLoading(false))
} else {
let gateway = bondingInformation.nodeDetails as Gateway
client
.bondGateway(gateway, amount)
.then((value) => {
console.log('bonded gateway!', value)
})
.catch(setBondingError)
.finally(() => setIsLoading(false))
}
}
const getBondContent = () => {
// we're not signed in
if (client === null) {
return <NoClientError />
}
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}!`}
/>
)
// 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 (!isLoading) {
return (
<>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<BondNodeForm
onSubmit={bondNode}
type={nodeType}
minimumGatewayBond={minimumGatewayBond}
minimumMixnodeBond={minimumMixnodeBond}
/>
</>
)
}
// We started bonding
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>
);
};
<Confirmation
isLoading={isLoading}
error={bondingError}
progressMessage={`${nodeType} bonding is in progress...`}
successMessage={`${nodeType} bonding was successful!`}
failureMessage={`Failed to bond the ${nodeType}!`}
/>
)
}
export default BondNode;
return (
<>
<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>
</>
)
}
export default BondNode
+142 -109
View File
@@ -1,121 +1,154 @@
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";
import React, { useState, useEffect } from 'react'
import Grid from '@material-ui/core/Grid'
import { Button, InputAdornment } from '@material-ui/core'
import TextField from '@material-ui/core/TextField'
import { Alert } from '@material-ui/lab'
import { DENOM } from '../../pages/_app'
import { theme } from '../../lib/theme'
import {
basicRawCoinValueValidation,
checkAllocationSize,
makeBasicStyle,
validateIdentityKey,
} from '../../common/helpers'
import { useGetBalance } from '../../hooks/useGetBalance'
import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency'
type DelegateFormProps = {
onSubmit: (event: any) => void
onSubmit: (event: any) => void
}
export default function DelegateForm(props: DelegateFormProps) {
const classes = makeBasicStyle(theme);
const classes = makeBasicStyle(theme)
const [validAmount, setValidAmount] = React.useState(true)
const [validIdentity, setValidIdentity] = React.useState(true)
// const [checkboxSet, setCheckboxSet] = React.useState(false)
const [isValidAmount, setIsValidAmount] = useState(true)
const [validIdentity, setValidIdentity] = useState(true)
const [allocationWarning, setAllocationWarning] = useState<string>()
const { getBalance, accountBalance } = useGetBalance()
// const handleCheckboxToggle = () => {
// setCheckboxSet((prevSet) => !prevSet);
// }
useEffect(() => {
getBalance()
}, [getBalance])
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 handleAmountChange = (event: any) => {
// 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
const parsed = +event.target.value
const balance = +accountBalance.amount
if (isNaN(parsed)) {
setIsValidAmount(false)
} else {
try {
const allocationCheck = checkAllocationSize(
+printableBalanceToNative(event.target.value),
balance,
'delegate'
)
if (allocationCheck.error) {
setAllocationWarning(allocationCheck.message)
setIsValidAmount(false)
} else {
setAllocationWarning(allocationCheck.message)
setIsValidAmount(true)
}
} catch {
setIsValidAmount(false)
}
}
}
const validateForm = (event: any): boolean => {
let validIdentity = validateIdentityKey(event.target.identity.value)
let validAmount = validateAmount(event.target.amount.value)
setValidIdentity(validIdentity)
setIsValidAmount(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>
const validateForm = (event: any): boolean => {
let validIdentity = validateIdentityKey(event.target.identity.value);
let validAmount = validateAmount(event.target.amount.value);
<Grid item xs={12} sm={6}>
<TextField
required
id='amount'
name='amount'
label='Amount to delegate'
error={!isValidAmount}
helperText={isValidAmount ? '' : 'Please enter a valid amount'}
onChange={handleAmountChange}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position='end'>{DENOM}</InputAdornment>
),
}}
/>
</Grid>
{allocationWarning && (
<Grid item>
<Alert severity={!isValidAmount ? 'error' : 'info'}>
{allocationWarning}
</Alert>
</Grid>
)}
setValidIdentity(validIdentity)
setValidAmount(validAmount)
{/*<Grid item xs={12}>*/}
{/* <FormControlLabel*/}
{/* control={*/}
{/* <Checkbox*/}
{/* checked={checkboxSet}*/}
{/* onChange={handleCheckboxToggle}*/}
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>
)
}
{/* />*/}
{/* }*/}
{/* label="checkbox text"*/}
{/* />*/}
{/*</Grid>*/}
</Grid>
<div className={classes.buttons}>
<Button
variant='contained'
color='primary'
type='submit'
className={classes.button}
disabled={!isValidAmount}
>
Delegate stake
</Button>
</div>
</form>
)
}
+110 -99
View File
@@ -1,112 +1,123 @@
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";
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 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 [isLoading, setIsLoading] = 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("")
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])
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 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))
setIsLoading(true)
if (nodeType == NodeType.Mixnode) {
client
.delegateToMixnode(nodeIdentity, delegationValue)
.then((value) => {
console.log('delegated to mixnode!', value)
})
.catch(setDelegationError)
.finally(() => setIsLoading(false))
} else {
client
.delegateToGateway(nodeIdentity, delegationValue)
.then((value) => {
console.log('delegated to gateway!', value)
})
.catch(setDelegationError)
.finally(() => setIsLoading(false))
}
}
const getDelegationContent = () => {
// we're not signed in
if (client === null) {
return <NoClientError />
}
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}!`}
/>
)
// we haven't clicked delegate button yet
if (!isLoading) {
return (
<>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<DelegateForm onSubmit={delegateToNode} />
</>
)
}
// We started delegation
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>
);
<Confirmation
isLoading={isLoading}
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 (
<>
<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>
</>
)
}
export default DelegateToNode;
export default DelegateToNode
@@ -1,114 +1,129 @@
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";
import React, { useContext, useEffect } from 'react'
import { printableCoin } from '@nymproject/nym-validator-client'
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 NodeTypeChooser from '../NodeTypeChooser'
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 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 [isLoading, setIsLoading] = 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("")
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
const [stakeValue, setStakeValue] = React.useState<string>()
const [nodeIdentity, setNodeIdentity] = React.useState('')
useEffect(() => {
const checkClient = async () => {
if (client === null) {
await router.push('/')
}
}
checkClient()
}, [client])
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()
// 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)
}
console.log(`CHECK DELEGATION button pressed`)
let identity = event.target.identity.value
setNodeIdentity(identity)
setIsLoading(true)
if (nodeType == NodeType.Mixnode) {
client
.getMixDelegation(identity, client.address)
.then((value) => {
setStakeValue(printableCoin(value.amount))
})
.catch(handleDelegationCheckError)
.finally(() => setIsLoading(false))
} else {
client
.getGatewayDelegation(identity, client.address)
.then((value) => {
setStakeValue(printableCoin(value.amount))
})
.catch(handleDelegationCheckError)
.finally(() => setIsLoading(false))
}
}
const getDelegationCheckContent = () => {
console.log(isLoading)
// we're not signed in
if (client === null) {
return <NoClientError />
}
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}!`}
/>
)
// we haven't clicked delegate button yet
if (!isLoading && !stakeValue) {
return (
<>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<NodeIdentityForm
onSubmit={checkDelegation}
buttonText='Check stake value'
/>
</>
)
}
// We started the check
const stakeMessage = `Current stake on ${nodeType} ${nodeIdentity} is ${stakeValue}`
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>
);
<Confirmation
isLoading={isLoading}
error={checkError}
progressMessage={`${nodeType} (${nodeIdentity}) stake check is in progress...`}
successMessage={stakeMessage}
failureMessage={`Failed to check stake value on ${nodeType} ${nodeIdentity}!`}
/>
)
}
return (
<>
<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>
</>
)
}
export default DelegationCheck;
export default DelegationCheck
+134 -127
View File
@@ -1,141 +1,148 @@
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";
import React, { useContext, useEffect } from 'react'
import Typography from '@material-ui/core/Typography'
import { Grid, LinearProgress, Paper } from '@material-ui/core'
import { useRouter } from 'next/router'
import { NodeType } from '../../common/node'
import { ValidatorClientContext } from '../../contexts/ValidatorClient'
import NoClientError from '../NoClientError'
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 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 [isLoading, setIsLoading] = 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 [checkedOwnership, setCheckedOwnership] = React.useState(false)
const [ownsMixnode, setOwnsMixnode] = React.useState(false)
const [ownsGateway, setOwnsGateway] = React.useState(false)
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
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}!`}
/>
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) => {
setIsLoading(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(() => setIsLoading(false))
} else {
client
.unbondGateway()
.then((value) => console.log('unbonded gateway!', value))
.catch((err) => setUnbondingError(err))
.finally(() => setIsLoading(false))
}
}
const getUnbondContent = () => {
// we're not signed in
if (client === null) {
return <NoClientError />
}
let headerText = "Node"
if (ownsGateway || ownsGateway) {
headerText = nodeType
// 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 (!isLoading) {
return <UnbondNotice onClick={unbondNode} />
}
// We started unbonding
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>
);
<Confirmation
isLoading={isLoading}
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 (
<>
<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>
</>
)
}
export default UnbondNode
export default UnbondNode
@@ -1,101 +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 { theme } from "../../lib/theme";
import { makeBasicStyle } from "../../common/helpers";
import NodeTypeChooser from "../NodeTypeChooser";
import NodeIdentityForm from "../NodeIdentityForm";
import ExecFeeNotice from "../ExecFeeNotice";
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 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 [isLoading, setIsLoading] = React.useState(false)
const [undelegationError, setUndelegationError] = React.useState(null)
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
const [nodeType, setNodeType] = React.useState(NodeType.Mixnode)
useEffect(() => {
const checkClient = async () => {
if (client === null) {
await router.push("/")
}
}
checkClient()
}, [client])
useEffect(() => {
const checkClient = async () => {
if (client === null) {
await router.push('/')
}
}
checkClient()
}, [client])
const undelegateFromNode = async (event) => {
event.preventDefault()
const undelegateFromNode = async (event) => {
event.preventDefault();
console.log(`UNDELEGATE button pressed`)
console.log(`UNDELEGATE button pressed`);
let address = event.target.identity.value
setIsLoading(true)
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(() => setIsLoading(false))
} else {
client
.removeGatewayDelegation(address)
.then((value) => {
console.log('undelegated from gateway!', value)
})
.catch(setUndelegationError)
.finally(() => setIsLoading(false))
}
}
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 />
}
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 (
// we haven't clicked undelegate button yet
if (!undelegationStarted) {
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>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<NodeIdentityForm
onSubmit={undelegateFromNode}
buttonText={'Remove delegation'}
/>
</React.Fragment>
);
)
}
// We started delegation
return (
<Confirmation
isLoading={isLoading}
error={undelegationError}
progressMessage={`${nodeType} undelegation is in progress...`}
successMessage={`${nodeType} undelegation was successful!`}
failureMessage={`Failed to undelegate from a ${nodeType}!`}
/>
)
}
return (
<>
<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>
</>
)
}
export default UndelegateFromNode;
export default UndelegateFromNode
+7 -8
View File
@@ -1,15 +1,14 @@
import React from 'react';
import React from "react";
import ValidatorClient from "@nymproject/nym-validator-client";
type ClientContext = {
client: ValidatorClient,
setClient: (client: ValidatorClient) => void
}
client: ValidatorClient;
setClient: (client: ValidatorClient) => void;
};
const defaultValue: ClientContext = {
client: null,
setClient: () => { },
}
client: null,
setClient: () => {},
};
export const ValidatorClientContext = React.createContext(defaultValue);
+32
View File
@@ -0,0 +1,32 @@
import { useCallback, useContext, useState } from 'react'
import { Coin, printableCoin } from '@nymproject/nym-validator-client'
import { ValidatorClientContext } from '../contexts/ValidatorClient'
export const useGetBalance = () => {
const { client } = useContext(ValidatorClientContext)
const [isLoading, setIsLoading] = useState(false)
const [balanceCheckError, setBalanceCheckError] = useState(null)
const [accountBalance, setAccountBalance] = useState<Coin>()
const getBalance = useCallback(async () => {
if (client) {
setIsLoading(true)
try {
const value = await client.getBalance(client.address)
setAccountBalance(value)
setIsLoading(false)
} catch (e) {
setBalanceCheckError(e)
}
}
}, [])
return {
balanceCheckError,
isBalanceLoading: isLoading,
accountBalance,
printedBalance: printableCoin(accountBalance),
getBalance,
}
}
+6577
View File
File diff suppressed because it is too large Load Diff
+96 -116
View File
@@ -1,128 +1,108 @@
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";
import React, { useContext, useEffect } from 'react'
import Paper from '@material-ui/core/Paper'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
import RefreshIcon from '@material-ui/icons/Refresh'
import { makeStyles } from '@material-ui/core/styles'
import { useRouter } from 'next/router'
import { ValidatorClientContext } from '../contexts/ValidatorClient'
import MainNav from '../components/MainNav'
import Confirmation from '../components/Confirmation'
import NoClientError from '../components/NoClientError'
import { useGetBalance } from '../hooks/useGetBalance'
const useStyles = makeStyles((theme) => ({
appBar: {
position: 'relative',
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',
},
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),
},
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),
},
}));
},
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 classes = useStyles()
const router = useRouter()
const { client } = useContext(ValidatorClientContext)
const { client } = useContext(ValidatorClientContext)
const { getBalance, isBalanceLoading, balanceCheckError, printedBalance } =
useGetBalance()
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)
})
useEffect(() => {
const updateBalance = async () => {
if (client === null) {
await router.push('/')
} else {
await getBalance()
}
}
updateBalance()
}, [client])
const balanceMessage = `Current account balance is ${accountBalance}`
const balanceMessage = `Current account balance is ${printedBalance}`
return (
<React.Fragment>
<MainNav />
<main className={classes.layout}>
<Paper className={classes.paper}>
<Typography component="h1" variant="h4" align="center">
Check Balance
</Typography>
return (
<>
<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 >
)
}
{client === null ? (
<NoClientError />
) : (
<>
<Confirmation
isLoading={isBalanceLoading}
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={isBalanceLoading}
className={classes.button}
startIcon={<RefreshIcon />}
>
Refresh
</Button>
</div>
</>
)}
</Paper>
</main>
</>
)
}
+246 -242
View File
@@ -1,258 +1,262 @@
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";
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',
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',
},
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),
},
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),
},
}));
},
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'];
const steps = ['Enter addresses', 'Review & send', 'Await confirmation']
export interface SendFundsMsg {
sender: string,
recipient: string,
coin?: Coin,
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 = () => {
const getStepContent = (step) => {
switch (step) {
case 0:
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>
<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
isLoading={isLoading}
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 send: SendFundsMsg = { sender: '', recipient: '', coin: null }
const [activeStep, setActiveStep] = React.useState(0)
const [transaction, setTransaction] = React.useState(send)
const [formFilled, setFormFilled] = React.useState(false)
const [isLoading, setIsLoading] = 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)
setIsLoading(true)
console.log('starting funds transfer')
sendFunds(transaction)
.then(() => {
console.log('funds transfer is finished!')
setIsLoading(false)
})
.catch((err) => {
setSendingError(err)
setIsLoading(false)
})
} else {
console.log('resetting the progress')
setIsLoading(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 isLoading
}
return (
<React.Fragment>
<MainNav />
<main className={classes.layout}>
<Paper className={classes.paper}>
<Typography component="h1" variant="h4" align="center">
Send Nym
</Typography>
return false
}
{client === null ? (
<NoClientError />
) : (
getStepperContent()
)}
</Paper>
</main>
</React.Fragment >
);
const getStepperContent = () => {
return (
<>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<>
{activeStep === steps.length ? (
<>
<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>
</>
) : (
<>
<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>
</>
)}
</>
</>
)
}
return (
<>
<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>
</>
)
}
+1884 -1883
View File
File diff suppressed because it is too large Load Diff