diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index 90dc77ba85..1007e86328 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -316,7 +316,6 @@ "Peers": "Peers", "Pending": "Pending", "Pending...": "Pending...", - "Percentage of successfully forged blocks in relation to all blocks (forged and missed).": "Percentage of successfully forged blocks in relation to all blocks (forged and missed).", "Perfect! Almost done": "Perfect! Almost done", "Performance": "Performance", "Personalize each transaction with a custom message. Look up its value in a fiat currency of your choice.": "Personalize each transaction with a custom message. Look up its value in a fiat currency of your choice.", diff --git a/src/app/type.css b/src/app/type.css index 1ac5908bf1..2ce1407b84 100644 --- a/src/app/type.css +++ b/src/app/type.css @@ -149,6 +149,16 @@ small { text-align: center; } +:global .text-left { + text-align: left; + justify-content: flex-start; +} + +:global .text-right { + text-align: right; + justify-content: flex-end; +} + input, textarea, select { diff --git a/src/components/screens/monitor/delegates/delegates.css b/src/components/screens/monitor/delegates/delegates.css index d215e476cb..b6a26c474a 100644 --- a/src/components/screens/monitor/delegates/delegates.css +++ b/src/components/screens/monitor/delegates/delegates.css @@ -227,7 +227,7 @@ background-color: var(--color-yellow-transparent); } - &.non-eligible { + &.nonEligible { color: var(--color-blue-gray); background-color: var(--color-blue-manatee-transparent); } diff --git a/src/components/screens/monitor/delegates/delegates.js b/src/components/screens/monitor/delegates/delegates.js index ec9d6c7d95..c62a7982a3 100644 --- a/src/components/screens/monitor/delegates/delegates.js +++ b/src/components/screens/monitor/delegates/delegates.js @@ -1,17 +1,16 @@ import React, { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { withTranslation } from 'react-i18next'; import { Input } from '@toolbox/inputs'; import Box from '@toolbox/box'; import BoxHeader from '@toolbox/box/header'; import BoxContent from '@toolbox/box/content'; import BoxTabs from '@toolbox/tabs'; +import { ROUND_LENGTH } from '@constants'; import styles from './delegates.css'; -import Overview from './overview'; +import DelegatesOverview from './overview/delegatesOverview'; +import ForgingDetails from './overview/forgingDetails'; import LatestVotes from './latestVotes'; import DelegatesTable from './delegatesTable'; -import ForgingDetails from './forgingDetails'; // eslint-disable-next-line max-statements const DelegatesMonitor = ({ @@ -25,25 +24,15 @@ const DelegatesMonitor = ({ standByDelegates, networkStatus, applyFilters, - delegates, filters, + blocks, votes, t, }) => { const [activeTab, setActiveTab] = useState('active'); - const { forgingTimes, total } = useSelector(state => state.blocks); - const delegatesWithForgingTimes = { - ...delegates, - data: delegates.data.map( - data => ({ ...data, forgingTime: forgingTimes[data.publicKey] }), - ), - }; - const watchedDelegatesWithForgingTimes = { - ...watchedDelegates, - data: watchedDelegates.data.map( - data => ({ ...data, forgingTime: forgingTimes[data.publicKey] }), - ), - }; + const { total, forgers, latestBlocks } = blocks; + const delegatesWithForgingTimes = { data: forgers }; + const forgedInRound = latestBlocks.length ? latestBlocks[0].height % ROUND_LENGTH : 0; useEffect(() => { const addressList = votes.data && votes.data.reduce((acc, data) => { @@ -109,7 +98,7 @@ const DelegatesMonitor = ({ return (
- - + {tabs.tabs.length === 1 ?

{tabs.tabs[0].name}

@@ -145,7 +136,7 @@ const DelegatesMonitor = ({ setActiveTab={setActiveTab} delegates={delegatesWithForgingTimes} watchList={watchList} - watchedDelegates={watchedDelegatesWithForgingTimes} + watchedDelegates={watchedDelegates} standByDelegates={standByDelegates} sanctionedDelegates={sanctionedDelegates} filters={filters} @@ -160,4 +151,4 @@ const DelegatesMonitor = ({ ); }; -export default withTranslation()(DelegatesMonitor); +export default DelegatesMonitor; diff --git a/src/components/screens/monitor/delegates/delegates.test.js b/src/components/screens/monitor/delegates/delegates.test.js index c6b550d12b..6f270e21b5 100644 --- a/src/components/screens/monitor/delegates/delegates.test.js +++ b/src/components/screens/monitor/delegates/delegates.test.js @@ -46,16 +46,12 @@ describe('Delegates monitor page', () => { }); }; + const { blocks } = store.getState(); + beforeEach(() => { props = { t: key => key, - delegates: { - isLoading: true, - data: activeDelegates, - loadData: jest.fn(), - clearData: jest.fn(), - urlSearchParams: {}, - }, + blocks, standByDelegates: { isLoading: true, data: [], @@ -145,6 +141,6 @@ describe('Delegates monitor page', () => { it('renders the forging status', () => { wrapper = setup(props); - expect(wrapper.find('a.delegate-row')).toHaveLength(delegatesList.length + 1); + expect(wrapper.find('a.delegate-row')).toHaveLength(blocks.forgers.length); }); }); diff --git a/src/components/screens/monitor/delegates/delegatesTable/dataColumns.js b/src/components/screens/monitor/delegates/delegatesTable/dataColumns.js new file mode 100644 index 0000000000..796b1d330a --- /dev/null +++ b/src/components/screens/monitor/delegates/delegatesTable/dataColumns.js @@ -0,0 +1,145 @@ +import React from 'react'; +import grid from 'flexboxgrid/dist/flexboxgrid.css'; + +import { formatAmountBasedOnLocale } from '@utils/formattedNumber'; +import { fromRawLsk } from '@utils/lsk'; +import { truncateAddress } from '@utils/account'; +import Tooltip from '@toolbox/tooltip/tooltip'; +import Icon from '@toolbox/icon'; +import AccountVisual from '@toolbox/accountVisual'; +import { + getStatusClass, + getDelegateWeightClass, + getRoundStateClass, + getForgingTimeClass, +} from './tableHeader'; +import styles from '../delegates.css'; + +const roundStatus = { + forging: 'Forging', + awaitingSlot: 'Awaiting slot', + missedBlock: 'Missed block', +}; + +const icons = { + forging: 'delegateForged', + awaitingSlot: 'delegateAwaiting', + missedBlock: 'delegateMissed', +}; + +const delegateStatus = { + active: 'Active', + standby: 'Standby', + banned: 'Banned', + punished: 'Punished', + nonEligible: 'Non-eligible to forge', +}; + +export const DelegateWeight = ({ value, activeTab }) => { + const formatted = formatAmountBasedOnLocale({ + value: fromRawLsk(value), + format: '0a', + }); + + return ( + + {formatted} + + ); +}; + +export const DelegateDetails = ({ + t, watched = false, data, activeTab, removeFromWatchList, addToWatchList, +}) => { + const showEyeIcon = activeTab === 'active' || activeTab === 'standby' || activeTab === 'watched'; + return ( + +
+ + + + )} + > +

+ {watched ? t('Remove from watched') : t('Add to watched')} +

+
+ +
+ +
+

+ {data.username} +

+

{truncateAddress(data.address)}

+
+
+
+
+ ); +}; + +export const RoundStatus = ({ + activeTab, data, t, time, +}) => ( + + + )} + footer={( +

{data.status === 'missedBlock' ? '-' : time}

+ )} + > +

+ {data.lastBlock && `Last block forged ${data.lastBlock}`} +

+
+ {data.isBanned && ( + } + footer={( +

{time}

+ )} + > +

+ {t('This delegate will be punished in upcoming rounds')} +

+
+ )} +
+); + +export const DelegateStatus = ({ activeTab, data }) => { + const status = data.totalVotesReceived < 1e11 ? 'nonEligible' : data.status; + return ( + + {delegateStatus[status]} + + ); +}; + +export const ForgingTime = ({ activeTab, time, status }) => ( + + {status === 'missedBlock' ? '-' : time} + +); diff --git a/src/components/screens/monitor/delegates/delegatesTable/delegateRow.js b/src/components/screens/monitor/delegates/delegatesTable/delegateRow.js index fe340b025c..81faca8bfe 100644 --- a/src/components/screens/monitor/delegates/delegatesTable/delegateRow.js +++ b/src/components/screens/monitor/delegates/delegatesTable/delegateRow.js @@ -1,156 +1,35 @@ -/* eslint-disable no-nested-ternary */ import React from 'react'; import { Link } from 'react-router-dom'; -import grid from 'flexboxgrid/dist/flexboxgrid.css'; import { useDispatch } from 'react-redux'; import { routes } from '@constants'; -import { formatAmountBasedOnLocale } from '@utils/formattedNumber'; -import { truncateAddress } from '@utils/account'; import { addedToWatchList, removedFromWatchList } from '@actions'; -import Tooltip from '@toolbox/tooltip/tooltip'; -import Icon from '@toolbox/icon'; -import AccountVisual from '@toolbox/accountVisual'; import styles from '../delegates.css'; -import DelegateWeight from './delegateWeight'; +import { + DelegateWeight, + DelegateDetails, + RoundStatus, + DelegateStatus, + ForgingTime, +} from './dataColumns'; -const roundStatus = { - forging: 'Forging', - awaitingSlot: 'Awaiting slot', - missedBlock: 'Missed block', -}; - -const icons = { - forging: 'delegateForged', - awaitingSlot: 'delegateAwaiting', - missedBlock: 'delegateMissed', -}; - -const delegateStatus = { - active: 'Active', - standby: 'Standby', - banned: 'Banned', - punished: 'Punished', - 'non-eligible': 'Non-eligible to forge', -}; - -const getForgingTime = (data) => { - if (!data || data.time === undefined) return '-'; - if (data.time === 0) return 'now'; - const { time } = data; - const absTime = Math.abs(time); +const getForgingTime = (time) => { + if (!time) return '-'; + const diff = time - Math.floor((new Date()).getTime() / 1000); + if (Math.abs(diff) < 9) return 'now'; + const absTime = Math.abs(diff); const minutes = absTime / 60 >= 1 ? `${Math.floor(absTime / 60)}m ` : ''; const seconds = absTime % 60 >= 1 ? `${absTime % 60}s` : ''; - if (data.time > 0) { + if (diff > 0) { return `in ${minutes}${seconds}`; } return `${minutes}${seconds} ago`; }; -// eslint-disable-next-line complexity -const DelegateDetails = ({ - t, watched = false, data, activeTab, removeFromWatchList, addToWatchList, -}) => { - const showEyeIcon = activeTab === 'active' || activeTab === 'standby' || activeTab === 'watched'; - return ( -
- - - - )} - > -

- {watched ? t('Remove from watched') : t('Add to watched')} -

-
- -
- -
-

- {data.username} -

-

{truncateAddress(data.address)}

-
-
-
- ); -}; - -const RoundStatus = ({ data, t, formattedForgingTime }) => ( - <> - - )} - footer={( -

{formattedForgingTime}

- )} - > -

