Skip to content

Commit

Permalink
create nymnode bonding flow
Browse files Browse the repository at this point in the history
  • Loading branch information
fmtabbara committed Oct 14, 2024
1 parent bd17a9f commit e2f20d3
Show file tree
Hide file tree
Showing 9 changed files with 590 additions and 8 deletions.
6 changes: 2 additions & 4 deletions nym-wallet/src/components/Bonding/BondUpdateCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
import { isMixnode, Network } from 'src/types';
import { Network } from 'src/types';
import { NymCard } from 'src/components';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';

export const BondUpdateCard = ({
mixnode,
network,
setSuccesfullUpdate,
}: {
mixnode: TBondedMixnode;
Expand Down
5 changes: 2 additions & 3 deletions nym-wallet/src/components/Bonding/BondedNymNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import { urls } from 'src/context';
import { NymCard } from 'src/components';
import { IdentityKey } from 'src/components/IdentityKey';
import { getIntervalAsDate } from 'src/utils';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { Node as NodeIcon } from '../../svg-icons/node';
import { Cell, Header, NodeTable } from './NodeTable';
import { BondedNymNodeActions } from './BondedNymNodeActions';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { TBondedNymNodeActions } from './BondedNymNodeActions';
import { BondedNymNodeActions, TBondedNymNodeActions } from './BondedNymNodeActions';

const textWhenNotName = 'This node has not yet set a name';

Expand Down
87 changes: 87 additions & 0 deletions nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { createContext, useContext, useState } from 'react';
import { CurrencyDenom } from '@nymproject/types';
import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types';

const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
host: '1.1.1.1',
};

const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({
interval_operating_cost: { amount: '40', denom },
profit_margin_percent: '10',
});

const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({
amount: '100',
denom,
});

interface FormContextType {
step: 1 | 2 | 3 | 4;
setStep: React.Dispatch<React.SetStateAction<1 | 2 | 3 | 4>>;
nymNodeData: TBondNymNodeArgs['nymNode'];
setNymNodeData: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['nymNode']>>;
costParams: TBondNymNodeArgs['costParams'];
setCostParams: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['costParams']>>;
amountData: TBondMixNodeArgs['pledge'];
setAmountData: React.Dispatch<React.SetStateAction<TBondMixNodeArgs['pledge']>>;
signature?: string;
setSignature: React.Dispatch<React.SetStateAction<string | undefined>>;
onError: (e: string) => void;
}

const FormContext = createContext<FormContextType>({
step: 1,
setStep: () => {},
nymNodeData: defaultNymNodeValues,
setNymNodeData: () => {},
costParams: defaultCostParams('nym'),
setCostParams: () => {},
amountData: defaultAmount('nym'),
setAmountData: () => {},
signature: undefined,
setSignature: () => {},

onError: (e: string) => {},
});

const FormContextProvider = ({ children }: { children: React.ReactNode }) => {
// TODO - Make denom dynamic
const denom = 'nym';

const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
const [nymNodeData, setNymNodeData] = useState<TBondNymNodeArgs['nymNode']>(defaultNymNodeValues);
const [costParams, setCostParams] = useState<TBondNymNodeArgs['costParams']>(defaultCostParams(denom));
const [amountData, setAmountData] = useState<TBondNymNodeArgs['pledge']>(defaultAmount(denom));
const [signature, setSignature] = useState<string>();

const onError = (e: string) => {
console.error(e);
};

return (
<FormContext.Provider
value={{
step,
setStep,
nymNodeData,
setNymNodeData,
costParams,
setCostParams,
amountData,
setAmountData,
signature,
setSignature,
onError,
}}
>
{children}
</FormContext.Provider>
);
};

export const useFormContext = () => useContext(FormContext);

export default FormContextProvider;
119 changes: 119 additions & 0 deletions nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Stack, TextField, Box, FormHelperText } from '@mui/material';
import { useForm } from 'react-hook-form';
import { TBondNymNodeArgs } from 'src/types';
import { yupResolver } from '@hookform/resolvers/yup';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { nymNodeAmountSchema } from './amountValidationSchema';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { checkHasEnoughFunds } from 'src/utils';

const defaultNymNodeCostParamValues: TBondNymNodeArgs['costParams'] = {
profit_margin_percent: '10',
interval_operating_cost: { amount: '40', denom: 'nym' },
};

const defaultNymNodePledgeValue: TBondNymNodeArgs['pledge'] = {
amount: '100',
denom: 'nym',
};

type NymNodeDataProps = {
onClose: () => void;
onBack: () => void;
onNext: () => Promise<void>;
step: number;
};

