ソースを参照

Tokenomics: WG rewards + stakes

Joystream Stats 2 年 前
コミット
bb7d4b6f79

+ 73 - 11
src/App.tsx

@@ -6,6 +6,7 @@ import { Modals, Routes, Loading, Footer, Status } from "./components";
 import * as get from "./lib/getters";
 import { domain, apiLocation, wsLocation } from "./config";
 import axios from "axios";
+import moment from "moment";
 
 import { Api, IState } from "./types";
 import { types } from "@joystream/types";
@@ -25,7 +26,7 @@ const initialState = {
   blocks: [],
   nominators: [],
   validators: [],
-  mints: {},
+  mints: [],
   channels: [],
   posts: [],
   councils: [],
@@ -43,6 +44,7 @@ const initialState = {
   showStatus: false,
   editKpi: false,
   status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },
+  groups: [],
 };
 
 class App extends React.Component<IProps, IState> {
@@ -64,18 +66,18 @@ class App extends React.Component<IProps, IState> {
       this.handleBlock(api, head)
     );
     this.fetchMints(api, [2, 3, 4]);
+    this.fetchWorkingGroups(api);
     this.updateStatus(api);
   }
 
   async fetchMints(api: Api, ids: number[]) {
     console.debug(`Fetching mints`);
-    let mints = {};
-
-    ids.map(
-      async (id) => (mints[id] = (await api.query.minting.mints(id)).toJSON())
-    );
-
-    this.save(`mints`, mints);
+    let mints = [];
+    Promise.all(
+      ids.map(
+        async (id) => (mints[id] = (await api.query.minting.mints(id)).toJSON())
+      )
+    ).then(() => this.save(`mints`, mints));
   }
 
   async fetchAssets() {
@@ -152,11 +154,11 @@ 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();
     }
