Skip to content
This repository has been archived by the owner on Jul 22, 2020. It is now read-only.

feat: tds scoring and api implementation #527

Merged
merged 2 commits into from
Oct 15, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions api/loaders/tourdesol/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {FriendlyGet} from '../../friendlyGet';

/**
* loadTourDeSolIndex: retrieves raw data from the data store and returns it for formatting
*
* @param redisX
* @returns {Promise<{__errors__: *, clusterInfo: *}>}
*/
export async function loadTourDeSolIndex(redisX, {isDemo, activeStage}) {
const {__errors__, redisKeys} = await new FriendlyGet()
.with('redisKeys', redisX.mgetAsync('!clusterInfo', '!blk-last-slot'))
.get();

const [clusterInfoJson, lastSlotString] = redisKeys;
const clusterInfo = JSON.parse(clusterInfoJson || {});
const lastSlot = parseInt(lastSlotString);

return {
__errors__,
isDemo,
activeStage,
clusterInfo,
lastSlot,
};
}
21 changes: 21 additions & 0 deletions api/network-explorer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {TourDeSolIndexView} from './views/tourdesol';
import {BlockIndexView} from './views/blocks/index';
import {BlockDetailView} from './views/blocks/detail';
import {TransactionIndexView} from './views/transactions/index';
import {TransactionDetailView} from './views/transactions/detail';
import {ApplicationIndexView} from './views/applications/index';
import {ApplicationDetailView} from './views/applications/detail';
import {DEFAULT_PAGE_SIZE} from './loaders/timeline';
import {loadTourDeSolIndex} from './loaders/tourdesol';
import {loadBlockIndex} from './loaders/blocks/index';
import {loadBlockDetail} from './loaders/blocks/detail';
import {loadTransactionIndex} from './loaders/transactions/index';
Expand Down Expand Up @@ -32,6 +34,25 @@ function prettify(req, data) {
// |_|
//
export function addNetworkExplorerRoutes(redisX, app) {
// Network Explorer Tour de Sol Index
app.get('/explorer/tourdesol/index', async (req, res) => {
const q = req.query || {};

const isDemo = q.isDemo || false;
const activeStage = q.activeStage && parseInt(q.activeStage);
const version = q.v || 'TourDeSolIndexView@latest';
const {__errors__, rawData} = await new FriendlyGet()
.with('rawData', loadTourDeSolIndex(redisX, {isDemo, activeStage}), {})
.get();

res.send(
prettify(
req,
new TourDeSolIndexView().asVersion(rawData, __errors__, version),
),
);
});

// Network Explorer Block Index
app.get('/explorer/blocks/index', async (req, res) => {
const q = req.query || {};
Expand Down
2 changes: 2 additions & 0 deletions api/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import _ from 'lodash';
import Base58 from 'base-58';
const b58e = Base58.encode;

export const LAMPORT_SOL_RATIO = 0.0000000000582;

export function transactionFromJson(x, outMessage = {}) {
let txn = Transaction.from(Buffer.from(x));
let tx = {};
Expand Down
197 changes: 197 additions & 0 deletions api/views/tourdesol/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import {filter, reduce, orderBy} from 'lodash/fp';

import {LAMPORT_SOL_RATIO} from '../../util';

const slotsPerDay = (1.0 * 24 * 60 * 60) / 0.8;
sunnygleason marked this conversation as resolved.
Show resolved Hide resolved
const TDS_DEFAULT_STAGE_LENGTH_BLOCKS = slotsPerDay * 5.0;

const stages = [
{id: 0, title: 'Stage 0', hidden: true},
{
id: 1,
title: 'Stage 1',
isTbd: true,
startDate: '2019-09-09T17:00:00.0Z',
endDate: '2019-09-13T17:00:00.0Z',
duration: TDS_DEFAULT_STAGE_LENGTH_BLOCKS,
},
{
id: 2,
title: 'Stage 2',
isTbd: true,
startDate: '2019-10-07T17:00:00.0Z',
endDate: '2019-10-11T17:00:00.0Z',
duration: TDS_DEFAULT_STAGE_LENGTH_BLOCKS,
},
{
id: 3,
title: 'Stage 3',
isTbd: true,
startDate: '2019-11-04T17:00:00.0Z',
endDate: '2019-11-08T17:00:00.0Z',
duration: TDS_DEFAULT_STAGE_LENGTH_BLOCKS,
},
];

/**
* TourDeSolIndexView : supports the Tour de Sol index page
*
* Changes:
* - 20190912.01 : initial version
*/
const __VERSION__ = '[email protected]';
export class TourDeSolIndexView {
asVersion(rawData, __errors__, version) {
if (__errors__) {
return {
__VERSION__,
__errors__,
};
}

const {isDemo, activeStage, clusterInfo, lastSlot} = rawData;

const activeValidatorsRaw = filter(
node => node.what === 'Validator' && node.activatedStake,
)(clusterInfo.network);
const inactiveValidatorsRaw = filter(
node => node.what === 'Validator' && !node.activatedStake,
)(clusterInfo.network);

const currentStage = activeStage ? stages[activeStage] : null;
const slotsLeftInStage =
currentStage && currentStage.duration && currentStage.duration - lastSlot;
const daysLeftInStage =
slotsLeftInStage && (slotsLeftInStage / slotsPerDay).toFixed(3);

const clusterStats = {
lastSlot,
slotsLeftInStage,
daysLeftInStage,
stageDurationBlocks: currentStage && currentStage.duration,
networkInflationRate: clusterInfo.networkInflationRate,
totalSupply: clusterInfo.supply * LAMPORT_SOL_RATIO,
totalStaked: clusterInfo.totalStaked * LAMPORT_SOL_RATIO,
activeValidators: activeValidatorsRaw.length,
inactiveValidators: inactiveValidatorsRaw.length,
};

const scoreParams = this.computeScoreParams(
activeValidatorsRaw,
isDemo,
lastSlot,
currentStage,
);

const activeValidatorsPre = reduce((a, x) => {
const pubkey = x.nodePubkey;
const slot = x.currentSlot;
const {name, avatarUrl} = x.identity;
const activatedStake = x.activatedStake * LAMPORT_SOL_RATIO;
const uptimePercent =
x.uptime &&
x.uptime.uptime &&
x.uptime.uptime.length &&
100.0 * parseFloat(x.uptime.uptime[0].percentage);
const score = this.computeNodeScore(x, scoreParams);

const validator = {
name,
pubkey,
avatarUrl,
activatedStake,
slot,
uptimePercent,
score,
};

a.push(validator);

return a;
})([], activeValidatorsRaw);

const activeValidatorsRank = orderBy('score', 'desc')(activeValidatorsPre);

const result = reduce((a, x) => {
const {lastScore, lastRank, accum} = a;
const {score} = x;
const rank = score < lastScore ? lastRank + 1 : lastRank;

x.rank = rank;

accum.push(x);

return {lastScore: score, lastRank: rank, accum};
})({lastScore: 101, lastRank: 0, accum: []}, activeValidatorsRank);

if (version === 'TourDeSolIndexView@latest' || version === __VERSION__) {
return {
__VERSION__,
rawData,
clusterStats,
activeValidators: result.accum,
stages,
activeStage,
slotsPerDay,
};
}

return {
error: 'UnsupportedVersion',
currentVersion: __VERSION__,
desiredVersion: version,
};
}

computeNodeScore(x, scoreParams) {
const {minValF, spreadF, maxBlock, maxFactor} = scoreParams;
const currentBlock = x.currentSlot;

// the node's position based on block count only
const nonWeightedPosition = (currentBlock * 1.0) / (maxBlock * 1.0);

// synthetic function for 'closeness' : sin(), which is max at middle of race
const weightFactor = Math.sin(nonWeightedPosition * Math.PI) * maxFactor;

const nodePosF =
spreadF !== 0
? (x.activatedStake * 1.0 - minValF) / spreadF
: Math.random();

const weightShare = nodePosF * weightFactor;
let weightedPosition = (nonWeightedPosition + weightShare) * 100.0;
weightedPosition = Math.max(Math.floor(weightedPosition), 0);
weightedPosition = Math.min(Math.ceil(weightedPosition), 100);

return weightedPosition;
}

computeScoreParams(validators, isDemo, lastSlot, currentStage) {
const firstStake =
(validators && validators.length && validators[0].activatedStake) || 0;

const [minVal, maxVal] = reduce((a, x) => {
let stake = x.activatedStake;
return [Math.min(a[0], stake), Math.max(a[1], stake)];
})([firstStake, firstStake], validators);

const maxValF = maxVal * 1.0; // the maximum observed stake
const minValF = minVal * 1.0; // the minimum observed stake
const spreadF = maxValF - minValF; // the spread between min and max

const currentBlock = !isDemo ? lastSlot : new Date().getTime() % 60000;
const maxBlock = !isDemo ? currentStage && currentStage.duration : 60000;

// maximum weight factor
const maxFactor = spreadF !== 0 ? 0.1 : 0.05;

return {
maxValF,
minValF,
spreadF,
currentBlock,
maxBlock,
maxFactor,
};
}
}
17 changes: 17 additions & 0 deletions src/v2/api/tourdesol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import _ from 'lodash';

import api from '.';

const TOURDESOL_INDEX_VERSION = '[email protected]';

export function apiGetTourDeSolIndexPage({version, activeStage, demo}) {
const queryString = _.toPairs({
v: version || TOURDESOL_INDEX_VERSION,
activeStage: activeStage || 0,
isDemo: demo || false,
})
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');

return api(`/explorer/tourdesol/index?${queryString}`);
}
32 changes: 17 additions & 15 deletions src/v2/components/TourDeSol/Cards/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@
import {map} from 'lodash/fp';
import React from 'react';
import {observer} from 'mobx-react-lite';
import NodesStore from 'v2/stores/nodes';
import Card from 'v2/components/UI/StatCard';

import Socket from '../../../stores/socket';
import Loader from '../../UI/Loader';
import useStyles from './styles';
import {LAMPORT_SOL_RATIO} from '../../../constants';

const Cards = ({
stageDurationBlocks = null,
blocksLeftInStage = null,
daysLeftInStage = null,
isLoading,
clusterStats,
}: {
isLoading: Boolean,
clusterStats: Object,
}) => {
const classes = useStyles();
const {
stageDurationBlocks,
slotsLeftInStage,
daysLeftInStage,
activeValidators,
inactiveValidators,
supply,
totalSupply,
totalStaked,
networkInflationRate,
} = NodesStore;
const {isLoading} = Socket;
} = clusterStats;

const cards = [
{
Expand All @@ -36,7 +37,7 @@ const Cards = ({
},
{
title: 'Blocks Left In Stage',
value: blocksLeftInStage || '...',
value: slotsLeftInStage || '...',
changes: '',
period: 'since yesterday',
helpText:
Expand All @@ -53,31 +54,32 @@ const Cards = ({
},
{
title: 'Circulating SOL',
value: (supply / Math.pow(2, 34)).toFixed(2),
value: totalSupply && totalSupply.toFixed(2),
sunnygleason marked this conversation as resolved.
Show resolved Hide resolved
changes: '',
period: 'since yesterday',
helpText: 'The total number of SOL in existence.',
helpTerm: '',
},
{
title: 'Staked SOL',
value: (totalStaked * LAMPORT_SOL_RATIO).toFixed(8),
value: totalStaked && totalStaked.toFixed(8),
changes: '',
period: 'since yesterday',
helpText: 'Amount of SOL staked to validators and activated',
helpTerm: '',
},
{
title: 'Current Network Inflation Rate',
value: (networkInflationRate * 100.0).toFixed(3) + '%',
value:
networkInflationRate && (networkInflationRate * 100.0).toFixed(3) + '%',
changes: '',
period: 'since yesterday',
helpText: "The network's current annual SOL inflation rate.",
helpTerm: '',
},
{
title: 'Active Validators',
value: activeValidators.length,
value: activeValidators,
changes: '',
period: 'since yesterday',
helpText:
Expand All @@ -86,7 +88,7 @@ const Cards = ({
},
{
title: 'Inactive Validators',
value: inactiveValidators.length,
value: inactiveValidators,
changes: '',
period: 'since yesterday',
helpText:
Expand Down
Loading