const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
const {
formState: { errors },
register,
getValues,
setValue,
setError,
handleSubmit,
} = useForm({
mode: 'all',
defaultValues: {
pledge: defaultNymNodePledgeValue,
...defaultNymNodeCostParamValues,
},
resolver: yupResolver(nymNodeAmountSchema()),
});

console.log(errors, 'errors');

const handleRequestValidation = async () => {
const values = getValues();

const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount);

if (hasSufficientTokens) {
handleSubmit(onNext)();
} else {
setError('pledge.amount', { message: 'Not enough tokens' });
}
};

return (
<SimpleModal
open
onOk={handleRequestValidation}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
okLabel="Next"
onBack={onBack}
okDisabled={Object.keys(errors).length > 0}
>
<Stack gap={3}>
<CurrencyFormField
required
fullWidth
label="Amount"
autoFocus
onChanged={(newValue) => {
setValue('pledge.amount', newValue.amount, { shouldValidate: true });
}}
validationError={errors.pledge?.amount?.message}
denom={defaultNymNodePledgeValue.denom}
initialValue={defaultNymNodePledgeValue.amount}
/>

<Box>
<CurrencyFormField
required
fullWidth
label="Operating cost"
onChanged={(newValue) => {
setValue('interval_operating_cost', newValue, { shouldValidate: true });
}}
validationError={errors.interval_operating_cost?.amount?.message}
denom={defaultNymNodeCostParamValues.interval_operating_cost.denom}
initialValue={defaultNymNodeCostParamValues.interval_operating_cost.amount}
/>
<FormHelperText>
Monthly operational costs of running your node. If your node is in the active set the amount will be paid
back to you from the rewards.
</FormHelperText>
</Box>
<Box>
<TextField
{...register('profit_margin_percent')}
name="profit_margin_percent"
label="Profit margin"
error={Boolean(errors.profit_margin_percent)}
helperText={errors.profit_margin_percent?.message}
fullWidth
/>
<FormHelperText>
The percentage of node rewards that you as the node operator take before rewards are distributed to operator
and delegators.
</FormHelperText>
</Box>
</Stack>
</SimpleModal>
);
};

export default NymNodeAmount;
113 changes: 113 additions & 0 deletions nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material';
import { useForm } from 'react-hook-form';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { TBondNymNodeArgs } from 'src/types';
import { useFormContext } from './FormContext';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { isValidHostname, validateRawPort } from 'src/utils';
import { SimpleModal } from 'src/components/Modals/SimpleModal';

const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
host: '1.1.1.1',
};

const yupValidationSchema = yup.object().shape({
identity_key: yup.string().required('Identity key is required'),
host: yup
.string()
.required('A host is required')
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
.test('valid-host', 'A valid host is required', (value) => {
return value ? isValidHostname(value) : false;
}),

custom_http_port: yup
.number()
.required('A custom http port is required')
.test('valid-http', 'A valid http port is required', (value) => (value ? validateRawPort(value) : false)),
});

type NymNodeDataProps = {
onClose: () => void;
onBack: () => void;
onNext: () => Promise<void>;
step: number;
};

const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
const {
formState: { errors },
register,
setValue,
handleSubmit,
} = useForm({
mode: 'all',
defaultValues: defaultNymNodeValues,
resolver: yupResolver(yupValidationSchema),
});

const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false);

const handleNext = async () => {
handleSubmit(onNext)();
};

return (
<SimpleModal
open
onOk={handleNext}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
okLabel="Next"
okDisabled={Object.keys(errors).length > 0}
>
<Stack gap={3}>
<IdentityKeyFormField
autoFocus
required
fullWidth
label="Identity Key"
initialValue={defaultNymNodeValues.identity_key}
errorText={errors.identity_key?.message?.toString()}
onChanged={(value) => setValue('identity_key', value, { shouldValidate: true })}
showTickOnValid={false}
/>

<TextField
{...register('host')}
name="host"
label="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
InputLabelProps={{ shrink: true }}
/>

<FormControlLabel
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
label="Show advanced options"
/>
{showAdvancedOptions && (
<Stack direction="row" gap={3} sx={{ mb: 2 }}>
<TextField
{...register('custom_http_port')}
name="custom_http_port"
label="Custom HTTP port"
error={Boolean(errors.custom_http_port)}
helperText={errors.custom_http_port?.message}
fullWidth
InputLabelProps={{ shrink: true }}
/>
</Stack>
)}
</Stack>
</SimpleModal>
);
};

export default NymNodeData;
Loading

0 comments on commit e2f20d3

Please sign in to comment.