Joystream Stats пре 2 година
родитељ
комит
978f4abcc0

+ 99 - 85
src/App.tsx

@@ -4,6 +4,17 @@ import "./index.css";
 import { Modals, Routes, Loading, Footer, Status } from "./components";
 
 import * as get from "./lib/getters";
+import {
+  updateElection,
+  getCouncilApplicants,
+  getElectionStage,
+  getCouncilRound,
+  getCouncilSize,
+  getVotes,
+  finalizedBlockHeight,
+  getValidatorsData,
+} from "./lib/election";
+import { PromiseAllObj } from "./lib/util";
 import { domain, apiLocation, wsLocation } from "./config";
 import axios from "axios";
 import moment from "moment";
@@ -30,6 +41,11 @@ const initialState = {
   channels: [],
   posts: [],
   councils: [],
+  election: {
+    applicants: [],
+    votes: [],
+    councilSize: 20,
+  },
   categories: [],
   threads: [],
   proposals: [],
@@ -61,19 +77,21 @@ class App extends React.Component<IProps, IState> {
     });
   }
 
-  async handleApi(api: Api) {
+  async handleApi(api: ApiPromise) {
+    this.fetchFromApi();
     api.rpc.chain.subscribeNewHeads((head: Header) =>
       this.handleBlock(api, head)
     );
+    this.updateStatus(api);
     this.fetchMints(api, [2, 3, 4]);
     this.fetchWorkingGroups(api);
-    this.updateStatus(api);
+    this.getChainState(api);
   }
 
   async fetchMints(api: Api, ids: number[]) {
     console.debug(`Fetching mints`);
     let mints = [];
-    Promise.all(
+    return Promise.all(
       ids.map(
         async (id) => (mints[id] = (await api.query.minting.mints(id)).toJSON())
       )
@@ -161,12 +179,13 @@ class App extends React.Component<IProps, IState> {
     }
   }
 
-  async updateStatus(api: Api, id = 0) {
+  async updateStatus(api: ApiPromise, id = 0): Promise<Status> {
     console.debug(`Updating status for block ${id}`);
-
     let { status, councils } = this.state;
     status.era = await this.updateEra(api);
-    status.election = await this.updateElection(api);
+    status.election = await updateElection(api);
+    if (Object.keys(status.election.stage)[0] === "revealing")
+      this.getElectionStatus(api);
     councils.forEach((c) => {
       if (c.round > status.council) status.council = c;
     });
@@ -184,6 +203,31 @@ class App extends React.Component<IProps, IState> {
     status.proposalPosts = await api.query.proposalsDiscussion.postCount();
     status.version = version;
     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;
+      election.councilSize = councilSize;
+      this.save("election", election);
+    });
+    getVotes(api).then((votes) => {
+      let election = this.state.election;
+      election.votes = votes;
+      this.save("election", election);
+    });
+    getCouncilApplicants(api).then((applicants) => {
+      let election = this.state.election;
+      election.applicants = applicants;
+      this.save("election", election);
+    });
   }
 
   updateActiveProposals() {
@@ -215,31 +259,6 @@ class App extends React.Component<IProps, IState> {
     return era;
   }
 
-  async updateElection(api: Api) {
-    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();
-    let stageEndsAt = 0;
-    if (stage) {
-      const key = Object.keys(stage)[0];
-      stageEndsAt = stage[key];
-    }
-
-    const stages = [
-      "announcingPeriod",
-      "votingPeriod",
-      "revealingPeriod",
-      "newTermDuration",
-    ];
-
-    let durations = await Promise.all(
-      stages.map((s) => api.query.councilElection[s]())
-    ).then((stages) => stages.map((stage) => stage.toJSON()));
-    durations.push(durations.reduce((a, b) => a + b, 0));
-    return { round, stageEndsAt, termEndsAt, stage, durations };
-  }
-
   async fetchCouncils() {
     const { data } = await axios.get(`${apiLocation}/v1/councils`);
     if (!data || data.error) return console.error(`failed to fetch from API`);
@@ -264,13 +283,16 @@ class App extends React.Component<IProps, IState> {
   }
 
   async fetchWorkingGroups(api: ApiPromise) {
-    const openings = {
-      curators: await this.fetchOpenings(api, "contentDirectory"),
-      storageProviders: await this.fetchOpenings(api, "storage"),
-      operationsGroup: await this.fetchOpenings(api, "operations"),
-      _lastUpdate: moment().valueOf(),
-    };
-    this.save("openings", openings);
+    const openingsUpdated = this.state.openings?._lastUpdate;
+    if (moment().valueOf() > moment(openingsUpdated).add(1, `hour`).valueOf()) {
+      const openings = {
+        curators: await this.fetchOpenings(api, "contentDirectory"),
+        storageProviders: await this.fetchOpenings(api, "storage"),
+        operationsGroup: await this.fetchOpenings(api, "operations"),
+        _lastUpdate: moment().valueOf(),
+      };
+      this.save("openings", openings);
+    }
 
     const lastUpdate = this.state.workers?._lastUpdate;
     if (lastUpdate && moment() < moment(lastUpdate).add(1, `hour`)) return;
@@ -283,6 +305,7 @@ class App extends React.Component<IProps, IState> {
     this.save("workers", workers);
     const council = await api.query.council.activeCouncil();
     this.save("council", council);
+    return workers;
   }
 
   async fetchOpenings(api: ApiPromise, wg: string) {
@@ -317,30 +340,27 @@ class App extends React.Component<IProps, IState> {
 
   async fetchApplications(api: ApiPromise, group: string, ids: number[]) {
     const { members } = this.state;
-    return Promise.all(
-      ids.map(async (wgApplicationId) => {
-        const wgApplication: ApplicationOf = (
-          await api.query[group].applicationById(wgApplicationId)
-        ).toJSON();
-        const account = wgApplication.role_account_id;
-        const openingId = wgApplication.opening_id;
-        const memberId: number = wgApplication.member_id;
-        const member = members.find((m) => +m.id === +memberId);
-        const handle = member ? member.handle : null;
-        const id = wgApplication.application_id;
-        const application = (
-          await api.query.hiring.applicationById(id)
-        ).toJSON();
-        return {
-          id,
-          account,
-          openingId,
-          memberId,
-          member: { handle },
-          application,
-        };
-      })
-    );
+    return ids.reduce(async (applications, wgApplicationId) => {
+      console.log(`fetching application ${wgApplicationId}`);
+      const wgApplication: ApplicationOf = (
+        await api.query[group].applicationById(wgApplicationId)
+      ).toJSON();
+      const account = wgApplication.role_account_id;
+      const openingId = wgApplication.opening_id;
+      const memberId: number = wgApplication.member_id;
+      const member = members.find((m) => +m.id === +memberId);
+      const handle = member ? member.handle : null;
+      const id = wgApplication.application_id;
+      const application = (await api.query.hiring.applicationById(id)).toJSON();
+      return applications.concat({
+        id,
+        account,
+        openingId,
+        memberId,
+        member: { handle },
+        application,
+      });
+    }, []);
   }
 
   async fetchWorkers(api: ApiPromise, wg: string) {
@@ -597,14 +617,10 @@ class App extends React.Component<IProps, IState> {
 
   async loadData() {
     const status = this.load("status");
-    if (status) {
-      console.debug(`Status`, status, `Version`, version);
-      if (status.version !== version) return this.clearData();
-      this.setState({ status });
-    }
+    if (status) this.setState({ status });
     console.debug(`Loading data`);
     this.loadMembers();
-    "assets providers councils council workers categories channels proposals posts threads  mints openings tokenomics transactions reports validators nominators stakes stars"
+    "assets providers councils council election workers categories channels proposals posts threads  mints openings tokenomics transactions reports validators nominators stakes stars"
       .split(" ")
       .map((key) => this.load(key));
   }
@@ -674,32 +690,30 @@ class App extends React.Component<IProps, IState> {
     );
   }
 
-  connectEndpoint() {
+  joyApi() {
     console.debug(`Connecting to ${wsLocation}`);
     const provider = new WsProvider(wsLocation);
-    ApiPromise.create({ provider, types }).then((api) =>
-      api.isReady.then(() => {
-        console.log(`Connected to ${wsLocation}`);
-        this.setState({ connected: true });
-        this.handleApi(api);
-      })
-    );
+    ApiPromise.create({ provider, types }).then(async (api) => {
+      await api.isReady;
+      console.log(`Connected to ${wsLocation}`);
+      this.setState({ connected: true });
+      this.handleApi(api);
+    });
   }
 
   async fetchFromApi() {
-    await this.fetchProposals();
-    await this.updateForum();
-    await this.fetchMembers();
-    await this.fetchCouncils();
-    await this.fetchStorageProviders();
-    await this.fetchAssets();
-    //await this.fetchFAQ();
+    this.fetchProposals();
+    this.updateForum();
+    this.fetchMembers();
+    this.fetchCouncils();
+    this.fetchStorageProviders();
+    this.fetchAssets();
+    //this.fetchFAQ();
   }
 
   componentDidMount() {
+    this.joyApi();
     this.loadData();
-    this.fetchFromApi();
-    this.connectEndpoint();
     setTimeout(() => this.fetchTokenomics(), 30000);
     //this.initializeSocket();
   }

+ 1 - 1
src/components/Councils/ApplicantVotes.tsx

@@ -7,7 +7,7 @@ import {
   Typography,
 } from "@material-ui/core";
 import { IApplicant, IVote } from "../../types";
-import { formatJoy } from "./election-status";
+import { formatJoy } from "./../../lib/util";
 import { tooltipStyles } from "./styles";
 import { electionStyles } from "./styles";
 

+ 1 - 1
src/components/Councils/BackerVote.tsx

@@ -1,6 +1,6 @@
 import { Grid, Typography, Chip, Divider } from "@material-ui/core";
 import { IVote } from "../../types";
-import { formatJoy } from "./election-status";
+import { formatJoy } from "./../../lib/util";
 import { electionStyles } from "./styles";
 
 const BackerVote = (props: { index: number; vote: IVote }) => {

+ 16 - 16
src/components/Councils/CouncilApplicant.tsx

@@ -2,43 +2,43 @@ import { Grid, Typography, Chip } from "@material-ui/core";
 import { IApplicant, IElectionState } from "../../types";
 import ApplicantVotes from "./ApplicantVotes";
 import { electionStyles } from "./styles";
-import { calculateOtherVotes, formatJoy } from "./election-status";
+import { calculateOtherVotes, formatJoy } from "../../lib/util";
 
-const CouncilApplicant = (props: { applicant: IApplicant; electionState: IElectionState, index: number }) => {
+const CouncilApplicant = (props: {
+  applicant: IApplicant;
+  election: IElectionState;
+  index: number;
+}) => {
   const classes = electionStyles();
-  const othersStake = calculateOtherVotes(
-    props.electionState.votes,
-    props.applicant
-  );
+  const { index, election, applicant } = props;
+  const { electionStake } = applicant;
+  const othersStake = calculateOtherVotes(election.votes, applicant);
   return (
     <Grid item lg={2} md={4} sm={6} xs={12}>
       <div className={classes.applicant}>
         <Typography variant="h6">
-          {`${props.index + 1}. ${props.applicant.member.handle}`}
+          {`${index + 1}. ${applicant.member.handle}`}
         </Typography>
         <Chip
           className={classes.stakeChip}
           label={`Total Stake: ${formatJoy(
-            Number(props.applicant.electionStake.new) +
-              Number(props.applicant.electionStake.transferred) +
-              othersStake
+            +electionStake.new + +electionStake.transferred + othersStake
           )}`}
-          color={props.index < props.electionState.councilSize ? "primary" : "default"}
+          color={index < election.councilSize ? "primary" : "default"}
         />
         <Chip
           className={classes.stakeChip}
           label={`Own Stake: ${formatJoy(
-            Number(props.applicant.electionStake.new) +
-              Number(props.applicant.electionStake.transferred)
+            +electionStake.new + +electionStake.transferred
           )}`}
-          color={props.index < props.electionState.councilSize ? "primary" : "default"}
+          color={index < election.councilSize ? "primary" : "default"}
         />
         <Chip
           className={classes.stakeChip}
           label={`Others Stake: ${formatJoy(othersStake)}`}
-          color={props.index < props.electionState.councilSize ? "primary" : "default"}
+          color={index < election.councilSize ? "primary" : "default"}
         />
-        <ApplicantVotes votes={props.electionState.votes} applicant={props.applicant} />
+        <ApplicantVotes votes={election.votes} applicant={applicant} />
       </div>
     </Grid>
   );

+ 1 - 1
src/components/Councils/CouncilSeat.tsx

@@ -7,7 +7,7 @@ import {
   Tooltip,
 } from "@material-ui/core";
 import { useState } from "react";
-import { formatJoy } from "./election-status";
+import { formatJoy } from "../../lib/util";
 import { electionStyles, tooltipStyles } from "./styles";
 import { BakerData, SeatData } from "./types";
 

+ 98 - 137
src/components/Councils/Election.tsx

@@ -13,7 +13,7 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 import { useEffect, useState } from "react";
 import { ElectionStatus } from "..";
 import { IState, IApplicant, IElectionState, IVote } from "../../types";
-import { calculateOtherVotes, getElectionStatus } from "./election-status";
+import { calculateOtherVotes } from "../../lib/util";
 import axios from "axios";
 import pako from "pako";
 import { electionStyles } from "./styles";
@@ -23,29 +23,53 @@ import { tasksEndpoint } from "../../config";
 import CouncilApplicant from "./CouncilApplicant";
 import BackerVote from "./BackerVote";
 
+const sortVotes = (votes: IVote[]) => {
+  return votes.sort((v1, v2) => {
+    if (`${v1.candidateHandle}` && !`${v2.candidateHandle}`) return -1;
+    else if (`${v2.candidateHandle}` && !`${v1.candidateHandle}`) return 1;
+    const v1Stake = Number(v1.newStake) + Number(v1.transferredStake);
+    const v2Stake = Number(v2.newStake) + Number(v2.transferredStake);
+    if (v1Stake === v2Stake) return 0;
+    return v1Stake > v2Stake ? -1 : 1;
+  });
+};
+
+const Applicants = (props: {
+  election: IElectionState;
+  applicants: IApplicant[];
+}) => {
+  const { applicants, election } = props;
+  return applicants.map((applicant, index: number) => (
+    <CouncilApplicant
+      applicant={applicant}
+      election={election}
+      index={index}
+      key={index}
+    />
+  ));
+};
+
+const ElectionVotes = (props: { votes: IVote[] }) => {
+  const { votes } = props;
+  return sortVotes(votes).map((vote, index: number) => {
+    return <BackerVote key={index} index={index} vote={vote} />;
+  });
+};
+
 const Election = (props: IState) => {
-  const { status, domain } = props;
   const classes = electionStyles();
+  const { block, round, stage, termEndsAt, domain, election } = props;
+  const { applicants, votes, councilSize } = election;
   const [error, setError] = useState(undefined);
-  const [isLoading, setIsLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
   const [rounds, setRounds] = useState({
     rounds: [],
   } as ICouncilRounds);
-  const [electionState, setElectionState] = useState({
-    applicants: [],
-    votes: [],
-    stage: {},
-    councilRound: 0,
-    councilSize: 16,
-  } as IElectionState);
 
   useEffect(() => {
-    setIsLoading(true);
     const councilJson = `${tasksEndpoint}/public/council.json.gz`;
     axios
-      .get(councilJson, {
-        responseType: "arraybuffer",
-      })
+      .get(councilJson, { responseType: "arraybuffer" })
       .then((response) => {
         try {
           const binData = new Uint8Array(response.data);
@@ -65,74 +89,24 @@ const Election = (props: IState) => {
       });
   }, []);
 
-  useEffect(() => {
-    setIsLoading(true);
-    let votes = [] as IVote[];
-    const sortByStake = (a: IApplicant, b: IApplicant) => {
-      const votesA = calculateOtherVotes(votes, a);
-      const votesB = calculateOtherVotes(votes, b);
-      const totalStakeA =
-        Number(a.electionStake.new) +
-        Number(a.electionStake.transferred) +
-        votesA;
-      const totalStakeB =
-        Number(b.electionStake.new) +
-        Number(b.electionStake.transferred) +
-        votesB;
-      return totalStakeA < totalStakeB ? 1 : -1;
-    };
-
-    getElectionStatus().then(
-      (state: IElectionState) => {
-        votes = state.votes;
-        setElectionState({
-          ...state,
-          votes: sortVotes(votes),
-          applicants: state.applicants.sort(sortByStake),
-        });
-        setIsLoading(false);
-        setError(undefined);
-      },
-      (err) => {
-        setError(err);
-        setIsLoading(false);
-      }
-    );
-  }, []);
-
-  const currentStage = () => {
-    if (electionState.stage) {
-      const keys = Object.keys(electionState.stage);
-      if (keys.length > 0) {
-        return (
-          <div className={classes.chips}>
-            <Chip label={`ROUND ${electionState.councilRound}`} color="primary" />
-            <Chip label={keys[0].toUpperCase()} color="secondary" />
-          </div>
-        );
-      }
-    } 
-    return "";
+  const sortByStake = (a: IApplicant, b: IApplicant) => {
+    const votesA = calculateOtherVotes(votes, a);
+    const votesB = calculateOtherVotes(votes, b);
+    const stakeA = +a.electionStake.new + +a.electionStake.transferred + votesA;
+    const stakeB = +b.electionStake.new + +b.electionStake.transferred + votesB;
+    return stakeA < stakeB ? 1 : -1;
   };
 
-  const sortVotes = (votes: IVote[]) => {
-    return votes.sort((v1, v2) => {
-      if (
-        `${v1.candidateHandle}` !== "undefined" &&
-        `${v2.candidateHandle}` === "undefined"
-      ) {
-        return -1;
-      } else if (
-        `${v2.candidateHandle}` !== "undefined" &&
-        `${v1.candidateHandle}` === "undefined"
-      ) {
-        return 1;
-      }
-      const v1Stake = Number(v1.newStake) + Number(v1.transferredStake);
-      const v2Stake = Number(v2.newStake) + Number(v2.transferredStake);
-      if (v1Stake === v2Stake) return 0;
-      return v1Stake > v2Stake ? -1 : 1;
-    });
+  const currentStage = () => {
+    if (!stage) return "";
+    const keys = Object.keys(stage);
+    if (!keys.length) return "";
+    return (
+      <div className={classes.chips}>
+        <Chip label={`ROUND ${round}`} color="primary" />
+        <Chip label={keys[0].toUpperCase()} color="secondary" />
+      </div>
+    );
   };
 
   return (
@@ -142,74 +116,61 @@ const Election = (props: IState) => {
         {currentStage()}
         {isLoading && <CircularProgress color="inherit" />}
       </Typography>
-      {error !== undefined && (
+      {error && (
         <Alert
           style={{ marginTop: 12 }}
           onClose={() => setError(undefined)}
           severity="error"
         >
-          {`Error loading election status: [${error}]`}
+          {`Error loading election status: ${JSON.stringify(error)}`}
         </Alert>
       )}
-      {!isLoading && (
-        <Grid className={classes.grid} item lg={12}>
-          <Accordion className={classes.acc}>
-            <AccordionSummary
-              className={classes.accSummary}
-              expandIcon={<ExpandMoreIcon style={{ color: "#fff" }} />}
-              aria-controls={`election-content`}
-              id={`election-header`}
-            >
-              <Typography variant="h6" className={classes.title}>
-                <ElectionStatus
-                  domain={domain}
-                  block={status.block?.id}
-                  election={status.election}
-                />
-                {`Ongoing Election (${
-                  electionState.applicants.length
-                } applicants${
-                  electionState.votes.length > 0
-                    ? `, ${electionState.votes.length} votes`
-                    : ""
-                }, max ${electionState.councilSize} seats)`}
-              </Typography>
-            </AccordionSummary>
-            <AccordionDetails>
-              <Grid container spacing={1} className={classes.applicants}>
-                <Grid item lg={12} className={classes.applicants}>
-                  <Grid container spacing={1} className={classes.applicants}>
-                    {electionState.applicants.map(
-                      (applicant: IApplicant, index: number) => {
-                        return (
-                          <CouncilApplicant
-                            applicant={applicant}
-                            electionState={electionState}
-                            index={index}
-                            key={index}
-                          />
-                        );
-                      }
-                    )}
-                  </Grid>
+
+      <Grid className={classes.grid} item lg={12}>
+        <Accordion className={classes.acc}>
+          <AccordionSummary
+            className={classes.accSummary}
+            expandIcon={<ExpandMoreIcon style={{ color: "#fff" }} />}
+            aria-controls={`election-content`}
+            id={`election-header`}
+          >
+            <Typography variant="h6" className={classes.title}>
+              <ElectionStatus
+                domain={domain}
+                block={block}
+                stage={stage}
+                termEndsAt={termEndsAt}
+              />
+              {`Ongoing Election (${applicants.length} applicants${
+                votes.length > 0 ? `, ${votes.length} votes` : ""
+              }, max ${councilSize} seats)`}
+            </Typography>
+          </AccordionSummary>
+          <AccordionDetails>
+            <Grid container spacing={1} className={classes.applicants}>
+              <Grid item lg={12} className={classes.applicants}>
+                <Grid container spacing={1} className={classes.applicants}>
+                  <Applicants
+                    applicants={applicants.sort(sortByStake)}
+                    election={election}
+                  />
                 </Grid>
-                {electionState.votes.length > 0 && <Grid item lg={12} className={classes.applicants}>
-                  <Typography variant="h5">{`Votes`}</Typography>
-                </Grid>}
+              </Grid>
+              {votes.length && (
                 <Grid item lg={12} className={classes.applicants}>
-                  <Grid container spacing={1} className={classes.applicants}>
-                    {electionState.votes.map((vote: IVote, index: number) => {
-                      return (
-                        <BackerVote key={index} index={index} vote={vote} />
-                      );
-                    })}
-                  </Grid>
+                  <Typography variant="h5">{`Votes`}</Typography>
+                </Grid>
+              )}
+              <Grid item lg={12} className={classes.applicants}>
+                <Grid container spacing={1} className={classes.applicants}>
+                  <ElectionVotes votes={votes} />
                 </Grid>
               </Grid>
-            </AccordionDetails>
-          </Accordion>
-        </Grid>
-      )}
+            </Grid>
+          </AccordionDetails>
+        </Accordion>
+      </Grid>
+
       <Divider />
       <CouncilRounds rounds={rounds} />
     </Grid>

+ 7 - 8
src/components/Dashboard/ElectionStatus.tsx → src/components/Councils/ElectionStatus.tsx

@@ -12,11 +12,10 @@ const timeLeft = (blocks: number) => {
 
 const Stage = (props: {
   block: number;
-  election: ElectionStage;
+  stage: ElectionStage;
   domain: string;
 }) => {
-  const { block, election, domain } = props;
-  const { stage, termEndsAt } = election;
+  const { block, stage, termEndsAt, domain } = props;
 
   if (!stage) {
     if (!block || !termEndsAt) return <span />;
@@ -53,14 +52,14 @@ const Stage = (props: {
 const Election = (props: {
   block: number;
   domain: string;
-  election: ElectionStage;
+  termEndsAt: number;
+  stage: ElectionStage;
 }) => {
-  const { domain, election, block } = props;
-
+  const { block, stage } = props;
   return (
     <div className="text-white float-right">
-      {block && election ? (
-        <Stage block={block} election={election} domain={domain} />
+      {block && stage ? (
+        <Stage {...props} />
       ) : (
         <Spinner animation="border" variant="dark" size="sm" />
       )}

+ 0 - 39
src/components/Councils/election-status.ts

@@ -1,39 +0,0 @@
-import { IApplicant, IElectionState, IVote } from "../../types";
-import { JoyApi } from "../../joyApi";
-import { PromiseAllObj } from "../ValidatorReport/utils";
-
-const api = new JoyApi();
-
-export async function getElectionStatus(): Promise<IElectionState> {
-  await api.init;
-  return (await PromiseAllObj({
-    applicants: await api.getCouncilApplicants(),
-    stage: await api.stage(),
-    councilRound: await api.councilRound(),
-    councilSize: await api.councilSize(),
-    votes: await api.getVotes(),
-  })) as IElectionState;
-}
-
-export const calculateOtherVotes = (votes: IVote[], applicant: IApplicant) =>
-  votes
-    .filter((v) => `${v.candidateHandle}` === `${applicant.member.handle}`)
-    .reduce((othersStake: Number, vote: IVote) => {
-      return (
-        Number(othersStake) +
-        Number(vote.newStake) +
-        Number(vote.transferredStake)
-      );
-    }, 0);
-
-export const formatJoy = (stake: number): String => {
-  if (stake >= 1000000) {
-    return `${(stake / 1000000).toFixed(4)} MJOY`;
-  }
-
-  if (stake >= 1000) {
-    return `${(stake / 1000).toFixed(4)} kJOY`;
-  }
-
-  return `${stake} JOY`;
-};

+ 29 - 29
src/components/Councils/types.tsx

@@ -1,32 +1,32 @@
 import { AccountId } from "@polkadot/types/interfaces";
 
 export interface SeatData {
-    accountId: AccountId;
-    member: MemberData;
-    ownStake: number;
-    backersStake: number;
-    jsgStake: number;
-    totalStake: number;
-    backers: BakerData[];
-  }
-  
-  export interface MemberData {
-    accountId: AccountId;
-    handle: string;
-    id: number;
-  }
-  
-  export interface BakerData {
-    member: MemberData;
-    stake: number;
-  }
-  
-  export interface CouncilRound {
-    round: number;
-    termEndsAt: number;
-    seats: SeatData[];
-  }
-  
-  export interface ICouncilRounds {
-    rounds: CouncilRound[];
-  }
+  accountId: AccountId;
+  member: MemberData;
+  ownStake: number;
+  backersStake: number;
+  jsgStake: number;
+  totalStake: number;
+  backers: BakerData[];
+}
+
+export interface MemberData {
+  accountId: AccountId;
+  handle: string;
+  id: number;
+}
+
+export interface BakerData {
+  member: MemberData;
+  stake: number;
+}
+
+export interface CouncilRound {
+  round: number;
+  termEndsAt: number;
+  seats: SeatData[];
+}
+
+export interface ICouncilRounds {
+  rounds: CouncilRound[];
+}

+ 4 - 3
src/components/Dashboard/Council.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { MemberBox, Spinner } from "..";
-import ElectionStatus from "./ElectionStatus";
+import ElectionStatus from "../Councils/ElectionStatus";
 import {
   Paper,
   Grid,
@@ -65,8 +65,9 @@ const CouncilGrid = (props: {
             <Typography variant="h6" className={classes.title}>
               <ElectionStatus
                 domain={domain}
-                block={status.block?.id}
-                election={election}
+                block={status?.block?.id}
+		stage={election?.stage}
+		termEndsAt={election?.termEndsAt}
               />
               Council
             </Typography>

+ 17 - 3
src/components/Routes/index.tsx

@@ -34,7 +34,7 @@ interface IProps extends IState {
 }
 
 const Routes = (props: IProps) => {
-  const { faq, proposals, toggleEditKpi } = props;
+  const { chain, faq, proposals, toggleEditKpi } = props;
 
   return (
     <div>
@@ -122,7 +122,12 @@ const Routes = (props: IProps) => {
               />
               <Route
                 path="/validator-report"
-                render={(routeprops) => <ValidatorReport />}
+                render={(routeprops) => (
+                  <ValidatorReport
+                    lastBlock={props.status?.block?.id}
+                    activeValidators={chain?.validators || []}
+                  />
+                )}
               />
               <Route
                 path="/storage"
@@ -145,7 +150,16 @@ const Routes = (props: IProps) => {
               <Route path="/faq" render={(routeprops) => <FAQ faq={faq} />} />
               <Route
                 path="/election"
-                render={(routeprops) => <Election {...props} />}
+                render={(routeprops) => (
+                  <Election
+                    block={props.status?.block?.id}
+                    round={props.status?.election?.round}
+                    stage={props.status?.election?.stage}
+                    termEndsAt={props.status?.election?.termEndsAt}
+                    domain={props.domain}
+                    election={props.election}
+                  />
+                )}
               />
               <Route
                 path="/kpi"

+ 0 - 13
src/components/ValidatorReport/get-status.ts

@@ -1,13 +0,0 @@
-import { JoyApi } from "../../joyApi";
-import { PromiseAllObj } from "./utils";
-
-const api = new JoyApi();
-
-export async function getChainState() {
-  await api.init;
-
-  return await PromiseAllObj({
-    finalizedBlockHeight: await api.finalizedBlockHeight(),
-    validators: await api.validatorsData(),
-  });
-}

+ 3 - 19
src/components/ValidatorReport/index.tsx

@@ -1,4 +1,3 @@
-import { getChainState } from "./get-status";
 import moment from "moment";
 import {
   Card,
@@ -60,12 +59,12 @@ const useStyles = makeStyles((theme: Theme) =>
 
 const oldChainStatsFileName = "validators-old-testnet.json.gz";
 const oldChainStatsLocation = `https://joystreamstats.live/static/${oldChainStatsFileName}`;
-const ValidatorReport = () => {
+
+const ValidatorReport = (props: {}) => {
+  const { lastBlock = 0, activeValidators = [] } = props;
   const dateFormat = "yyyy-MM-DD";
   const [oldChainLastDate, setOldChainLastDate] = useState(moment());
   const [oldChainPageSize, setOldChainPageSize] = useState(50);
-  const [activeValidators, setActiveValidators] = useState([]);
-  const [lastBlock, setLastBlock] = useState(0);
   const chains = ["Chain 4 - Babylon", "Chain 5 - Antioch"];
   const [chain, setChain] = useState(chains[1]);
   const [stash, setStash] = useState(
@@ -239,21 +238,6 @@ const ValidatorReport = () => {
     );
   };
 
-  useEffect(() => {
-    updateChainState();
-    const interval = setInterval(() => {
-      updateChainState();
-    }, 10000);
-    return () => clearInterval(interval);
-  }, []);
-
-  const updateChainState = () => {
-    getChainState().then((chainState) => {
-      setLastBlock(chainState.finalizedBlockHeight);
-      setActiveValidators(chainState.validators.validators);
-    });
-  };
-
   const handlePageChange = (page: number) => {
     if (report.totalCount > 0) {
       loadReport(page);

+ 0 - 14
src/components/ValidatorReport/utils.ts

@@ -1,14 +0,0 @@
-const fromEntries = (xs: [string | number | symbol, any][]) =>
-  xs.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
-
-export function 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));
-}

+ 1 - 1
src/components/index.ts

@@ -5,7 +5,7 @@ export { default as Calendar } from "./Calendar";
 export { default as Routes } from "./Routes";
 export { default as Councils } from "./Councils";
 export { default as Council } from "./Dashboard/Council";
-export { default as ElectionStatus } from "./Dashboard/ElectionStatus";
+export { default as ElectionStatus } from "./Councils/ElectionStatus";
 export { default as Curation } from "./Curation";
 export { default as Dashboard } from "./Dashboard";
 export { default as Forum } from "./Forum";

+ 0 - 187
src/joyApi.ts

@@ -1,187 +0,0 @@
-import { ApiPromise, WsProvider } from "@polkadot/api";
-import { types } from "@joystream/types";
-import { AccountId, Hash } from "@polkadot/types/interfaces";
-import { config } from "dotenv";
-import BN from "bn.js";
-import { Option, Vec } from "@polkadot/types";
-import { wsLocation } from "./config";
-import { MemberId } from "@joystream/types/members";
-import { Member, IApplicant, IVote } from "./types";
-import { PromiseAllObj } from "./components/ValidatorReport/utils";
-import { IElectionStake, SealedVote } from "@joystream/types/council";
-
-config();
-
-export class JoyApi {
-  endpoint: string;
-  isReady: Promise<ApiPromise>;
-  api!: ApiPromise;
-
-  constructor() {
-    this.endpoint = wsLocation;
-    this.isReady = (async () =>
-      await new ApiPromise({ provider: new WsProvider(wsLocation), types })
-        .isReadyOrError)();
-  }
-
-  get init(): Promise<JoyApi> {
-    return this.isReady.then((instance) => {
-      this.api = instance;
-      return this;
-    });
-  }
-
-  async finalizedHash() {
-    return this.api.rpc.chain.getFinalizedHead();
-  }
-
-  async councilRound(): Promise<Number> {
-    return Number((await this.api.query.councilElection.round()).toJSON());
-  }
-
-  async councilSize(): Promise<Number> {
-    return Number((await this.api.query.councilElection.councilSize()).toJSON());
-  }
-
-  async termEndsAt(): Promise<Number> {
-    return Number((await this.api.query.council.termEndsAt()).toJSON());
-  }
-
-  async stage(): Promise<{ [key: string]: Number }> {
-    const stage = (await this.api.query.councilElection.stage()).toJSON();
-    return stage as unknown as { [key: string]: Number };
-  }
-
-  async getCouncilApplicants(): Promise<IApplicant[]> {
-    const addresses: AccountId[] = (
-      await this.api.query.councilElection.applicants()
-    ).toJSON() as unknown as AccountId[];
-    const members = await Promise.all(
-      addresses.map(async (address) => {
-        return PromiseAllObj({
-          address: address,
-          memberId: await this.api.query.members.memberIdsByRootAccountId(
-            address as unknown as AccountId
-          ),
-        });
-      })
-    );
-    return (await Promise.all(
-      members.map(async (member) => {
-        const { memberId, address } = member;
-        const id = (memberId as unknown as MemberId[])[0] as MemberId;
-        return PromiseAllObj({
-          member: (await this.api.query.members.membershipById(
-            id
-          )) as unknown as Member,
-          electionStake: (
-            await this.api.query.councilElection.applicantStakes(address)
-          ).toJSON() as unknown as IElectionStake,
-        } as IApplicant);
-      })
-    )) as IApplicant[];
-  }
-
-  async getVotes(): Promise<IVote[]> {
-    const commitments: AccountId[] = (
-      await this.api.query.councilElection.commitments()
-    ).toJSON() as unknown as AccountId[];
-    const votes = await Promise.all(
-      commitments.map(async (hash) => {
-        const vote = (await this.api.query.councilElection.votes(
-          hash
-        )) as unknown as SealedVote;
-        const newStake = vote.stake.new;
-        const transferredStake = vote.stake.transferred;
-        const voterId = await this.api.query.members.memberIdsByRootAccountId(
-          vote.voter
-        );
-        const voterMembership = (await this.api.query.members.membershipById(
-          voterId
-        )) as unknown as Member;
-        const voterHandle = voterMembership.handle;
-        const candidateId = `${vote.vote}`;
-        if (
-          candidateId === "" ||
-          candidateId === null ||
-          candidateId === undefined
-        ) {
-          return {
-            voterHandle: voterHandle,
-            voterId: voterId as unknown as Number,
-            newStake: newStake as unknown as Number,
-            transferredStake: transferredStake as unknown as Number,
-          } as IVote;
-        } else {
-          const voteId = await this.api.query.members.memberIdsByRootAccountId(
-            candidateId
-          );
-          const voteMembership = (await this.api.query.members.membershipById(
-            voteId
-          )) as unknown as Member;
-          const voteHandle = voteMembership.handle;
-          return {
-            voterHandle: voterHandle,
-            voterId: voterId as unknown as Number,
-            candidateHandle: voteHandle,
-            candidateId: voteId as unknown as Number,
-            newStake: newStake as unknown as Number,
-            transferredStake: transferredStake as unknown as Number,
-          } as IVote;
-        }
-      })
-    );
-    return votes;
-  }
-
-  async finalizedBlockHeight() {
-    const finalizedHash = await this.finalizedHash();
-    const { number } = await this.api.rpc.chain.getHeader(`${finalizedHash}`);
-    return number.toNumber();
-  }
-
-  async findActiveValidators(
-    hash: Hash,
-    searchPreviousBlocks: boolean
-  ): Promise<AccountId[]> {
-    const block = await this.api.rpc.chain.getBlock(hash);
-
-    let currentBlockNr = block.block.header.number.toNumber();
-    let activeValidators;
-    do {
-      let currentHash = (await this.api.rpc.chain.getBlockHash(
-        currentBlockNr
-      )) as Hash;
-      let allValidators = (await this.api.query.staking.snapshotValidators.at(
-        currentHash
-      )) as Option<Vec<AccountId>>;
-      if (!allValidators.isEmpty) {
-        let max = (
-          await this.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;
-  }
-
-  async validatorsData() {
-    const validators = await this.api.query.session.validators();
-    const era = await this.api.query.staking.currentEra();
-    const totalStake = era.isSome
-      ? await this.api.query.staking.erasTotalStake(era.unwrap())
-      : new BN(0);
-
-    return {
-      count: validators.length,
-      validators: validators.toJSON(),
-      total_stake: totalStake.toNumber(),
-    };
-  }
-}

+ 188 - 0
src/lib/election.ts

@@ -0,0 +1,188 @@
+import { ApiPromise } from "@polkadot/api";
+import { AccountId, Hash } from "@polkadot/types/interfaces";
+import BN from "bn.js";
+import { Option, Vec } from "@polkadot/types";
+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 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();
+  let stageEndsAt = 0;
+  if (stage) {
+    const key = Object.keys(stage)[0];
+    stageEndsAt = stage[key];
+  }
+
+  const stages = [
+    "announcingPeriod",
+    "votingPeriod",
+    "revealingPeriod",
+    "newTermDuration",
+  ];
+
+  let durations = await Promise.all(
+    stages.map((s) => api.query.councilElection[s]())
+  ).then((stages) => stages.map((stage) => stage.toJSON()));
+  durations.push(durations.reduce((a, b) => a + b, 0));
+  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[]> => {
+  const addresses: AccountId[] = (
+    await api.query.councilElection.applicants()
+  ).toJSON() as unknown as AccountId[];
+  const members = await Promise.all(
+    addresses.map(async (address) => {
+      return PromiseAllObj({
+        address: address,
+        memberId: await api.query.members.memberIdsByRootAccountId(
+          address as unknown as AccountId
+        ),
+      });
+    })
+  );
+  return (await Promise.all(
+    members.map(async (member) => {
+      const { memberId, address } = member;
+      const id = (memberId as unknown as MemberId[])[0] as MemberId;
+      return PromiseAllObj({
+        member: (await api.query.members.membershipById(
+          id
+        )) as unknown as Member,
+        electionStake: (
+          await api.query.councilElection.applicantStakes(address)
+        ).toJSON() as unknown as IElectionStake,
+      } as IApplicant);
+    })
+  )) as IApplicant[];
+};
+
+export const getVotes = async (api: ApiPromise): Promise<IVote[]> => {
+  const commitments: AccountId[] = (
+    await api.query.councilElection.commitments()
+  ).toJSON() as unknown as AccountId[];
+  const votes = await Promise.all(
+    commitments.map(async (hash) => {
+      const vote = (await api.query.councilElection.votes(
+        hash
+      )) as unknown as SealedVote;
+      const newStake = vote.stake.new;
+      const transferredStake = vote.stake.transferred;
+      const voterId = await api.query.members.memberIdsByRootAccountId(
+        vote.voter
+      );
+      const voterMembership = (await api.query.members.membershipById(
+        voterId
+      )) as unknown as Member;
+      const voterHandle = voterMembership.handle;
+      const candidateId = `${vote.vote}`;
+      if (
+        candidateId === "" ||
+        candidateId === null ||
+        candidateId === undefined
+      ) {
+        return {
+          voterHandle: voterHandle,
+          voterId: voterId as unknown as Number,
+          newStake: newStake as unknown as Number,
+          transferredStake: transferredStake as unknown as Number,
+        } as IVote;
+      } else {
+        const voteId = await api.query.members.memberIdsByRootAccountId(
+          candidateId
+        );
+        const voteMembership = (await api.query.members.membershipById(
+          voteId
+        )) as unknown as Member;
+        const voteHandle = voteMembership.handle;
+        return {
+          voterHandle: voterHandle,
+          voterId: voterId as unknown as Number,
+          candidateHandle: voteHandle,
+          candidateId: voteId as unknown as Number,
+          newStake: newStake as unknown as Number,
+          transferredStake: transferredStake as unknown as Number,
+        } as 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(),
+  };
+};

+ 44 - 3
src/lib/util.ts

@@ -1,9 +1,9 @@
-import { Options, Proposals } from "../types";
+import { Options, Proposals, IApplicant, IVote } from "../types";
 import moment from "moment";
 
 export const parseArgs = (args: string[]): Options => {
   const inArgs = (term: string): boolean => {
-    return args.find(a => a.search(term) > -1) ? true : false;
+    return args.find((a) => a.search(term) > -1) ? true : false;
   };
 
   const options: Options = {
@@ -11,7 +11,7 @@ export const parseArgs = (args: string[]): Options => {
     channel: inArgs("--channel"),
     council: inArgs("--council"),
     forum: inArgs("--forum"),
-    proposals: inArgs("--proposals")
+    proposals: inArgs("--proposals"),
   };
 
   if (options.verbose > 1) console.debug("args", args, "\noptions", options);
@@ -59,3 +59,44 @@ export const exit = (log: (s: string) => void) => {
   log("\nNo connection, exiting.\n");
   process.exit();
 };
+
+// Election
+
+export const calculateOtherVotes = (votes: IVote[], applicant: IApplicant) =>
+  votes
+    .filter((v) => `${v.candidateHandle}` === `${applicant.member.handle}`)
+    .reduce((othersStake: Number, vote: IVote) => {
+      return (
+        Number(othersStake) +
+        Number(vote.newStake) +
+        Number(vote.transferredStake)
+      );
+    }, 0);
+
+export const formatJoy = (stake: number): String => {
+  if (stake >= 1000000) {
+    return `${(stake / 1000000).toFixed(4)} MJOY`;
+  }
+
+  if (stake >= 1000) {
+    return `${(stake / 1000).toFixed(4)} kJOY`;
+  }
+
+  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));
+};

+ 0 - 2
src/types.ts

@@ -61,8 +61,6 @@ export interface IApplicant {
 export interface IElectionState {
   applicants: IApplicant[];
   votes: IVote[];
-  stage: { [key: string]: Number };
-  councilRound: Number;
   councilSize: Number;
 }