- {data.lastBlock && `Last block forged ${data.lastBlock}`} -

-
- {data.isBanned && ( - } - footer={( -

{formattedForgingTime}

- )} - > -

- {t('This delegate will be punished in upcoming rounds')} -

-
- )} - -); - -const DelegateStatus = ({ activeTab, data }) => { - const status = data.totalVotesReceived < 100000000000 ? 'non-eligible' : data.status; - return ( - - {delegateStatus[status]} - - ); -}; - -// eslint-disable-next-line complexity const DelegateRow = ({ data, className, t, activeTab, watchList, setActiveTab, }) => { - const formattedForgingTime = data.forgingTime && getForgingTime(data.forgingTime); + const formattedForgingTime = getForgingTime(data.nextForgingTime); const dispatch = useDispatch(); const isWatched = watchList.find(address => address === data.address); @@ -175,56 +54,22 @@ const DelegateRow = ({ className={`${className} delegate-row ${styles.tableRow}`} to={`${routes.account.path}?address=${data.address}`} > - - - - - {`${formatAmountBasedOnLocale({ value: data.productivity })} %`} - - {/* - - {`#${data.rank}`} - */} - {activeTab !== 'sanctioned' && ( - - - - )} + + {(activeTab === 'active' || activeTab === 'watched') && ( <> - - {formattedForgingTime} - - - - + + )} - {(activeTab === 'watched' || activeTab !== 'active') && } + {(activeTab !== 'active') && } ); }; diff --git a/src/components/screens/monitor/delegates/delegatesTable/delegateWeight.js b/src/components/screens/monitor/delegates/delegatesTable/delegateWeight.js deleted file mode 100644 index b7b2fb8ee7..0000000000 --- a/src/components/screens/monitor/delegates/delegatesTable/delegateWeight.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { formatAmountBasedOnLocale } from '@utils/formattedNumber'; -import { fromRawLsk } from '@utils/lsk'; - -const DelegateWeight = ({ value }) => { - const formatted = formatAmountBasedOnLocale({ - value: fromRawLsk(value), - format: '0a', - }); - - return {formatted}; -}; - -export default DelegateWeight; diff --git a/src/components/screens/monitor/delegates/delegatesTable/index.js b/src/components/screens/monitor/delegates/delegatesTable/index.js index 4b4810f7bf..5f6600cf69 100644 --- a/src/components/screens/monitor/delegates/delegatesTable/index.js +++ b/src/components/screens/monitor/delegates/delegatesTable/index.js @@ -7,11 +7,8 @@ import header from './tableHeader'; const TableWrapper = compose( withLocalSort('delegates', 'forgingTime:asc', { - forgingTime: (a, b, direction) => { - if (!a.forgingTime || a.forgingTime.time === undefined) return 1; - if (!b.forgingTime || b.forgingTime.time === undefined) return -1; - return ((a.forgingTime.time > b.forgingTime.time) ? 1 : -1) * (direction === 'asc' ? 1 : -1); - }, + forgingTime: (a, b, direction) => + ((a.nextForgingTime > b.nextForgingTime) ? 1 : -1) * (direction === 'asc' ? 1 : -1), }), )(({ delegates, handleLoadMore, t, activeTab, diff --git a/src/components/screens/monitor/delegates/delegatesTable/tableHeader.js b/src/components/screens/monitor/delegates/delegatesTable/tableHeader.js index 87da64bc67..41429ea2ed 100644 --- a/src/components/screens/monitor/delegates/delegatesTable/tableHeader.js +++ b/src/components/screens/monitor/delegates/delegatesTable/tableHeader.js @@ -2,40 +2,61 @@ import grid from 'flexboxgrid/dist/flexboxgrid.css'; import styles from '../delegates.css'; +export const getStatusClass = (activeTab) => { + switch (activeTab) { + case 'active': + return 'hidden'; + case 'sanctioned': + return grid['col-xs-7']; + case 'watched': + return grid['col-xs-2']; + default: + return grid['col-xs-4']; + } +}; + +export const getDelegateWeightClass = (activeTab) => { + switch (activeTab) { + case 'sanctioned': + return 'hidden'; + case 'watched': + return `${grid['col-xs-2']} ${styles.voteWeight}`; + default: + return `${grid['col-xs-3']} ${styles.voteWeight}`; + } +}; + +export const getRoundStateClass = (activeTab) => { + switch (activeTab) { + case 'active': + return `${grid['col-xs-1']} ${styles.statusTitle} text-right ${styles.roundStateHeader}`; + case 'watched': + return `${grid['col-xs-1']} ${styles.statusTitle} ${styles.roundStateHeader}`; + default: + return 'hidden'; + } +}; + +export const getForgingTimeClass = (activeTab) => { + switch (activeTab) { + case 'active': + return grid['col-xs-3']; + case 'watched': + return grid['col-xs-2']; + default: + return 'hidden'; + } +}; + // eslint-disable-next-line complexity export default (activeTab, changeSort, t) => ([ { title: t('Delegate'), - classList: `${grid['col-xs-4']} ${styles.delegateHeader}`, - }, - { - title: t('Productivity'), - classList: activeTab === 'active' || activeTab === 'watched' - ? `${grid['col-xs-2']}` - : activeTab === 'sanctioned' ? `${grid['col-xs-4']}` - : `${grid['col-xs-3']}`, - tooltip: { - title: t('Productivity'), - message: t('Percentage of successfully forged blocks in relation to all blocks (forged and missed).'), - position: 'top', - }, - }, - /* - { - title: t('Rank'), - classList: activeTab === 'sanctioned' - ? `${grid['col-xs-3']}` - : activeTab === 'watched' ? `${grid['col-xs-1']}` - : `${grid['col-xs-2']}`, - sort: { - fn: changeSort, - key: 'rank', - }, + classList: `${grid['col-xs-5']} ${styles.delegateHeader}`, }, - */ { title: t('Delegate weight'), - classList: activeTab === 'sanctioned' ? 'hidden' : `${grid['col-xs-2']} ${styles.voteWeight}`, + classList: getDelegateWeightClass(activeTab), tooltip: { title: t('Delegate weight'), message: t('The total LSK voted to a delegate.'), @@ -44,10 +65,7 @@ export default (activeTab, changeSort, t) => ([ }, { title: t('Forging time'), - classList: activeTab === 'active' - ? `${grid['col-xs-3']}` - : activeTab === 'watched' ? `${grid['col-xs-2']}` - : 'hidden', + classList: getForgingTimeClass(activeTab), sort: { fn: changeSort, key: 'forgingTime', @@ -55,15 +73,10 @@ export default (activeTab, changeSort, t) => ([ }, { title: t('Round state'), - classList: activeTab === 'active' || activeTab === 'watched' - ? `${grid['col-xs-1']} ${styles.statusTitle} ${styles.roundStateHeader}` - : 'hidden', + classList: getRoundStateClass(activeTab), }, { title: t('Status'), - classList: activeTab === 'watched' - ? `${grid['col-xs-1']}` - : activeTab === 'sanctioned' ? `${grid['col-xs-4']}` - : activeTab !== 'active' ? `${grid['col-xs-3']}` : 'hidden', + classList: getStatusClass(activeTab), }, ]); diff --git a/src/components/screens/monitor/delegates/index.js b/src/components/screens/monitor/delegates/index.js index 6f719a3c61..61e9163f99 100644 --- a/src/components/screens/monitor/delegates/index.js +++ b/src/components/screens/monitor/delegates/index.js @@ -4,41 +4,48 @@ import { withRouter } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { getForgers, getDelegates } from '@api/delegate'; +import { getDelegates } from '@api/delegate'; import { getNetworkStatus } from '@api/network'; import { getTransactions, getRegisteredDelegates } from '@api/transaction'; import withData from '@utils/withData'; import withFilters from '@utils/withFilters'; -import { MODULE_ASSETS_NAME_ID_MAP, MAX_BLOCKS_FORGED, tokenMap } from '@constants'; - +import { MODULE_ASSETS_NAME_ID_MAP, tokenMap } from '@constants'; import Delegates from './delegates'; const defaultUrlSearchParams = { search: '' }; -const delegatesKey = 'delegates'; -const standByDelegatesKey = 'standByDelegates'; -const transformDelegatesResponse = (response, oldData = []) => ( - [...oldData, ...response.data.filter( - delegate => !oldData.find(({ username }) => username === delegate.username), +/** + * Merges two arrays and ensures there's no duplicated item + * + * @param {String} key - Key to find in array members to ensure uniqueness + * @param {Array} newData - The new data array + * @param {Array} oldData - The old data array + * @returns {Array} Array build by merging the given two arrays + */ +const mergeUniquely = (key, newData, oldData = []) => ( + [...oldData, ...newData.data.filter( + newItem => !oldData.find(oldItem => oldItem[key] === newItem[key]), )] ); - -const transformAccountsIsDelegateResponse = (response, oldData = []) => { +const mergeUniquelyByUsername = mergeUniquely.bind(this, 'username'); +const mergeUniquelyById = mergeUniquely.bind(this, 'id'); + +/** + * Strips down the account data to have a + * similar structure to getForgers used to + * retrieve in-round delegates + */ +const stripAccountDataAndMerge = (response, oldData = []) => { response.data = response.data.map(del => ({ address: del.summary.address, ...del.dpos.delegate, })); - return transformDelegatesResponse(response, oldData); + return mergeUniquelyByUsername(response, oldData); }; -const transformVotesResponse = (response, oldData = []) => ( - [...oldData, ...response.data.filter( - vote => !oldData.find(({ id }) => id === vote.id), - )] -); - const mapStateToProps = state => ({ watchList: state.watchList, + blocks: state.blocks, }); const ComposedDelegates = compose( @@ -46,16 +53,7 @@ const ComposedDelegates = compose( connect(mapStateToProps), withData( { - [delegatesKey]: { - apiUtil: (network, params) => getForgers( - { network, params: { ...params, limit: MAX_BLOCKS_FORGED } }, - ), - defaultData: [], - autoload: true, - transformResponse: transformDelegatesResponse, - }, - - [standByDelegatesKey]: { + standByDelegates: { apiUtil: (network, params) => getDelegates({ network, params: { @@ -66,7 +64,7 @@ const ComposedDelegates = compose( }), defaultData: [], autoload: true, - transformResponse: transformAccountsIsDelegateResponse, + transformResponse: stripAccountDataAndMerge, }, delegatesCount: { @@ -99,7 +97,7 @@ const ComposedDelegates = compose( getApiParams: state => ({ token: state.settings.token.active }), autoload: true, defaultData: [], - transformResponse: transformVotesResponse, + transformResponse: mergeUniquelyById, }, networkStatus: { @@ -113,7 +111,7 @@ const ComposedDelegates = compose( apiUtil: (network, params) => getDelegates({ network, params: { ...params, status: 'punished,banned' } }), defaultData: [], autoload: true, - transformResponse: transformAccountsIsDelegateResponse, + transformResponse: stripAccountDataAndMerge, }, votedDelegates: { @@ -121,7 +119,7 @@ const ComposedDelegates = compose( getDelegates({ network: networks.LSK, params }), defaultData: {}, transformResponse: (response) => { - const transformedResponse = transformDelegatesResponse(response); + const transformedResponse = mergeUniquelyByUsername(response); const responseMap = transformedResponse.reduce((acc, delegate) => { acc[delegate.address] = delegate.summary?.address; return acc; @@ -135,11 +133,11 @@ const ComposedDelegates = compose( getDelegates({ network: networks.LSK, params }), defaultData: [], getApiParams: state => ({ addressList: state.watchList }), - transformResponse: transformDelegatesResponse, + transformResponse: stripAccountDataAndMerge, }, }, ), - withFilters(standByDelegatesKey, defaultUrlSearchParams), + withFilters('standByDelegates', defaultUrlSearchParams), withTranslation(), )(Delegates); diff --git a/src/components/screens/monitor/delegates/overview.js b/src/components/screens/monitor/delegates/overview/delegatesOverview.js similarity index 96% rename from src/components/screens/monitor/delegates/overview.js rename to src/components/screens/monitor/delegates/overview/delegatesOverview.js index f3a4b8ffe6..8f29f05e58 100644 --- a/src/components/screens/monitor/delegates/overview.js +++ b/src/components/screens/monitor/delegates/overview/delegatesOverview.js @@ -2,7 +2,7 @@ import React from 'react'; import { fromRawLsk } from '@utils/lsk'; -import { colorPalette, MAX_BLOCKS_FORGED } from '@constants'; +import { colorPalette, ROUND_LENGTH } from '@constants'; import Box from '@toolbox/box'; import BoxHeader from '@toolbox/box/header'; import BoxContent from '@toolbox/box/content'; @@ -28,7 +28,7 @@ const Overview = ({ datasets: [ { label: 'delegates', - data: [Math.max(0, delegatesCount.data - MAX_BLOCKS_FORGED), MAX_BLOCKS_FORGED], + data: [Math.max(0, delegatesCount.data - ROUND_LENGTH), ROUND_LENGTH], }, ], }; diff --git a/src/components/screens/monitor/delegates/overview/forger.js b/src/components/screens/monitor/delegates/overview/forger.js new file mode 100644 index 0000000000..576e9ad4c5 --- /dev/null +++ b/src/components/screens/monitor/delegates/overview/forger.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { routes } from '@constants'; +import AccountVisual from '@toolbox/accountVisual'; +import styles from './overview.css'; + +const Forger = ({ forger }) => ( +
+ + + {forger.username} + +
+); + +export default Forger; diff --git a/src/components/screens/monitor/delegates/forgingDetails.js b/src/components/screens/monitor/delegates/overview/forgingDetails.js similarity index 75% rename from src/components/screens/monitor/delegates/forgingDetails.js rename to src/components/screens/monitor/delegates/overview/forgingDetails.js index 1a8d87178d..91818cd7de 100644 --- a/src/components/screens/monitor/delegates/forgingDetails.js +++ b/src/components/screens/monitor/delegates/overview/forgingDetails.js @@ -1,11 +1,8 @@ // istanbul ignore file import React from 'react'; -import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; import moment from 'moment'; -import { routes, colorPalette, MAX_BLOCKS_FORGED } from '@constants'; +import { colorPalette, ROUND_LENGTH } from '@constants'; import { DoughnutChart } from '@toolbox/charts'; -import AccountVisual from '@toolbox/accountVisual'; import Box from '@toolbox/box'; import BoxHeader from '@toolbox/box/header'; import BoxContent from '@toolbox/box/content'; @@ -13,6 +10,7 @@ import BoxEmptyState from '@toolbox/box/emptyState'; import GuideTooltip, { GuideTooltipItem } from '@toolbox/charts/guideTooltip'; import Icon from '@toolbox/icon'; import NumericInfo from './numericInfo'; +import Forger from './forger'; import styles from './overview.css'; const FORGERS_TO_SHOW = 6; @@ -24,58 +22,42 @@ const getForgingStats = (data, forgedInRound) => { awaitingSlot: 0, missedBlock: 0, }; - Object.values(data) - .forEach((item) => { - statuses[item.status]++; - }); + data.forEach((item) => { + statuses[item.status]++; + }); return Object.values(statuses); }; -const Forger = ({ forger }) => ( -
- - - {forger.username} - -
-); - const ProgressBar = ({ forgedInRound }) => (
-
+
); const formatToTwoDigits = str => str.toLocaleString('en-US', { minimumIntegerDigits: 2 }); -const getPassedMinutes = (lastBlock = {}, firstRoundBlock = {}) => { - const seconds = lastBlock.timestamp - firstRoundBlock.timestamp; +const getPassedMinutes = (startTime) => { + const seconds = Math.floor((new Date()).getTime() / 1000) - startTime; if (!seconds) return '00:00'; const duration = moment.duration({ seconds }); return `${formatToTwoDigits(duration.minutes())}:${formatToTwoDigits(duration.seconds())}`; }; const ForgingDetails = ({ - t, chartDelegatesForging, + t, forgers, forgedInRound, startTime, }) => { const delegatesForgedLabels = [ t('Forging'), t('Awaiting slot'), t('Missed block'), ]; - const { latestBlocks, awaitingForgers } = useSelector(state => state.blocks); - const forgedInRound = latestBlocks.length - ? latestBlocks[0].height % MAX_BLOCKS_FORGED : 0; const doughnutChartData = { labels: delegatesForgedLabels, datasets: [ { label: 'status', - data: getForgingStats(chartDelegatesForging, forgedInRound), + data: getForgingStats(forgers, forgedInRound), }, ], }; @@ -91,13 +73,8 @@ const ForgingDetails = ({ }, }; - const forgersListToShow = awaitingForgers.slice(forgedInRound, forgedInRound + FORGERS_TO_SHOW); + const forgersListToShow = forgers.slice(1, FORGERS_TO_SHOW + 1); - if (forgersListToShow.length < FORGERS_TO_SHOW) { - forgersListToShow.push( - ...awaitingForgers.slice(0, FORGERS_TO_SHOW - forgersListToShow.length), - ); - } return ( @@ -106,7 +83,7 @@ const ForgingDetails = ({
{ - Object.keys(chartDelegatesForging).length + forgers.length ? (

{t('Delegates Forging Status')}

@@ -153,13 +130,13 @@ const ForgingDetails = ({

{`${forgedInRound} / `} - {` ${MAX_BLOCKS_FORGED}`} + {` ${ROUND_LENGTH}`}

diff --git a/src/components/screens/monitor/delegates/numericInfo.js b/src/components/screens/monitor/delegates/overview/numericInfo.js similarity index 100% rename from src/components/screens/monitor/delegates/numericInfo.js rename to src/components/screens/monitor/delegates/overview/numericInfo.js diff --git a/src/components/screens/monitor/delegates/overview.css b/src/components/screens/monitor/delegates/overview/overview.css similarity index 98% rename from src/components/screens/monitor/delegates/overview.css rename to src/components/screens/monitor/delegates/overview/overview.css index adf8fdc414..13f3574f4d 100644 --- a/src/components/screens/monitor/delegates/overview.css +++ b/src/components/screens/monitor/delegates/overview/overview.css @@ -1,4 +1,4 @@ -@import '../../../../app/mixins.css'; +@import '../../../../../app/mixins.css'; .wrapper { & .content { diff --git a/src/components/screens/wallet/delegateProfile/delegateProfile.js b/src/components/screens/wallet/delegateProfile/delegateProfile.js index fbd982e055..4af921120b 100644 --- a/src/components/screens/wallet/delegateProfile/delegateProfile.js +++ b/src/components/screens/wallet/delegateProfile/delegateProfile.js @@ -24,8 +24,8 @@ import DelegateVotesView from './delegateVotesView'; const DelegateProfile = ({ delegate, account, t, voters, - // awaitingForgers, forgingTimes, lastBlockForged, + // forgers, }) => { const { data } = delegate; useEffect(() => { diff --git a/src/components/screens/wallet/delegateProfile/index.js b/src/components/screens/wallet/delegateProfile/index.js index f92a93ff32..6e3ce5174f 100644 --- a/src/components/screens/wallet/delegateProfile/index.js +++ b/src/components/screens/wallet/delegateProfile/index.js @@ -8,8 +8,7 @@ import { getBlocks } from '@api/block'; import DelegateProfile from './delegateProfile'; const mapStateToProps = state => ({ - awaitingForgers: state.blocks.awaitingForgers, - forgingTimes: state.blocks.forgingTimes, + forgers: state.blocks.forgers, }); const defaultVoters = { diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js index 85bb7ce7de..7534a5cac6 100644 --- a/src/constants/actionTypes.js +++ b/src/constants/actionTypes.js @@ -68,7 +68,7 @@ const actionTypes = { resetTransactionResult: 'RESET_TRANSACTION_RESULTS', broadcastedTransactionSuccess: 'BROADCAST_TRANSACTION_SUCCESS', broadcastedTransactionError: 'BROADCASTED_TRANSACTION_ERROR', - forgingTimesRetrieved: 'FORGING_TIME_RETRIEVED', + forgersRetrieved: 'FORGERS_RETRIEVED', appUpdateAvailable: 'APP_UPDATE_AVAILABLE', transactionsRetrieved: 'TRANSACTION_RETRIEVED', }; diff --git a/src/constants/delegates.js b/src/constants/delegates.js index 133e66a995..0708071760 100644 --- a/src/constants/delegates.js +++ b/src/constants/delegates.js @@ -1,2 +1,2 @@ // eslint-disable-next-line import/prefer-default-export -export const MAX_BLOCKS_FORGED = 103; +export const ROUND_LENGTH = 103; diff --git a/src/constants/index.js b/src/constants/index.js index 5d9258ad23..9062589b0c 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -15,7 +15,7 @@ export { default as account } from './account'; export { default as actionTypes } from './actionTypes'; export { chartStyles, colorPalette } from './chart'; export { firstBlockTime } from './datetime'; -export { MAX_BLOCKS_FORGED } from './delegates'; +export { ROUND_LENGTH } from './delegates'; export { default as externalLinks } from './externalLinks'; export { default as feedbackLinks } from './feedbackLinks'; export { default as routes, modals } from './routes'; diff --git a/src/store/actions/blocks.js b/src/store/actions/blocks.js index 858a8637a1..864b7a89dd 100644 --- a/src/store/actions/blocks.js +++ b/src/store/actions/blocks.js @@ -1,4 +1,4 @@ -import { actionTypes, MAX_BLOCKS_FORGED } from '@constants'; +import { actionTypes, ROUND_LENGTH } from '@constants'; import { convertUnixSecondsToLiskEpochSeconds } from '@utils/datetime'; import { getBlocks } from '@api/block'; import { getForgers } from '@api/delegate'; @@ -24,7 +24,6 @@ const loadLastBlocks = async (params, network) => { }; }; -// eslint-disable-next-line import/prefer-default-export export const olderBlocksRetrieved = () => async (dispatch, getState) => { const blocksFetchLimit = 100; const { network } = getState(); @@ -46,63 +45,42 @@ export const olderBlocksRetrieved = () => async (dispatch, getState) => { }); }; -const retrieveNextForgers = async (network) => { +/** + * Fire this action after network is set. + * It retrieves the list of forgers in the current + * round and determines their status as forging, missedBlock + * and awaitingSlot. + */ +export const forgersRetrieved = () => async (dispatch, getState) => { + const { network, blocks: { latestBlocks } } = getState(); + const forgedBlocksInRound = latestBlocks[0].height % ROUND_LENGTH; + const remainingBlocksInRound = ROUND_LENGTH - forgedBlocksInRound; const { data } = await getForgers({ network, - params: { limit: MAX_BLOCKS_FORGED }, + params: { limit: ROUND_LENGTH }, }); + let forgers = []; - if (data) { - return data; - } - - return []; -}; - -// eslint-disable-next-line max-statements -export const forgingTimesRetrieved = nextForgers => async (dispatch, getState) => { - const { network } = getState(); - const { latestBlocks } = getState().blocks; - const forgedInRoundNum = latestBlocks[0].height % MAX_BLOCKS_FORGED; - const awaitingForgers = nextForgers ?? await retrieveNextForgers(network); - const forgingTimes = {}; - const latestBlockTimestamp = latestBlocks[0].timestamp; + // Get the list of usernames that already forged in this round + const haveForgedInRound = latestBlocks + .filter((b, i) => forgedBlocksInRound >= i) + .map(b => b.generatorUsername); - // First we iterate the latest blocks and set the forging time - latestBlocks - .slice(0, forgedInRoundNum) - .forEach((item) => { - if (!forgingTimes[item.generatorPublicKey]) { - forgingTimes[item.generatorPublicKey] = { - time: -(latestBlockTimestamp - item.timestamp), - status: 'forging', - }; + // check previous blocks and define missed blocks + if (data) { + forgers = data.map((forger, index) => { + if (haveForgedInRound.indexOf(forger.username) > -1) { + return { ...forger, status: 'forging' }; } - }); - - // Now we set awaitingForgers as awaitingSlot if they are upfront - // of the current number and to missed block in case that they did - // not forge - awaitingForgers - .forEach((item, index) => { - if (index >= forgedInRoundNum) { - forgingTimes[item.publicKey] = { - time: item.nextForgingTime - latestBlockTimestamp, - status: 'awaitingSlot', - }; - } else if (!forgingTimes[item.publicKey]) { - forgingTimes[item.publicKey] = { - time: undefined, - status: 'missedBlock', - }; + if (index < remainingBlocksInRound) { + return { ...forger, status: 'awaitingSlot' }; } + return { ...forger, status: 'missedBlock' }; }); + } dispatch({ - type: actionTypes.forgingTimesRetrieved, - data: { - forgingTimes, - awaitingForgers, - }, + type: actionTypes.forgersRetrieved, + data: forgers, }); }; diff --git a/src/store/middlewares/block.js b/src/store/middlewares/block.js index 10d2197f57..6b5b67c506 100644 --- a/src/store/middlewares/block.js +++ b/src/store/middlewares/block.js @@ -1,7 +1,11 @@ import { blockSubscribe, blockUnsubscribe } from '@api/block'; -import { forgersSubscribe, forgersUnsubscribe, getForgers } from '@api/delegate'; +import { forgersSubscribe, forgersUnsubscribe } from '@api/delegate'; import { tokenMap, actionTypes } from '@constants'; -import { olderBlocksRetrieved, forgingTimesRetrieved, networkStatusUpdated } from '@actions'; +import { + olderBlocksRetrieved, + forgersRetrieved, + networkStatusUpdated, +} from '@actions'; const oneMinute = 1000 * 60; @@ -19,28 +23,23 @@ const blockListener = ({ getState, dispatch }) => { // eslint-disable-next-line max-statements const callback = (block) => { - const { settings, network, blocks } = getState(); + const { settings, network } = getState(); const activeToken = settings.token && state.settings.token.active; const lastBtcUpdate = network.lastBtcUpdate || 0; const now = new Date(); - if ((activeToken !== tokenMap.BTC.key) || now - lastBtcUpdate > oneMinute) { + if (activeToken === tokenMap.LSK.key) { dispatch({ type: actionTypes.newBlockCreated, data: { block }, }); - if (activeToken === tokenMap.BTC.key) { - dispatch({ - type: actionTypes.lastBtcUpdateSet, - data: now, - }); - } + dispatch(forgersRetrieved()); } - - if (Object.keys(blocks.forgingTimes).length === 0 || blocks.awaitingForgers.length === 0) { - dispatch(forgingTimesRetrieved()); - } else { - dispatch(forgingTimesRetrieved(blocks.awaitingForgers)); + if (activeToken === tokenMap.BTC.key && (now - lastBtcUpdate > oneMinute)) { + dispatch({ + type: actionTypes.lastBtcUpdateSet, + data: now, + }); } }; @@ -56,19 +55,7 @@ const forgingListener = ({ getState, dispatch }) => { const state = getState(); forgersUnsubscribe(); - const callback = async () => { - if (getState().blocks.latestBlocks.length) { - try { - const delegates = await getForgers({ - params: { limit: 103 }, - network: state.network, - }); - dispatch(forgingTimesRetrieved(delegates.data)); - } catch (e) { - dispatch(forgingTimesRetrieved()); - } - } - }; + const callback = () => dispatch(forgersRetrieved()); forgersSubscribe( state.network, @@ -87,6 +74,9 @@ const blockMiddleware = store => ( blockListener(store); forgingListener(store); break; + case actionTypes.olderBlocksRetrieved: + store.dispatch(forgersRetrieved()); + break; default: break; diff --git a/src/store/reducers/blocks.js b/src/store/reducers/blocks.js index 6ba7491a71..3e052f1a05 100644 --- a/src/store/reducers/blocks.js +++ b/src/store/reducers/blocks.js @@ -1,9 +1,8 @@ -import { actionTypes } from '@constants'; +import { actionTypes, ROUND_LENGTH } from '@constants'; const initialState = { latestBlocks: [], - forgingTimes: {}, - awaitingForgers: [], + forgers: [], total: 0, }; @@ -15,7 +14,7 @@ const blocks = (state = initialState, action) => { latestBlocks: [ action.data.block, ...state.latestBlocks, - ].slice(0, 103 * 2), + ].slice(0, ROUND_LENGTH * 2), }; case actionTypes.olderBlocksRetrieved: return { @@ -25,11 +24,10 @@ const blocks = (state = initialState, action) => { ], total: action.data.total, }; - case actionTypes.forgingTimesRetrieved: + case actionTypes.forgersRetrieved: return { ...state, - forgingTimes: action.data.forgingTimes, - awaitingForgers: action.data.awaitingForgers, + forgers: action.data, }; default: return state; diff --git a/src/store/reducers/blocks.test.js b/src/store/reducers/blocks.test.js index 97785d2ebb..82fb76342a 100644 --- a/src/store/reducers/blocks.test.js +++ b/src/store/reducers/blocks.test.js @@ -1,6 +1,6 @@ -import { expect } from 'chai'; import { actionTypes } from '@constants'; import blocksReducer from './blocks'; +import { genesis } from '../../../test/constants/accounts'; describe('Reducer: blocks(state, action)', () => { const blocks = [{ @@ -24,7 +24,7 @@ describe('Reducer: blocks(state, action)', () => { }, }; const changedBlocks = blocksReducer(state, action); - expect(changedBlocks).to.deep.equal({ + expect(changedBlocks).toEqual({ latestBlocks: blocks, }); }); @@ -42,30 +42,35 @@ describe('Reducer: blocks(state, action)', () => { }, }; const changedBlocks = blocksReducer(state, action); - expect(changedBlocks).to.deep.equal({ + expect(changedBlocks).toEqual({ latestBlocks: blocks, total: 1000, }); }); - it('stores forgingTimes in the event of forgingTimesRetrieved', () => { + it('stores forgers in the event of forgersRetrieved', () => { const state = { + forgers: [], latestBlocks: [], - forgingTimes: {}, }; const action = { - type: actionTypes.forgingTimesRetrieved, - data: { - forgingTimes: ['12345678', { time: 0, status: 'forging', tense: 'past' }], - awaitingForgers: [{ address: '12345678', publicKey: '12345678', username: 'test' }], - }, + type: actionTypes.forgersRetrieved, + data: [ + { + totalVotesReceived: 1e9, + status: 'awaitingSlot', + lastBlock: 10000, + username: genesis.dpos.delegate.username, + nextForgingTime: 1620049927, + address: genesis.summary.address, + }, + ], }; const changedBlocks = blocksReducer(state, action); - expect(changedBlocks).to.deep.equal({ + expect(changedBlocks).toEqual({ latestBlocks: [], - forgingTimes: action.data.forgingTimes, - awaitingForgers: action.data.awaitingForgers, + forgers: action.data, }); }); }); diff --git a/src/utils/api/delegate/index.js b/src/utils/api/delegate/index.js index cdf56af2bd..153be7a75e 100644 --- a/src/utils/api/delegate/index.js +++ b/src/utils/api/delegate/index.js @@ -13,7 +13,7 @@ export const httpPaths = { }; export const wsMethods = { - delegates: 'get.delegates', + delegates: 'get.accounts', forgers: 'get.delegates.next_forgers', forgersRound: 'update.round', }; @@ -72,7 +72,7 @@ const getRequests = (values) => { .filter(item => regex[paramList.name].test(item)) .map(item => ({ method: wsMethods.delegates, - params: { [paramList.name]: item }, + params: { [paramList.name]: item, isDelegate: true }, })); } return false; diff --git a/src/utils/api/delegate/index.test.js b/src/utils/api/delegate/index.test.js index 7967382c10..8b9a8a5ce3 100644 --- a/src/utils/api/delegate/index.test.js +++ b/src/utils/api/delegate/index.test.js @@ -107,8 +107,14 @@ describe('API: LSK Delegates', () => { expect(ws).toHaveBeenCalledWith({ baseUrl: network.serviceUrl, requests: [ - { params: { address: addressList[0] }, method: delegate.wsMethods.delegates }, - { params: { address: addressList[1] }, method: delegate.wsMethods.delegates }, + { + params: { address: addressList[0], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, + { + params: { address: addressList[1], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, ], }); }); @@ -125,8 +131,14 @@ describe('API: LSK Delegates', () => { expect(ws).toHaveBeenCalledWith({ baseUrl: network.serviceUrl, requests: [ - { params: { address: addressList[0] }, method: delegate.wsMethods.delegates }, - { params: { address: addressList[1] }, method: delegate.wsMethods.delegates }, + { + params: { address: addressList[0], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, + { + params: { address: addressList[1], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, ], }); }); @@ -154,8 +166,14 @@ describe('API: LSK Delegates', () => { expect(ws).toHaveBeenCalledWith({ baseUrl, requests: [ - { params: { address: addressList[0] }, method: delegate.wsMethods.delegates }, - { params: { address: addressList[1] }, method: delegate.wsMethods.delegates }, + { + params: { address: addressList[0], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, + { + params: { address: addressList[1], isDelegate: true }, + method: delegate.wsMethods.delegates, + }, ], }); delegate.getDelegates({ diff --git a/test/unit-test-utils/fakeStore.js b/test/unit-test-utils/fakeStore.js index 6ef34353ae..e38ad4d233 100644 --- a/test/unit-test-utils/fakeStore.js +++ b/test/unit-test-utils/fakeStore.js @@ -1,14 +1,15 @@ import configureStore from 'redux-mock-store'; +import accounts from '../constants/accounts'; import delegates from '../constants/delegates'; -const forgingTimes = delegates.reduce((acc, item, index) => { - acc[item.account.publicKey] = { - time: index * 10, - tense: 'past', - status: 'forging', - }; - return acc; -}, {}); +const forgers = Object.values(accounts).slice(0, 9).map((account, index) => ({ + username: `genesis_${index}`, + totalVotesReceived: '100000000000', + address: account.summary.address, + minActiveHeight: 1, + isConsensusParticipant: true, + nextForgingTime: 1620049927 + 10 * index, +})); const fakeStore = configureStore(); const defaultStore = { @@ -47,7 +48,8 @@ const defaultStore = { }, blocks: { latestBlocks: delegates, - forgingTimes, + forgers, + total: 10000, }, };