diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 465cd04dd5..ad4df082ae 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -16,12 +16,13 @@ import { import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { visuallyHidden } from '@mui/utils'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import { DelegationWithEverything } from '@nymproject/types'; +import { DelegationEvent, DelegationWithEverything } from '@nymproject/types'; import { Link } from '@nymproject/react/link/Link'; import { format, formatDistanceToNow, parseISO } from 'date-fns'; import { styled } from '@mui/material/styles'; import { tableCellClasses } from '@mui/material/TableCell'; import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; +import { DelegationWithEvent, isPendingDelegation, TDelegations } from '../../context/delegations'; const StyledTooltipTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -71,13 +72,30 @@ function descendingComparator(a: T, b: T, orderBy: keyof T) { return 0; } +function sortPendingDelegation(a: DelegationWithEvent, b: DelegationWithEvent) { + if (isPendingDelegation(a) && isPendingDelegation(b)) return 0; + if (isPendingDelegation(b)) return -1; + if (isPendingDelegation(a)) return 1; + return 2; +} + function getComparator( order: Order, orderBy: Key, -): (a: DelegationWithEverything, b: DelegationWithEverything) => number { +): (a: DelegationWithEvent, b: DelegationWithEvent) => number { return order === 'desc' - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); + ? (a, b) => { + const pendingSort = sortPendingDelegation(a, b); + if (pendingSort === 2) + return descendingComparator(a as DelegationWithEverything, b as DelegationWithEverything, orderBy); + return pendingSort; + } + : (a, b) => { + const pendingSort = -sortPendingDelegation(a, b); + if (pendingSort === 2) + return -descendingComparator(a as DelegationWithEverything, b as DelegationWithEverything, orderBy); + return pendingSort; + }; } const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { @@ -119,7 +137,7 @@ const EnhancedTableHead: React.FC = ({ order, orderBy, onReq export const DelegationList: React.FC<{ isLoading?: boolean; - items?: DelegationWithEverything[]; + items?: TDelegations; onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; explorerUrl: string; }> = ({ isLoading, items, onItemActionClick, explorerUrl }) => { @@ -132,6 +150,15 @@ export const DelegationList: React.FC<{ setOrderBy(property); }; + const getRewardValue = (item: DelegationWithEvent) => { + if (isPendingDelegation(item)) { + return ''; + } + // eslint-disable-next-line @typescript-eslint/naming-convention + const { accumulated_rewards } = item; + return !accumulated_rewards ? '-' : `${accumulated_rewards.amount} ${accumulated_rewards.denom}`; + }; + return ( @@ -149,12 +176,19 @@ export const DelegationList: React.FC<{ noIcon /> - {!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`} - {!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`} - {!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`} + {!isPendingDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)} + + + {!isPendingDelegation(item) && (!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`)} + + + {!isPendingDelegation(item) && + (!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`)} + + + {!isPendingDelegation(item) && format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} - {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} - {item.history.map((historyItem) => ( - - - {formatDistanceToNow(parseISO(historyItem.delegated_on_iso_datetime), { - addSuffix: true, - })} - - - - {`${historyItem.amount.amount} ${historyItem.amount.denom}`} - {historyItem.uses_vesting_contract_tokens && ( - - )} - - - {historyItem.block_height} - - ))} + {!isPendingDelegation(item) && + item.history.map((historyItem) => ( + + + {formatDistanceToNow(parseISO(historyItem.delegated_on_iso_datetime), { + addSuffix: true, + })} + + + + {`${historyItem.amount.amount} ${historyItem.amount.denom}`} + {historyItem.uses_vesting_contract_tokens && ( + + )} + + + {historyItem.block_height} + + ))}
} arrow > - {`${item.amount.amount} ${item.amount.denom}`} + + {!isPendingDelegation(item) && `${item.amount.amount} ${item.amount.denom}`} + - - {!item.accumulated_rewards - ? '-' - : `${item.accumulated_rewards.amount} ${item.accumulated_rewards.denom}`} - + {getRewardValue(item)} - {!item.pending_events.length ? ( + {!isPendingDelegation(item) && !item.pending_events.length && ( (onItemActionClick ? onItemActionClick(item, action) : undefined)} disableRedeemingRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'} disableCompoundRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'} /> - ) : ( + )} + {!isPendingDelegation(item) && item.pending_events.length > 0 && ( - + + + )} + {isPendingDelegation(item) && ( + + )} diff --git a/nym-wallet/src/components/Delegation/PendingEvents.tsx b/nym-wallet/src/components/Delegation/PendingEvents.tsx deleted file mode 100644 index ac3afa1be5..0000000000 --- a/nym-wallet/src/components/Delegation/PendingEvents.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { FC } from 'react'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { - Box, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableSortLabel, - Tooltip, - Typography, -} from '@mui/material'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { DelegationEvent } from '@nymproject/types'; -import { ArrowDropDown } from '@mui/icons-material'; -import { visuallyHidden } from '@mui/utils'; -import { Link } from '@nymproject/react/link/Link'; - -type Order = 'asc' | 'desc'; - -interface HeadCell { - id: keyof DelegationEvent; - label: string; - sortable: boolean; - disablePadding?: boolean; -} - -interface EnhancedTableProps { - onRequestSort: (event: React.MouseEvent, property: keyof DelegationEvent) => void; - order: Order; - orderBy: string; -} - -const headCells: HeadCell[] = [ - { id: 'node_identity', label: 'Node ID', sortable: true }, - { id: 'amount', label: 'Delegation', sortable: true }, - { id: 'kind', label: 'Type', sortable: true }, -]; - -function descendingComparator(a: T, b: T, orderBy: keyof T) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function getComparator( - order: Order, - orderBy: Key, -): (a: DelegationEvent, b: DelegationEvent) => number { - return order === 'desc' - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - -const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { - const createSortHandler = (property: keyof DelegationEvent) => (event: React.MouseEvent) => { - onRequestSort(event, property); - }; - - return ( - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === 'desc' ? 'sorted descending' : 'sorted ascending'} - - ) : null} - - - ))} - - - ); -}; - -export const PendingEvents: FC<{ pendingEvents: DelegationEvent[]; explorerUrl: string }> = ({ - pendingEvents, - explorerUrl, -}) => { - const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('node_identity'); - - const handleRequestSort = (event: React.MouseEvent, property: keyof DelegationEvent) => { - const isAsc = orderBy === property && order === 'asc'; - setOrder(isAsc ? 'desc' : 'asc'); - setOrderBy(property); - }; - - if (pendingEvents.length === 0) return No pending events; - - return ( - - - - - {pendingEvents.sort(getComparator(order, orderBy)).map((item) => ( - - - - Copy identity key {item.node_identity} to clipboard - - } - /> - - Click to view {item.node_identity} in the Network Explorer - - } - placement="right" - arrow - > - - - - {!item.amount ? '-' : `${item.amount?.amount} ${item.amount?.denom.toUpperCase()}`} - - {item.kind === 'Delegate' ? 'Delegation' : 'Undelegation'} - {item.proxy && ( - - - - )} - - - ))} - -
-
- ); -}; diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx index 476624beba..7f54dba56b 100644 --- a/nym-wallet/src/context/delegations.tsx +++ b/nym-wallet/src/context/delegations.tsx @@ -14,7 +14,7 @@ import { TPoolOption } from 'src/components'; export type TDelegationContext = { isLoading: boolean; error?: string; - delegations?: DelegationWithEverything[]; + delegations?: TDelegations; pendingDelegations?: DelegationEvent[]; totalDelegations?: string; totalRewards?: string; @@ -35,6 +35,12 @@ export type TDelegationTransaction = { transactionUrl: string; }; +export type DelegationWithEvent = DelegationWithEverything | DelegationEvent; +export type TDelegations = DelegationWithEvent[]; + +export const isPendingDelegation = (delegation: DelegationWithEvent): delegation is DelegationEvent => + 'kind' in delegation; + export const DelegationContext = createContext({ isLoading: true, refresh: async () => undefined, @@ -51,7 +57,7 @@ export const DelegationContextProvider: FC<{ }> = ({ network, children }) => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(); - const [delegations, setDelegations] = useState(); + const [delegations, setDelegations] = useState(); const [totalDelegations, setTotalDelegations] = useState(); const [totalRewards, setTotalRewards] = useState(); const [pendingDelegations, setPendingDelegations] = useState(); @@ -87,8 +93,13 @@ export const DelegationContextProvider: FC<{ const data = await getDelegationSummary(); const pending = await getAllPendingDelegations(); + const pendingOnNewNodes = pending.filter((event) => { + const some = data.delegations.some(({ node_identity }) => node_identity === event.node_identity); + return !some; + }); + setPendingDelegations(pending); - setDelegations(data.delegations); + setDelegations([...data.delegations, ...pendingOnNewNodes]); setTotalDelegations(`${data.total_delegations.amount} ${data.total_delegations.denom}`); setTotalRewards(`${data.total_rewards.amount} ${data.total_rewards.denom}`); } catch (e) { diff --git a/nym-wallet/src/context/mocks/rewards.tsx b/nym-wallet/src/context/mocks/rewards.tsx index a71ee958ea..d8a7d9f531 100644 --- a/nym-wallet/src/context/mocks/rewards.tsx +++ b/nym-wallet/src/context/mocks/rewards.tsx @@ -1,5 +1,5 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { TransactionExecuteResult } from '@nymproject/types'; +import { DelegationWithEverything, TransactionExecuteResult } from '@nymproject/types'; import { RewardsContext, TRewardsTransaction } from '../rewards'; import { useDelegationContext } from '../delegations'; import { mockSleep } from './utils'; @@ -9,7 +9,7 @@ export const MockRewardsContextProvider: FC = ({ children }) => { const [error, setError] = useState(); const [totalRewards, setTotalRewards] = useState(); const { delegations } = useDelegationContext(); - const delegationsHash = delegations?.map((d) => d.accumulated_rewards).join(','); + const delegationsHash = delegations?.map((d) => (d as DelegationWithEverything).accumulated_rewards).join(','); const resetState = () => { setIsLoading(true); @@ -19,7 +19,7 @@ export const MockRewardsContextProvider: FC = ({ children }) => { const recalculate = () => { const sum: number | undefined = delegations - ?.map((d) => (d.accumulated_rewards ? Number(10) : Number(0))) + ?.map((d) => ((d as DelegationWithEverything).accumulated_rewards ? Number(10) : Number(0))) .reduce((acc, cur) => acc + cur, Number(0)); setTotalRewards(sum ? `${sum} NYM` : undefined); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 6c78b9d773..15cbdbe75f 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -5,7 +5,6 @@ import { DelegationWithEverything, FeeDetails, DecCoin } from '@nymproject/types import { Link } from '@nymproject/react/link/Link'; import { AppContext, urls } from 'src/context/main'; import { DelegationList } from 'src/components/Delegation/DelegationList'; -import { PendingEvents } from 'src/components/Delegation/PendingEvents'; import { TPoolOption } from 'src/components'; import { Console } from 'src/utils/console'; import { CompoundModal } from 'src/components/Rewards/CompoundModal'; @@ -49,7 +48,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const { delegations, - pendingDelegations, totalDelegations, totalRewards, isLoading, @@ -322,15 +320,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { - {pendingDelegations && ( - - - Pending Delegation Events - - - - )} - {showNewDelegationModal && (