diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 526a1385b4..7a7a0a1df4 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -49,7 +49,7 @@ "string-to-color": "^2.2.2", "use-clipboard-copy": "^0.2.0", "uuid": "^8.3.2", - "yup": "^0.32.9" + "yup": "^1.0.0-beta.4" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/nym-wallet/src/components/Nav.tsx b/nym-wallet/src/components/Nav.tsx index b8119316d9..275b32467f 100644 --- a/nym-wallet/src/components/Nav.tsx +++ b/nym-wallet/src/components/Nav.tsx @@ -3,7 +3,57 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material'; import { AppContext } from '../context/main'; -import { Bond, Delegate, Unbond } from '../svg-icons'; +import { Bond, Delegate, Unbond, Bonding } from '../svg-icons'; + +const routesSchema = [ + { + label: 'Balance', + route: '/balance', + Icon: AccountBalanceWalletOutlined, + }, + { + label: 'Send', + route: '/send', + Icon: ArrowForward, + }, + { + label: 'Receive', + route: '/receive', + Icon: ArrowBack, + }, + { + label: 'Bond', + route: '/bond', + Icon: Bond, + }, + { + label: 'Bonding', + route: '/bonding', + Icon: Bonding, + }, + { + label: 'Unbond', + route: '/unbond', + Icon: Unbond, + }, + { + label: 'Delegation', + route: '/delegation', + Icon: Delegate, + }, + { + label: 'Docs', + route: '/docs', + Icon: Description, + mode: 'dev', + }, + { + label: 'Admin', + route: '/admin', + Icon: Settings, + mode: 'admin', + }, +]; export const Nav = () => { const location = useLocation(); diff --git a/nym-wallet/src/context/index.tsx b/nym-wallet/src/context/index.tsx index 0077fb3147..c3fd46b7fd 100644 --- a/nym-wallet/src/context/index.tsx +++ b/nym-wallet/src/context/index.tsx @@ -1,3 +1,5 @@ export * from './main'; export * from './auth'; export * from './accounts'; +export * from './bonding'; + diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 4bd570e1c5..f036268ccd 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -36,7 +36,7 @@ const TxResultMock: TransactionExecuteResult = { fee: { amount: '1', denom: 'NYM' }, }; -export const BondingContextProvider = ({ +export const MockBondingContextProvider = ({ network, children, }: { diff --git a/nym-wallet/src/pages/bonding/AmountModal.tsx b/nym-wallet/src/pages/bonding/AmountModal.tsx new file mode 100644 index 0000000000..fe983fb1d5 --- /dev/null +++ b/nym-wallet/src/pages/bonding/AmountModal.tsx @@ -0,0 +1,98 @@ +import React, { useContext } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Divider, Stack, Typography } from '@mui/material'; +import { SimpleModal } from '../../components/Modals/SimpleModal'; +import { AmountData, NodeType } from './types'; +import { CurrencyInput } from './bond-form/CurrencyInput'; +import { AppContext } from '../../context'; +import { amountSchema } from './amountSchema'; +import { TokenPoolSelector } from '../../components'; +import { TextFieldInput } from './bond-form'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../utils'; + +export interface Props { + nodeType: NodeType; + open: boolean; + onClose?: () => void; + onSubmit: (data: AmountData) => Promise; + header?: string; + buttonText?: string; +} + +export const AmountModal = ({ open, onClose, onSubmit, header, buttonText, nodeType }: Props) => { + const { + control, + setValue, + setError, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(amountSchema), + defaultValues: { + tokenPool: 'balance', + profitMargin: 10, + }, + }); + + const { userBalance, clientDetails } = useContext(AppContext); + + const onSubmitForm = async (data: AmountData) => { + if (data.tokenPool === 'balance' && !(await checkHasEnoughFunds(data.amount.amount || ''))) { + return setError('amount.amount', { message: 'Not enough funds in wallet' }); + } + + if (data.tokenPool === 'locked' && !(await checkHasEnoughLockedTokens(data.amount.amount || ''))) { + return setError('amount.amount', { message: 'Not enough locked tokens' }); + } + return onSubmit(data); + }; + + return ( + +
+ {nodeType === 'mixnode' && ( + + )} + + {userBalance.originalVesting && ( + setValue('tokenPool', pool)} disabled={false} /> + )} + + + + + Account balance + {userBalance.balance?.printable_balance || 0} + + + Est. fee for this transaction will be cauculated in the next page +
+ ); +}; diff --git a/nym-wallet/src/pages/bonding/BondingCard.tsx b/nym-wallet/src/pages/bonding/BondingCard.tsx new file mode 100644 index 0000000000..434dcab093 --- /dev/null +++ b/nym-wallet/src/pages/bonding/BondingCard.tsx @@ -0,0 +1,184 @@ +import React, { useContext, useEffect, useReducer } from 'react'; +import { Box, Button, Typography } from '@mui/material'; +import { Gateway, MajorCurrencyAmount, MixNode } from '@nymproject/types'; +import { NymCard } from '../../components'; +import { NodeIdentityModal } from './NodeIdentityModal'; +import { ACTIONTYPE, AmountData, BondState, FormStep, NodeData, NodeType } from './types'; +import { AmountModal } from './AmountModal'; +import { AppContext } from '../../context'; +import { SummaryModal } from './SummaryModal'; +import { bond, vestingBond } from '../../requests'; +import { TBondArgs } from '../../types'; +import { SimpleModal } from '../../components/Modals/SimpleModal'; + +const initialState: BondState = { + showModal: false, + formStep: 1, +}; + +function reducer(state: BondState, action: ACTIONTYPE) { + let step; + switch (action.type) { + case 'change_bond_type': + return { ...state, type: action.payload }; + case 'set_node_data': + return { ...state, nodeData: action.payload }; + case 'set_amount_data': + return { ...state, amountData: action.payload }; + case 'set_step': + return { ...state, formStep: action.payload }; + case 'set_tx': + return { ...state, tx: action.payload }; + case 'next_step': + step = state.formStep + 1; + return { ...state, formStep: step <= 4 ? (step as FormStep) : 4 }; + case 'previous_step': + step = state.formStep - 1; + return { ...state, formStep: step >= 1 ? (step as FormStep) : 1 }; + case 'show_modal': + return { ...state, showModal: true }; + case 'close_modal': + return { ...state, showModal: false }; + case 'reset': + return initialState; + default: + throw new Error(); + } +} + +export const BondingCard = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const { formStep, showModal } = state; + console.log(state); + + const { userBalance, clientDetails } = useContext(AppContext); + + useEffect(() => { + dispatch({ type: 'reset' }); + }, [clientDetails]); + + const formatData = (nodeType: NodeType, nodeData: NodeData, amountData: AmountData): MixNode | Gateway => + nodeType === 'mixnode' + ? { + host: nodeData.host, + mix_port: nodeData.mixPort, + verloc_port: nodeData.verlocPort, + http_api_port: nodeData.httpApiPort, + sphinx_key: nodeData.sphinxKey, + identity_key: nodeData.identityKey, + version: nodeData.version, + profit_margin_percent: amountData.profitMargin as number, + } + : { + host: nodeData.host, + mix_port: nodeData.mixPort, + clients_port: nodeData.clientsPort, + location: nodeData.location as string, + sphinx_key: nodeData.sphinxKey, + identity_key: nodeData.identityKey, + version: nodeData.version, + }; + + const onSubmit = async () => { + const { nodeData, amountData } = state; + if (!nodeData || !amountData) { + throw new Error(''); + } + const request = amountData.tokenPool === 'balance' ? bond : vestingBond; + dispatch({ type: 'next_step' }); + return request({ + type: nodeData.nodeType, + ownerSignature: nodeData.signature, + [nodeData.nodeType]: formatData(nodeData.nodeType, nodeData, amountData), + pledge: amountData.amount, + } as TBondArgs) + .then(async (tx) => { + if (amountData.tokenPool === 'balance') { + await userBalance.fetchBalance(); + } else { + await userBalance.fetchTokenAllocation(); + } + dispatch({ type: 'set_tx', payload: tx }); + dispatch({ type: 'next_step' }); + }) + .catch(() => { + // TODO do something + }); + }; + + return ( + + + Bond a node or a gateway + + + {formStep === 1 && showModal && ( + dispatch({ type: 'reset' })} + onSubmit={async (data) => { + dispatch({ type: 'set_node_data', payload: data }); + dispatch({ type: 'next_step' }); + }} + header="Bond" + buttonText="Next" + /> + )} + {formStep === 2 && showModal && ( + dispatch({ type: 'reset' })} + onSubmit={async (data) => { + dispatch({ type: 'set_amount_data', payload: data }); + dispatch({ type: 'next_step' }); + }} + header="Bond" + buttonText="Next" + nodeType={state.nodeData?.nodeType || 'mixnode'} + /> + )} + {formStep === 3 && showModal && ( + dispatch({ type: 'reset' })} + onSubmit={onSubmit} + header="Bond details" + buttonText="Confirm" + nodeType={state.nodeData?.nodeType as NodeType} + identityKey={state.nodeData?.identityKey as string} + amount={state.amountData?.amount as MajorCurrencyAmount} + /> + )} + {true && ( + { + dispatch({ type: 'close_modal' }); + dispatch({ type: 'reset' }); + }} + header="Bonding successful" + okLabel="Done" + > + Link to transaction on network explorer + + )} + + ); +}; diff --git a/nym-wallet/src/pages/bonding/BondingPage.stories.tsx b/nym-wallet/src/pages/bonding/BondingPage.stories.tsx index c2afd989f9..ede7eaa20a 100644 --- a/nym-wallet/src/pages/bonding/BondingPage.stories.tsx +++ b/nym-wallet/src/pages/bonding/BondingPage.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { BondingPage } from './index'; -import { MockBondingContextProvider } from '../../context/mocks/delegations'; +import { MockBondingContextProvider } from '../../context/mocks/bonding'; export default { title: 'Bonding/Flows/Mock', diff --git a/nym-wallet/src/pages/bonding/BoundedGatewayCard.tsx b/nym-wallet/src/pages/bonding/BoundedGatewayCard.tsx new file mode 100644 index 0000000000..26c6cd1dca --- /dev/null +++ b/nym-wallet/src/pages/bonding/BoundedGatewayCard.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Grid } from '@mui/material'; +import { NymCard } from '../../components'; + +export const BondedGatewayCard = () => ( + + + bonded gateway data table + + +); diff --git a/nym-wallet/src/pages/bonding/BoundedMixnodeCard.tsx b/nym-wallet/src/pages/bonding/BoundedMixnodeCard.tsx new file mode 100644 index 0000000000..ba4f9ecc7d --- /dev/null +++ b/nym-wallet/src/pages/bonding/BoundedMixnodeCard.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Alert, Button, Grid, Link, Typography } from '@mui/material'; +import { NymCard } from '../../components'; + +export const BondedMixnodeCard = () => ( + + + bonded mixnode data table + + +); diff --git a/nym-wallet/src/pages/bonding/NodeIdentityModal.tsx b/nym-wallet/src/pages/bonding/NodeIdentityModal.tsx new file mode 100644 index 0000000000..b4598a2436 --- /dev/null +++ b/nym-wallet/src/pages/bonding/NodeIdentityModal.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { Stack } from '@mui/material'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { SimpleModal } from '../../components/Modals/SimpleModal'; +import { NodeData, NodeType } from './types'; +import { RadioInput, TextFieldInput, CheckboxInput } from './bond-form'; +import { nodeSchema } from './nodeSchema'; + +export interface Props { + open: boolean; + onClose?: () => void; + onSubmit: (data: NodeData) => Promise; + header?: string; + buttonText?: string; +} + +const radioOptions: { label: string; value: NodeType }[] = [ + { + label: 'Mixnode', + value: 'mixnode', + }, + { + label: 'Gateway', + value: 'gateway', + }, +]; + +export const NodeIdentityModal = ({ open, onClose, onSubmit, header, buttonText }: Props) => { + const { + control, + getValues, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + nodeType: radioOptions[0].value, + advancedOpt: false, + mixPort: 1789, + verlocPort: 1790, + httpApiPort: 8000, + clientsPort: 9000, + }, + resolver: yupResolver(nodeSchema), + }); + + const nodeType = useWatch({ name: 'nodeType', control }); + const advancedOpt = useWatch({ name: 'advancedOpt', control }); + + const onSubmitForm = (data: NodeData) => { + onSubmit(data); + }; + + return ( + +
+ + + + + {nodeType === 'gateway' && ( + + )} + + + + + + {advancedOpt && ( + + + {nodeType === 'mixnode' ? ( + <> + + + + ) : ( + + )} + + )} + +
+ ); +}; diff --git a/nym-wallet/src/pages/bonding/SummaryModal.tsx b/nym-wallet/src/pages/bonding/SummaryModal.tsx new file mode 100644 index 0000000000..c236b57637 --- /dev/null +++ b/nym-wallet/src/pages/bonding/SummaryModal.tsx @@ -0,0 +1,52 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Divider, Stack, Typography } from '@mui/material'; +import { MajorCurrencyAmount } from '@nymproject/types'; +import { SimpleModal } from '../../components/Modals/SimpleModal'; +import { getGasFee } from '../../requests'; +import { NodeType } from './types'; +import { AppContext } from '../../context'; + +export interface Props { + open: boolean; + onClose?: () => void; + onSubmit: () => Promise; + header: string; + buttonText: string; + identityKey: string; + nodeType: NodeType; + amount: MajorCurrencyAmount; +} + +export const SummaryModal = ({ open, onClose, onSubmit, header, buttonText, identityKey, nodeType, amount }: Props) => { + const onConfirm = async () => onSubmit(); + const [fee, setFee] = useState('-'); + const { clientDetails } = useContext(AppContext); + + const getFee = async (op: 'BondMixnode' | 'BondGateway') => { + const res = await getGasFee(op); + setFee(`${res.amount} ${clientDetails?.denom}`); + }; + + useEffect(() => { + getFee(nodeType === 'mixnode' ? 'BondMixnode' : 'BondGateway'); + }, [clientDetails, nodeType]); + + return ( + + + Identity Key + {identityKey} + + + + Amount + {`${amount.amount} ${amount.denom}`} + + + + Fee for this operation + {fee} + + + ); +}; diff --git a/nym-wallet/src/pages/bonding/amountSchema.ts b/nym-wallet/src/pages/bonding/amountSchema.ts new file mode 100644 index 0000000000..c87babbdce --- /dev/null +++ b/nym-wallet/src/pages/bonding/amountSchema.ts @@ -0,0 +1,17 @@ +import { number, object, string } from 'yup'; +import { validateAmount } from '../../utils'; + +export const amountSchema = object().shape({ + amount: object().shape({ + amount: string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + profitMargin: number().required('Profit Percentage is required').min(0).max(100), +}); diff --git a/nym-wallet/src/pages/bonding/bond-form/CheckboxInput.tsx b/nym-wallet/src/pages/bonding/bond-form/CheckboxInput.tsx new file mode 100644 index 0000000000..ea586cbedf --- /dev/null +++ b/nym-wallet/src/pages/bonding/bond-form/CheckboxInput.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Control, useController } from 'react-hook-form'; +import { Checkbox, CheckboxProps, FormControlLabel, FormControlLabelProps, FormGroup, SxProps } from '@mui/material'; + +interface Props { + name: string; + label: string; + control: Control; + defaultValue: boolean; + muiCheckboxProps?: CheckboxProps; + muiFormControlLabelProps?: FormControlLabelProps; + sx?: SxProps; +} + +export const CheckboxInput = ({ + name, + control, + defaultValue, + label, + muiCheckboxProps, + muiFormControlLabelProps, + sx, +}: Props) => { + const { + field: { onChange, onBlur, value, ref }, + } = useController({ + name, + control, + defaultValue, + }); + return ( + + + } + label={label} + {...muiFormControlLabelProps} + /> + + ); +}; diff --git a/nym-wallet/src/pages/bonding/bond-form/CurrencyInput.tsx b/nym-wallet/src/pages/bonding/bond-form/CurrencyInput.tsx new file mode 100644 index 0000000000..51ef03e553 --- /dev/null +++ b/nym-wallet/src/pages/bonding/bond-form/CurrencyInput.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Control, useController } from 'react-hook-form'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { CurrencyDenom } from '@nymproject/types'; + +interface Props { + name: string; + label: string; + control: Control; + required?: boolean; + fullWidth?: boolean; + errorMessage?: string; + currencyDenom?: CurrencyDenom; +} + +export const CurrencyInput = ({ name, label, control, errorMessage, currencyDenom, required, fullWidth }: Props) => { + const { + field: { onChange }, + } = useController({ + name, + control, + }); + return ( + + ); +}; diff --git a/nym-wallet/src/pages/bonding/bond-form/RadioInput.tsx b/nym-wallet/src/pages/bonding/bond-form/RadioInput.tsx new file mode 100644 index 0000000000..96851e3fb4 --- /dev/null +++ b/nym-wallet/src/pages/bonding/bond-form/RadioInput.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { Control, useController } from 'react-hook-form'; +import { + FormControl, + FormControlLabel, + FormLabel, + FormLabelProps, + Radio, + RadioGroup, + RadioGroupProps, + RadioProps, +} from '@mui/material'; + +interface Props { + name: string; + label: string; + control: Control; + options: { label: string; value: any }[]; + defaultValue: any; + muiRadioGroupProps?: RadioGroupProps; + muiRadioProps?: RadioProps; + muiFormLabelProps?: FormLabelProps; +} + +export const RadioInput = ({ + label, + control, + options, + defaultValue, + name, + muiRadioGroupProps, + muiRadioProps, + muiFormLabelProps, +}: Props) => { + const { + field: { onChange, value, ref }, + } = useController({ + name, + control, + rules: { required: true }, + defaultValue, + }); + return ( + + + {label} + + + {options.map(({ value: v, label: l }) => ( + } label={l} /> + ))} + + + ); +}; diff --git a/nym-wallet/src/pages/bonding/bond-form/TextFieldInput.tsx b/nym-wallet/src/pages/bonding/bond-form/TextFieldInput.tsx new file mode 100644 index 0000000000..06c4817934 --- /dev/null +++ b/nym-wallet/src/pages/bonding/bond-form/TextFieldInput.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { Control, useController } from 'react-hook-form'; +import { SxProps, TextField, TextFieldProps } from '@mui/material'; +import { RegisterOptions } from 'react-hook-form/dist/types/validator'; + +interface Props { + name: string; + label: string; + placeholder?: string; + control: Control; + defaultValue?: string; + required?: boolean; + error?: boolean; + muiTextFieldProps?: TextFieldProps; + helperText?: string; + sx?: SxProps; + registerOptions?: RegisterOptions; +} + +export const TextFieldInput = ({ + name, + label, + control, + defaultValue, + placeholder, + muiTextFieldProps, + required, + error, + helperText, + registerOptions, + sx, +}: Props) => { + const { + field: { onChange, onBlur, value, ref }, + } = useController({ + name, + control, + defaultValue, + rules: registerOptions, + }); + return ( + + ); +}; diff --git a/nym-wallet/src/pages/bonding/bond-form/index.ts b/nym-wallet/src/pages/bonding/bond-form/index.ts new file mode 100644 index 0000000000..99fb2384fa --- /dev/null +++ b/nym-wallet/src/pages/bonding/bond-form/index.ts @@ -0,0 +1,3 @@ +export * from './CheckboxInput'; +export * from './RadioInput'; +export * from './TextFieldInput'; diff --git a/nym-wallet/src/pages/bonding/index.tsx b/nym-wallet/src/pages/bonding/index.tsx index 70b786d12e..f4109ec65e 100644 --- a/nym-wallet/src/pages/bonding/index.tsx +++ b/nym-wallet/src/pages/bonding/index.tsx @@ -1 +1,48 @@ -// TODO +import React, { useContext, useEffect, useState } from 'react'; +import { AppContext } from 'src/context/main'; +import { Box } from '@mui/material'; +import { useBondingContext, BondingContextProvider } from '../../context'; +import { PageLayout } from '../../layouts'; +import { BondingCard } from './BondingCard'; +import { BondedMixnodeCard } from './BoundedMixnodeCard'; +import { BondedGatewayCard } from './BoundedGatewayCard'; +import { EnumRequestStatus } from '../../components'; +import { useCheckOwnership } from '../../hooks/useCheckOwnership'; + +const Bonding = () => { + const [status] = useState(EnumRequestStatus.initial); + const { bondedMixnode, bondedGateway } = useBondingContext(); + const { checkOwnership, ownership, isLoading } = useCheckOwnership(); + + useEffect(() => { + if (status === EnumRequestStatus.initial) { + const initialiseForm = async () => { + await checkOwnership(); + }; + initialiseForm(); + } + }, [status, checkOwnership]); + + return ( + + + {!bondedMixnode && + !bondedGateway && + status === EnumRequestStatus.initial && + !ownership.hasOwnership && + !isLoading && } + {bondedMixnode && } + {bondedGateway && } + + + ); +}; + +export const BondingPage = () => { + const { network } = useContext(AppContext); + return ( + + + + ); +}; diff --git a/nym-wallet/src/pages/bonding/nodeSchema.ts b/nym-wallet/src/pages/bonding/nodeSchema.ts new file mode 100644 index 0000000000..c04982104b --- /dev/null +++ b/nym-wallet/src/pages/bonding/nodeSchema.ts @@ -0,0 +1,55 @@ +import { boolean, lazy, mixed, number, object, string } from 'yup'; +import { isValidHostname, validateKey, validateLocation, validateRawPort, validateVersion } from '../../utils'; +import { NodeType } from './types'; + +export const nodeSchema = object().shape({ + nodeType: string().required(), + identityKey: string() + .required('An indentity key is required') + .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)), + + sphinxKey: string() + .required('A sphinx key is required') + .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)), + + signature: string() + .required('Signature is required') + .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)), + + host: string() + .required('A host is required') + .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), + + version: string() + .required('A version is required') + .test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)), + + advancedOpt: boolean().required(), + + location: lazy((value) => { + if (value) { + return string() + .required('A location is required') + .test('valid-location', 'A valid version is required', (locationValueTest) => + locationValueTest ? validateLocation(locationValueTest) : false, + ); + } + return mixed().notRequired(); + }), + + mixPort: number() + .required('A mixport is required') + .test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)), + + verlocPort: number() + .required('A verloc port is required') + .test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)), + + httpApiPort: number() + .required('A http-api port is required') + .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), + + clientsPort: number() + .required('A clients port is required') + .test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)), +}); diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts new file mode 100644 index 0000000000..d441892318 --- /dev/null +++ b/nym-wallet/src/pages/bonding/types.ts @@ -0,0 +1,45 @@ +import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types'; + +export type FormStep = 1 | 2 | 3 | 4; +export type NodeType = 'mixnode' | 'gateway'; + +export type ACTIONTYPE = + | { type: 'change_bond_type'; payload: NodeType } + | { type: 'set_node_data'; payload: NodeData } + | { type: 'set_amount_data'; payload: AmountData } + | { type: 'set_step'; payload: FormStep } + | { type: 'set_tx'; payload: TransactionExecuteResult } + | { type: 'next_step' } + | { type: 'previous_step' } + | { type: 'show_modal' } + | { type: 'close_modal' } + | { type: 'reset' }; + +export interface NodeData { + nodeType: NodeType; + identityKey: string; + sphinxKey: string; + signature: string; + host: string; + location?: string; + version: string; + advancedOpt: boolean; + mixPort: number; + verlocPort: number; + clientsPort: number; + httpApiPort: number; +} + +export interface AmountData { + amount: MajorCurrencyAmount; + tokenPool: string; + profitMargin?: number; +} + +export interface BondState { + showModal: boolean; + formStep: FormStep; + nodeData?: NodeData; + amountData?: AmountData; + tx?: TransactionExecuteResult; +} diff --git a/nym-wallet/src/pages/index.ts b/nym-wallet/src/pages/index.ts index b02d0413c9..bd89a16b1f 100644 --- a/nym-wallet/src/pages/index.ts +++ b/nym-wallet/src/pages/index.ts @@ -7,3 +7,5 @@ export * from './auth'; export * from './settings'; export * from './unbond'; export * from './delegation'; +export * from './bonding'; + diff --git a/nym-wallet/src/routes/app.tsx b/nym-wallet/src/routes/app.tsx index 19f4c769e4..c46f7eda22 100644 --- a/nym-wallet/src/routes/app.tsx +++ b/nym-wallet/src/routes/app.tsx @@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import { ApplicationLayout } from 'src/layouts'; import { Terminal } from 'src/pages/terminal'; import { Send } from 'src/components/Send'; -import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings } from '../pages'; +import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings, BondingPage } from '../pages'; export const AppRoutes = () => ( @@ -14,6 +14,7 @@ export const AppRoutes = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/nym-wallet/src/svg-icons/bonding.tsx b/nym-wallet/src/svg-icons/bonding.tsx new file mode 100644 index 0000000000..32d7a2fd14 --- /dev/null +++ b/nym-wallet/src/svg-icons/bonding.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { SvgIcon, SvgIconProps } from '@mui/material'; + +export const Bonding = (props: SvgIconProps) => ( + + + + + +); diff --git a/nym-wallet/src/svg-icons/index.ts b/nym-wallet/src/svg-icons/index.ts index 2671250474..ec0d4b9c49 100644 --- a/nym-wallet/src/svg-icons/index.ts +++ b/nym-wallet/src/svg-icons/index.ts @@ -2,3 +2,4 @@ export * from './delegate'; export * from './undelegate'; export * from './bond'; export * from './unbond'; +export * from './bonding'; diff --git a/yarn.lock b/yarn.lock index 68608c4205..3a33412862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1157,7 +1157,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -5279,11 +5279,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== -"@types/lodash@^4.14.175": - version "4.14.179" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5" - integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w== - "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -13188,11 +13183,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -18101,12 +18091,12 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-invariant@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" - integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== +tiny-case@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== -tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: +tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -19542,17 +19532,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.32.9: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== +yup@^1.0.0-beta.4: + version "1.0.0-beta.4" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.0.0-beta.4.tgz#df10f42df6aa0212c22aab58d6d026b1610ff8a3" + integrity sha512-g5uuQH2rN+0Z4L/ix8KoYIS6v7vnrykRzM/u7ExSA0WA33vrw2YOUKShBhaueh9N95oz54slgqBQTHx7E6EHwg== dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" property-expr "^2.0.4" + tiny-case "^1.0.2" toposort "^2.0.2" zwitch@^1.0.0: