feat(wallet-bonding): add node table component

This commit is contained in:
pierre
2022-06-22 11:25:32 +02:00
committed by fmtabbara
parent 8c2c0f8033
commit 79bc2ab493
10 changed files with 274 additions and 56 deletions
+18 -7
View File
@@ -1,4 +1,4 @@
import { TransactionExecuteResult } from '@nymproject/types';
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
@@ -15,12 +15,12 @@ import {
export interface BondedMixnode {
key: string;
ip: string;
stake: number;
bond: number;
stake: MajorCurrencyAmount;
bond: MajorCurrencyAmount;
stakeSaturation: number;
profitMargin: number;
nodeRewards: number;
operatorRewards: number;
nodeRewards: MajorCurrencyAmount;
operatorRewards: MajorCurrencyAmount;
delegators: number;
}
@@ -28,7 +28,7 @@ export interface BondedMixnode {
export interface BondedGateway {
key: string;
ip: string;
bond: number;
bond: MajorCurrencyAmount;
location?: string; // TODO not yet available, only available in Network Explorer API
}
@@ -89,7 +89,17 @@ export const BondingContextProvider = ({
};
const refresh = useCallback(async () => {
const bounded = null;
const bounded: BondedMixnode = {
bond: { denom: 'NYM', amount: '1234' },
key: 'B2Xx4haarLWMajX8w259oHjtRZsC7nHwagbWrJNiA3QC',
delegators: 123,
ip: '1.2.34.5',
nodeRewards: { denom: 'NYM', amount: '123' },
operatorRewards: { denom: 'NYM', amount: '12' },
profitMargin: 10,
stake: { denom: 'NYM', amount: '99' },
stakeSaturation: 99,
};
// TODO fetch bondedMixnode and bondedGatway via tauri dedicated requests
/* try {
bounded = await fetchBondingData();
@@ -181,6 +191,7 @@ export const BondingContextProvider = ({
isLoading,
error,
bondMixnode,
bondedMixnode,
bondGateway,
unbondMixnode,
unbondGateway,
+7 -7
View File
@@ -6,22 +6,22 @@ import { mockSleep } from './utils';
const SLEEP_MS = 1000;
const bondedMixnodeMock = {
const bondedMixnodeMock: BondedMixnode = {
key: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.56',
stake: 35847.221,
bond: 12576.32745,
stake: { denom: 'NYM', amount: '1234' },
bond: { denom: 'NYM', amount: '1234' },
stakeSaturation: 95,
profitMargin: 15,
nodeRewards: 12576.32745,
operatorRewards: 12576.32,
nodeRewards: { denom: 'NYM', amount: '1234' },
operatorRewards: { denom: 'NYM', amount: '1234' },
delegators: 5423,
};
const bondedGatewayMock = {
const bondedGatewayMock: BondedGateway = {
key: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: 12576.32745,
bond: { denom: 'NYM', amount: '1234' },
};
const TxResultMock: TransactionExecuteResult = {
+20 -13
View File
@@ -1,15 +1,17 @@
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 { 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 { AppContext, urls } from '../../context';
import { SummaryModal } from './SummaryModal';
import { bond, vestingBond } from '../../requests';
import { TBondArgs } from '../../types';
import { SimpleModal } from '../../components/Modals/SimpleModal';
import { Console } from '../../utils/console';
import { SimpleDialog } from './SimpleDialog';
const initialState: BondState = {
showModal: false,
@@ -51,7 +53,7 @@ export const BondingCard = () => {
const { formStep, showModal } = state;
console.log(state);
const { userBalance, clientDetails } = useContext(AppContext);
const { userBalance, clientDetails, network } = useContext(AppContext);
useEffect(() => {
dispatch({ type: 'reset' });
@@ -101,11 +103,17 @@ export const BondingCard = () => {
dispatch({ type: 'set_tx', payload: tx });
dispatch({ type: 'next_step' });
})
.catch(() => {
.catch((e: any) => {
Console.error('Failed to bond', e);
// TODO do something
});
};
const onConfirm = () => {
dispatch({ type: 'close_modal' });
dispatch({ type: 'reset' });
};
return (
<NymCard title="Bonding">
<Box
@@ -167,17 +175,16 @@ export const BondingCard = () => {
/>
)}
{formStep === 4 && showModal && (
<SimpleModal
<SimpleDialog
open={formStep === 4 && showModal}
onOk={() => {
dispatch({ type: 'close_modal' });
dispatch({ type: 'reset' });
}}
header="Bonding successful"
okLabel="Done"
onClose={onConfirm}
title="Bonding successful"
confirmText="Done"
>
Link to transaction on network explorer
</SimpleModal>
<Link href={`${urls(network).blockExplorer}/transaction/${state.tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</SimpleDialog>
)}
</NymCard>
);
@@ -1,11 +0,0 @@
import React from 'react';
import { Alert, Button, Grid, Link, Typography } from '@mui/material';
import { NymCard } from '../../components';
export const BondedMixnodeCard = () => (
<NymCard title="Balance">
<Grid container direction="column" spacing={2}>
<Grid item>bonded mixnode data table</Grid>
</Grid>
</NymCard>
);
@@ -2,7 +2,7 @@ import React from 'react';
import { Grid } from '@mui/material';
import { NymCard } from '../../components';
export const BondedGatewayCard = () => (
export const GatewayCard = () => (
<NymCard title="Balance">
<Grid container direction="column" spacing={2}>
<Grid item>bonded gateway data table</Grid>
@@ -0,0 +1,110 @@
import React, { useMemo } from 'react';
import { IconButton, Typography } from '@mui/material';
import { MoreVert } from '@mui/icons-material';
import { Link } from '@nymproject/react/link/Link';
import { NymCard } from '../../components';
import { BondedMixnode } from '../../context';
import { Cell, Header, NodeTable } from './NodeTable';
const headers: Header[] = [
{
header: 'Stake',
id: 'stake',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
header: 'Stake saturation',
id: 'stake-saturation',
tooltip: 'TODO',
},
{
header: 'PM',
id: 'profit-margin',
tooltip:
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Node rewards',
id: 'node-rewards',
tooltip: 'This is the total rewards for this node in this epoch including delegates and the operators share.',
},
{
header: 'Operator rewards',
id: 'operator-rewards',
tooltip:
'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.',
},
{
header: 'No. delegators',
id: 'delegators',
},
{
id: 'menu-button',
size: 'small',
sx: { pr: 0 },
},
];
export const MixnodeCard = ({ mixnode }: { mixnode: BondedMixnode }) => {
const { stake, bond, stakeSaturation, profitMargin, nodeRewards, operatorRewards, delegators } = mixnode;
const cells: Cell[] = useMemo(
() => [
{
cell: `${stake.amount} ${stake.denom}`,
id: 'stake-cell',
sx: { pl: 0 },
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: `${stakeSaturation}%`,
id: 'stake-saturation-cell',
},
{
cell: `${profitMargin}%`,
id: 'pm-cell',
},
{
cell: `${nodeRewards.amount} ${nodeRewards.denom}`,
id: 'node-rewards-cell',
},
{
cell: `${operatorRewards.amount} ${operatorRewards.denom}`,
id: 'operator-rewards-cell',
},
{
cell: delegators,
id: 'delegators-cell',
},
{
cell: (
<IconButton sx={{ fontSize: '1rem', padding: 0 }}>
<MoreVert fontSize="inherit" sx={{ color: 'text.primary' }} />
</IconButton>
),
id: 'menu-button-cell',
align: 'center',
size: 'small',
sx: { pr: 0 },
},
],
[mixnode],
);
return (
<NymCard title="Monster node">
<NodeTable headers={headers} cells={cells} />
<Typography sx={{ mt: 2 }}>
Check more stats of your node on the{' '}
<Link href="url" target="_blank">
explorer
</Link>
</Typography>
</NymCard>
);
};
@@ -0,0 +1,79 @@
import React from 'react';
import {
Box,
Stack,
SxProps,
Table,
TableBody,
TableCell as MUITableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
} from '@mui/material';
import { InfoOutlined } from '@mui/icons-material';
export interface TableCell {
children: React.ReactNode;
color?: string;
align?: 'center' | 'inherit' | 'justify' | 'left' | 'right';
size?: 'small' | 'medium';
sx?: SxProps;
}
export type TableHeader = TableCell & { tooltip?: React.ReactNode };
const CellHeader = ({ children, tooltip, sx, size, align, color }: TableHeader) => (
<MUITableCell sx={{ py: 1.2, ...sx }} size={size} align={align} color={color}>
{tooltip ? (
<Tooltip title={tooltip} arrow placement="top-start">
<Stack direction="row" alignItems="center" fontSize="0.8rem">
<InfoOutlined fontSize="inherit" sx={{ mr: 0.5 }} />
<Typography>{children}</Typography>
</Stack>
</Tooltip>
) : (
<Typography>{children}</Typography>
)}
</MUITableCell>
);
const CellValue = ({ children, align, size, color, sx }: TableCell) => (
<MUITableCell component="th" scope="row" sx={{ py: 1, ...sx }} align={align} size={size} color={color}>
{children}
</MUITableCell>
);
export type Header = Omit<TableHeader, 'children'> & { header?: React.ReactNode; id: string };
export type Cell = Omit<TableCell, 'children'> & { cell: React.ReactNode; id: string };
export interface TableProps {
headers: Header[];
cells: Cell[];
}
export const NodeTable = ({ headers, cells }: TableProps) => (
<TableContainer component={Box}>
<Table sx={{ minWidth: 650 }} aria-label="node-table">
<TableHead>
<TableRow>
{headers.map(({ header, id, tooltip, sx }) => (
<CellHeader tooltip={tooltip} key={id} sx={sx}>
{header}
</CellHeader>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow key="node-data">
{cells.map(({ cell, id, align, size, color, sx }) => (
<CellValue align={align} size={size} key={id} sx={sx} color={color}>
{cell}
</CellValue>
))}
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
@@ -0,0 +1,28 @@
import React from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
interface Props {
open: boolean;
onClose: () => void;
children?: React.ReactNode;
title: string;
confirmText: string;
}
export const SimpleDialog = ({ open, onClose, children, title, confirmText }: Props) => (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="responsive-dialog-title"
maxWidth="sm"
sx={{ textAlign: 'center' }}
>
<DialogTitle id="responsive-dialog-title">{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 2, pb: 2 }}>
<Button onClick={onClose} variant="contained" autoFocus fullWidth>
{confirmText}
</Button>
</DialogActions>
</Dialog>
);
@@ -34,18 +34,18 @@ export const SummaryModal = ({ open, onClose, onSubmit, header, buttonText, iden
return (
<SimpleModal open={open} onClose={onClose} onOk={onConfirm} header={header} okLabel={buttonText}>
<Stack direction="row" justifyContent="space-between" mt={3}>
<Typography fontWeight={600}>Identity Key</Typography>
<Typography fontWeight={600}>{identityKey}</Typography>
<Typography>Identity Key</Typography>
<Typography>{identityKey}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between" mt={3}>
<Typography fontWeight={600}>Amount</Typography>
<Typography fontWeight={600}>{`${amount.amount} ${amount.denom}`}</Typography>
<Stack direction="row" justifyContent="space-between">
<Typography>Amount</Typography>
<Typography>{`${amount.amount} ${amount.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between" mt={3}>
<Typography fontWeight={600}>Fee for this operation</Typography>
<Typography fontWeight={600}>{fee}</Typography>
<Stack direction="row" justifyContent="space-between" mb={1}>
<Typography>Fee for this operation</Typography>
<Typography>{fee}</Typography>
</Stack>
</SimpleModal>
);
+3 -9
View File
@@ -4,8 +4,8 @@ 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 { MixnodeCard } from './MixnodeCard';
import { GatewayCard } from './GatewayCard';
import { EnumRequestStatus } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
@@ -26,13 +26,7 @@ const Bonding = () => {
return (
<PageLayout>
<Box display="flex" flexDirection="column" gap={2}>
{!bondedMixnode &&
!bondedGateway &&
status === EnumRequestStatus.initial &&
!ownership.hasOwnership &&
!isLoading && <BondingCard />}
{bondedMixnode && <BondedMixnodeCard />}
{bondedGateway && <BondedGatewayCard />}
{bondedMixnode && <MixnodeCard mixnode={bondedMixnode} />}
</Box>
</PageLayout>
);