Joystream Stats 2 лет назад
Родитель
Сommit
76f17a57ff

+ 51 - 97
src/App.tsx

@@ -7,13 +7,16 @@ import * as get from "./lib/getters";
 import {
   updateElection,
   getCouncilApplicants,
-  getCouncilRound,
   getCouncilSize,
-  getValidatorsData,
   getVotes,
 } from "./lib/election";
-import { PromiseAllObj } from "./lib/util";
-import { domain, apiLocation, wsLocation } from "./config";
+import {
+  getValidators,
+  getEraRewardPoints,
+  getLastReward,
+  getTotalStake,
+} from "./lib/validators";
+import { domain, apiLocation, wsLocation, historyDepth } from "./config";
 import axios from "axios";
 import moment from "moment";
 
@@ -24,8 +27,6 @@ import { Header } from "@polkadot/types/interfaces";
 
 interface IProps {}
 
-const version = 6;
-
 const initialState = {
   assets: [],
   connected: false,
@@ -59,6 +60,11 @@ const initialState = {
   editKpi: false,
   status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },
   groups: [],
+  rewardPoints: {
+    total: 0,
+    eraTotals: {},
+    validators: {},
+  },
 };
 
 class App extends React.Component<IProps, IState> {
@@ -82,8 +88,6 @@ class App extends React.Component<IProps, IState> {
     );
     this.updateStatus(api);
     this.fetchMints(api, [2, 3, 4]);
-    this.fetchWorkingGroups(api);
-    this.getChainState(api);
   }
 
   async fetchMints(api: Api, ids: number[]) {
@@ -170,7 +174,6 @@ class App extends React.Component<IProps, IState> {
     this.setState({ blocks });
 
     if (id / 50 === Math.floor(id / 50)) {
-      this.fetchLastReward(api);
       this.updateStatus(api, id);
       this.fetchTokenomics();
       this.updateActiveProposals();
@@ -202,17 +205,12 @@ class App extends React.Component<IProps, IState> {
     status.threads = await get.currentThreadId(api);
     status.categories = await get.currentCategoryId(api);
     status.proposalPosts = await api.query.proposalsDiscussion.postCount();
-    status.version = version;
+    status.lastReward = await getLastReward(api, status.era);
+    status.validatorStake = await getTotalStake(api, status.era);
     this.save("status", status);
     return status;
   }
 
-  async getChainState(api: ApiPromise) {
-    return PromiseAllObj({
-      validators: await getValidatorsData(api),
-    }).then((chain) => this.save("chain", chain));
-  }
-
   async getElectionStatus(api: ApiPromise): Promise<IElectionState> {
     getCouncilSize(api).then((councilSize) => {
       let election = this.state.election;
@@ -247,16 +245,11 @@ class App extends React.Component<IProps, IState> {
   }
 
   async updateEra(api: Api) {
-    const era = Number(await api.query.staking.currentEra());
-    this.fetchEraRewardPoints(api, era);
-
     const { status, validators } = this.state;
-    if (era > status.era || !validators.length) {
-      console.debug(`Updating validators`);
-      this.fetchLastReward(api, status.era);
-      const validators = await this.fetchValidators(api);
-      this.fetchStakes(api, era, validators);
-    } else if (!status.lastReward) this.fetchLastReward(api);
+    const era = Number(await api.query.staking.currentEra());
+    this.updateWorkingGroups(api);
+    this.updateValidatorPoints(api, status.era);
+    if (era > status.era || !validators.length) this.updateValidators(api);
     return era;
   }
 
@@ -275,6 +268,7 @@ class App extends React.Component<IProps, IState> {
     status.council = council;
     this.save("status", status);
   }
+
   async fetchProposals() {
     const { data } = await axios.get(`${apiLocation}/v2/proposals`);
     if (!data || data.error) return console.error(`failed to fetch from API`);
@@ -283,7 +277,7 @@ class App extends React.Component<IProps, IState> {
     this.save("proposals", proposals);
   }
 
-  async fetchWorkingGroups(api: ApiPromise) {
+  async updateWorkingGroups(api: ApiPromise) {
     const openingsUpdated = this.state.openings?._lastUpdate;
     if (
       !openingsUpdated ||
@@ -423,12 +417,14 @@ class App extends React.Component<IProps, IState> {
     console.debug(`posts`, data);
     this.save("posts", data);
   }
+
   async fetchThreads() {
     const { data } = await axios.get(`${apiLocation}/v1/threads`);
     if (!data || data.error) return console.error(`failed to fetch from API`);
     console.debug(`threads`, data);
     this.save("threads", data);
   }
+
   async fetchCategories() {
     const { data } = await axios.get(`${apiLocation}/v1/categories`);
     if (!data || data.error) return console.error(`failed to fetch from API`);
@@ -456,18 +452,6 @@ class App extends React.Component<IProps, IState> {
     return array.filter((i) => i.id !== item.id).concat(item);
   }
 
-  async fetchLastReward(api: Api) {
-    const era: number = await this.updateEra(api);
-    const lastReward = Number(
-      await api.query.staking.erasValidatorReward(era - 2)
-    );
-
-    console.debug(`reward era ${era}: ${lastReward} tJOY`);
-    let { status } = this.state;
-    status.lastReward = lastReward;
-    this.save("status", status);
-  }
-
   async fetchTokenomics() {
     const now = new Date();
     if (this.state.tokenomics?.timestamp + 300000 > now) return;
@@ -478,64 +462,39 @@ class App extends React.Component<IProps, IState> {
     this.save("tokenomics", data);
   }
 
-  // validators
-
-  async fetchValidators(api: Api) {
-    const validatorEntries = await api.query.session.validators();
-    const validators = validatorEntries.map((v: any) => String(v));
-    this.save("validators", validators);
+  async updateValidators(api: ApiPromise) {
+    this.save("validators", await getValidators(api));
+    this.save("nominators", await getNominators(api));
+    const stashes = await getStashes(api);
+    this.save("stashes", stashes);
+    const { members } = this.state;
+    this.save("stakes", await getValidatorStakes(api, era, stashes, members));
+  }
 
-    const stashes = await api.derive.staking.stashes();
-    this.save(
-      "stashes",
-      stashes.map((s: any) => String(s))
-    );
-    this.fetchNominators(api);
-    return validators;
-  }
-
-  async fetchNominators(api: Api) {
-    const nominatorEntries = await api.query.staking.nominators.entries();
-    const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()));
-    this.save("nominators", nominators);
-  }
-
-  async fetchStakes(api: Api, era: number, validators: string[]) {
-    const { members, stashes } = this.state;
-    if (!stashes) return;
-    stashes.forEach(async (validator: string) => {
-      try {
-        const prefs = await api.query.staking.erasValidatorPrefs(
-          era,
-          validator
-        );
-        const commission = Number(prefs.commission) / 10000000;
-
-        const data = await api.query.staking.erasStakers(era, validator);
-        let { total, own, others } = data.toJSON();
-        let { stakes = {} } = this.state;
-        others = others.map(({ who, value }) => {
-          const member = members.find((m) => m.rootKey === who);
-          return { who, value, member };
-        });
+  async updateValidatorPoints(api: ApiPromise, currentEra: number) {
+    let points = this.state.rewardPoints;
 
-        stakes[validator] = { total, own, others, commission };
-        this.save("stakes", stakes);
-      } catch (e) {
-        console.warn(
-          `Failed to fetch stakes for ${validator} in era ${era}`,
-          e
-        );
-      }
-    });
-  }
+    const updateTotal = (eraTotals) => {
+      let total = 0;
+      Object.keys(eraTotals).forEach((era) => (total += eraTotals[era]));
+      return total;
+    };
 
-  async fetchEraRewardPoints(api: Api, era: number) {
-    const data = await api.query.staking.erasRewardPoints(era);
-    this.setState({ rewardPoints: data.toJSON() });
+    for (let era = currentEra; era > currentEra - historyDepth; --era) {
+      if (era < currentEra && points.eraTotals[era]) continue;
+      getEraRewardPoints(api, era).then((eraPoints) => {
+        console.debug(`era ${era}: ${eraPoints.total} points`);
+        points.eraTotals[era] = eraPoints.total;
+        points.total = updateTotal(points.eraTotals);
+        Object.keys(eraPoints.individual).forEach((validator: string) => {
+          if (!points.validators[validator]) points.validators[validator] = {};
+          points.validators[validator][era] = eraPoints.individual[validator];
+        });
+        this.save("rewardPoints", points);
+      });
+    }
   }
 
-  // Validators
   toggleStar(account: string) {
     let { stars } = this.state;
     stars[account] = !stars[account];
@@ -612,14 +571,9 @@ class App extends React.Component<IProps, IState> {
     if (posts) this.setState({ posts });
   }
 
-  clearData() {
-    console.log(`Resetting db to version ${version}`);
-    localStorage.clear();
-  }
-
   async loadData() {
     console.debug(`Loading data`);
-    "status members assets providers councils council election workers categories channels proposals posts threads  mints openings tokenomics transactions reports validators nominators stakes stars"
+    "status members assets providers councils council election workers categories channels proposals posts threads  mints openings tokenomics transactions reports validators nominators staches stakes rewardPoints stars"
       .split(" ")
       .map((key) => this.load(key));
   }

+ 7 - 7
src/components/Dashboard/index.tsx

@@ -48,7 +48,6 @@ const Dashboard = (props: IProps) => {
             validators={validators}
             status={status}
           />
-          <Openings openings={openings} />
           <Council
             getMember={getMember}
             councils={councils}
@@ -61,6 +60,13 @@ const Dashboard = (props: IProps) => {
             domain={domain}
             gridSize={6}
           />
+          <Forum
+            updateForum={props.updateForum}
+            posts={posts}
+            threads={threads}
+            startTime={status.startTime}
+          />
+          <Openings openings={openings} />
           <Validators
             toggleStar={toggleStar}
             councils={councils}
@@ -76,12 +82,6 @@ const Dashboard = (props: IProps) => {
             tokenomics={tokenomics}
             status={status}
           />
-          <Forum
-            updateForum={props.updateForum}
-            posts={posts}
-            threads={threads}
-            startTime={status.startTime}
-          />
         </Grid>
       </Container>
     </div>

+ 2 - 2
src/components/Routes/index.tsx

@@ -34,7 +34,7 @@ interface IProps extends IState {
 }
 
 const Routes = (props: IProps) => {
-  const { chain, faq, proposals, toggleEditKpi } = props;
+  const { faq, proposals, toggleEditKpi } = props;
 
   return (
     <div>
@@ -125,7 +125,7 @@ const Routes = (props: IProps) => {
                 render={(routeprops) => (
                   <ValidatorReport
                     lastBlock={props.status?.block?.id}
-                    activeValidators={chain?.validators || []}
+                    validators={props.validators}
                   />
                 )}
               />

+ 2 - 2
src/components/ValidatorReport/index.tsx

@@ -61,7 +61,7 @@ const oldChainStatsFileName = "validators-old-testnet.json.gz";
 const oldChainStatsLocation = `https://joystreamstats.live/static/${oldChainStatsFileName}`;
 
 const ValidatorReport = (props: {}) => {
-  const { lastBlock = 0, activeValidators = [] } = props;
+  const { lastBlock = 0, validators } = props;
   const dateFormat = "yyyy-MM-DD";
   const [oldChainLastDate, setOldChainLastDate] = useState(moment());
   const [oldChainPageSize, setOldChainPageSize] = useState(50);
@@ -367,7 +367,7 @@ const ValidatorReport = (props: {}) => {
             <Autocomplete
               freeSolo
               style={{ width: "100%" }}
-              options={activeValidators}
+              options={validators}
               onChange={updateStash}
               onBlur={updateStashOnBlur}
               value={stash}

+ 2 - 0
src/components/Validators/Content.tsx

@@ -1,5 +1,6 @@
 import Stats from "./MinMax";
 import ValidatorList from "./ValidatorList";
+import Leaderboard from "./Leaderboard";
 import { Spinner } from "..";
 
 const Content = (props: IState) => {
@@ -24,6 +25,7 @@ const Content = (props: IState) => {
         waiting={waiting.length}
         reward={status.lastReward}
       />
+      <Leaderboard members={props.members} points={props.rewardPoints} />
       <ValidatorList
         toggleStar={props.toggleStar}
         waiting={waiting}

+ 23 - 0
src/components/Validators/Leaderboard/Eras.tsx

@@ -0,0 +1,23 @@
+import { maxEras } from "./config";
+
+const Eras = (props: {
+  eras: { [key: number]: number };
+  validator: string;
+}) => {
+  const { eras, validator } = props;
+  if (!eras) return <div />;
+  return Object.keys(eras)
+    .sort((a, b) => b - a)
+    .slice(0, maxEras)
+    .map((era) => (
+      <div
+        key={era}
+        className="d-none d-md-inline col-2 col-lg-1 text-right"
+        title={`Era ${era}: ${validator}`}
+      >
+        {eras[era]}
+      </div>
+    ));
+};
+
+export default Eras;

+ 18 - 0
src/components/Validators/Leaderboard/Validator.tsx

@@ -0,0 +1,18 @@
+const Validator = (props: {
+  validator: string;
+  member: { handle: string };
+}) => {
+  const { member, validator } = props;
+  const short = (key: string) =>
+    key.slice(0, 6) + `..` + key.slice(key.length - 6);
+  return (
+    <a
+      href={`/validators#${validator}`}
+      className="col-6 col-md-3 col-lg-2 text-right"
+    >
+      {member ? member.handle : short(validator)}
+    </a>
+  );
+};
+
+export default Validator;

+ 2 - 0
src/components/Validators/Leaderboard/config.ts

@@ -0,0 +1,2 @@
+export const maxEras = 8;
+export const maxValidators = 50;

+ 99 - 0
src/components/Validators/Leaderboard/index.tsx

@@ -0,0 +1,99 @@
+import { ErasRewardPoints } from "../../types";
+import Eras from "./Eras";
+import Validator from "./Validator";
+import { maxEras, maxValidators } from "./config";
+
+const getSums = (points: ErasRewardPoints) => {
+  let sums = [];
+  Object.keys(points.validators).forEach((validator: string) => {
+    const erasObj = points.validators[validator];
+    if (!erasObj) return;
+    let sum = 0;
+    Object.keys(erasObj).forEach((era: number) => (sum += erasObj[era]));
+    sums.push({ validator, sum });
+  });
+  return sums.sort((a, b) => b.sum - a.sum).slice(0, maxValidators);
+};
+
+const formatPoints = (points: number): string =>
+  points > 1000000
+    ? (points / 1000000).toFixed(1) + "M"
+    : points > 1000
+    ? (points / 1000).toFixed(2) + "K"
+    : points;
+
+const Leaderboard = (props: { points: ErasRewardPoints }) => {
+  const { members, points } = props;
+  if (!points?.total) return <div />;
+  const count = Object.keys(points.eraTotals).length;
+  const title = `Sum of ${count} eras. (source: api.query.staking.erasRewardPoints)`;
+
+  return (
+    <div className="mb-3">
+      <h3 className="text-center">Era Reward Points</h3>
+      <div className="d-flex flex-column">
+        <div className="d-flex flex-row btn-dark p-1 overflow-hidden">
+          <div className="font-weight-bold text-right">#</div>
+          <div className="col-6 col-md-3 col-lg-2 font-weight-bold text-right">
+            Validator
+          </div>
+          <div className="col-3 col-md-1 font-weight-bold text-right">
+            Points
+          </div>
+          {Object.keys(points.eraTotals)
+            .sort((a, b) => b - a)
+            .slice(0, maxEras)
+            .map((era) => (
+              <div
+                key={era}
+                className="d-none d-md-block col-2 col-lg-1 font-weight-bold text-right"
+              >
+                {era}
+              </div>
+            ))}
+        </div>
+        <div className="d-flex flex-row py-1 bg-info overflow-hidden">
+          <div className="text-right pl-1">-</div>
+          <div className="col-6 col-md-3 col-lg-2 font-weight-bold text-right">
+            Total
+          </div>
+          <div className="col-3 col-md-1 text-right" title={title}>
+            {formatPoints(points.total)}
+          </div>
+          {Object.keys(points.eraTotals)
+            .sort((a, b) => b - a)
+            .slice(0, maxEras)
+            .map((era) => (
+              <div
+                key={era}
+                title={`Era ${era}`}
+                className="d-none d-md-block col-2 col-lg-1 text-right"
+              >
+                {points.eraTotals[era]}
+              </div>
+            ))}
+        </div>
+
+        <div style={{ maxHeight: 500, overflowY: "auto", overflowX: "hidden" }}>
+          {getSums(points).map(({ validator, sum }, index) => (
+            <div key={validator} className="d-flex flex-row pr-2 py-1">
+              <div className="text-right">
+                {(index + 1).toString().padStart(2, "0")}
+              </div>
+              <Validator
+                validator={validator}
+                member={members.find((m) => m.rootKey === validator)}
+              />
+              <div className="col-3 col-md-1 text-right" title={title}>
+                {formatPoints(sum)}
+              </div>
+              <Eras eras={points.validators[validator]} validator={validator} />
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Leaderboard;

+ 2 - 8
src/components/Validators/Validator.tsx

@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { Activity, Star } from "react-feather";
 import Nominators from "./Nominators";
 import MemberBox from "../Members/MemberBox";
-import { ProposalDetail, Seat, Stakes, RewardPoints } from "../../types";
+import { ProposalDetail, Seat, Stakes } from "../../types";
 import { domain } from "../../config";
 
 interface IProps {
@@ -15,7 +15,6 @@ interface IProps {
   startTime: number;
   starred: string | undefined;
   stakes?: { [key: string]: Stakes };
-  rewardPoints?: RewardPoints;
   reward?: number;
 }
 
@@ -55,13 +54,11 @@ class Validator extends Component<IProps, IState> {
       startTime,
       starred,
       stakes,
-      rewardPoints,
       reward = 0,
     } = this.props;
     const { expandNominators, hidden } = this.state;
     if (hidden) return <div />;
 
-    const points = rewardPoints ? rewardPoints.individual[validator] : "";
     const stake = stakes ? stakes[validator] : undefined;
     let totalStake = 0;
     let ownStake = 0;
@@ -82,7 +79,7 @@ class Validator extends Component<IProps, IState> {
     }
 
     return (
-      <div className="p-2 col-12 col-md-6" style={{ fontSize: 11 }}>
+      <div id={validator} className="p-2 col-12 col-md-6" style={{ fontSize: 11 }}>
         <MemberBox
           account={validator}
           placement={"right"}
@@ -96,9 +93,6 @@ class Validator extends Component<IProps, IState> {
         />
 
         <div className="mt-2 d-flex flex-row justify-content-around">
-          <div onClick={() => sortBy("points")} title="era points">
-            {points}
-          </div>
           <div onClick={() => sortBy("commission")} title="commission">
             {commission}
           </div>

+ 1 - 0
src/components/Validators/ValidatorList.tsx

@@ -35,6 +35,7 @@ const ValidatorList = (props: IState) => {
     );
   return (
     <div className="d-flex flex-column">
+      <h3 className="text-center">Stakes</h3>
       <div className="d-flex flex-wrap">
         {sortValidators(sortBy, starred, stakes, rewardPoints).map((v) => (
           <Validator

+ 1 - 3
src/components/Validators/index.tsx

@@ -24,8 +24,6 @@ const useStyles = makeStyles((theme: Theme) =>
     paper: {
       backgroundColor: "#4038FF",
       color: "#fff",
-      minHeight: 600,
-      maxHeight: 600,
       overflow: "auto",
     },
   })
@@ -47,7 +45,7 @@ const Validators = (props: IState) => {
       </Paper>
     </div>
   ) : (
-    <Grid className={classes.grid} item lg={6}>
+    <Grid className={classes.grid} item lg={12}>
       <Paper className={classes.paper}>
         <AppBar className={classes.root} position="static">
           <Toolbar>

+ 1 - 0
src/config.ts

@@ -1,3 +1,4 @@
+export const historyDepth = 336;
 export const domain = "https://pioneer.joystreamstats.live";
 export const wsLocation = "wss://joystreamstats.live:9945";
 export const apiLocation = "https://api.joystreamstats.live/api"

+ 37 - 76
src/lib/election.ts

@@ -1,17 +1,37 @@
 import { ApiPromise } from "@polkadot/api";
-import { AccountId, Hash } from "@polkadot/types/interfaces";
-import BN from "bn.js";
-import { Option, Vec } from "@polkadot/types";
+import { AccountId } from "@polkadot/types/interfaces";
 import { MemberId } from "@joystream/types/members";
 import { Member, IApplicant, IVote } from "./types";
-import { PromiseAllObj } from "./util";
 import { IElectionStake, SealedVote } from "@joystream/types/council";
 
+export const finalizedBlockHeight = async (api: ApiPromise) => {
+  const hash = await finalizedHash(api);
+  const { number } = await api.rpc.chain.getHeader(`${hash}`);
+  return number.toNumber();
+};
+
+export const finalizedHash = (api: ApiPromise) =>
+  api.rpc.chain.getFinalizedHead();
+
+export const getCouncilSize = async (api: ApiPromise): Promise<Number> =>
+  Number((await api.query.councilElection.councilSize()).toJSON());
+
+export const getCouncilRound = async (api: ApiPromise): Promise<Number> =>
+  Number((await api.query.councilElection.round()).toJSON());
+
+export const getTermEndsAt = async (api: ApiPromise): Promise<Number> =>
+  Number((await api.query.council.termEndsAt()).toJSON());
+
+export const getElectionStage = async (
+  api: ApiPromise
+): Promise<{ [key: string]: Number }> =>
+  (await api.query.councilElection.stage()).toJSON();
+
 export const updateElection = async (api: ApiPromise) => {
   console.debug(`Updating election status`);
-  const round = Number((await api.query.councilElection.round()).toJSON());
-  const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-  const stage = (await api.query.councilElection.stage()).toJSON();
+  const round = await getCouncilRound(api);
+  const termEndsAt = await getTermEndsAt(api);
+  const stage = await getElectionStage(api);
   let stageEndsAt = 0;
   if (stage) {
     const key = Object.keys(stage)[0];
@@ -32,25 +52,6 @@ export const updateElection = async (api: ApiPromise) => {
   return { round, stageEndsAt, termEndsAt, stage, durations };
 };
 
-export const finalizedHash = (api: ApiPromise) =>
-  api.rpc.chain.getFinalizedHead();
-
-export const getCouncilRound = async (api: ApiPromise): Promise<Number> =>
-  Number((await api.query.councilElection.round()).toJSON());
-
-export const getCouncilSize = async (api: ApiPromise): Promise<Number> =>
-  Number((await api.query.councilElection.councilSize()).toJSON());
-
-export const termEndsAt = async (api: ApiPromise): Promise<Number> =>
-  Number((await api.query.council.termEndsAt()).toJSON());
-
-export const getElectionStage = async (
-  api: ApiPromise
-): Promise<{ [key: string]: Number }> => {
-  const stage = (await api.query.councilElection.stage()).toJSON();
-  return stage as unknown as { [key: string]: Number };
-};
-
 export const getCouncilApplicants = async (
   api: ApiPromise
 ): Promise<IApplicant[]> => {
@@ -135,54 +136,14 @@ export const getVotes = async (api: ApiPromise): Promise<IVote[]> => {
   return votes;
 };
 
-export const finalizedBlockHeight = async (api: ApiPromise) => {
-  const hash = await finalizedHash(api);
-  const { number } = await api.rpc.chain.getHeader(`${hash}`);
-  return number.toNumber();
-};
-
-export const findActiveValidators = async (
-  api: ApiPromise,
-  hash: Hash,
-  searchPreviousBlocks: boolean
-): Promise<AccountId[]> => {
-  const block = await api.rpc.chain.getBlock(hash);
-
-  let currentBlockNr = block.block.header.number.toNumber();
-  let activeValidators;
-  do {
-    let currentHash = (await api.rpc.chain.getBlockHash(
-      currentBlockNr
-    )) as Hash;
-    let allValidators = (await api.query.staking.snapshotValidators.at(
-      currentHash
-    )) as Option<Vec<AccountId>>;
-    if (!allValidators.isEmpty) {
-      let max = (
-        await api.query.staking.validatorCount.at(currentHash)
-      ).toNumber();
-      activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
-    }
-
-    if (searchPreviousBlocks) {
-      --currentBlockNr;
-    } else {
-      ++currentBlockNr;
-    }
-  } while (activeValidators === undefined);
-  return activeValidators;
-};
-
-export const getValidatorsData = async (api: ApiPromise) => {
-  const validators = await api.query.session.validators();
-  const era = await api.query.staking.currentEra();
-  const totalStake = era.isSome
-    ? await api.query.staking.erasTotalStake(era.unwrap())
-    : new BN(0);
-
-  return {
-    count: validators.length,
-    validators: validators.toJSON(),
-    total_stake: totalStake.toNumber(),
-  };
+export const PromiseAllObj = (obj: {
+  [k: string]: any;
+}): Promise<{ [k: string]: any }> => {
+  return Promise.all(
+    Object.entries(obj).map(([key, val]) =>
+      val instanceof Promise
+        ? val.then((res) => [key, res])
+        : new Promise((res) => res([key, val]))
+    )
+  ).then((res: any[]) => fromEntries(res));
 };

+ 0 - 16
src/lib/util.ts

@@ -84,19 +84,3 @@ export const formatJoy = (stake: number): String => {
 
   return `${stake} JOY`;
 };
-
-// Validator data
-const fromEntries = (xs: [string | number | symbol, any][]) =>
-  xs.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
-
-export const PromiseAllObj = (obj: {
-  [k: string]: any;
-}): Promise<{ [k: string]: any }> => {
-  return Promise.all(
-    Object.entries(obj).map(([key, val]) =>
-      val instanceof Promise
-        ? val.then((res) => [key, res])
-        : new Promise((res) => res([key, val]))
-    )
-  ).then((res: any[]) => fromEntries(res));
-};

+ 81 - 0
src/lib/validators.ts

@@ -0,0 +1,81 @@
+import { ApiPromise } from "@polkadot/api";
+
+// total reward per era
+export const getLastReward = async (api: ApiPromise, era: number) =>
+  Number(await api.query.staking.erasValidatorReward(era - 2));
+
+// active validators
+export const getValidators = async (api: ApiPromise) => {
+  const validatorEntries = await api.query.session.validators();
+  return validatorEntries.map((v: any) => String(v));
+};
+
+// validators including waiting
+export const getStashes = async (api: ApiPromise) => {
+  const stashes = await api.derive.staking.stashes();
+  return stashes.map((s: any) => String(s));
+};
+
+export const getNominators = async (api: ApiPromise) => {
+  const nominatorEntries = await api.query.staking.nominators.entries();
+  return nominatorEntries.map((n: any) => String(n[0].toHuman()));
+};
+
+export const getValidatorStakes = async (
+  api: ApiPromise,
+  era: number,
+  stashes: string[],
+  members: Member[]
+) => {
+  return stashes.reduce(async (allAtakes, validator: string) => {
+    const prefs = await api.query.staking.erasValidatorPrefs(era, validator);
+    const commission = Number(prefs.commission) / 10000000;
+
+    const data = await api.query.staking.erasStakers(era, validator);
+    let { total, own, others } = data.toJSON();
+
+    others = others.map(({ who, value }) => {
+      const member = members.find((m) => m.rootKey === who);
+      return { who, value, member };
+    });
+    return allStakes.concat({ total, own, others, commission });
+  }, []);
+};
+
+export const getEraRewardPoints = async (api: Api, era: EraId | number) =>
+  (await api.query.staking.erasRewardPoints(era)).toJSON();
+
+export const findActiveValidators = async (
+  api: ApiPromise,
+  hash: Hash,
+  searchPreviousBlocks: boolean
+): Promise<AccountId[]> => {
+  const block = await api.rpc.chain.getBlock(hash);
+
+  let currentBlockNr = block.block.header.number.toNumber();
+  let activeValidators;
+  do {
+    let currentHash = (await api.rpc.chain.getBlockHash(
+      currentBlockNr
+    )) as Hash;
+    let allValidators = (await api.query.staking.snapshotValidators.at(
+      currentHash
+    )) as Option<Vec<AccountId>>;
+    if (!allValidators.isEmpty) {
+      let max = (
+        await api.query.staking.validatorCount.at(currentHash)
+      ).toNumber();
+      activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
+    }
+
+    if (searchPreviousBlocks) {
+      --currentBlockNr;
+    } else {
+      ++currentBlockNr;
+    }
+  } while (activeValidators === undefined);
+  return activeValidators;
+};
+
+export const getTotalStake = async (api: ApiPromise, era: number) =>
+  api.query.staking.erasTotalStake(era);

+ 7 - 2
src/types.ts

@@ -79,7 +79,6 @@ export interface Status {
   proposalPosts: any;
   councilApplicants: IApplicant[];
   councilVotes: IVote[];
-  version: number;
   now: number;
   block: Block;
   era: number;
@@ -132,7 +131,7 @@ export interface IState {
   [key: string]: any;
   stars: { [key: string]: boolean };
   stakes?: { [key: string]: Stakes };
-  rewardPoints?: RewardPoints;
+  rewardPoints: ErasRewardPoints;
   hideFooter: boolean;
   showStatus: boolean;
   editKpi: any; // TODO
@@ -396,6 +395,12 @@ export interface ValidatorReportLineItem {
   blocksCount: number;
 }
 
+export interface ErasRewardPoints {
+  total: number;
+  eraTotals: { [key: sting]: number };
+  validators: { [key: sting]: number[] };
+}
+
 export interface CalendarGroup {
   id: number;
   title: string;