start form validation
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@hookform/resolvers": "^2.8.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
@@ -19,7 +20,10 @@
|
||||
"clsx": "^1.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0"
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
@@ -30,6 +34,7 @@
|
||||
"@tauri-apps/cli": "^1.0.0-beta.9",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/semver": "^7.3.8",
|
||||
"babel-loader": "^8.2.2",
|
||||
"css-loader": "^6.2.0",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
@@ -39,6 +44,6 @@
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
"webpack-dev-server": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,33 +69,35 @@ export const AddressCard = () => {
|
||||
noPadding
|
||||
Action={
|
||||
<Tooltip title={!copyState ? 'Copy address' : 'Copied'}>
|
||||
<IconButton
|
||||
disabled={!!copyState}
|
||||
onClick={async () => {
|
||||
setCopyState(EnumCopyState.copying)
|
||||
await handleCopy({
|
||||
text: clientDetails?.client_address || '',
|
||||
cb: (isCopied) => {
|
||||
if (isCopied) {
|
||||
setCopyState(EnumCopyState.copySuccess)
|
||||
setTimeout(() => {
|
||||
setCopyState(undefined)
|
||||
}, 2500)
|
||||
}
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{copyState === EnumCopyState.copying ? (
|
||||
<CircularProgress size={24} />
|
||||
) : copyState === EnumCopyState.copySuccess ? (
|
||||
<CheckCircleOutline
|
||||
style={{ color: theme.palette.success.main }}
|
||||
/>
|
||||
) : (
|
||||
<FileCopy />
|
||||
)}
|
||||
</IconButton>
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={!!copyState}
|
||||
onClick={async () => {
|
||||
setCopyState(EnumCopyState.copying)
|
||||
await handleCopy({
|
||||
text: clientDetails?.client_address || '',
|
||||
cb: (isCopied) => {
|
||||
if (isCopied) {
|
||||
setCopyState(EnumCopyState.copySuccess)
|
||||
setTimeout(() => {
|
||||
setCopyState(undefined)
|
||||
}, 2500)
|
||||
}
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{copyState === EnumCopyState.copying ? (
|
||||
<CircularProgress size={24} />
|
||||
) : copyState === EnumCopyState.copySuccess ? (
|
||||
<CheckCircleOutline
|
||||
style={{ color: theme.palette.success.main }}
|
||||
/>
|
||||
) : (
|
||||
<FileCopy />
|
||||
)}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -8,20 +8,84 @@ import {
|
||||
TextField,
|
||||
Theme,
|
||||
} from '@material-ui/core'
|
||||
import { EnumNodeType } from '../../types/global'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import * as Yup from 'yup'
|
||||
import { EnumNodeType } from '../../types/global'
|
||||
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||
import {
|
||||
isValidHostname,
|
||||
validateAmount,
|
||||
validateKey,
|
||||
validateVersion,
|
||||
} from '../../utils'
|
||||
|
||||
type TBondNodeFormProps = {
|
||||
// minimumBond: Coin
|
||||
// onSubmit: (values: BondingInformation) => void
|
||||
}
|
||||
|
||||
type TBondFormFields = {
|
||||
identityKey: string
|
||||
sphinxKey: string
|
||||
amount: string
|
||||
host: string
|
||||
version: string
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
identityKey: Yup.string()
|
||||
.required('An indentity key is required')
|
||||
.test('valid-id-key', 'A valid identity key is required', function (value) {
|
||||
return validateKey(value || '')
|
||||
}),
|
||||
sphinxKey: Yup.string()
|
||||
.required('A sphinx key is required')
|
||||
.test(
|
||||
'valid-sphinx-key',
|
||||
'A valid sphinx key is required',
|
||||
function (value) {
|
||||
return validateKey(value || '')
|
||||
}
|
||||
),
|
||||
amount: Yup.string()
|
||||
.required('An amount is required')
|
||||
.test(
|
||||
'valid-amount',
|
||||
'A valid amount is required (min 100 punks)',
|
||||
function (value) {
|
||||
return validateAmount(value || '', '100000000')
|
||||
// minimum amount needs to come from the backend - replace when available
|
||||
}
|
||||
),
|
||||
|
||||
host: Yup.string()
|
||||
.required('A host is required')
|
||||
.test('valid-amount', 'A valid host is required', function (value) {
|
||||
return !!value ? isValidHostname(value) : false
|
||||
}),
|
||||
version: Yup.string()
|
||||
.required('A version is required')
|
||||
.test('valid-version', 'A valid version is required', function (value) {
|
||||
return !!value ? validateVersion(value) : false
|
||||
}),
|
||||
})
|
||||
|
||||
export const BondNodeForm = () => {
|
||||
const [advancedShown, setAdvancedShown] = React.useState(false)
|
||||
const [nodeType, setNodeType] = useState(EnumNodeType.Mixnode)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<TBondFormFields>({ resolver: yupResolver(validationSchema) })
|
||||
|
||||
const theme: Theme = useTheme()
|
||||
console.log(errors)
|
||||
|
||||
const onSubmit = (data: TBondFormFields) => console.log(data)
|
||||
|
||||
return (
|
||||
<form>
|
||||
@@ -35,32 +99,41 @@ export const BondNodeForm = () => {
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('identityKey')}
|
||||
variant="outlined"
|
||||
required
|
||||
id="identityKey"
|
||||
name="identityKey"
|
||||
label="Identity key"
|
||||
fullWidth
|
||||
error={!!errors.identityKey}
|
||||
helperText={errors.identityKey?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('sphinxKey')}
|
||||
variant="outlined"
|
||||
required
|
||||
id="sphinxKey"
|
||||
name="sphinxKey"
|
||||
label="Sphinx key"
|
||||
error={!!errors.sphinxKey}
|
||||
helperText={errors.sphinxKey?.message}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={9}>
|
||||
<TextField
|
||||
{...register('amount')}
|
||||
variant="outlined"
|
||||
required
|
||||
id="amount"
|
||||
name="amount"
|
||||
label="Amount to bond"
|
||||
fullWidth
|
||||
error={!!errors.amount}
|
||||
helperText={errors.amount?.message}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">punks</InputAdornment>
|
||||
@@ -71,12 +144,15 @@ export const BondNodeForm = () => {
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
{...register('host')}
|
||||
variant="outlined"
|
||||
required
|
||||
id="host"
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
error={!!errors.host}
|
||||
helperText={errors.host?.message}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -96,12 +172,15 @@ export const BondNodeForm = () => {
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
{...register('version')}
|
||||
variant="outlined"
|
||||
required
|
||||
id="version"
|
||||
name="version"
|
||||
label="Version"
|
||||
fullWidth
|
||||
error={!!errors.version}
|
||||
helperText={errors.version?.message}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -185,6 +264,7 @@ export const BondNodeForm = () => {
|
||||
type="submit"
|
||||
size="large"
|
||||
disableElevation
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
Bond
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import bs58 from 'bs58'
|
||||
import { minor, valid } from 'semver'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
|
||||
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 (e) {
|
||||
console.log(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const validateAmount = async (
|
||||
rawValue: string,
|
||||
minimum: string
|
||||
): Promise<boolean> => {
|
||||
// tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc
|
||||
if (!basicRawCoinValueValidation(rawValue)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const nativeValueString: string = await invoke(
|
||||
'printable_balance_to_native',
|
||||
{ amount: rawValue }
|
||||
)
|
||||
|
||||
let nativeValue = parseInt(nativeValueString)
|
||||
|
||||
return nativeValue >= parseInt(minimum)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// this conversion seems really iffy but I'm not sure how to better approach it
|
||||
}
|
||||
|
||||
export const basicRawCoinValueValidation = (rawAmount: string): boolean => {
|
||||
let amountFloat = parseFloat(rawAmount)
|
||||
if (isNaN(amountFloat)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it cannot have more than 6 decimal places
|
||||
if (amountFloat != parseFloat(amountFloat.toFixed(6))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it cannot be larger than the total supply
|
||||
if (amountFloat > 1_000_000_000_000_000) {
|
||||
return false
|
||||
}
|
||||
|
||||
// it can't be lower than one micro coin
|
||||
return amountFloat >= 0.000001
|
||||
}
|
||||
|
||||
export const isValidHostname = (value: string) => {
|
||||
// regex for ipv4 and ipv6 and hhostname- source http://jsfiddle.net/DanielD/8S4nq/
|
||||
const hostnameRegex =
|
||||
/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/
|
||||
|
||||
return hostnameRegex.test(value)
|
||||
}
|
||||
|
||||
export const validateVersion = (version: string): boolean => {
|
||||
try {
|
||||
const minorVersion = minor(version)
|
||||
const validVersion = valid(version)
|
||||
return validVersion !== null && minorVersion >= 11
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
+6301
-5937
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user