From 4b552db19f8a5420e10a7a06b59d0db83e111720 Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 8 Jul 2022 18:31:49 +0200 Subject: [PATCH] refactor(wallet-bonding): bonding flow with new gasFee estimation --- .../src/pages/bonding/bonding/BondingCard.tsx | 169 ++++++++++++------ .../bonding/bonding/NodeIdentityModal.tsx | 30 ++-- .../pages/bonding/bonding/SummaryModal.tsx | 99 +++++++--- .../bonding/components/TextFieldInput.tsx | 3 + nym-wallet/src/pages/bonding/index.tsx | 9 +- nym-wallet/src/pages/bonding/types.ts | 38 +++- 6 files changed, 247 insertions(+), 101 deletions(-) diff --git a/nym-wallet/src/pages/bonding/bonding/BondingCard.tsx b/nym-wallet/src/pages/bonding/bonding/BondingCard.tsx index c91bc40a52..69f7e4ad4f 100644 --- a/nym-wallet/src/pages/bonding/bonding/BondingCard.tsx +++ b/nym-wallet/src/pages/bonding/bonding/BondingCard.tsx @@ -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; + 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; + 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 && ( diff --git a/nym-wallet/src/pages/bonding/bonding/NodeIdentityModal.tsx b/nym-wallet/src/pages/bonding/bonding/NodeIdentityModal.tsx index 138c27b500..49d6d935d1 100644 --- a/nym-wallet/src/pages/bonding/bonding/NodeIdentityModal.tsx +++ b/nym-wallet/src/pages/bonding/bonding/NodeIdentityModal.tsx @@ -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({ 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>).location)} + helperText={(errors as FieldErrors>).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>).verlocPort)} + helperText={ + (errors as FieldErrors>).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>).httpApiPort)} + helperText={ + (errors as FieldErrors>).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>).clientsPort)} + helperText={ + (errors as FieldErrors>).clientsPort?.message && + 'A valid port value is required' + } required registerOptions={{ valueAsNumber: true }} sx={{ mb: 2.5 }} diff --git a/nym-wallet/src/pages/bonding/bonding/SummaryModal.tsx b/nym-wallet/src/pages/bonding/bonding/SummaryModal.tsx index 080804b640..27e55b919b 100644 --- a/nym-wallet/src/pages/bonding/bonding/SummaryModal.tsx +++ b/nym-wallet/src/pages/bonding/bonding/SummaryModal.tsx @@ -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; - 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('-'); - 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).verlocPort, + http_api_port: (node as NodeData).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).location, + clients_port: (node as NodeData).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 ( { + resetFeeState(); + onClose(); + }} + onCancel={() => { + resetFeeState(); + onCancel(); + }} onConfirm={onConfirm} title="Bond details" confirmButton="Confirm" @@ -45,17 +92,21 @@ const SummaryModal = ({ open, onClose, onSubmit, identityKey, nodeType, amount, > Identity Key - {identityKey} + {node.identityKey} Amount - {`${amount.amount} ${amount.denom}`} + {`${amount.amount.amount} ${amount.amount.denom}`} Fee for this operation - {fee} + {isFeeLoading ? ( + loading + ) : ( + {fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''} + )} ); diff --git a/nym-wallet/src/pages/bonding/components/TextFieldInput.tsx b/nym-wallet/src/pages/bonding/components/TextFieldInput.tsx index 0bd55761ee..efbf5ceeb7 100644 --- a/nym-wallet/src/pages/bonding/components/TextFieldInput.tsx +++ b/nym-wallet/src/pages/bonding/components/TextFieldInput.tsx @@ -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} /> ); }; diff --git a/nym-wallet/src/pages/bonding/index.tsx b/nym-wallet/src/pages/bonding/index.tsx index 97734c06e3..6ddf5f4481 100644 --- a/nym-wallet/src/pages/bonding/index.tsx +++ b/nym-wallet/src/pages/bonding/index.tsx @@ -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 ( - {bondedMixnode && } - {bondedGateway && } + + {/* {bondedMixnode && } + {bondedGateway && } */} ); diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts index 7aebfa90dd..7c3fbd7f87 100644 --- a/nym-wallet/src/pages/bonding/types.ts +++ b/nym-wallet/src/pages/bonding/types.ts @@ -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; + +export type NodeData = { + 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; }