update state management and validation
This commit is contained in:
@@ -1,336 +1,51 @@
|
||||
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 React from 'react'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
} from '@material-ui/core'
|
||||
import bs58 from 'bs58'
|
||||
import semver from 'semver'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { Coin, nativeToPrintable } from '@nymproject/nym-validator-client'
|
||||
import { NodeType } from '../../common/node'
|
||||
import { theme } from '../../lib/theme'
|
||||
import {
|
||||
basicRawCoinValueValidation,
|
||||
checkAllocationSize,
|
||||
makeBasicStyle,
|
||||
validateRawPort,
|
||||
} from '../../common/helpers'
|
||||
import { DENOM } from '../../pages/_app'
|
||||
import { theme } from '../../lib/theme'
|
||||
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
|
||||
import { useBondForm } from './useBondForm'
|
||||
|
||||
type TBondNodeFormProps = {
|
||||
type: NodeType
|
||||
minimumMixnodeBond: Coin
|
||||
minimumGatewayBond: Coin
|
||||
onSubmit: (event: any) => void
|
||||
minimumBond: Coin
|
||||
onSubmit: (values: BondingInformation) => void
|
||||
}
|
||||
|
||||
type TFormStringValue = { value: string }
|
||||
|
||||
type TFormInput = {
|
||||
amount: TFormStringValue
|
||||
identityKey: TFormStringValue
|
||||
sphinxKey: TFormStringValue
|
||||
host: TFormStringValue
|
||||
version: TFormStringValue
|
||||
mixPort?: TFormStringValue
|
||||
verlocPort?: TFormStringValue
|
||||
location?: TFormStringValue
|
||||
clientsPort?: TFormStringValue
|
||||
httpApiPort?: TFormStringValue
|
||||
}
|
||||
|
||||
type TFormData = {
|
||||
amount: string
|
||||
identityKey: string
|
||||
sphinxKey: string
|
||||
host: string
|
||||
version: string
|
||||
mixPort?: string
|
||||
verlocPort?: string
|
||||
location?: string
|
||||
clientsPort?: string
|
||||
httpApiPort?: string
|
||||
}
|
||||
|
||||
export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
const classes = makeBasicStyle(theme)
|
||||
|
||||
const [validity, setValidity] = React.useState({
|
||||
validAmount: true,
|
||||
validSphinxKey: true,
|
||||
validIdentityKey: true,
|
||||
validHost: true,
|
||||
validVersion: true,
|
||||
validLocation: true,
|
||||
validMixPort: true,
|
||||
|
||||
// this should have probably be somehow split to be subclasses of the validity matrix
|
||||
// the above is more true now as more fields are added. This looks kinda disgusting...
|
||||
// mixnode-specific:
|
||||
validVerlocPort: true,
|
||||
validHttpApiPort: true,
|
||||
|
||||
// gateway-specific:
|
||||
validClientsPort: true,
|
||||
})
|
||||
|
||||
export const BondNodeForm = ({
|
||||
type,
|
||||
minimumBond,
|
||||
onSubmit,
|
||||
}: TBondNodeFormProps) => {
|
||||
const [advancedShown, setAdvancedShown] = React.useState(false)
|
||||
const [allocationWarning, setAllocationWarning] = useState<string>()
|
||||
const [isValidAmount, setIsValidAmount] = useState(true)
|
||||
const { getBalance, accountBalance } = useGetBalance()
|
||||
|
||||
useEffect(() => {
|
||||
getBalance()
|
||||
}, [getBalance])
|
||||
const manageForm = useBondForm({ type, minimumBond })
|
||||
|
||||
const matches = useMediaQuery('(min-width:768px)')
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if arguments are undefined we provided a default value for
|
||||
// verlocPort, mixPort, httpApiPort and clientsPort
|
||||
const validateForm = ({
|
||||
amount,
|
||||
sphinxKey,
|
||||
identityKey,
|
||||
host,
|
||||
version,
|
||||
location,
|
||||
verlocPort = DEFAULT_VERLOC_PORT.toString(),
|
||||
mixPort = DEFAULT_MIX_PORT.toString(),
|
||||
httpApiPort = DEFAULT_HTTP_API_PORT.toString(),
|
||||
clientsPort = DEFAULT_CLIENTS_PORT.toString(),
|
||||
}: TFormData): boolean => {
|
||||
let newValidity = {
|
||||
validAmount: validateAmount(amount),
|
||||
validSphinxKey: validateKey(sphinxKey),
|
||||
validIdentityKey: validateKey(identityKey),
|
||||
validHost: validateHost(host),
|
||||
validVersion: validateVersion(version),
|
||||
validMixPort: validateRawPort(mixPort),
|
||||
validLocation:
|
||||
props.type == NodeType.Gateway ? validateLocation(location) : true,
|
||||
validVerlocPort:
|
||||
props.type === NodeType.Mixnode ? validateRawPort(verlocPort) : true,
|
||||
validHttpApiPort:
|
||||
props.type === NodeType.Mixnode ? validateRawPort(httpApiPort) : true,
|
||||
validClientsPort:
|
||||
props.type === NodeType.Gateway ? validateRawPort(clientsPort) : true,
|
||||
}
|
||||
|
||||
setValidity((previousState) => {
|
||||
return { ...previousState, ...newValidity }
|
||||
})
|
||||
|
||||
// check if all values are true and return result
|
||||
return Object.values({ ...validity, ...newValidity }).every(
|
||||
(isValid) => isValid === 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.trim().split('.').length - 1
|
||||
const colon_occurrences = host.trim().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 = ({
|
||||
amount,
|
||||
host,
|
||||
httpApiPort,
|
||||
mixPort,
|
||||
verlocPort,
|
||||
sphinxKey,
|
||||
identityKey,
|
||||
version,
|
||||
}: TFormData): BondingInformation => {
|
||||
return {
|
||||
amount,
|
||||
nodeDetails: {
|
||||
host,
|
||||
http_api_port: httpApiPort
|
||||
? parseInt(httpApiPort)
|
||||
: DEFAULT_HTTP_API_PORT,
|
||||
mix_port: mixPort ? parseInt(mixPort) : DEFAULT_MIX_PORT,
|
||||
verloc_port: verlocPort ? parseInt(verlocPort) : DEFAULT_VERLOC_PORT,
|
||||
sphinx_key: sphinxKey,
|
||||
identity_key: identityKey,
|
||||
version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const constructGatewayBondingInfo = ({
|
||||
amount,
|
||||
host,
|
||||
mixPort,
|
||||
clientsPort,
|
||||
sphinxKey,
|
||||
identityKey,
|
||||
version,
|
||||
location,
|
||||
}: TFormData): BondingInformation => {
|
||||
return {
|
||||
amount,
|
||||
nodeDetails: {
|
||||
host,
|
||||
mix_port: mixPort ? parseInt(mixPort) : DEFAULT_MIX_PORT,
|
||||
clients_port: clientsPort
|
||||
? parseInt(clientsPort)
|
||||
: DEFAULT_CLIENTS_PORT,
|
||||
sphinx_key: sphinxKey,
|
||||
identity_key: identityKey,
|
||||
version,
|
||||
location,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const target = event.target as typeof event.target & TFormInput
|
||||
|
||||
const data: TFormData = {
|
||||
amount: target.amount.value,
|
||||
identityKey: target.identityKey.value,
|
||||
sphinxKey: target.sphinxKey.value,
|
||||
host: target.host.value,
|
||||
version: target.version.value,
|
||||
mixPort: target.mixPort?.value,
|
||||
verlocPort: target.verlocPort?.value,
|
||||
location: target.location?.value,
|
||||
clientsPort: target.clientsPort?.value,
|
||||
httpApiPort: target.httpApiPort?.value,
|
||||
}
|
||||
|
||||
if (validateForm(data)) {
|
||||
let dataToSubmit
|
||||
if (props.type == NodeType.Mixnode) {
|
||||
dataToSubmit = constructMixnodeBondingInfo(data)
|
||||
return props.onSubmit(dataToSubmit)
|
||||
} else {
|
||||
dataToSubmit = constructGatewayBondingInfo(data)
|
||||
return props.onSubmit(dataToSubmit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}>
|
||||
<form
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
manageForm.handleSubmit(onSubmit)
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
required
|
||||
value={manageForm.formData.amount.value}
|
||||
onChange={manageForm.handleAmountChange}
|
||||
id="amount"
|
||||
name="amount"
|
||||
label={`Amount to bond ${
|
||||
@@ -338,9 +53,9 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
? '(minimum ' + nativeToPrintable(minimumBond.amount) + ')'
|
||||
: ''
|
||||
}`}
|
||||
error={!validateAmount}
|
||||
error={manageForm.formData.amount.isValid === false}
|
||||
helperText={
|
||||
!validateAmount
|
||||
manageForm.formData.amount.isValid === false
|
||||
? `Enter a valid bond amount (minimum ${nativeToPrintable(
|
||||
minimumBond.amount
|
||||
)})`
|
||||
@@ -352,48 +67,58 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
<InputAdornment position="end">{DENOM}</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={handleAmountChange}
|
||||
/>
|
||||
</Grid>
|
||||
{allocationWarning && (
|
||||
{manageForm.allocationWarning.message && (
|
||||
<Grid item>
|
||||
<Alert severity={!isValidAmount ? 'error' : 'info'}>
|
||||
{allocationWarning}
|
||||
<Alert
|
||||
severity={manageForm.allocationWarning.error ? 'error' : 'info'}
|
||||
>
|
||||
{manageForm.allocationWarning.message}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
error={!validity.validIdentityKey}
|
||||
value={manageForm.formData.identityKey.value}
|
||||
onChange={manageForm.handleIdentityKeyChange}
|
||||
error={manageForm.formData.identityKey.isValid === false}
|
||||
required
|
||||
id="identityKey"
|
||||
name="identityKey"
|
||||
label="Identity key"
|
||||
fullWidth
|
||||
{...(manageForm.formData.identityKey.isValid === false
|
||||
? { helperText: 'Enter a valid identity key' }
|
||||
: {})}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
error={!validity.validSphinxKey}
|
||||
value={manageForm.formData.sphinxKey.value}
|
||||
onChange={manageForm.handleShinxKeyChange}
|
||||
error={manageForm.formData.sphinxKey.isValid === false}
|
||||
required
|
||||
id="sphinxKey"
|
||||
name="sphinxKey"
|
||||
label="Sphinx key"
|
||||
fullWidth
|
||||
{...(!validity.validSphinxKey
|
||||
{...(manageForm.formData.sphinxKey.isValid === false
|
||||
? { helperText: 'Enter a valid sphinx key' }
|
||||
: {})}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={!validity.validHost}
|
||||
value={manageForm.formData.host.value}
|
||||
onChange={manageForm.handleHostChange}
|
||||
error={manageForm.formData.host.isValid === false}
|
||||
required
|
||||
id="host"
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
{...(!validity.validHost
|
||||
{...(manageForm.formData.host.isValid === false
|
||||
? { helperText: 'Enter a valid IP or a hostname (without port)' }
|
||||
: {})}
|
||||
/>
|
||||
@@ -401,15 +126,17 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
|
||||
{/* if it's a gateway - get location */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
{props.type === NodeType.Gateway && (
|
||||
{type === NodeType.Gateway && (
|
||||
<TextField
|
||||
error={!validity.validLocation}
|
||||
value={manageForm.formData.location.value}
|
||||
onChange={manageForm.handleLocationChange}
|
||||
error={manageForm.formData.location.isValid === false}
|
||||
required
|
||||
id="location"
|
||||
name="location"
|
||||
label="Location"
|
||||
fullWidth
|
||||
{...(!validity.validLocation
|
||||
{...(manageForm.formData.location.isValid === false
|
||||
? { helperText: 'Enter a valid location of your node' }
|
||||
: {})}
|
||||
/>
|
||||
@@ -418,13 +145,15 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
error={!validity.validVersion}
|
||||
value={manageForm.formData.version.value}
|
||||
onChange={manageForm.handleVersionChange}
|
||||
error={manageForm.formData.version.isValid === false}
|
||||
required
|
||||
id="version"
|
||||
name="version"
|
||||
label="Version"
|
||||
fullWidth
|
||||
{...(!validity.validVersion
|
||||
{...(manageForm.formData.version.isValid === false
|
||||
? {
|
||||
helperText:
|
||||
'Enter a valid version (min. 0.11.0), like 0.11.0',
|
||||
@@ -438,10 +167,15 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={advancedShown}
|
||||
onChange={handleCheckboxToggle}
|
||||
onChange={() => {
|
||||
setAdvancedShown((shown) => {
|
||||
if (shown) manageForm.initialisePorts()
|
||||
return !shown
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Show advanced options"
|
||||
label="Use advanced options"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -449,53 +183,63 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
<>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validMixPort}
|
||||
value={manageForm.formData.mixPort.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
manageForm.handlePortChange('mixPort', e.target.value)
|
||||
}
|
||||
error={manageForm.formData.mixPort.isValid === false}
|
||||
variant="outlined"
|
||||
id="mixPort"
|
||||
name="mixPort"
|
||||
label="Mix Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_MIX_PORT}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/*yes, I also hate so many layers of indentation here*/}
|
||||
{props.type === NodeType.Mixnode ? (
|
||||
{type === NodeType.Mixnode ? (
|
||||
<>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<TextField
|
||||
error={!validity.validVerlocPort}
|
||||
value={manageForm.formData.verlocPort.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
manageForm.handlePortChange('verlocPort', e.target.value)
|
||||
}
|
||||
error={manageForm.formData.verlocPort.isValid === false}
|
||||
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}
|
||||
value={manageForm.formData.httpApiPort.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
manageForm.handlePortChange('httpApiPort', e.target.value)
|
||||
}
|
||||
error={manageForm.formData.httpApiPort.isValid === false}
|
||||
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}
|
||||
value={manageForm.formData.clientsPort.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
manageForm.handlePortChange('clientsPort', e.target.value)
|
||||
}
|
||||
error={manageForm.formData.clientsPort.isValid === false}
|
||||
variant="outlined"
|
||||
id="clientsPort"
|
||||
name="clientsPort"
|
||||
label="client WS API Port"
|
||||
fullWidth
|
||||
defaultValue={DEFAULT_CLIENTS_PORT}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -503,13 +247,20 @@ export default function BondNodeForm(props: TBondNodeFormProps) {
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<div className={classes.buttons}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
disabled={!isValidAmount}
|
||||
size="large"
|
||||
disabled={!manageForm.isValidForm}
|
||||
>
|
||||
Bond
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useContext, useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
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'
|
||||
@@ -7,12 +8,11 @@ import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/
|
||||
import Confirmation from '../Confirmation'
|
||||
import { ValidatorClientContext } from '../../contexts/ValidatorClient'
|
||||
import NoClientError from '../NoClientError'
|
||||
import { useRouter } from 'next/router'
|
||||
import BondNodeForm from './BondNodeForm'
|
||||
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 { checkNodesOwnership } from '../../common/helpers'
|
||||
import NodeTypeChooser from '../NodeTypeChooser'
|
||||
import ExecFeeNotice from '../ExecFeeNotice'
|
||||
import { UDENOM } from '../../pages/_app'
|
||||
@@ -23,7 +23,6 @@ export type BondingInformation = {
|
||||
}
|
||||
|
||||
const BondNode = () => {
|
||||
const classes = makeBasicStyle(theme)
|
||||
const router = useRouter()
|
||||
const { client } = useContext(ValidatorClientContext)
|
||||
|
||||
@@ -62,15 +61,11 @@ const BondNode = () => {
|
||||
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.nodeDetails)
|
||||
|
||||
if (nodeType == NodeType.Mixnode) {
|
||||
let mixnode = bondingInformation.nodeDetails as MixNode
|
||||
client
|
||||
@@ -141,8 +136,11 @@ const BondNode = () => {
|
||||
<BondNodeForm
|
||||
onSubmit={bondNode}
|
||||
type={nodeType}
|
||||
minimumGatewayBond={minimumGatewayBond}
|
||||
minimumMixnodeBond={minimumMixnodeBond}
|
||||
minimumBond={
|
||||
nodeType === NodeType.Mixnode
|
||||
? minimumMixnodeBond
|
||||
: minimumGatewayBond
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Coin } from '@cosmjs/amino'
|
||||
import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency'
|
||||
import { BondingInformation } from './NodeBond'
|
||||
import {
|
||||
formatDataForSubmission,
|
||||
isValidHostname,
|
||||
validateAmount,
|
||||
validateKey,
|
||||
validateLocation,
|
||||
validateVersion,
|
||||
} from './utils'
|
||||
import { checkAllocationSize, validateRawPort } from '../../common/helpers'
|
||||
import { NodeType } from '../../common/node'
|
||||
import { useGetBalance } from '../../hooks/useGetBalance'
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
MIX_PORT: 1789,
|
||||
VERLOC_PORT: 1790,
|
||||
HTTP_API_PORT: 8000,
|
||||
CLIENTS_PORT: 9000,
|
||||
}
|
||||
|
||||
const initialPorts = {
|
||||
mixPort: { value: DEFAULT_PORTS.MIX_PORT.toString(), isValid: true },
|
||||
verlocPort: {
|
||||
value: DEFAULT_PORTS.VERLOC_PORT.toString(),
|
||||
isValid: true,
|
||||
},
|
||||
clientsPort: {
|
||||
value: DEFAULT_PORTS.CLIENTS_PORT.toString(),
|
||||
isValid: true,
|
||||
},
|
||||
httpApiPort: {
|
||||
value: DEFAULT_PORTS.HTTP_API_PORT.toString(),
|
||||
isValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
const initialData = {
|
||||
amount: { value: '', isValid: undefined },
|
||||
identityKey: { value: '', isValid: undefined },
|
||||
sphinxKey: { value: '', isValid: undefined },
|
||||
host: { value: '', isValid: undefined },
|
||||
version: { value: '', isValid: undefined },
|
||||
location: { value: '', isValid: true },
|
||||
...initialPorts,
|
||||
}
|
||||
|
||||
type TDataField = { value: string; isValid?: boolean }
|
||||
|
||||
type TData = {
|
||||
amount: TDataField
|
||||
identityKey: TDataField
|
||||
sphinxKey: TDataField
|
||||
host: TDataField
|
||||
version: TDataField
|
||||
location: TDataField
|
||||
mixPort: TDataField
|
||||
verlocPort: TDataField
|
||||
clientsPort: TDataField
|
||||
httpApiPort: TDataField
|
||||
}
|
||||
|
||||
export const useBondForm = ({
|
||||
type,
|
||||
minimumBond,
|
||||
}: {
|
||||
type: NodeType
|
||||
minimumBond: Coin
|
||||
}) => {
|
||||
const [formData, setFormData] = useState(initialData)
|
||||
const [isValidForm, setIsValidForm] = useState(false)
|
||||
const [allocationWarning, setAllocationWarning] = useState({
|
||||
error: false,
|
||||
message: undefined,
|
||||
})
|
||||
const { getBalance, accountBalance } = useGetBalance()
|
||||
|
||||
useEffect(() => {
|
||||
getBalance()
|
||||
}, [getBalance])
|
||||
|
||||
useEffect(() => {
|
||||
if (type === NodeType.Mixnode)
|
||||
setFormData((data) => ({ ...data, location: initialData.location }))
|
||||
}, [type])
|
||||
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(formData)
|
||||
const isValid = keys
|
||||
.map((key) => formData[key].isValid)
|
||||
.every((value) => value === true)
|
||||
setIsValidForm(isValid)
|
||||
}, [formData])
|
||||
|
||||
const handleSubmit = (cb: (data: BondingInformation) => void) => {
|
||||
const keys = Object.keys(formData)
|
||||
const values = keys.reduce((a, c: keyof TData) => {
|
||||
return {
|
||||
...a,
|
||||
[c]: formData[c].value,
|
||||
}
|
||||
}, {})
|
||||
const formatted = formatDataForSubmission(values, type)
|
||||
cb(formatted)
|
||||
}
|
||||
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let isValid = false
|
||||
|
||||
try {
|
||||
if (
|
||||
!isNaN(+e.target.value) &&
|
||||
validateAmount(e.target.value, minimumBond.amount)
|
||||
)
|
||||
isValid = true
|
||||
|
||||
const allocationCheck = checkAllocationSize(
|
||||
+printableBalanceToNative(e.target.value),
|
||||
+accountBalance.amount,
|
||||
'bond'
|
||||
)
|
||||
setAllocationWarning(allocationCheck)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
amount: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleIdentityKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isValid = validateKey(e.target.value)
|
||||
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
identityKey: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleShinxKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isValid = validateKey(e.target.value)
|
||||
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
sphinxKey: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleHostChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isValid = isValidHostname(e.target.value)
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
host: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleVersionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isValid = validateVersion(e.target.value)
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
version: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isValid = validateLocation(e.target.value)
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
location: { value: e.target.value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const handlePortChange = (
|
||||
port: 'mixPort' | 'verlocPort' | 'clientsPort' | 'httpApiPort',
|
||||
value: string
|
||||
) => {
|
||||
const isValid = validateRawPort(value)
|
||||
setFormData((data) => ({
|
||||
...data,
|
||||
[port]: { value, isValid },
|
||||
}))
|
||||
}
|
||||
|
||||
const initialisePorts = () =>
|
||||
setFormData((data) => ({ ...data, ...initialPorts }))
|
||||
|
||||
return {
|
||||
formData,
|
||||
isValidForm,
|
||||
allocationWarning,
|
||||
handleAmountChange,
|
||||
handleIdentityKeyChange,
|
||||
handleShinxKeyChange,
|
||||
handleHostChange,
|
||||
handleVersionChange,
|
||||
handleLocationChange,
|
||||
handlePortChange,
|
||||
handleSubmit,
|
||||
initialisePorts,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency'
|
||||
import { Gateway, MixNode } from '@nymproject/nym-validator-client/dist/types'
|
||||
import bs58 from 'bs58'
|
||||
import semver from 'semver'
|
||||
import { basicRawCoinValueValidation } from '../../common/helpers'
|
||||
import { NodeType } from '../../common/node'
|
||||
import { BondingInformation } from './NodeBond'
|
||||
|
||||
export const validateAmount = (rawValue: string, minimum: 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)
|
||||
console.log(nativeValue, minimum)
|
||||
return nativeValue >= parseInt(minimum)
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
}
|
||||
|
||||
export const isValidHostname = (value) => {
|
||||
if (typeof value !== 'string') return false
|
||||
|
||||
const validHostnameChars = /^[a-zA-Z0-9-.]{1,253}\.?$/g
|
||||
if (!validHostnameChars.test(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.endsWith('.')) {
|
||||
value = value.slice(0, value.length - 1)
|
||||
}
|
||||
|
||||
if (value.length > 253) {
|
||||
return false
|
||||
}
|
||||
|
||||
const labels = value.split('.')
|
||||
|
||||
const isValid = labels.every(function (label) {
|
||||
const validLabelChars = /^([a-zA-Z0-9-]+)$/g
|
||||
|
||||
const validLabel =
|
||||
validLabelChars.test(label) &&
|
||||
label.length < 64 &&
|
||||
!label.startsWith('-') &&
|
||||
!label.endsWith('-')
|
||||
|
||||
return validLabel
|
||||
})
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// check if its a valid semver
|
||||
export const validateVersion = (version: string): boolean =>
|
||||
semver.valid(version) && semver.minor(version) >= 11
|
||||
|
||||
export 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')
|
||||
}
|
||||
|
||||
export const formatDataForSubmission = <T>(
|
||||
{
|
||||
amount,
|
||||
host,
|
||||
identityKey,
|
||||
sphinxKey,
|
||||
clientsPort,
|
||||
httpApiPort,
|
||||
mixPort,
|
||||
verlocPort,
|
||||
location,
|
||||
version,
|
||||
}: { [key: string]: string | undefined },
|
||||
nodeType: NodeType
|
||||
) => {
|
||||
let data = {
|
||||
amount,
|
||||
nodeDetails: {
|
||||
identity_key: identityKey,
|
||||
sphinx_key: sphinxKey,
|
||||
host,
|
||||
version,
|
||||
mix_port: parseInt(mixPort),
|
||||
},
|
||||
}
|
||||
|
||||
if (nodeType === NodeType.Mixnode) {
|
||||
data = {
|
||||
...data,
|
||||
nodeDetails: {
|
||||
...data.nodeDetails,
|
||||
verloc_port: parseInt(verlocPort),
|
||||
http_api_port: parseInt(httpApiPort),
|
||||
} as MixNode,
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
...data,
|
||||
nodeDetails: {
|
||||
...data.nodeDetails,
|
||||
location,
|
||||
clients_port: parseInt(clientsPort),
|
||||
} as Gateway,
|
||||
}
|
||||
}
|
||||
return data as BondingInformation
|
||||
}
|
||||
Reference in New Issue
Block a user