refactor(wallet-bonding): bonding flow with new gasFee estimation

This commit is contained in:
pierre
2022-07-08 18:31:49 +02:00
committed by fmtabbara
parent 75aa2579a0
commit 4b552db19f
6 changed files with 247 additions and 101 deletions
@@ -1,20 +1,34 @@
import React, { useContext, useEffect, useReducer } from 'react';
import { Box, Button, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { Gateway, MajorCurrencyAmount, MixNode } from '@nymproject/types';
import { TransactionExecuteResult } from '@nymproject/types';
import { ConfirmationModal, NymCard } from '../../../components';
import NodeIdentityModal from './NodeIdentityModal';
import { ACTIONTYPE, AmountData, BondState, FormStep, NodeData, NodeType } from '../types';
import {
ACTIONTYPE,
BondState,
FormStep,
GatewayAmount,
GatewayData,
MixnodeAmount,
MixnodeData,
NodeData,
} from '../types';
import AmountModal from './AmountModal';
import { AppContext, urls } from '../../../context';
import SummaryModal from './SummaryModal';
import { bond, vestingBond } from '../../../requests';
import { TBondArgs } from '../../../types';
import { Console } from '../../../utils/console';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
vestingBondGateway,
vestingBondMixNode,
} from '../../../requests';
import { useGetFee } from '../../../hooks/useGetFee';
const initialState: BondState = {
showModal: false,
formStep: 1,
bondStatus: 'init',
};
function reducer(state: BondState, action: ACTIONTYPE) {
@@ -30,6 +44,8 @@ function reducer(state: BondState, action: ACTIONTYPE) {
return { ...state, formStep: action.payload };
case 'set_tx':
return { ...state, tx: action.payload };
case 'set_bond_status':
return { ...state, bondStatus: action.payload };
case 'next_step':
step = state.formStep + 1;
return { ...state, formStep: step <= 4 ? (step as FormStep) : 4 };
@@ -50,62 +66,107 @@ function reducer(state: BondState, action: ACTIONTYPE) {
const BondingCard = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { formStep, showModal } = state;
console.log(state);
const { userBalance, clientDetails, network } = useContext(AppContext);
const { fee } = useGetFee();
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 bondMixnode = async () => {
let tx: TransactionExecuteResult | undefined;
const { signature, identityKey, sphinxKey, host, version, mixPort, verlocPort, httpApiPort } =
state.nodeData as NodeData<MixnodeData>;
const { profitMargin, amount, tokenPool } = state.amountData as MixnodeAmount;
const payload = {
ownerSignature: signature,
mixnode: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
profit_margin_percent: profitMargin,
verloc_port: verlocPort,
http_api_port: httpApiPort,
},
pledge: amount,
fee: fee?.fee,
};
if (tokenPool !== 'locked' && tokenPool !== 'balance') {
throw new Error(`token pool [${tokenPool}] not supported`);
}
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(payload);
await userBalance.fetchTokenAllocation();
}
dispatch({ type: 'set_bond_status', payload: 'success' });
return tx;
} catch (e) {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return undefined;
};
const bondGateway = async () => {
let tx: TransactionExecuteResult | undefined;
const { signature, identityKey, sphinxKey, host, version, location, mixPort, clientsPort } =
state.nodeData as NodeData<GatewayData>;
const { amount, tokenPool } = state.amountData as GatewayAmount;
const payload = {
ownerSignature: signature,
gateway: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
location,
clients_port: clientsPort,
},
pledge: amount,
fee: fee?.fee,
};
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(payload);
await userBalance.fetchTokenAllocation();
}
dispatch({ type: 'set_bond_status', payload: 'success' });
return tx;
} catch (e) {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return tx;
};
const onSubmit = async () => {
const { nodeData, amountData } = state;
if (!nodeData || !amountData) {
throw new Error('');
const { nodeData } = state;
let tx: TransactionExecuteResult | undefined;
// TODO show a special UI for loading state
dispatch({ type: 'set_bond_status', payload: 'loading' });
if ((nodeData as NodeData).nodeType === 'mixnode') {
tx = await bondMixnode();
} else {
tx = await bondGateway();
}
dispatch({ type: 'set_tx', payload: tx });
if (state.bondStatus === 'success') {
dispatch({ type: 'next_step' });
}
if (state.bondStatus === 'error') {
// TODO show a special UI for 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((e: any) => {
Console.error('Failed to bond', e);
// TODO do something
});
};
const onConfirm = () => {
@@ -163,9 +224,9 @@ const BondingCard = () => {
onClose={() => dispatch({ type: 'reset' })}
onCancel={() => dispatch({ type: 'prev_step' })}
onSubmit={onSubmit}
nodeType={state.nodeData?.nodeType as NodeType}
identityKey={state.nodeData?.identityKey as string}
amount={state.amountData?.amount as MajorCurrencyAmount}
node={state.nodeData as NodeData}
amount={state.amountData as MixnodeAmount | GatewayAmount}
onError={() => {}}
/>
)}
{formStep === 4 && showModal && (
@@ -2,7 +2,8 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Stack } from '@mui/material';
import { yupResolver } from '@hookform/resolvers/yup';
import { NodeData, NodeType } from '../types';
import { FieldErrors } from 'react-hook-form/dist/types/errors';
import { GatewayData, MixnodeData, NodeData, NodeType } from '../types';
import { RadioInput, TextFieldInput, CheckboxInput } from '../components';
import nodeSchema from './nodeSchema';
import { SimpleDialog } from '../../../components';
@@ -32,7 +33,7 @@ const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
formState: { errors },
} = useForm<NodeData>({
defaultValues: {
nodeType: radioOptions[0].value,
nodeType: 'mixnode',
advancedOpt: false,
mixPort: 1789,
verlocPort: 1790,
@@ -111,8 +112,8 @@ const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
defaultValue=""
label="Location"
placeholder="Location"
error={Boolean(errors.location)}
helperText={errors.location?.message}
error={Boolean((errors as FieldErrors<NodeData<GatewayData>>).location)}
helperText={(errors as FieldErrors<NodeData<GatewayData>>).location?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
@@ -171,8 +172,11 @@ const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
control={control}
label="Verloc Port"
placeholder="Verloc Port"
error={Boolean(errors.verlocPort)}
helperText={errors.verlocPort?.message && 'A valid port value is required'}
error={Boolean((errors as FieldErrors<NodeData<MixnodeData>>).verlocPort)}
helperText={
(errors as FieldErrors<NodeData<MixnodeData>>).verlocPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
@@ -182,8 +186,11 @@ const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
control={control}
label="HTTP API Port"
placeholder="HTTP API Port"
error={Boolean(errors.httpApiPort)}
helperText={errors.httpApiPort?.message && 'A valid port value is required'}
error={Boolean((errors as FieldErrors<NodeData<MixnodeData>>).httpApiPort)}
helperText={
(errors as FieldErrors<NodeData<MixnodeData>>).httpApiPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
@@ -195,8 +202,11 @@ const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
control={control}
label="client WS API Port"
placeholder="client WS API Port"
error={Boolean(errors.clientsPort)}
helperText={errors.clientsPort?.message && 'A valid port value is required'}
error={Boolean((errors as FieldErrors<NodeData<GatewayData>>).clientsPort)}
helperText={
(errors as FieldErrors<NodeData<GatewayData>>).clientsPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
@@ -1,40 +1,87 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { MajorCurrencyAmount } from '@nymproject/types';
import { getGasFee } from '../../../requests';
import { NodeType } from '../types';
import { AppContext } from '../../../context';
import {
simulateBondGateway,
simulateBondMixnode,
simulateVestingBondGateway,
simulateVestingBondMixnode,
} from '../../../requests';
import { GatewayAmount, GatewayData, MixnodeAmount, MixnodeData, NodeData } from '../types';
import { SimpleDialog } from '../../../components';
import { useGetFee } from '../../../hooks/useGetFee';
export interface Props {
open: boolean;
onClose?: () => void;
onCancel?: () => void;
onClose: () => void;
onCancel: () => void;
onSubmit: () => Promise<void>;
identityKey: string;
nodeType: NodeType;
amount: MajorCurrencyAmount;
onError: (message?: string) => void;
node: NodeData;
amount: MixnodeAmount | GatewayAmount;
}
const SummaryModal = ({ open, onClose, onSubmit, identityKey, nodeType, amount, onCancel }: Props) => {
const onConfirm = async () => onSubmit();
const [fee, setFee] = useState<string>('-');
const { clientDetails } = useContext(AppContext);
const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError }: Props) => {
const { fee, getFee, resetFeeState, feeError, isFeeLoading } = useGetFee();
const getFee = async (op: 'BondMixnode' | 'BondGateway') => {
const res = await getGasFee(op);
setFee(`${res.amount} ${clientDetails?.denom}`);
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
const fetchFee = async () => {
const { signature, host, version, mixPort, identityKey, sphinxKey } = node;
try {
if (node.nodeType === 'mixnode') {
await getFee(amount.tokenPool === 'locked' ? simulateVestingBondMixnode : simulateBondMixnode, {
ownerSignature: signature,
mixnode: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
profit_margin_percent: (amount as MixnodeAmount).profitMargin,
verloc_port: (node as NodeData<MixnodeData>).verlocPort,
http_api_port: (node as NodeData<MixnodeData>).httpApiPort,
},
pledge: amount.amount,
});
} else {
await getFee(amount.tokenPool === 'locked' ? simulateVestingBondGateway : simulateBondGateway, {
ownerSignature: signature,
gateway: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
location: (node as NodeData<GatewayData>).location,
clients_port: (node as NodeData<GatewayData>).clientsPort,
},
pledge: amount.amount,
});
}
} catch (e) {
onError(e as string);
}
};
useEffect(() => {
getFee(nodeType === 'mixnode' ? 'BondMixnode' : 'BondGateway');
}, [clientDetails, nodeType]);
fetchFee();
}, [node, amount]);
const onConfirm = async () => onSubmit();
return (
<SimpleDialog
open={open}
onClose={onClose}
onCancel={onCancel}
onClose={() => {
resetFeeState();
onClose();
}}
onCancel={() => {
resetFeeState();
onCancel();
}}
onConfirm={onConfirm}
title="Bond details"
confirmButton="Confirm"
@@ -45,17 +92,21 @@ const SummaryModal = ({ open, onClose, onSubmit, identityKey, nodeType, amount,
>
<Stack direction="row" justifyContent="space-between">
<Typography>Identity Key</Typography>
<Typography>{identityKey}</Typography>
<Typography>{node.identityKey}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Amount</Typography>
<Typography>{`${amount.amount} ${amount.denom}`}</Typography>
<Typography>{`${amount.amount.amount} ${amount.amount.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Fee for this operation</Typography>
<Typography>{fee}</Typography>
{isFeeLoading ? (
<Typography>loading</Typography>
) : (
<Typography>{fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''}</Typography>
)}
</Stack>
</SimpleDialog>
);
@@ -15,6 +15,7 @@ interface Props {
helperText?: string;
sx?: SxProps;
registerOptions?: RegisterOptions;
disabled?: boolean;
}
const TextFieldInput = ({
@@ -29,6 +30,7 @@ const TextFieldInput = ({
helperText,
registerOptions,
sx,
disabled,
}: Props) => {
const {
field: { onChange, onBlur, value, ref },
@@ -54,6 +56,7 @@ const TextFieldInput = ({
helperText={helperText}
{...muiTextFieldProps}
sx={sx}
disabled={disabled}
/>
);
};
+5 -4
View File
@@ -4,8 +4,8 @@ import { Box } from '@mui/material';
import { useBondingContext, BondingContextProvider } from '../../context';
import { PageLayout } from '../../layouts';
import BondingCard from './bonding';
import MixnodeCard from './mixnode';
import GatewayCard from './gateway';
/* import MixnodeCard from './mixnode';
import GatewayCard from './gateway'; */
import { EnumRequestStatus } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
@@ -26,8 +26,9 @@ const Bonding = () => {
return (
<PageLayout>
<Box display="flex" flexDirection="column" gap={2}>
{bondedMixnode && <MixnodeCard mixnode={bondedMixnode} />}
{bondedGateway && <GatewayCard gateway={bondedGateway} />}
<BondingCard />
{/* {bondedMixnode && <MixnodeCard mixnode={bondedMixnode} />}
{bondedGateway && <GatewayCard gateway={bondedGateway} />} */}
</Box>
</PageLayout>
);
+29 -9
View File
@@ -1,34 +1,53 @@
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import { MajorCurrencyAmount, TNodeType, TransactionExecuteResult } from '@nymproject/types';
export type FormStep = 1 | 2 | 3 | 4;
export type NodeType = 'mixnode' | 'gateway';
export type NodeType = TNodeType;
export type BondStatus = 'init' | 'success' | 'error' | 'loading';
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: 'set_tx'; payload: TransactionExecuteResult | undefined }
| { type: 'set_bond_status'; payload: BondStatus }
| { type: 'next_step' }
| { type: 'prev_step' }
| { type: 'show_modal' }
| { type: 'close_modal' }
| { type: 'reset' };
export interface NodeData {
nodeType: NodeType;
export type NodeIdentity = {
identityKey: string;
sphinxKey: string;
signature: string;
host: string;
location?: string;
version: string;
advancedOpt: boolean;
mixPort: number;
};
export type MixnodeData = NodeIdentity & {
verlocPort: number;
clientsPort: number;
httpApiPort: number;
}
};
export type MixnodeAmount = {
amount: MajorCurrencyAmount;
tokenPool: string;
profitMargin: number;
};
export type GatewayData = NodeIdentity & {
location: string;
clientsPort: number;
};
export type GatewayAmount = Omit<MixnodeAmount, 'profitMargin'>;
export type NodeData<N = MixnodeData | GatewayData> = {
nodeType: TNodeType;
} & N;
export interface AmountData {
amount: MajorCurrencyAmount;
@@ -40,6 +59,7 @@ export interface BondState {
showModal: boolean;
formStep: FormStep;
nodeData?: NodeData;
amountData?: AmountData;
amountData?: MixnodeAmount | GatewayAmount;
tx?: TransactionExecuteResult;
bondStatus: BondStatus;
}