feat: cashing out for each peer and single page accounting (#69)
* refactor: simplified accounting, removed cashing out * feat: load uncashed amounts for all pears that have settlement * feat: added cashout button * refactor: changed accounting to work with current version of bee * chore: addressed PR review comments * chore: simplified the uncashed expression in balance table
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { ReactElement, useState } from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Input from '@material-ui/core/Input'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
@@ -11,10 +10,15 @@ import { Snackbar, Container, CircularProgress } from '@material-ui/core'
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
|
||||
import EthereumAddress from './EthereumAddress'
|
||||
import { fromBZZbaseUnit } from '../utils'
|
||||
|
||||
export default function DepositModal(): ReactElement {
|
||||
interface Props {
|
||||
peerId: string
|
||||
uncashedAmount: number
|
||||
}
|
||||
|
||||
export default function DepositModal({ peerId, uncashedAmount }: Props): ReactElement {
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [peerId, setPeerId] = useState('')
|
||||
const [loadingCashout, setLoadingCashout] = useState<boolean>(false)
|
||||
const [showToast, setToastVisibility] = useState<boolean>(false)
|
||||
const [toastContent, setToastContent] = useState<JSX.Element | null>(null)
|
||||
@@ -67,32 +71,33 @@ export default function DepositModal(): ReactElement {
|
||||
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={showToast} message={toastContent} />
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
|
||||
{loadingCashout ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<DialogContent>
|
||||
<DialogContentText style={{ marginTop: '20px' }}>
|
||||
Specify the peer Id of the peer you would like to cashout.
|
||||
</DialogContentText>
|
||||
<Input
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="peerId"
|
||||
type="text"
|
||||
placeholder="Peer Id"
|
||||
fullWidth
|
||||
onChange={e => setPeerId(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogContent>
|
||||
<DialogContentText style={{ marginTop: '20px', overflowWrap: 'break-word' }}>
|
||||
{loadingCashout && (
|
||||
<>
|
||||
<span>
|
||||
Cashing out <strong>{fromBZZbaseUnit(uncashedAmount).toFixed(7)}</strong> from Peer{' '}
|
||||
<strong>{peerId}</strong>. Please wait...
|
||||
</span>
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
{!loadingCashout && (
|
||||
<span>
|
||||
Are you sure you want to cashout <strong>{fromBZZbaseUnit(uncashedAmount).toFixed(7)} BZZ</strong> from
|
||||
Peer <strong>{peerId}</strong>?
|
||||
</span>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCashout} color="primary">
|
||||
Cashout
|
||||
<Button onClick={handleCashout} color="primary" disabled={loadingCashout}>
|
||||
Yes Cashout
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { LastCashoutActionResponse, PeerBalance, Settlements } from '@ethersphere/bee-js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { beeDebugApi } from '../services/bee'
|
||||
import { useApiPeerBalances, useApiSettlements } from './apiHooks'
|
||||
|
||||
interface UseAccountingHook {
|
||||
isLoading: boolean
|
||||
isLoadingUncashed: boolean
|
||||
error: Error | null
|
||||
totalsent: number
|
||||
totalreceived: number
|
||||
accounting: Accounting[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the balances, settlements and uncashedAmounts arrays into single array which is sorted by uncashed amounts (if any)
|
||||
*
|
||||
* @param balances Balances for all peers
|
||||
* @param settlements Settlements for all peers which has some settlement
|
||||
* @param uncashedAmounts Array of getPeerLastCashout responses which is needed to calculate uncashed amount
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function mergeAccounting(
|
||||
balances?: PeerBalance[],
|
||||
settlements?: Settlements[],
|
||||
uncashedAmounts?: LastCashoutActionResponse[],
|
||||
): Accounting[] | null {
|
||||
// Settlements or balances are still loading or there is an error -> return null
|
||||
if (!balances || !settlements) return null
|
||||
|
||||
const accounting: Record<string, Accounting> = {}
|
||||
|
||||
balances.forEach(
|
||||
// Some peers may not have settlement but all have balance (therefore initialize sent, received and uncashed to 0)
|
||||
({ peer, balance }) =>
|
||||
(accounting[peer] = { peer, balance, sent: 0, received: 0, uncashedAmount: 0, total: balance }),
|
||||
)
|
||||
|
||||
settlements.forEach(
|
||||
({ peer, sent, received }) =>
|
||||
(accounting[peer] = { ...accounting[peer], sent, received, total: accounting[peer].balance + received - sent }),
|
||||
)
|
||||
|
||||
// If there are no cheques (and hence last cashout actions), we don't need to sort and can return values right away
|
||||
if (!uncashedAmounts) return Object.values(accounting)
|
||||
|
||||
uncashedAmounts?.forEach(
|
||||
({ peer, cumulativePayout }) => (accounting[peer].uncashedAmount = accounting[peer].received - cumulativePayout),
|
||||
)
|
||||
|
||||
return Object.values(accounting).sort((a, b) => b.uncashedAmount - a.uncashedAmount)
|
||||
}
|
||||
|
||||
export const useAccounting = (): UseAccountingHook => {
|
||||
const settlements = useApiSettlements()
|
||||
const balances = useApiPeerBalances()
|
||||
|
||||
const [err, setErr] = useState<Error | null>(null)
|
||||
const [isLoadingUncashed, setIsloadingUncashed] = useState<boolean>(false)
|
||||
const [uncashedAmounts, setUncashedAmounts] = useState<LastCashoutActionResponse[] | undefined>(undefined)
|
||||
|
||||
const error = balances.error || settlements.error || err
|
||||
|
||||
useEffect(() => {
|
||||
// We don't have any settlements loaded yet or we are already loading/have loaded the uncashed amounts
|
||||
if (isLoadingUncashed || !settlements.settlements || uncashedAmounts || error) return
|
||||
|
||||
setIsloadingUncashed(true)
|
||||
const promises = settlements.settlements.settlements.map(({ peer }) =>
|
||||
beeDebugApi.chequebook.getPeerLastCashout(peer),
|
||||
)
|
||||
Promise.all(promises)
|
||||
.then(setUncashedAmounts)
|
||||
.catch(setErr)
|
||||
.finally(() => setIsloadingUncashed(false))
|
||||
}, [settlements, isLoadingUncashed, uncashedAmounts, error])
|
||||
|
||||
const accounting = mergeAccounting(
|
||||
balances.peerBalances?.balances,
|
||||
settlements.settlements?.settlements,
|
||||
uncashedAmounts,
|
||||
)
|
||||
|
||||
return {
|
||||
isLoading: settlements.isLoadingSettlements || balances.isLoadingPeerBalances,
|
||||
isLoadingUncashed,
|
||||
error,
|
||||
accounting,
|
||||
totalsent: settlements.settlements?.totalsent || 0,
|
||||
totalreceived: settlements.settlements?.totalreceived || 0,
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Card, CardContent, Typography, Grid } from '@material-ui/core/'
|
||||
import { Card, CardContent, Typography, Theme } from '@material-ui/core/'
|
||||
import { Skeleton } from '@material-ui/lab'
|
||||
import WithdrawModal from '../../containers/WithdrawModal'
|
||||
import DepositModal from '../../containers/DepositModal'
|
||||
import CashoutModal from '../../components/CashoutModal'
|
||||
|
||||
import { fromBZZbaseUnit } from '../../utils'
|
||||
|
||||
import type { AllSettlements, ChequebookAddressResponse } from '@ethersphere/bee-js'
|
||||
import type { ChequebookAddressResponse } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
details: {
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
columnGap: theme.spacing(1),
|
||||
},
|
||||
address: {
|
||||
color: 'grey',
|
||||
fontWeight: 400,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
status: {
|
||||
color: '#fff',
|
||||
backgroundColor: '#76a9fa',
|
||||
gridContainer: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
marginLeft: theme.spacing(1),
|
||||
marginRight: theme.spacing(1),
|
||||
columnGap: theme.spacing(1),
|
||||
rowGap: theme.spacing(1),
|
||||
flex: '0 1 auto',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -40,81 +40,58 @@ interface ChequebookBalance {
|
||||
|
||||
interface Props {
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
isLoadingChequebookAddress: boolean
|
||||
chequebookBalance: ChequebookBalance | null
|
||||
isLoadingChequebookBalance: boolean
|
||||
settlements: AllSettlements | null
|
||||
isLoadingSettlements: boolean
|
||||
totalsent: number
|
||||
totalreceived: number
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
function AccountCard(props: Props): ReactElement {
|
||||
function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ justifyContent: 'space-between', display: 'flex' }}>
|
||||
<h2 style={{ marginTop: '0px' }}>Accounting</h2>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<h2 style={{ marginTop: '0px' }}>Chequebook</h2>
|
||||
<div className={classes.buttons}>
|
||||
<WithdrawModal />
|
||||
<DepositModal />
|
||||
<CashoutModal />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className={classes.root}>
|
||||
{!props.isLoadingChequebookBalance && !props.isLoadingSettlements && props.chequebookBalance ? (
|
||||
<div className={classes.details}>
|
||||
<CardContent className={classes.content}>
|
||||
<Grid container spacing={5}>
|
||||
<Grid item>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Total Balance (BZZ)
|
||||
</Typography>
|
||||
<Typography component="p" variant="h5">
|
||||
{fromBZZbaseUnit(props.chequebookBalance.totalBalance)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Available Balance (BZZ)
|
||||
</Typography>
|
||||
<Typography component="p" variant="h5">
|
||||
{fromBZZbaseUnit(props.chequebookBalance.availableBalance)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Total Sent / Received (BZZ)
|
||||
</Typography>
|
||||
<Typography component="div" variant="h5">
|
||||
<span style={{ marginRight: '7px' }}>
|
||||
{fromBZZbaseUnit(props.settlements?.totalsent || 0)} /{' '}
|
||||
{fromBZZbaseUnit(props.settlements?.totalreceived || 0)}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
props.settlements && props.settlements?.totalsent > props.settlements?.totalreceived
|
||||
? '#c9201f'
|
||||
: '#32c48d',
|
||||
}}
|
||||
>
|
||||
(
|
||||
{fromBZZbaseUnit(
|
||||
(props.settlements && props.settlements?.totalsent - props.settlements?.totalreceived) || 0,
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.details}>
|
||||
<Skeleton width={180} height={110} animation="wave" style={{ marginLeft: '12px', marginRight: '12px' }} />
|
||||
<Skeleton width={180} height={110} animation="wave" style={{ marginLeft: '12px', marginRight: '12px' }} />
|
||||
<Skeleton width={180} height={110} animation="wave" style={{ marginLeft: '12px', marginRight: '12px' }} />
|
||||
{!isLoading && (
|
||||
<CardContent className={classes.gridContainer}>
|
||||
<div>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Total Balance
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
{fromBZZbaseUnit(chequebookBalance?.totalBalance || 0).toFixed(7)} BZZ
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Available Uncommitted Balance
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
{fromBZZbaseUnit(chequebookBalance?.availableBalance || 0).toFixed(7)} BZZ
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography component="h2" variant="h6" color="primary" gutterBottom>
|
||||
Total Sent / Received
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
{fromBZZbaseUnit(totalsent).toFixed(7)} / {fromBZZbaseUnit(totalreceived).toFixed(7)} BZZ
|
||||
</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className={classes.gridContainer}>
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
<Skeleton width={180} height={110} animation="wave" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,79 +1,89 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
TableHead,
|
||||
Paper,
|
||||
Container,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core'
|
||||
import { Table, TableBody, TableCell, TableContainer, TableRow, TableHead, Paper } from '@material-ui/core'
|
||||
|
||||
import { fromBZZbaseUnit } from '../../utils'
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import CashoutModal from '../../components/CashoutModal'
|
||||
import PeerDetailDrawer from './PeerDetail'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
values: {
|
||||
textAlign: 'right',
|
||||
fontFamily: 'monospace, monospace',
|
||||
},
|
||||
})
|
||||
|
||||
interface PeerBalance {
|
||||
balance: number
|
||||
peer: string
|
||||
}
|
||||
|
||||
interface PeerBalances {
|
||||
balances: Array<PeerBalance>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
peerBalances: PeerBalances | null
|
||||
loading?: boolean
|
||||
isLoadingUncashed: boolean
|
||||
accounting: Accounting[] | null
|
||||
}
|
||||
|
||||
function BalancesTable(props: Props): ReactElement {
|
||||
function BalancesTable({ accounting, isLoadingUncashed }: Props): ReactElement | null {
|
||||
if (accounting === null) return null
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.loading ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small" aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Peer</TableCell>
|
||||
<TableCell style={{ textAlign: 'right' }}>Balance (BZZ)</TableCell>
|
||||
<TableCell align="right"></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.peerBalances?.balances.map((peerBalance: PeerBalance) => (
|
||||
<TableRow key={peerBalance.peer}>
|
||||
<TableCell>{peerBalance.peer}</TableCell>
|
||||
<TableCell
|
||||
style={{
|
||||
color: fromBZZbaseUnit(peerBalance.balance) > 0 ? '#32c48d' : '#c9201f',
|
||||
textAlign: 'right',
|
||||
fontFamily: 'monospace, monospace',
|
||||
}}
|
||||
>
|
||||
{fromBZZbaseUnit(peerBalance.balance).toFixed(7).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell align="right"></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</div>
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small" aria-label="Balances Table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Peer</TableCell>
|
||||
<TableCell align="right">Outstanding Balance</TableCell>
|
||||
<TableCell align="right">Settlements Sent / Received</TableCell>
|
||||
<TableCell align="right">Total</TableCell>
|
||||
<TableCell align="right">Uncashed Amount</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{accounting.map(({ peer, balance, received, sent, uncashedAmount, total }) => (
|
||||
<TableRow key={peer}>
|
||||
<TableCell>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<small>
|
||||
<PeerDetailDrawer peerId={peer} />
|
||||
</small>
|
||||
<ClipboardCopy value={peer} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>
|
||||
<span
|
||||
style={{
|
||||
color: balance > 0 ? '#32c48d' : '#c9201f',
|
||||
}}
|
||||
>
|
||||
{fromBZZbaseUnit(balance).toFixed(7).toLocaleString()}
|
||||
</span>{' '}
|
||||
BZZ
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>
|
||||
-{fromBZZbaseUnit(sent).toFixed(7)} / {fromBZZbaseUnit(received).toFixed(7)} BZZ
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>
|
||||
<span
|
||||
style={{
|
||||
color: total > 0 ? '#32c48d' : '#c9201f',
|
||||
}}
|
||||
>
|
||||
{fromBZZbaseUnit(total).toFixed(7)}
|
||||
</span>{' '}
|
||||
BZZ
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>
|
||||
{isLoadingUncashed && 'loading...'}
|
||||
{!isLoadingUncashed && <>{uncashedAmount > 0 ? fromBZZbaseUnit(uncashedAmount).toFixed(7) : '0'} BZZ</>}
|
||||
</TableCell>
|
||||
<TableCell className={classes.values}>
|
||||
{uncashedAmount > 0 && <CashoutModal uncashedAmount={uncashedAmount} peerId={peer} />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
TableHead,
|
||||
Paper,
|
||||
Container,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core'
|
||||
|
||||
import { fromBZZbaseUnit } from '../../utils'
|
||||
import EthereumAddress from '../../components/EthereumAddress'
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import PeerDetailDrawer from './PeerDetailDrawer'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
})
|
||||
|
||||
interface ChequeEvent {
|
||||
beneficiary: string
|
||||
chequebook: string
|
||||
payout: number
|
||||
}
|
||||
|
||||
interface PeerCheque {
|
||||
lastreceived?: ChequeEvent
|
||||
lastsent?: ChequeEvent
|
||||
peer: string
|
||||
}
|
||||
|
||||
interface PeerCheques {
|
||||
lastcheques: Array<PeerCheque>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
peerCheques: PeerCheques | null
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
function ChequebookTable(props: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.loading ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<div>
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small" aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Peer</TableCell>
|
||||
<TableCell>Last Received</TableCell>
|
||||
<TableCell>Last Sent</TableCell>
|
||||
<TableCell align="right"></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.peerCheques?.lastcheques.map((peerCheque: PeerCheque) => (
|
||||
<TableRow key={peerCheque.peer}>
|
||||
<TableCell>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<small>
|
||||
<PeerDetailDrawer peerId={peerCheque.peer} />
|
||||
</small>
|
||||
<ClipboardCopy value={peerCheque.peer} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: '320px' }}>
|
||||
<p style={{ marginBottom: '0px', fontFamily: 'monospace, monospace', display: 'flex' }}>
|
||||
<span style={{ whiteSpace: 'nowrap', marginRight: '12px', paddingTop: '3px' }}>
|
||||
{peerCheque.lastreceived?.payout
|
||||
? `${fromBZZbaseUnit(peerCheque.lastreceived?.payout).toFixed(7).toLocaleString()} from`
|
||||
: '-'}
|
||||
</span>
|
||||
{peerCheque.lastreceived ? (
|
||||
<EthereumAddress
|
||||
hideBlockie
|
||||
truncate
|
||||
network="goerli"
|
||||
address={peerCheque.lastreceived.beneficiary}
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: '320px' }}>
|
||||
<p style={{ marginBottom: '0px', fontFamily: 'monospace, monospace', display: 'flex' }}>
|
||||
<span style={{ whiteSpace: 'nowrap', marginRight: '12px', paddingTop: '3px' }}>
|
||||
{peerCheque.lastsent?.payout
|
||||
? `${fromBZZbaseUnit(peerCheque.lastsent?.payout).toFixed(7).toLocaleString()} to`
|
||||
: '-'}
|
||||
</span>
|
||||
{peerCheque.lastsent ? (
|
||||
<EthereumAddress
|
||||
hideBlockie
|
||||
truncate
|
||||
network="goerli"
|
||||
address={peerCheque.lastsent.beneficiary}
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell align="right"></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChequebookTable
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { Typography } from '@material-ui/core'
|
||||
|
||||
function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10) {
|
||||
return `${str.substring(0, firstCharCount)}...${str.substring(str.length - endCharCount, str.length)}`
|
||||
}
|
||||
|
||||
interface Props {
|
||||
peerId: string
|
||||
}
|
||||
|
||||
export default function PeerDetail(props: Props): ReactElement {
|
||||
return (
|
||||
<Typography
|
||||
variant="button"
|
||||
style={{
|
||||
fontFamily: 'monospace, monospace',
|
||||
}}
|
||||
>
|
||||
{truncStringPortion(props.peerId)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { Paper, Container, Drawer, Button, Typography, CircularProgress, Grid } from '@material-ui/core'
|
||||
import ClipboardCopy from '../../components/ClipboardCopy'
|
||||
import { beeDebugApi } from '../../services/bee'
|
||||
import EthereumAddress from '../../components/EthereumAddress'
|
||||
import { fromBZZbaseUnit } from '../../utils'
|
||||
import { LastCashoutActionResponse, LastChequesForPeerResponse } from '@ethersphere/bee-js'
|
||||
|
||||
function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10) {
|
||||
let convertedStr = ''
|
||||
convertedStr += str.substring(0, firstCharCount)
|
||||
convertedStr += '.'.repeat(3)
|
||||
convertedStr += str.substring(str.length - endCharCount, str.length)
|
||||
|
||||
return convertedStr
|
||||
}
|
||||
|
||||
interface Props {
|
||||
peerId: string
|
||||
}
|
||||
|
||||
export default function Index(props: Props): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [peerCashout, setPeerCashout] = useState<LastCashoutActionResponse | null>(null)
|
||||
const [peerCheque, setPeerCheque] = useState<LastChequesForPeerResponse | null>(null)
|
||||
|
||||
const [isLoadingPeerCheque, setIsLoadingPeerCheque] = useState<boolean>(false)
|
||||
const [isLoadingPeerCashout, setIsLoadingPeerCashout] = useState<boolean>(false)
|
||||
|
||||
const handleClickOpen = (peerId: string) => {
|
||||
setIsLoadingPeerCashout(true)
|
||||
beeDebugApi.chequebook
|
||||
.getPeerLastCashout(peerId)
|
||||
.then(res => {
|
||||
setPeerCashout(res)
|
||||
})
|
||||
.catch(() => {
|
||||
// FIXME: handle the error
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingPeerCashout(false)
|
||||
})
|
||||
|
||||
setIsLoadingPeerCheque(true)
|
||||
beeDebugApi.chequebook
|
||||
.getPeerLastCheques(peerId)
|
||||
.then(res => {
|
||||
setPeerCheque(res)
|
||||
})
|
||||
.catch(() => {
|
||||
// FIXME: handle the error
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingPeerCheque(false)
|
||||
})
|
||||
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button color="primary" onClick={() => handleClickOpen(props.peerId)}>
|
||||
{truncStringPortion(props.peerId)}
|
||||
</Button>
|
||||
<Drawer anchor={'right'} open={open} onClose={handleClose}>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Typography variant="h5" gutterBottom style={{ display: 'flex' }}>
|
||||
<span>Peer: {truncStringPortion(props.peerId)}</span>
|
||||
<ClipboardCopy value={props.peerId} />
|
||||
</Typography>
|
||||
<Paper>
|
||||
{isLoadingPeerCashout || isLoadingPeerCheque ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<div style={{ textAlign: 'left', padding: '10px' }}>
|
||||
<h3>Last Cheque</h3>
|
||||
<Grid container spacing={1}>
|
||||
<Grid key={1} item xs={12} sm={12} xl={6}>
|
||||
<h5>Last Sent</h5>
|
||||
<p>
|
||||
Payout:
|
||||
<span style={{ marginBottom: '0px', fontFamily: 'monospace, monospace' }}>
|
||||
{' '}
|
||||
{peerCheque?.lastsent?.payout ? fromBZZbaseUnit(peerCheque?.lastsent?.payout) : '-'}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Beneficiary:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCheque?.lastsent?.beneficiary} />
|
||||
</p>
|
||||
<p>
|
||||
Chequebook:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCheque?.lastsent?.chequebook} />
|
||||
</p>
|
||||
</Grid>
|
||||
<Grid key={1} item xs={12} sm={12} xl={6}>
|
||||
<h5>Last Received</h5>
|
||||
<p>
|
||||
Payout:
|
||||
<span style={{ marginBottom: '0px', fontFamily: 'monospace, monospace' }}>
|
||||
{' '}
|
||||
{peerCheque?.lastreceived?.payout ? fromBZZbaseUnit(peerCheque?.lastreceived?.payout) : '-'}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Beneficiary:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCheque?.lastreceived?.beneficiary} />
|
||||
</p>
|
||||
<p>
|
||||
Chequebook:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCheque?.lastreceived?.chequebook} />
|
||||
</p>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<h3>Last Cashout</h3>
|
||||
{peerCashout && peerCashout?.cumulativePayout > 0 ? (
|
||||
<div>
|
||||
<p>
|
||||
Cumulative Payout:
|
||||
<span style={{ marginBottom: '0px', fontFamily: 'monospace, monospace' }}>
|
||||
{peerCashout?.cumulativePayout ? fromBZZbaseUnit(peerCashout?.cumulativePayout) : '-'}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Last Payout:
|
||||
<span style={{ marginBottom: '0px', fontFamily: 'monospace, monospace' }}>
|
||||
{' '}
|
||||
{peerCashout?.result.lastPayout ? fromBZZbaseUnit(peerCashout?.result.lastPayout) : '-'}
|
||||
</span>
|
||||
<span> {peerCashout?.result.bounced ? 'Bounced' : ''}</span>
|
||||
</p>
|
||||
<p>
|
||||
Beneficiary:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCashout?.beneficiary} />
|
||||
</p>
|
||||
<p>
|
||||
Chequebook:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCashout?.chequebook} />
|
||||
</p>
|
||||
<p>
|
||||
Recipient:
|
||||
<EthereumAddress network={'goerli'} hideBlockie address={peerCashout?.result.recipient} />
|
||||
</p>
|
||||
<p>
|
||||
Transaction:
|
||||
<EthereumAddress
|
||||
transaction
|
||||
network={'goerli'}
|
||||
hideBlockie
|
||||
address={peerCashout?.transactionHash}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ReactElement } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
TableHead,
|
||||
Paper,
|
||||
Container,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core'
|
||||
|
||||
import { fromBZZbaseUnit } from '../../utils'
|
||||
|
||||
import type { AllSettlements, Settlements } from '@ethersphere/bee-js'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
nodeSettlements: AllSettlements | null
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
function SettlementsTable(props: Props): ReactElement {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.loading ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table className={classes.table} size="small" aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Peer</TableCell>
|
||||
<TableCell>Received (BZZ)</TableCell>
|
||||
<TableCell>Sent (BZZ)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.nodeSettlements?.settlements.map((item: Settlements) => (
|
||||
<TableRow key={item.peer}>
|
||||
<TableCell>{item.peer}</TableCell>
|
||||
<TableCell style={{ fontFamily: 'monospace, monospace' }}>
|
||||
{item.received > 0 ? fromBZZbaseUnit(item.received).toFixed(7).toLocaleString() : item.received}
|
||||
</TableCell>
|
||||
<TableCell style={{ fontFamily: 'monospace, monospace' }}>
|
||||
{item.sent > 0 ? fromBZZbaseUnit(item.sent).toFixed(7).toLocaleString() : item.sent}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettlementsTable
|
||||
+30
-138
@@ -1,11 +1,9 @@
|
||||
import { ReactElement, useState, ChangeEvent, ReactChild } from 'react'
|
||||
import { withStyles, Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Tabs, Tab, Box, Typography, Container, CircularProgress } from '@material-ui/core'
|
||||
import type { ReactElement } from 'react'
|
||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import { Container, CircularProgress } from '@material-ui/core'
|
||||
|
||||
import AccountCard from '../accounting/AccountCard'
|
||||
import BalancesTable from './BalancesTable'
|
||||
import ChequebookTable from './ChequebookTable'
|
||||
import SettlementsTable from './SettlementsTable'
|
||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||
|
||||
@@ -13,25 +11,10 @@ import {
|
||||
useApiNodeAddresses,
|
||||
useApiChequebookAddress,
|
||||
useApiChequebookBalance,
|
||||
useApiPeerBalances,
|
||||
useApiPeerCheques,
|
||||
useApiSettlements,
|
||||
useApiHealth,
|
||||
useDebugApiHealth,
|
||||
} from '../../hooks/apiHooks'
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: ReactChild
|
||||
index: number
|
||||
value: number
|
||||
}
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
}
|
||||
}
|
||||
import { useAccounting } from '../../hooks/accounting'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -44,137 +27,46 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
)
|
||||
|
||||
export default function Accounting(): ReactElement {
|
||||
const [value, setValue] = useState(0)
|
||||
const classes = useStyles()
|
||||
|
||||
const handleChange = (event: ChangeEvent<unknown>, newValue: number) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress()
|
||||
const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance()
|
||||
const { peerBalances, isLoadingPeerBalances } = useApiPeerBalances()
|
||||
const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses()
|
||||
const { health, isLoadingHealth } = useApiHealth()
|
||||
const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth()
|
||||
const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting()
|
||||
|
||||
const { peerCheques, isLoadingPeerCheques } = useApiPeerCheques()
|
||||
const { settlements, isLoadingSettlements } = useApiSettlements()
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props
|
||||
|
||||
if (isLoadingHealth || isLoadingNodeHealth) {
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box style={{ marginTop: '20px' }}>
|
||||
<Typography component="div">{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const AntTabs = withStyles({
|
||||
root: {
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
},
|
||||
indicator: {
|
||||
backgroundColor: '#3f51b5',
|
||||
},
|
||||
})(Tabs)
|
||||
|
||||
interface StyledTabProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
const AntTab = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
minWidth: 72,
|
||||
backgroundColor: 'transparent',
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
marginRight: theme.spacing(4),
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
'&:hover': {
|
||||
color: '#3f51b5',
|
||||
opacity: 1,
|
||||
},
|
||||
'&$selected': {
|
||||
color: '#3f51b5',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
'&:focus': {
|
||||
color: '#3f51b5',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)((props: StyledTabProps) => <Tab disableRipple {...props} />)
|
||||
if (nodeHealth?.status !== 'ok' || !health) return <TroubleshootConnectionCard />
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
// FIXME: this should be broken up
|
||||
/* eslint-disable no-nested-ternary */
|
||||
nodeHealth?.status === 'ok' && health ? (
|
||||
<div className={classes.root}>
|
||||
<AccountCard
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoadingChequebookAddress={isLoadingChequebookAddress}
|
||||
chequebookBalance={chequebookBalance}
|
||||
isLoadingChequebookBalance={isLoadingChequebookBalance}
|
||||
settlements={settlements}
|
||||
isLoadingSettlements={isLoadingSettlements}
|
||||
/>
|
||||
<EthereumAddressCard
|
||||
nodeAddresses={nodeAddresses}
|
||||
isLoadingNodeAddresses={isLoadingNodeAddresses}
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoadingChequebookAddress={isLoadingChequebookAddress}
|
||||
/>
|
||||
<div>
|
||||
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
|
||||
<AntTab label="Balances" {...a11yProps(0)} />
|
||||
<AntTab label="Chequebook" {...a11yProps(1)} />
|
||||
<AntTab label="Settlements" {...a11yProps(2)} />
|
||||
</AntTabs>
|
||||
<TabPanel value={value} index={0}>
|
||||
<BalancesTable peerBalances={peerBalances} loading={isLoadingPeerBalances} />
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={1}>
|
||||
<ChequebookTable peerCheques={peerCheques} loading={isLoadingPeerCheques} />
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={2}>
|
||||
<SettlementsTable nodeSettlements={settlements} loading={isLoadingSettlements} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
</div>
|
||||
) : isLoadingHealth || isLoadingNodeHealth ? (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<CircularProgress />
|
||||
</Container>
|
||||
) : (
|
||||
<TroubleshootConnectionCard />
|
||||
) /* eslint-enable no-nested-ternary */
|
||||
}
|
||||
<div className={classes.root}>
|
||||
<AccountCard
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoading={isLoadingChequebookAddress || isLoading || isLoadingChequebookBalance}
|
||||
chequebookBalance={chequebookBalance}
|
||||
totalsent={totalsent}
|
||||
totalreceived={totalreceived}
|
||||
/>
|
||||
<EthereumAddressCard
|
||||
nodeAddresses={nodeAddresses}
|
||||
isLoadingNodeAddresses={isLoadingNodeAddresses}
|
||||
chequebookAddress={chequebookAddress}
|
||||
isLoadingChequebookAddress={isLoadingChequebookAddress}
|
||||
/>
|
||||
{error && (
|
||||
<Container style={{ textAlign: 'center', padding: '50px' }}>
|
||||
Error loading accountin details: {error.message}
|
||||
</Container>
|
||||
)}
|
||||
{!error && <BalancesTable accounting={accounting} isLoadingUncashed={isLoadingUncashed} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -21,7 +21,17 @@ interface StatusEthereumConnectionHook extends StatusHookCommon {
|
||||
interface StatusTopologyHook extends StatusHookCommon {
|
||||
topology: Topology | null
|
||||
}
|
||||
|
||||
interface StatusChequebookHook extends StatusHookCommon {
|
||||
chequebookBalance: ChequebookBalanceResponse | null
|
||||
chequebookAddress: ChequebookAddressResponse | null
|
||||
}
|
||||
|
||||
interface Accounting {
|
||||
peer: string
|
||||
uncashedAmount: number
|
||||
balance: number
|
||||
received: number
|
||||
sent: number
|
||||
total: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user