-    if (!status.lastReward) this.fetchLastReward(api);
   }
 
   async updateStatus(api: Api, id = 0) {
@@ -260,6 +262,66 @@ class App extends React.Component<IProps, IState> {
     this.save("proposals", data);
   }
 
+  async fetchWorkingGroups(api: ApiPromise) {
+    const lastUpdate = this.state.workers?._lastUpdate;
+    if (lastUpdate && moment() < moment(lastUpdate).add(1, `hour`)) return;
+    const workers = {
+      content: await this.fetchWorkers(api, "contentDirectory"),
+      storage: await this.fetchWorkers(api, "storage"),
+      operations: await this.fetchWorkers(api, "operations"),
+      _lastUpdate: moment().valueOf(),
+    };
+    this.save("workers", workers);
+    const council = await api.query.council.activeCouncil();
+    this.save("council", council);
+  }
+
+  async fetchWorkers(api: ApiPromise, wg: string) {
+    const group = wg + "WorkingGroup";
+    const { members } = this.state;
+    let workers = [];
+    const count = (
+      (await api.query[group].nextWorkerId()) as WorkerId
+    ).toNumber();
+    const lead = await api.query[group].currentLead();
+    console.debug(`Fetching ${count} ${wg} workers`);
+    for (let id = 0; id < count; ++id) {
+      const isLead = id === +lead;
+      const worker: WorkerOf = await api.query[group].workerById(id);
+      if (!worker.is_active) continue;
+      const memberId = worker.member_id.toJSON();
+      const member: Membership = members.find((m) => m.id === memberId);
+      const handle = member?.handle;
+      let stake: Stake;
+      let reward: RewardRelationship;
+
+      if (worker.role_stake_profile.isSome) {
+        const roleStakeProfile = worker.role_stake_profile.unwrap();
+        const stakeId = roleStakeProfile.stake_id;
+        const { staking_status } = (
+          await api.query.stake.stakes(stakeId)
+        ).toJSON();
+        stake = staking_status?.staked?.staked_amount;
+      }
+
+      if (worker.reward_relationship.isSome) {
+        const rewardId = worker.reward_relationship.unwrap();
+        reward = (
+          await api.query.recurringRewards.rewardRelationships(rewardId)
+        ).toJSON();
+      }
+      workers.push({
+        id,
+        memberId,
+        handle,
+        stake,
+        reward,
+        isLead,
+      });
+    }
+    return workers;
+  }
+
   // forum
   updateForum() {
     this.fetchPosts();
@@ -475,7 +537,7 @@ class App extends React.Component<IProps, IState> {
     }
     console.debug(`Loading data`);
     this.loadMembers();
-    "assets providers councils categories channels proposals posts threads  mints tokenomics transactions reports validators nominators stakes stars"
+    "assets providers councils council workers categories channels proposals posts threads  mints tokenomics transactions reports validators nominators stakes stars"
       .split(" ")
       .map((key) => this.load(key));
   }
@@ -564,7 +626,7 @@ class App extends React.Component<IProps, IState> {
     await this.fetchCouncils();
     await this.fetchStorageProviders();
     await this.fetchAssets();
-    await this.fetchFAQ();
+    //await this.fetchFAQ();
   }
 
   componentDidMount() {

+ 3 - 7
src/components/Loading.tsx

@@ -1,17 +1,13 @@
 import React from "react";
 import { Button, Spinner } from "react-bootstrap";
 import { Grid } from "@material-ui/core";
-import {GridSize} from "@material-ui/core/Grid/Grid";
+import { GridSize } from "@material-ui/core/Grid/Grid";
 
-const Loading = (props: { target?: string, gridSize?: GridSize }) => {
+const Loading = (props: { target?: string; gridSize?: GridSize }) => {
   const { gridSize, target } = props;
   const title = target ? `Fetching ${target}` : "Connecting to Websocket";
   return (
-    <Grid
-      style={{ textAlign: "center", backgroundColor: "#000", color: "#fff" }}
-      lg={gridSize ? gridSize : 6}
-      item
-    >
+    <Grid style={{ textAlign: "center" }} lg={gridSize ? gridSize : 6} item>
       <Button variant="warning" className="m-1 py-0 mr-2 mt-3">
         <Spinner animation="border" variant="dark" size="sm" className="mr-1" />
         {title}

+ 0 - 1
src/components/Mint/config.ts

@@ -2,7 +2,6 @@ export const getStart = (round: number) => 57601 + round * 201600;
 export const getEnd = (round: number) => 57601 + (round + 1) * 201600;
 
 export const payoutInterval = 3600;
-export const mintTags = [``, ``, `Storage`, `Curators`, `Operators`];
 export const salaries: { [key: string]: number } = {
   storageLead: 21000,
   storageProvider: 10500,

+ 2 - 28
src/components/Mint/index.tsx

@@ -2,14 +2,7 @@ import React from "react";
 import ValidatorRewards from "./ValidatorRewards";
 import Loading from "../Loading";
 
-// TODO OPTIMIZE fetch live
-import {
-  getStart,
-  getEnd,
-  payoutInterval,
-  mintTags,
-  salaries,
-} from "./config.ts";
+import { getStart, getEnd, payoutInterval, salaries } from "./config.ts";
 
 interface IProps {
   mints: any[];
@@ -50,16 +43,8 @@ class Mint extends React.Component<IProps, IState> {
     this.setState({ [e.target.name]: parseInt(e.target.value) });
   }
 
-  formatMint(mint: { capacity: number; total_minted: number }) {
-    if (!mint) return `loading ..`;
-    const { capacity, total_minted } = mint;
-    const current = (capacity / 1000000).toFixed(1);
-    const total = (total_minted / 1000000).toFixed(1);
-    return `${current} M of ${total} M tJOY`;
-  }
-
   render() {
-    const { status, tokenomics, validators, payout, mints } = this.props;
+    const { status, tokenomics, validators, payout } = this.props;
     if (!tokenomics) return <Loading target="tokenomics" />;
     if (!status.council) return <Loading target="council round" />;
 
@@ -74,17 +59,6 @@ class Mint extends React.Component<IProps, IState> {
 
     return (
       <div className="p-3 text-light">
-        <h2 className="mb-3">Mints</h2>
-
-        <div>
-          {[2, 3, 4].map((m) => (
-            <div key={m} className="d-flex d-row p-1 m-1">
-              <div className="mint-label col-2">{mintTags[m]}</div>
-              <div>{this.formatMint(mints[m])}</div>
-            </div>
-          ))}
-        </div>
-
         <h3 className="my-3">Rewards</h3>
         <div className="form-group">
           <label>Token value</label>

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

@@ -34,7 +34,7 @@ interface IProps extends IState {
 }
 
 const Routes = (props: IProps) => {
-  const { faq, reports, tokenomics, toggleEditKpi } = props;
+  const { faq, toggleEditKpi } = props;
 
   return (
     <div>
@@ -50,8 +50,16 @@ const Routes = (props: IProps) => {
                 render={(routeprops) => (
                   <Tokenomics
                     {...routeprops}
-                    reports={reports}
-                    tokenomics={tokenomics}
+                    validators={{
+                      count: props.validators?.length,
+                      reward: props.status?.lastReward,
+                      stakes: props.stakes,
+                    }}
+                    mints={props.mints}
+                    council={props.council}
+                    reports={props.reports}
+                    tokenomics={props.tokenomics}
+                    workers={props.workers}
                   />
                 )}
               />

+ 3 - 1
src/components/Tokenomics/Burns.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import Chart from "../Chart";
+import { Link } from "react-router-dom";
 import { Exchange } from "../../types";
 
 const Burns = (props: {
@@ -19,7 +20,7 @@ const Burns = (props: {
 
   return (
     <div className="p-5">
-      <h3 >Burns</h3>
+      <h2 className="m-3 text-center">Burns</h2>
       <Chart
         data={data}
         x="time"
@@ -35,6 +36,7 @@ const Burns = (props: {
       <div className="my-1 text-left">
         Total Amount Burned: {executed} M JOY
       </div>
+      <Link to={`/burners`}>Top Burners</Link>
     </div>
   );
 };

+ 10 - 10
src/components/Tokenomics/DollarPoolChanges/index.tsx

@@ -11,11 +11,11 @@ const DollarPoolChanges = (props: {
       <div className="d-flex flex-row font-weight-bold">
         <div className="col-1">Block</div>
         <div className="col-2 text-right">Date</div>
-        <div className="col-1 text-right">Change</div>
+        <div className="col-1 text-right">Change ($)</div>
         <div className="col-3">Reason</div>
-        <div className="col-1 text-right">Dollar Pool</div>
-        <div className="col-1 text-right">Rate</div>
-        <div className="col-1 text-right">Issuance</div>
+        <div className="col-1 text-right">FiatPool</div>
+        <div className="col-1 text-right">Rate ($/M)</div>
+        <div className="col-1 text-right">Issuance (M)</div>
       </div>
 
       {props.dollarPoolChanges
@@ -42,18 +42,18 @@ const PoolChangeRow = (props: DollarPoolChange) => {
   const color = change > 0 ? "text-success" : "text-danger";
   return (
     <div className={`d-flex flex-row ${color}`}>
-      <a className="col-1" href={`${domain}/#/explorer/query/${blockHeight}`}>
+      <a className="col-1 text-dark" href={`${domain}/#/explorer/query/${blockHeight}`}>
         #{blockHeight}
       </a>
-      <div className="col-2 text-right">{blockTime}</div>
-      <div className="col-1 text-right">{change.toFixed(2)} $</div>
+      <div className="col-2 text-right">{blockTime.split(`T`)[0]}</div>
+      <div className="col-1 text-right">{change.toFixed(2)}</div>
       <div className="col-3">{reason}</div>
-      <div className="col-1 text-right">{valueAfter.toFixed()} $</div>
+      <div className="col-1 text-right">{valueAfter.toFixed()}</div>
       <div className="col-1 text-right">
-        {(rateAfter * 1000000).toFixed(1)} $ / M
+        {(rateAfter * 1000000).toFixed(1)}
       </div>
       <div className="col-1 text-right">
-        {(issuance / 1000000).toFixed(1)} M
+        {(issuance / 1000000).toFixed(1)}
       </div>
     </div>
   );

+ 96 - 0
src/components/Tokenomics/Groups.tsx

@@ -0,0 +1,96 @@
+import { Table } from "react-bootstrap";
+import { Link } from "react-router-dom";
+import Loading from "../Loading";
+
+const mintTags = [``, ``, `Storage`, `Content`, `Operations`];
+const mJoy = (joy: number) => (joy ? (joy / 1000000).toFixed(3) : ``);
+const formatMint = (mint: { capacity: number; total_minted: number }) => {
+  if (!mint) return `loading ..`;
+  const { capacity, total_minted } = mint;
+  const current = (capacity / 1000000).toFixed(1);
+  const total = (total_minted / 1000000).toFixed(1);
+  return `${current} M of ${total} M tJOY`;
+};
+
+const Mints = (props: { mints: number[] }) => {
+  const { mints, workers } = props;
+  if (!mints?.length) return <div />;
+  return (
+    <div className="mt-3">
+      <h2 className="m-3 text-center">Working Groups</h2>
+      <div className="d-flex flex-wrap">
+        <Group workers={workers?.storage} mint={mints[2]} tag={mintTags[2]} />
+        <Group workers={workers?.content} mint={mints[3]} tag={mintTags[3]} />
+        <Group
+          workers={workers?.operations}
+          mint={mints[4]}
+          tag={mintTags[4]}
+        />
+      </div>
+    </div>
+  );
+};
+
+const Group = (props: { workers: any[]; mint: {}; tag: string }) => {
+  const { workers, mint, tag } = props;
+  if (!workers)
+    return (
+      <div className=" p-3 col-lg-4">
+        <h3 className="m-3 text-center">{tag}</h3>
+        <div className="text-center">
+          <b>Mint:</b> {formatMint(mint)}
+        </div>
+        <Loading target="workers" />
+      </div>
+    );
+
+  const rewards = workers.reduce(
+    (sum, { reward }) => sum + (reward?.amount_per_payout || 0),
+    0
+  );
+  const stakes = workers.reduce((sum, { stake }) => sum + (stake || 0), 0);
+
+  return (
+    <div className=" p-3 col-lg-4">
+      <h3 className="m-3 text-center">{tag}</h3>
+      <div className="text-center">
+        <b>Mint:</b> {formatMint(mint)}
+      </div>
+      <Table>
+        <thead>
+          <tr>
+            <th>ID</th>
+            <th>Member</th>
+            <th>Term Reward [MJOY]</th>
+            <th>Stake [MJOY]</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td></td>
+            <td>
+              <b>Total</b>
+            </td>
+            <td>{mJoy(rewards * 28)}</td>
+            <td>{mJoy(stakes)}</td>
+          </tr>
+
+          {workers.map((w) => (
+            <tr key={w.id}>
+              <td>{w.id}</td>
+              <td>
+                <Link className="text-dark" to={`/members/${w.handle}`}>
+                  {w.handle}
+                </Link>
+              </td>
+              <td>{mJoy(w.reward?.amount_per_payout * 28)}</td>
+              <td>{mJoy(w.stake)}</td>
+            </tr>
+          ))}
+        </tbody>
+      </Table>
+    </div>
+  );
+};
+
+export default Mints;

+ 115 - 8
src/components/Tokenomics/Overview.tsx

@@ -2,22 +2,65 @@ import React from "react";
 import { Table } from "react-bootstrap";
 import { Tokenomics } from "../../types";
 
-const Overview = (props: { tokenomics: Tokenomics }) => {
-  const { price, totalIssuance, validators } = props;
+const mJoy = (joy: number) =>
+  joy ? (joy / 1000000).toFixed(3) + ` M JOY` : `Loading ..`;
+
+const Overview = (props: { groups: any[]; tokenomics: Tokenomics }) => {
+  const { groups, tokenomics } = props;
+  const { price, totalIssuance, dollarPool } = tokenomics;
+  const validators = groups.find((g) => g.id === "validators");
+  const council = groups.find((g) => g.id === "council");
+  const budget = dollarPool.replenishAmount;
+  const minted = groups.reduce((sum, { earning }) => sum + +earning, 0);
+  const staked = groups.reduce((sum, { stake }) => sum + +stake, 0);
+
   return (
-    <Table>
+    <Table className="w-50 m-auto">
       <tbody>
+        <tr>
+          <td>Exchange Rate</td>
+          <td>{(+price * 1000000).toFixed(2)} $/M JOY</td>
+        </tr>
         <tr>
           <td>Total Issuance</td>
-          <td>{totalIssuance} JOY</td>
+          <td>{mJoy(totalIssuance)} </td>
+        </tr>
+        <tr>
+          <td>Fiat Pool</td>
+          <td>${dollarPool.size.toFixed(2)}</td>
+        </tr>
+        <tr>
+          <td>Weekly Budget</td>
+          <Budget budget={budget} pool={dollarPool.size} />
         </tr>
         <tr>
-          <td>Validator Stake</td>
-          <td>{validators.total_stake} JOY</td>
+          <td>Weekly Minting</td>
+          <Minted minted={minted} issuance={totalIssuance} />
         </tr>
         <tr>
-          <td>Price</td>
-          <td>{(+price * 1000000).toFixed(2)} $ / 1 M JOY</td>
+          <td>Weekly Mint Value</td>
+          <MintedValue value={minted * +price} budget={budget} />
+        </tr>
+        <tr>
+          <td>Weekly Inflation</td>
+          <Inflation
+            budget={budget}
+            pool={dollarPool.size}
+            minted={minted}
+            issuance={totalIssuance}
+          />
+        </tr>
+        <tr>
+          <td>Total Stake</td>
+          <td>{mJoy(staked)}</td>
+        </tr>
+        <tr>
+          <td>Staked Value</td>
+          <StakedValue
+            staked={staked}
+            price={+price}
+            issuance={totalIssuance}
+          />
         </tr>
       </tbody>
     </Table>
@@ -25,3 +68,67 @@ const Overview = (props: { tokenomics: Tokenomics }) => {
 };
 
 export default Overview;
+
+const Budget = (props: { budget: number; pool: value }) => {
+  const { budget, pool } = props;
+  if (!budget) return <td>Loading ..</td>;
+  const budgetPercent = ((100 * budget) / pool).toFixed(2);
+  return (
+    <>
+      <td>${budget}</td>
+      <td>{budgetPercent}% / Fiat Pool</td>
+    </>
+  );
+};
+
+const Minted = (props: { minted: numbr; issuance: number }) => {
+  const { minted, issuance } = props;
+  if (!minted) return <td>Loading ..</td>;
+  const mintedPercent = ((100 * minted) / issuance).toFixed(2);
+  return (
+    <>
+      <td>{mJoy(minted)}</td>
+      <td>{mintedPercent}% / Issuance</td>
+    </>
+  );
+};
+
+const MintedValue = (props: { value: numbr; budget: number }) => {
+  const { value, budget } = props;
+  if (!value) return <td>Loading ..</td>;
+  const percent = (100 * value) / budget;
+  return (
+    <>
+      <td>${value.toFixed(2)}</td>
+      <td>{percent.toFixed(2)}% / Budget</td>
+    </>
+  );
+};
+
+const StakedValue = (props: {
+  staked: number;
+  price: number;
+  issuance: number;
+}) => {
+  const { staked, price, issuance } = props;
+  if (!staked) return <td>Loading ..</td>;
+  const percent = (100 * staked) / issuance;
+  return (
+    <>
+      <td>${(staked * price).toFixed(2)}</td>
+      <td>{percent.toFixed(1)}% / Issuance</td>
+    </>
+  );
+};
+
+const Inflation = (props: {
+  budget: number;
+  pool: number;
+  minted: number;
+  issuance: number;
+}) => {
+  const { budget, pool, minted, issuance } = props;
+  if (!minted) return <td>Loading ..</td>;
+  const inflation = 100 * (minted / issuance - budget / pool);
+  return <td>{inflation.toFixed(2)}%</td>;
+};

+ 49 - 0
src/components/Tokenomics/Spending.tsx

@@ -0,0 +1,49 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+import Loading from "../Loading";
+import { RoleSpending } from "../../types";
+
+const mJoy = (joy: number) => (joy / 1000000).toFixed(3);
+const percent = (joy, total) => ((100 * joy) / total).toFixed(2);
+const Spending = (props: { roles: RoleSpending[] }) => {
+  const { groups, price } = props;
+  if (!groups.length) return <div />;
+
+  const minted = groups.reduce((sum, { earning }) => earning + sum, 0);
+  const staked = groups.reduce((sum, { stake }) => stake + sum, 0);
+  return (
+    <div className="mt-3">
+      <h2 className="m-3 text-center"> Spending and Stake</h2>
+      <Table className="px-3">
+        <thead>
+          <tr>
+            <th>Role</th>
+            <th>Actors</th>
+            <th>Earning [MJOY]</th>
+            <th>Earning [USD]</th>
+            <th>Earning [%]</th>
+            <th>Stake [MJOY]</th>
+            <th>Stake [USD]</th>
+            <th>Stake [%]</th>
+          </tr>
+        </thead>
+        <tbody>
+          {groups.map((role, key: number) => (
+            <tr key={key}>
+              <td>{role.name}</td>
+              <td>{role.actors}</td>
+              <td>{mJoy(role.earning)}</td>
+              <td>{(role.earning * price).toFixed(2)}</td>
+              <td>{percent(role.earning, minted)}</td>
+              <td>{mJoy(role.stake)}</td>
+              <td>{(role.stake * price).toFixed(2)}</td>
+              <td>{percent(role.stake, staked)}</td>
+            </tr>
+          ))}
+        </tbody>
+      </Table>
+    </div>
+  );
+};
+
+export default Spending;

+ 1 - 1
src/components/Tokenomics/TokenValue.tsx

@@ -18,7 +18,7 @@ const TokenValue = (props: { tokenValue: any }) => {
 
   return (
     <div className="p-5 text-center">
-      <h3>Token Value</h3>
+      <h2 className="m-3 text-center">Token Value</h2>
 
       <Chart
         data={Object.values(tokenValue).sort((a, b) => a.date - b.date)}

+ 11 - 5
src/components/Tokenomics/index.tsx

@@ -5,7 +5,10 @@ import Overview from "./Overview";
 import DollarPoolChanges from "./DollarPoolChanges";
 import ReportBrowser from "./ReportBrowser";
 import TokenValue from "./TokenValue";
+import Spending from "./Spending";
+import Groups from "./Groups";
 import Loading from "../Loading";
+import { groupsMinting } from "./lib";
 
 import { Tokenomics } from "../../types";
 
@@ -16,14 +19,17 @@ interface IProps {
 }
 
 const TokenStats = (props: IProps) => {
-  const { reports, tokenomics } = props;
-  if (!tokenomics) return <Loading target="tokenomics" />;
+  if (!props.tokenomics) return <Loading target="tokenomics" />;
+  const { reports, tokenomics, council, mints, workers, validators } = props;
   const { dollarPoolChanges, exchanges, extecutedBurnsAmount } = tokenomics;
+  const groups = groupsMinting(council, workers, validators);
 
   return (
-    <Paper className="d-flex flex-column flex-grow-1 p-2">
-      <h2>Tokenomics</h2>
-      <Overview {...tokenomics} />
+    <Paper className="d-flex flex-column flex-grow-1 p-2 px-3 m-auto">
+      <h1 className="m-3 text-center">Tokenomics</h1>
+      <Overview groups={groups} tokenomics={tokenomics} />
+      <Groups mints={mints} workers={workers} />
+      <Spending groups={groups} price={tokenomics.price} />
       <Burns
         exchanges={exchanges}
         extecutedBurnsAmount={extecutedBurnsAmount}

+ 85 - 0
src/components/Tokenomics/lib.ts

@@ -0,0 +1,85 @@
+export const groupsMinting = (council, workers, validators) => {
+  if (!council || !workers || !validators) return [];
+
+  const termReward = (reward): number =>
+    reward
+      ? reward.payout_interval
+        ? (+reward.amount_per_payout / +reward.payout_interval) * 600 * 24 * 7
+        : +reward.amount_per_payout * 4 * 7
+      : 0;
+
+  const workerRewards = (workers: []): number =>
+    workers.reduce((sum, { reward }) => sum + termReward(reward), 0);
+  const workerStakes = (workers: []): number =>
+    workers.reduce((sum, { stake }) => sum + (stake || 0), 0);
+  const leadReward = ({ reward }): number => termReward(reward);
+  const leadStake = ({ stake }): number => stake || 0;
+
+  const groups = [
+    {
+      id: "validators",
+      name: "Validators & Nominators",
+      actors: validators.count,
+      earning: validators.reward * 24 * 7,
+      stake: Object.keys(validators.stakes).reduce(
+        (sum, key) => sum + +validators.stakes[key].total,
+        0
+      ),
+    },
+    {
+      id: "council",
+      name: "Council members",
+      actors: council?.length,
+      earning: 0,
+      stake: council?.reduce(
+        (sum, { stake, backers }) =>
+          sum + +stake + backers.reduce((s, voter) => s + +voter.stake, 0),
+        0
+      ),
+    },
+    {
+      id: "storage",
+      name: "Storage Providers",
+      actors: workers.storage.filter((w) => !w.isLead).length,
+      earning: workerRewards(workers.storage.filter((w) => !w.isLead)),
+      stake: workerStakes(workers.storage.filter((w) => !w.isLead)),
+    },
+    {
+      id: "storage-lead",
+      name: "Storage lead",
+      actors: 1,
+      earning: leadReward(workers.storage.find((w) => w.isLead)),
+      stake: leadStake(workers.storage.find((w) => w.isLead)),
+    },
+    {
+      id: "curators",
+      name: "Content Curators",
+      actors: workers.content.filter((w) => !w.isLead).length,
+      earning: workerRewards(workers.content.filter((w) => !w.isLead)),
+      stake: workerStakes(workers.content.filter((w) => !w.isLead)),
+    },
+    {
+      id: "curators-lead",
+      name: "Curators lead",
+      actors: 1,
+      earning: leadReward(workers.content.find((w) => w.isLead)),
+      stake: leadStake(workers.content.find((w) => w.isLead)),
+    },
+    {
+      id: "operations",
+      name: "Operations",
+      actors: workers.operations.filter((w) => !w.isLead).length,
+      earning: workerRewards(workers.operations.filter((w) => !w.isLead)),
+      stake: workerStakes(workers.operations.filter((w) => !w.isLead)),
+    },
+    {
+      id: "operations-lead",
+      name: "Operations lead",
+      actors: 1,
+      earning: leadReward(workers.operations.find((w) => w.isLead)),
+      stake: leadStake(workers.operations.find((w) => w.isLead)),
+    },
+  ];
+  //this.save("groups", groups);
+  return groups;
+};

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

@@ -13,7 +13,6 @@ const ValidatorList = (props: IState) => {
   const {
     stars,
     stakes,
-    stashes,
     validators,
     status,
     councils,

+ 10 - 1
src/types.ts

@@ -128,7 +128,7 @@ export interface IState {
   domain: string;
   proposalPosts: any[];
   members: Member[];
-  mints: any[];
+  mints: number[];
   tokenomics?: Tokenomics;
   reports: { [key: string]: string };
   [key: string]: any;
@@ -139,6 +139,7 @@ export interface IState {
   showStatus: boolean;
   editKpi: any; // TODO
   getMember: (m: string | number) => Member;
+  groups: RoleSpending[];
 }
 
 export interface RewardPoints {
@@ -312,6 +313,7 @@ export interface DollarPoolChange {
 }
 
 export interface Tokenomics {
+  dollarPool: { size: number; replenishAmount: numer };
   dollarPoolChanges: DollarPoolChange[];
   price: string;
   totalIssuance: string;
@@ -400,3 +402,10 @@ export interface CalendarGroup {
   id: number;
   title: string;
 }
+
+export interface RoleSpending {
+  name: string;
+  actors: number;
+  earning: { joy: number; dollar: number };
+  stake: { joy: number; dollar: number };
+}