Browse Source

Merge pull request #1229 from DzhideX/edvin/tokenomics-page

Implement tokenomics page
Mokhtar Naamani 4 years ago
parent
commit
c69f4e0639

+ 3 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -27,11 +27,13 @@ import proposals from './joy-proposals';
 import roles from './joy-roles';
 import media from './joy-media';
 import forum from './joy-forum';
+import tokenomics from './joy-tokenomics';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
       media(t),
+      tokenomics(t),
       members(t),
       roles(t),
       election(t),
@@ -48,6 +50,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
     ]
     : [
       media(t),
+      tokenomics(t),
       members(t),
       roles(t),
       election(t),

+ 15 - 0
pioneer/packages/apps-routing/src/joy-tokenomics.ts

@@ -0,0 +1,15 @@
+import { Route } from './types';
+
+import Tokenomics from '@polkadot/joy-tokenomics/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Tokenomics,
+    display: {
+      needsApi: []
+    },
+    text: t<string>('nav.tokenomics', 'Overview', { ns: 'apps-routing' }),
+    icon: 'th',
+    name: 'tokenomics'
+  };
+}

+ 1 - 0
pioneer/packages/apps/public/locales/en/index.json

@@ -26,6 +26,7 @@
   "joy-members.json",
   "joy-proposals.json",
   "joy-roles.json",
+  "joy-tokenomics.json",
   "joy-utils.json",
   "react-components.json",
   "react-params.json",

+ 3 - 0
pioneer/packages/apps/public/locales/en/joy-tokenomics.json

@@ -0,0 +1,3 @@
+{
+  "Tokenomics": "Tokenomics"
+}

+ 1 - 0
pioneer/packages/apps/public/locales/en/translation.json

@@ -669,6 +669,7 @@
   "Tip (optional)": "",
   "To council": "",
   "To ensure optimal fund security using the same stash/controller is strongly discouraged, but not forbidden.": "",
+  "Tokenomics": "",
   "Transfer": "",
   "Translate": "",
   "Treasury overview": "",

+ 201 - 0
pioneer/packages/joy-tokenomics/LICENSE

@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+  "License" shall mean the terms and conditions for use, reproduction,
+  and distribution as defined by Sections 1 through 9 of this document.
+
+  "Licensor" shall mean the copyright owner or entity authorized by
+  the copyright owner that is granting the License.
+
+  "Legal Entity" shall mean the union of the acting entity and all
+  other entities that control, are controlled by, or are under common
+  control with that entity. For the purposes of this definition,
+  "control" means (i) the power, direct or indirect, to cause the
+  direction or management of such entity, whether by contract or
+  otherwise, or (ii) ownership of fifty percent (50%) or more of the
+  outstanding shares, or (iii) beneficial ownership of such entity.
+
+  "You" (or "Your") shall mean an individual or Legal Entity
+  exercising permissions granted by this License.
+
+  "Source" form shall mean the preferred form for making modifications,
+  including but not limited to software source code, documentation
+  source, and configuration files.
+
+  "Object" form shall mean any form resulting from mechanical
+  transformation or translation of a Source form, including but
+  not limited to compiled object code, generated documentation,
+  and conversions to other media types.
+
+  "Work" shall mean the work of authorship, whether in Source or
+  Object form, made available under the License, as indicated by a
+  copyright notice that is included in or attached to the work
+  (an example is provided in the Appendix below).
+
+  "Derivative Works" shall mean any work, whether in Source or Object
+  form, that is based on (or derived from) the Work and for which the
+  editorial revisions, annotations, elaborations, or other modifications
+  represent, as a whole, an original work of authorship. For the purposes
+  of this License, Derivative Works shall not include works that remain
+  separable from, or merely link (or bind by name) to the interfaces of,
+  the Work and Derivative Works thereof.
+
+  "Contribution" shall mean any work of authorship, including
+  the original version of the Work and any modifications or additions
+  to that Work or Derivative Works thereof, that is intentionally
+  submitted to Licensor for inclusion in the Work by the copyright owner
+  or by an individual or Legal Entity authorized to submit on behalf of
+  the copyright owner. For the purposes of this definition, "submitted"
+  means any form of electronic, verbal, or written communication sent
+  to the Licensor or its representatives, including but not limited to
+  communication on electronic mailing lists, source code control systems,
+  and issue tracking systems that are managed by, or on behalf of, the
+  Licensor for the purpose of discussing and improving the Work, but
+  excluding communication that is conspicuously marked or otherwise
+  designated in writing by the copyright owner as "Not a Contribution."
+
+  "Contributor" shall mean Licensor and any individual or Legal Entity
+  on behalf of whom a Contribution has been received by Licensor and
+  subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  copyright license to reproduce, prepare Derivative Works of,
+  publicly display, publicly perform, sublicense, and distribute the
+  Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+  this License, each Contributor hereby grants to You a perpetual,
+  worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+  (except as stated in this section) patent license to make, have made,
+  use, offer to sell, sell, import, and otherwise transfer the Work,
+  where such license applies only to those patent claims licensable
+  by such Contributor that are necessarily infringed by their
+  Contribution(s) alone or by combination of their Contribution(s)
+  with the Work to which such Contribution(s) was submitted. If You
+  institute patent litigation against any entity (including a
+  cross-claim or counterclaim in a lawsuit) alleging that the Work
+  or a Contribution incorporated within the Work constitutes direct
+  or contributory patent infringement, then any patent licenses
+  granted to You under this License for that Work shall terminate
+  as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+  Work or Derivative Works thereof in any medium, with or without
+  modifications, and in Source or Object form, provided that You
+  meet the following conditions:
+
+  (a) You must give any other recipients of the Work or
+      Derivative Works a copy of this License; and
+
+  (b) You must cause any modified files to carry prominent notices
+      stating that You changed the files; and
+
+  (c) You must retain, in the Source form of any Derivative Works
+      that You distribute, all copyright, patent, trademark, and
+      attribution notices from the Source form of the Work,
+      excluding those notices that do not pertain to any part of
+      the Derivative Works; and
+
+  (d) If the Work includes a "NOTICE" text file as part of its
+      distribution, then any Derivative Works that You distribute must
+      include a readable copy of the attribution notices contained
+      within such NOTICE file, excluding those notices that do not
+      pertain to any part of the Derivative Works, in at least one
+      of the following places: within a NOTICE text file distributed
+      as part of the Derivative Works; within the Source form or
+      documentation, if provided along with the Derivative Works; or,
+      within a display generated by the Derivative Works, if and
+      wherever such third-party notices normally appear. The contents
+      of the NOTICE file are for informational purposes only and
+      do not modify the License. You may add Your own attribution
+      notices within Derivative Works that You distribute, alongside
+      or as an addendum to the NOTICE text from the Work, provided
+      that such additional attribution notices cannot be construed
+      as modifying the License.
+
+  You may add Your own copyright statement to Your modifications and
+  may provide additional or different license terms and conditions
+  for use, reproduction, or distribution of Your modifications, or
+  for any such Derivative Works as a whole, provided Your use,
+  reproduction, and distribution of the Work otherwise complies with
+  the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+  any Contribution intentionally submitted for inclusion in the Work
+  by You to the Licensor shall be under the terms and conditions of
+  this License, without any additional terms or conditions.
+  Notwithstanding the above, nothing herein shall supersede or modify
+  the terms of any separate license agreement you may have executed
+  with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+  names, trademarks, service marks, or product names of the Licensor,
+  except as required for reasonable and customary use in describing the
+  origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+  agreed to in writing, Licensor provides the Work (and each
+  Contributor provides its Contributions) on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+  implied, including, without limitation, any warranties or conditions
+  of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+  PARTICULAR PURPOSE. You are solely responsible for determining the
+  appropriateness of using or redistributing the Work and assume any
+  risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+  whether in tort (including negligence), contract, or otherwise,
+  unless required by applicable law (such as deliberate and grossly
+  negligent acts) or agreed to in writing, shall any Contributor be
+  liable to You for damages, including any direct, indirect, special,
+  incidental, or consequential damages of any character arising as a
+  result of this License or out of the use or inability to use the
+  Work (including but not limited to damages for loss of goodwill,
+  work stoppage, computer failure or malfunction, or any and all
+  other commercial damages or losses), even if such Contributor
+  has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+  the Work or Derivative Works thereof, You may choose to offer,
+  and charge a fee for, acceptance of support, warranty, indemnity,
+  or other liability obligations and/or rights consistent with this
+  License. However, in accepting such obligations, You may act only
+  on Your own behalf and on Your sole responsibility, not on behalf
+  of any other Contributor, and only if You agree to indemnify,
+  defend, and hold each Contributor harmless for any liability
+  incurred by, or claims asserted against, such Contributor by reason
+  of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+  To apply the Apache License to your work, attach the following
+  boilerplate notice, with the fields enclosed by brackets "[]"
+  replaced with your own identifying information. (Don't include
+  the brackets!)  The text should be enclosed in the appropriate
+  comment syntax for the file format. We also recommend that a
+  file or class name and description of purpose be included on the
+  same "printed page" as the copyright notice for easier
+  identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 22 - 0
pioneer/packages/joy-tokenomics/README.md

@@ -0,0 +1,22 @@
+# @polkadot/app-123code
+
+A simple template to get started with adding an "app" to this UI. It contains the bare minimum for a nicely hackable app (if you just want to code _somewhere_) and the steps needed to create, add and register an new app that appears in the UI.
+
+## adding an app
+
+If you want to add a new app to the UI, this is the place to start.
+
+1. Duplicate this `app-123code` folder and give it an appropriate name, in this case we will select `app-example` to keep things clear.
+2. Edit the `apps-example/package.json` app description, i.e. the name, author and relevant overview.
+
+And we have the basic app source setup, time to get the tooling correct.
+
+3. Add the new app to the TypeScript config in root, `tsconfig.json`, i.e. an entry such as `"@polkadot/app-example/*": [ "packages/app-example/src/*" ],`
+
+At this point the app should be buildable, but not quite reachable. The final step is to add it to the actual sidebar in `apps`.
+
+4. In `apps-routing/src` duplicate the `123code.ts` file to `example.ts` and edit it with the appropriate information, including the hash link, name and icon (any icon name from semantic-ui-react/font-awesome 4 should be appropriate).
+5. In the above description file, the `isHidden` field needs to be toggled to make it appear - the base template is hidden by default.
+6. Finally add the `template` to the `apps-routing/src/index.ts` file at the appropriate place for both full and light mode (either optional)
+
+Yes. After all that we have things hooked up. Run `yarn start` and your new app (non-coded) should show up. Now start having fun and building something great.

+ 16 - 0
pioneer/packages/joy-tokenomics/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "@polkadot/joy-tokenomics",
+  "version": "0.1.1",
+  "description": "Tokenomics page, basic overview of data from the whole website.",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Edvin Dzidic <edvindzidic2000@gmail.com>",
+  "maintainers": [
+    "Edvin Dzidic <edvindzidic2000@gmail.com>"
+  ],
+  "license": "Apache-2.0",
+  "dependencies": {
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1"
+  }
+}

+ 104 - 0
pioneer/packages/joy-tokenomics/src/Overview/OverviewTable.tsx

@@ -0,0 +1,104 @@
+import React from 'react';
+import { Table, Popup, Icon } from 'semantic-ui-react';
+import styled from 'styled-components';
+
+import { TokenomicsData, StatusServerData } from '@polkadot/joy-utils/src/types/tokenomics';
+
+const StyledTableRow = styled(Table.Row)`
+  .help-icon{
+    position: absolute !important;
+    right: 0rem !important;
+    top: 0 !important;
+    @media (max-width: 767px){
+      right: 1rem !important;
+      top:0.8rem !important;
+    }
+  }
+`;
+
+const OverviewTableRow: React.FC<{item: string; value: string; help?: string}> = ({ item, value, help }) => {
+  return (
+    <StyledTableRow>
+      <Table.Cell>
+        <div style={{ position: 'relative' }}>
+          {item}
+          {help &&
+            <Popup
+              trigger={<Icon className='help-icon' name='help circle' color='grey'/>}
+              content={help}
+              position='right center'
+            />}
+        </div>
+      </Table.Cell>
+      <Table.Cell>{value}</Table.Cell>
+    </StyledTableRow>
+  );
+};
+
+const OverviewTable: React.FC<{data?: TokenomicsData; statusData?: StatusServerData | null}> = ({ data, statusData }) => {
+  const displayStatusData = (val: string, unit: string): string => (
+    statusData === null ? 'Data currently unavailable...' : statusData ? `${val} ${unit}` : 'Loading...'
+  );
+
+  return (
+    <Table style={{ marginBottom: '1.5rem' }} celled>
+      <Table.Header>
+        <Table.Row>
+          <Table.HeaderCell width={10}>Item</Table.HeaderCell>
+          <Table.HeaderCell width={2}>Value</Table.HeaderCell>
+        </Table.Row>
+      </Table.Header>
+
+      <Table.Body>
+        <OverviewTableRow
+          item='Total Issuance'
+          help='The current supply of tokens.'
+          value={data ? `${data.totalIssuance} JOY` : 'Loading...'}
+        />
+        <OverviewTableRow
+          item='Fiat Pool'
+          help='The current value of the Fiat Pool.'
+          value={displayStatusData(statusData?.dollarPool.size.toFixed(2) || '', 'USD')}
+        />
+        <OverviewTableRow
+          item='Currently Staked Tokens'
+          value={data ? `${data.currentlyStakedTokens} JOY` : 'Loading...'}
+          help='All tokens currently staked for active roles.'
+        />
+        <OverviewTableRow
+          item='Currently Staked Value'
+          value={ data ? displayStatusData(`${(data.currentlyStakedTokens * Number(statusData?.price)).toFixed(2)}`, 'USD') : 'Loading...' }
+          help='The value of all tokens currently staked for active roles.'
+        />
+        <OverviewTableRow
+          item='Exchange Rate'
+          value={displayStatusData(`${(Number(statusData?.price) * 1000000).toFixed(2)}`, 'USD/1MJOY')}
+          help='The current exchange rate.'
+        />
+        {/* <OverviewTableRow help='Sum of all tokens burned through exchanges' item='Total Tokens Burned/Exchanged' value={statusData ? `${statusData.burned} JOY` : 'Loading...'}/> */}
+        <OverviewTableRow
+          item='Projected Weekly Token Mint Rate'
+          value={data ? `${Math.round(data.totalWeeklySpending)} JOY` : 'Loading...'}
+          help='Projection of tokens minted over the next week, based on current rewards for all roles.'
+        />
+        <OverviewTableRow
+          item='Projected Weekly Token Inflation Rate'
+          value={data ? `${((data.totalWeeklySpending / data.totalIssuance) * 100).toFixed(2)} %` : 'Loading...'}
+          help={'Based on \'Projected Weekly Token Mint Rate\'. Does not include any deflationary forces (fees, slashes, burns, etc.)'}
+        />
+        <OverviewTableRow
+          item='Projected Weekly Value Of Mint'
+          value={ data ? displayStatusData(`${(data.totalWeeklySpending * Number(statusData?.price)).toFixed(2)}`, 'USD') : 'Loading...'}
+          help={'Based on \'Projected Weekly Token Mint Rate\', and current \'Exchange Rate\'.'}
+        />
+        <OverviewTableRow
+          item='Weekly Top Ups'
+          value={displayStatusData((Number(statusData?.dollarPool.replenishAmount) / 2).toFixed(2) || '', 'USD')}
+          help={'The current weekly \'Fiat Pool\' replenishment amount. Does not include KPIs, or other potential top ups.'}
+        />
+      </Table.Body>
+    </Table>
+  );
+};
+
+export default OverviewTable;

+ 226 - 0
pioneer/packages/joy-tokenomics/src/Overview/SpendingAndStakeDistributionTable.tsx

@@ -0,0 +1,226 @@
+import React from 'react';
+import { Table, Popup, Icon } from 'semantic-ui-react';
+import styled from 'styled-components';
+import { useWindowDimensions } from '../../../joy-utils/src/react/hooks';
+
+import { TokenomicsData, StatusServerData } from '@polkadot/joy-utils/src/types/tokenomics';
+
+const round = (num: number): number => Math.round((num + Number.EPSILON) * 100) / 100;
+
+const applyCss = (columns: number[]): string => {
+  let columnString = '';
+
+  columns.forEach((column, index) => {
+    if (index === 0) {
+      columnString += `td:nth-of-type(${column}), th:nth-of-type(${column})`;
+    } else {
+      columnString += ` ,td:nth-of-type(${column}), th:nth-of-type(${column})`;
+    }
+  });
+
+  return columnString;
+};
+
+const StyledTable = styled(({ divideColumnsAt, ...rest }) => <Table {...rest} />)`
+  border: none !important;
+  width: 70% !important;
+  margin-bottom:1.5rem;
+  @media (max-width: 1400px){
+    width:100% !important;
+  }
+  & tr {
+    td:nth-of-type(1),
+    th:nth-of-type(1),
+    ${(props: { divideColumnsAt: number[]}): string => applyCss(props.divideColumnsAt)} {
+      border-left: 0.12rem solid rgba(20,20,20,0.3) !important;
+    }
+    td:nth-of-type(1){
+      position: relative !important;
+    }
+    td:last-child, th:last-child{
+      border-right: 0.12rem solid rgba(20,20,20,0.3) !important;
+    }
+  }
+  & tr:last-child > td{
+    border-bottom: 0.12rem solid rgba(20,20,20,0.3) !important;
+  }
+  & tr:last-child > td:nth-of-type(1){
+    border-bottom-left-radius: 0.2rem !important;
+  }
+  & tr:last-child > td:last-child{
+    border-bottom-right-radius: 0.2rem !important;
+  }
+  th{
+    border-top: 0.12rem solid rgba(20,20,20,0.3) !important;
+  }
+  & .tableColorBlock{
+    height: 1rem;
+    width:1rem;
+    margin: 0 auto;
+    @media (max-width: 768px){
+      margin: 0;
+    }
+  }
+`;
+
+const StyledTableRow = styled(Table.Row)`
+  .help-icon{
+    position: absolute !important;
+    right: 0.5rem !important;
+    top: 0.8rem !important;
+    @media (max-width: 767px){
+      top:0.8rem !important;
+    }
+  }
+`;
+
+const SpendingAndStakeTableRow: React.FC<{
+  role: string;
+  numberOfActors?: string;
+  groupEarning?: string;
+  groupEarningDollar?: string;
+  earningShare?: string;
+  groupStake?: string;
+  groupStakeDollar?: string;
+  stakeShare?: string;
+  color?: string;
+  active?: boolean;
+  helpContent?: string;
+}> = ({ role, numberOfActors, groupEarning, groupEarningDollar, earningShare, groupStake, groupStakeDollar, stakeShare, color, active, helpContent }) => {
+  const parseData = (data: string | undefined): string | JSX.Element => {
+    if (data && active) {
+      return <em>{data}</em>;
+    } else if (data) {
+      return data;
+    } else {
+      return 'Loading..';
+    }
+  };
+
+  return (
+    <StyledTableRow color={active && 'rgb(150, 150, 150)'}>
+      <Table.Cell>
+        {active ? <strong>{role}</strong> : role}
+        {helpContent && <Popup
+          trigger={<Icon className='help-icon' name='help circle' color='grey'/>}
+          content={helpContent}
+          position='right center'
+        />}
+      </Table.Cell>
+      <Table.Cell>{parseData(numberOfActors)}</Table.Cell>
+      <Table.Cell>{parseData(groupEarning)}</Table.Cell>
+      <Table.Cell>{parseData(groupEarningDollar)}</Table.Cell>
+      <Table.Cell>{parseData(earningShare)}</Table.Cell>
+      <Table.Cell>{parseData(groupStake)}</Table.Cell>
+      <Table.Cell>{parseData(groupStakeDollar)}</Table.Cell>
+      <Table.Cell>{parseData(stakeShare)}</Table.Cell>
+      <Table.Cell><div className='tableColorBlock' style={{ backgroundColor: color }}></div></Table.Cell>
+    </StyledTableRow>
+  );
+};
+
+const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; statusData?: StatusServerData | null}> = ({ data, statusData }) => {
+  const { width } = useWindowDimensions();
+
+  const displayStatusData = (group: 'validators' | 'council' | 'storageProviders' | 'storageProviderLead' | 'contentCurators', action: 'rewardsPerWeek' | 'totalStake'): string | undefined => {
+    if (group === 'storageProviderLead') {
+      return statusData === null ? 'Data currently unavailable...' : (data && statusData) && `${(data.storageProviders.lead[action] * Number(statusData.price)).toFixed(2)}`;
+    } else {
+      return statusData === null ? 'Data currently unavailable...' : (data && statusData) && `${(data[group][action] * Number(statusData.price)).toFixed(2)}`;
+    }
+  };
+
+  return (
+    <StyledTable divideColumnsAt={[3, 6, 9]} celled>
+      <Table.Header>
+        <Table.Row>
+          <Table.HeaderCell width={4}>Group/Role</Table.HeaderCell>
+          <Table.HeaderCell><div>Actors</div>[Number]</Table.HeaderCell>
+          <Table.HeaderCell><div>Group earning</div> [JOY/Week]</Table.HeaderCell>
+          <Table.HeaderCell><div>Group earning</div> [USD/Week]</Table.HeaderCell>
+          <Table.HeaderCell><div>Share</div> [%]</Table.HeaderCell>
+          <Table.HeaderCell><div>Group Stake</div> [JOY]</Table.HeaderCell>
+          <Table.HeaderCell><div>Group Stake</div> [USD]</Table.HeaderCell>
+          <Table.HeaderCell><div>Share</div> [%]</Table.HeaderCell>
+          <Table.HeaderCell width={1}>Color</Table.HeaderCell>
+        </Table.Row>
+      </Table.Header>
+
+      <Table.Body>
+        <SpendingAndStakeTableRow
+          role={width <= 1050 ? 'Validators' : 'Validators (Nominators)'}
+          helpContent='The current set of active Validators (and Nominators), and the sum of the sets projected rewards and total stakes (including Nominators).'
+          numberOfActors={data && `${data.validators.number} (${data.validators.nominators.number})`}
+          groupEarning={data && `${Math.round(data.validators.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('validators', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.validators.rewardsShare * 100)}`}
+          groupStake={data && `${data.validators.totalStake}`}
+          groupStakeDollar={displayStatusData('validators', 'totalStake')}
+          stakeShare={data && `${round(data.validators.stakeShare * 100)}`}
+          color='rgb(246, 109, 68)'
+        />
+        <SpendingAndStakeTableRow
+          role={width <= 1015 ? 'Council' : 'Council Members'}
+          helpContent='The current Council Members, and the sum of their projected rewards and total stakes (including voters/backers).'
+          numberOfActors={data && `${data.council.number}`}
+          groupEarning={data && `${Math.round(data.council.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('council', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.council.rewardsShare * 100)}`}
+          groupStake={data && `${data.council.totalStake}`}
+          groupStakeDollar={displayStatusData('council', 'totalStake')}
+          stakeShare={data && `${round(data.council.stakeShare * 100)}`}
+          color='rgb(254, 174, 101)'
+        />
+        <SpendingAndStakeTableRow
+          role={width <= 1015 ? 'Storage' : 'Storage Providers'}
+          helpContent='The current Storage Providers, and the sum of their projected rewards and stakes.'
+          numberOfActors={data && `${data.storageProviders.number}`}
+          groupEarning={data && `${Math.round(data.storageProviders.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('storageProviders', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.storageProviders.rewardsShare * 100)}`}
+          groupStake={data && `${data.storageProviders.totalStake}`}
+          groupStakeDollar={displayStatusData('storageProviders', 'totalStake')}
+          stakeShare={data && `${round(data.storageProviders.stakeShare * 100)}`}
+          color='rgb(230, 246, 157)'
+        />
+        <SpendingAndStakeTableRow
+          role={width <= 1015 ? 'S. Lead' : width <= 1050 ? 'Storage Lead' : 'Storage Provider Lead'}
+          helpContent='Current Storage Provider Lead, and their projected reward and stake.'
+          numberOfActors={data && `${data.storageProviders.lead.number}`}
+          groupEarning={data && `${Math.round(data.storageProviders.lead.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('storageProviderLead', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.storageProviders.lead.rewardsShare * 100)}`}
+          groupStake={data && `${data.storageProviders.lead.totalStake}`}
+          groupStakeDollar={displayStatusData('storageProviderLead', 'totalStake')}
+          stakeShare={data && `${round(data.storageProviders.lead.stakeShare * 100)}`}
+          color='rgb(170, 222, 167)'
+        />
+        <SpendingAndStakeTableRow
+          role={width <= 1015 ? 'Content' : 'Content Curators'}
+          helpContent='The current Content Curators (and their Lead), and the sum of their projected rewards and stakes.'
+          numberOfActors={data && `${data.contentCurators.number} (${data.contentCurators.contentCuratorLead})`}
+          groupEarning={data && `${Math.round(data.contentCurators.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('contentCurators', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.contentCurators.rewardsShare * 100)}`}
+          groupStake={data && `${data.contentCurators.totalStake}`}
+          groupStakeDollar={displayStatusData('contentCurators', 'totalStake')}
+          stakeShare={data && `${round(data.contentCurators.stakeShare * 100)}`}
+          color='rgb(100, 194, 166)'
+        />
+        <SpendingAndStakeTableRow
+          role='TOTAL'
+          active={true}
+          numberOfActors={data && `${data.totalNumberOfActors}`}
+          groupEarning={data && `${Math.round(data.totalWeeklySpending)}`}
+          groupEarningDollar={statusData === null ? 'Data currently unavailable..' : (data && statusData) && `${round(data.totalWeeklySpending * Number(statusData.price))}`}
+          earningShare={data && '100'}
+          groupStake={data && `${data.currentlyStakedTokens}`}
+          groupStakeDollar={statusData === null ? 'Data currently unavailable..' : (data && statusData) && `${round(data.currentlyStakedTokens * Number(statusData.price))}`}
+          stakeShare={data && '100'}
+        />
+      </Table.Body>
+    </StyledTable>
+  );
+};
+
+export default SpendingAndStakeDistributionTable;

+ 93 - 0
pioneer/packages/joy-tokenomics/src/Overview/TokenomicsCharts.tsx

@@ -0,0 +1,93 @@
+import React from 'react';
+import { Icon, Label } from 'semantic-ui-react';
+import PieChart from '../../../react-components/src/Chart/PieChart';
+import styled from 'styled-components';
+
+import { TokenomicsData } from '@polkadot/joy-utils/src/types/tokenomics';
+
+const StyledPieChart = styled(PieChart)`
+  width:15rem;
+  height:15rem;
+  margin-bottom:1rem;
+  @media (max-width: 1650px){
+    height:12rem;
+    width:12rem;
+  }
+  @media (max-width: 1400px){
+    height:15rem;
+    width:15rem;
+  }
+`;
+
+const ChartContainer = styled('div')`
+  display:flex;
+  flex-direction:column;
+  align-items:center;
+`;
+
+const TokenomicsCharts: React.FC<{data?: TokenomicsData; className?: string}> = ({ data, className }) => {
+  return (
+    <div className={className}>
+      {data ? <ChartContainer>
+        <StyledPieChart
+          values={[{
+            colors: ['rgb(246, 109, 68)'],
+            label: 'Validators',
+            value: data.validators.rewardsShare * 100
+          }, {
+            colors: ['rgb(254, 174, 101)'],
+            label: 'Council',
+            value: data.council.rewardsShare * 100
+          }, {
+            colors: ['rgb(230, 246, 157)'],
+            label: 'Storage Providers',
+            value: data.storageProviders.rewardsShare * 100
+          }, {
+            colors: ['rgb(170, 222, 167)'],
+            label: 'Storage Lead',
+            value: data.storageProviders.lead.rewardsShare * 100
+          }, {
+            colors: ['rgb(100, 194, 166)'],
+            label: 'Content Curators',
+            value: data.contentCurators.rewardsShare * 100
+          }
+          ]} />
+        <Label as='div'>
+          <Icon name='money' />
+          <span style={{ fontWeight: 600 }}>Spending</span>
+        </Label>
+      </ChartContainer> : <Icon name='circle notched' loading/>}
+      {data ? <ChartContainer>
+        <StyledPieChart
+          values={[{
+            colors: ['rgb(246, 109, 68)'],
+            label: 'Validators',
+            value: data.validators.stakeShare * 100
+          }, {
+            colors: ['rgb(254, 174, 101)'],
+            label: 'Council',
+            value: data.council.stakeShare * 100
+          }, {
+            colors: ['rgb(230, 246, 157)'],
+            label: 'Storage Providers',
+            value: data.storageProviders.stakeShare * 100
+          }, {
+            colors: ['rgb(170, 222, 167)'],
+            label: 'Storage Lead',
+            value: data.storageProviders.lead.stakeShare * 100
+          }, {
+            colors: ['rgb(100, 194, 166)'],
+            label: 'Content Curators',
+            value: data.contentCurators.stakeShare * 100
+          }
+          ]} />
+        <Label as='div'>
+          <Icon name='block layout' />
+          <span style={{ fontWeight: 600 }}>Stake</span>
+        </Label>
+      </ChartContainer> : <Icon name='circle notched' loading/>}
+    </div>
+  );
+};
+
+export default TokenomicsCharts;

+ 59 - 0
pioneer/packages/joy-tokenomics/src/Overview/index.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import OverviewTable from './OverviewTable';
+import SpendingAndStakeDistributionTable from './SpendingAndStakeDistributionTable';
+import TokenomicsCharts from './TokenomicsCharts';
+import styled from 'styled-components';
+
+import usePromise from '@polkadot/joy-utils/react/hooks/usePromise';
+import { useTransport } from '@polkadot/joy-utils/react/hooks';
+import { StatusServerData } from '@polkadot/joy-utils/src/types/tokenomics';
+
+const SpendingAndStakeContainer = styled('div')`
+  display:flex;
+  justify-content:space-between;
+  @media (max-width: 1400px){
+    flex-direction:column;
+  }
+`;
+
+const Title = styled('h2')`
+  border-bottom: 1px solid #ddd;
+  margin: 0 0 2rem 0;
+`;
+
+const StyledTokenomicsCharts = styled(TokenomicsCharts)`
+  width:30%;
+  display:flex;
+  align-items:center;
+  justify-content:space-evenly;
+  padding: 2rem 0;
+  @media (max-width: 1400px){
+    width:100%;
+  }
+  @media (max-width: 550px){
+    flex-direction:column;
+    & > div {
+      margin-bottom: 1.5rem;
+    }
+  }
+`;
+
+const Overview: React.FC = () => {
+  const transport = useTransport();
+  const [statusDataValue, statusDataError] = usePromise<StatusServerData | undefined>(() => fetch('https://status.joystream.org/status').then((res) => res.json().then((data) => data as StatusServerData)), undefined, []);
+  const [tokenomicsData] = usePromise(() => transport.tokenomics.getTokenomicsData(), undefined, []);
+
+  return (
+    <>
+      <Title> Overview </Title>
+      <OverviewTable data={tokenomicsData} statusData={statusDataError ? null : statusDataValue}/>
+      <Title> Spending and Stake Distribution </Title>
+      <SpendingAndStakeContainer>
+        <SpendingAndStakeDistributionTable data={tokenomicsData} statusData={statusDataError ? null : statusDataValue}/>
+        <StyledTokenomicsCharts data={tokenomicsData} />
+      </SpendingAndStakeContainer>
+    </>
+  );
+};
+
+export default Overview;

+ 34 - 0
pioneer/packages/joy-tokenomics/src/index.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import { useTranslation } from './translate';
+import { Route, Switch } from 'react-router';
+import { Tabs } from '@polkadot/react-components';
+import Overview from './Overview';
+import { AppProps } from '@polkadot/react-components/types';
+
+type Props = AppProps
+
+function App ({ basePath }: Props): React.ReactElement<Props> {
+  const { t } = useTranslation();
+
+  return (
+    <main>
+      <header>
+        <Tabs
+          basePath={basePath}
+          items={[
+            {
+              isRoot: true,
+              name: 'overview',
+              text: t('Tokenomics')
+            }
+          ]}
+        />
+      </header>
+      <Switch>
+        <Route component={Overview} />
+      </Switch>
+    </main>
+  );
+}
+
+export default App;

+ 9 - 0
pioneer/packages/joy-tokenomics/src/translate.ts

@@ -0,0 +1,9 @@
+// Copyright 2017-2019 @polkadot/app-123code authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { useTranslation as useTranslationBase, UseTranslationResponse } from 'react-i18next';
+
+export function useTranslation (): UseTranslationResponse {
+  return useTranslationBase('joy-tokenomics');
+}

+ 5 - 0
pioneer/packages/joy-utils/src/consts/staking.ts

@@ -0,0 +1,5 @@
+// Values based on REWARD_CURVE const in /runtime/src/lib.rs
+export const IDEAL_STAKING_RATE = 0.25;
+export const MIN_INFLATION_RATE = 0.05;
+export const MAX_INFLATION_RATE = 0.75;
+export const FALL_OFF_RATE = 0.05;

+ 32 - 0
pioneer/packages/joy-utils/src/functions/staking.ts

@@ -0,0 +1,32 @@
+import { IDEAL_STAKING_RATE, MIN_INFLATION_RATE, MAX_INFLATION_RATE, FALL_OFF_RATE } from '../consts/staking';
+
+// See: https://github.com/Joystream/helpdesk/tree/master/roles/validators#rewards-on-joystream for reference
+export function calculateValidatorsRewardsPerEra (
+  totalValidatorsStake: number,
+  totalIssuance: number,
+  minutesPerEra = 60
+): number {
+  let validatorsRewardsPerYear = 0;
+  const stakingRate = totalValidatorsStake / totalIssuance;
+  const minutesPerYear = 365.2425 * 24 * 60;
+
+  if (stakingRate > IDEAL_STAKING_RATE) {
+    validatorsRewardsPerYear =
+      totalIssuance * (
+        MIN_INFLATION_RATE + (
+          (MAX_INFLATION_RATE - MIN_INFLATION_RATE) *
+          (2 ** ((IDEAL_STAKING_RATE - stakingRate) / FALL_OFF_RATE))
+        )
+      );
+  } else if (stakingRate === IDEAL_STAKING_RATE) {
+    validatorsRewardsPerYear = totalIssuance * MAX_INFLATION_RATE;
+  } else {
+    validatorsRewardsPerYear =
+      totalIssuance * (
+        MIN_INFLATION_RATE +
+        (MAX_INFLATION_RATE - MIN_INFLATION_RATE) * (stakingRate / IDEAL_STAKING_RATE)
+      );
+  }
+
+  return validatorsRewardsPerYear / minutesPerYear * minutesPerEra;
+}

+ 1 - 0
pioneer/packages/joy-utils/src/react/hooks/index.ts

@@ -3,3 +3,4 @@ export { default as useMyMembership } from './useMyMembership';
 export { default as usePromise } from './usePromise';
 export { default as useTransport } from './useTransport';
 export { default as useProposalSubscription } from './proposals/useProposalSubscription';
+export { default as useWindowDimensions } from './useWindowDimensions';

+ 26 - 0
pioneer/packages/joy-utils/src/react/hooks/useWindowDimensions.ts

@@ -0,0 +1,26 @@
+import { useState, useEffect } from 'react';
+
+function getWindowDimensions () {
+  const { innerWidth: width, innerHeight: height } = window;
+
+  return {
+    width,
+    height
+  };
+}
+
+export default function useWindowDimensions () {
+  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+  useEffect(() => {
+    function handleResize () {
+      setWindowDimensions(getWindowDimensions());
+    }
+
+    window.addEventListener('resize', handleResize);
+
+    return () => window.removeEventListener('resize', handleResize);
+  }, []);
+
+  return windowDimensions;
+}

+ 3 - 0
pioneer/packages/joy-utils/src/transport/index.ts

@@ -7,6 +7,7 @@ import CouncilTransport from './council';
 import ValidatorsTransport from './validators';
 import WorkingGroupsTransport from './workingGroups';
 import { APIQueryCache } from './APIQueryCache';
+import TokenomicsTransport from './tokenomics';
 
 export default class Transport {
   protected api: ApiPromise;
@@ -19,6 +20,7 @@ export default class Transport {
   public contentWorkingGroup: ContentWorkingGroupTransport;
   public validators: ValidatorsTransport;
   public workingGroups: WorkingGroupsTransport;
+  public tokenomics: TokenomicsTransport
 
   constructor (api: ApiPromise) {
     this.api = api;
@@ -30,5 +32,6 @@ export default class Transport {
     this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
     this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
     this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
+    this.tokenomics = new TokenomicsTransport(api, this.cacheApi, this.council, this.workingGroups);
   }
 }

+ 344 - 0
pioneer/packages/joy-utils/src/transport/tokenomics.ts

@@ -0,0 +1,344 @@
+import BaseTransport from './base';
+import { ApiPromise } from '@polkadot/api';
+import CouncilTransport from './council';
+import WorkingGroupsTransport from './workingGroups';
+import { APIQueryCache } from './APIQueryCache';
+import { Seats } from '@joystream/types/council';
+import { Option } from '@polkadot/types';
+import { BlockNumber, BalanceOf, Exposure } from '@polkadot/types/interfaces';
+import { WorkerId } from '@joystream/types/working-group';
+import { RewardRelationshipId, RewardRelationship } from '@joystream/types/recurring-rewards';
+import { StakeId, Stake } from '@joystream/types/stake';
+import { CuratorId, Curator, LeadId } from '@joystream/types/content-working-group';
+import { TokenomicsData } from '@polkadot/joy-utils/src/types/tokenomics';
+import { calculateValidatorsRewardsPerEra } from '../functions/staking';
+
+export default class TokenomicsTransport extends BaseTransport {
+  private councilT: CouncilTransport;
+  private workingGroupT: WorkingGroupsTransport;
+
+  constructor (api: ApiPromise, cacheApi: APIQueryCache, councilTransport: CouncilTransport, workingGroups: WorkingGroupsTransport) {
+    super(api, cacheApi);
+    this.councilT = councilTransport;
+    this.workingGroupT = workingGroups;
+  }
+
+  async councilSizeAndStake () {
+    let totalCouncilStake = 0;
+    const activeCouncil = await this.council.activeCouncil() as Seats;
+
+    activeCouncil.map((member) => {
+      let stakeAmount = 0;
+
+      stakeAmount += member.stake.toNumber();
+      member.backers.forEach((backer) => {
+        stakeAmount += backer.stake.toNumber();
+      });
+      totalCouncilStake += stakeAmount;
+    });
+
+    return {
+      numberOfCouncilMembers: activeCouncil.length,
+      totalCouncilStake
+    };
+  }
+
+  private async councilRewardsPerWeek (numberOfCouncilMembers: number) {
+    const payoutInterval = Number((await this.api.query.council.payoutInterval() as Option<BlockNumber>).unwrapOr(0));
+    const amountPerPayout = (await this.api.query.council.amountPerPayout() as BalanceOf).toNumber();
+    const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
+      ? (amountPerPayout * numberOfCouncilMembers) / payoutInterval
+      : 0;
+
+    const { new_term_duration, voting_period, revealing_period, announcing_period } = await this.councilT.electionParameters();
+    const termDuration = new_term_duration.toNumber();
+    const votingPeriod = voting_period.toNumber();
+    const revealingPeriod = revealing_period.toNumber();
+    const announcingPeriod = announcing_period.toNumber();
+    const weekInBlocks = 100800;
+
+    const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
+    const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
+    const avgCouncilRewardPerWeek = avgCouncilRewardPerBlock * weekInBlocks;
+
+    return avgCouncilRewardPerWeek;
+  }
+
+  async getCouncilData () {
+    const { numberOfCouncilMembers, totalCouncilStake } = await this.councilSizeAndStake();
+    const totalCouncilRewardsInOneWeek = await this.councilRewardsPerWeek(numberOfCouncilMembers);
+
+    return {
+      numberOfCouncilMembers,
+      totalCouncilRewardsInOneWeek,
+      totalCouncilStake
+    };
+  }
+
+  private async storageProviderSizeAndIds () {
+    const stakeIds: StakeId[] = [];
+    const rewardIds: RewardRelationshipId[] = [];
+    let leadStakeId: StakeId | null = null;
+    let leadRewardId: RewardRelationshipId | null = null;
+    let numberOfStorageProviders = 0;
+    let leadNumber = 0;
+    const allWorkers = await this.workingGroupT.allWorkers('Storage');
+    const currentLeadId = (await this.api.query.storageWorkingGroup.currentLead() as Option<WorkerId>).unwrapOr(null)?.toNumber();
+
+    allWorkers.forEach(([workerId, worker]) => {
+      const stakeId = worker.role_stake_profile.isSome ? worker.role_stake_profile.unwrap().stake_id : null;
+      const rewardId = worker.reward_relationship.unwrapOr(null);
+
+      if (currentLeadId !== undefined && currentLeadId === workerId.toNumber()) {
+        leadStakeId = stakeId;
+        leadRewardId = rewardId;
+        leadNumber += 1;
+      } else {
+        numberOfStorageProviders += 1;
+
+        if (stakeId) {
+          stakeIds.push(stakeId);
+        }
+
+        if (rewardId) {
+          rewardIds.push(rewardId);
+        }
+      }
+    });
+
+    return {
+      numberOfStorageProviders,
+      stakeIds,
+      rewardIds,
+      leadNumber,
+      leadRewardId,
+      leadStakeId
+    };
+  }
+
+  private async storageProviderStakeAndRewards (
+    stakeIds: StakeId[],
+    leadStakeId: StakeId | null,
+    rewardIds: RewardRelationshipId[],
+    leadRewardId: RewardRelationshipId | null
+  ) {
+    let totalStorageProviderStake = 0;
+    let leadStake = 0;
+    let storageProviderRewardsPerBlock = 0;
+    let storageLeadRewardsPerBlock = 0;
+
+    (await this.api.query.stake.stakes.multi<Stake>(stakeIds)).forEach((stake) => {
+      totalStorageProviderStake += stake.value.toNumber();
+    });
+    (await this.api.query.recurringRewards.rewardRelationships.multi<RewardRelationship>(rewardIds)).map((rewardRelationship) => {
+      const amount = rewardRelationship.amount_per_payout.toNumber();
+      const payoutInterval = rewardRelationship.payout_interval.isSome
+        ? rewardRelationship.payout_interval.unwrap().toNumber()
+        : null;
+
+      if (amount && payoutInterval) {
+        storageProviderRewardsPerBlock += amount / payoutInterval;
+      }
+    });
+
+    if (leadStakeId !== null) {
+      leadStake += (await this.api.query.stake.stakes(leadStakeId) as Stake).value.toNumber();
+    }
+
+    if (leadRewardId !== null) {
+      const leadRewardData = (await this.api.query.recurringRewards.rewardRelationships(leadRewardId) as RewardRelationship);
+      const leadAmount = leadRewardData.amount_per_payout.toNumber();
+      const leadRewardInterval = leadRewardData.payout_interval.isSome ? leadRewardData.payout_interval.unwrap().toNumber() : null;
+
+      if (leadAmount && leadRewardInterval) {
+        storageLeadRewardsPerBlock += leadAmount / leadRewardInterval;
+      }
+    }
+
+    return {
+      totalStorageProviderStake,
+      leadStake,
+      storageProviderRewardsPerWeek: storageProviderRewardsPerBlock * 100800,
+      storageProviderLeadRewardsPerWeek: storageLeadRewardsPerBlock * 100800
+    };
+  }
+
+  async getStorageProviderData () {
+    const { numberOfStorageProviders, leadNumber, stakeIds, rewardIds, leadRewardId, leadStakeId } = await this.storageProviderSizeAndIds();
+    const { totalStorageProviderStake, leadStake, storageProviderRewardsPerWeek, storageProviderLeadRewardsPerWeek } =
+      await this.storageProviderStakeAndRewards(stakeIds, leadStakeId, rewardIds, leadRewardId);
+
+    return {
+      numberOfStorageProviders,
+      storageProviderLeadNumber: leadNumber,
+      totalStorageProviderStake,
+      totalStorageProviderLeadStake: leadStake,
+      storageProviderRewardsPerWeek,
+      storageProviderLeadRewardsPerWeek
+    };
+  }
+
+  private async contentCuratorSizeAndIds () {
+    const stakeIds: StakeId[] = []; const rewardIds: RewardRelationshipId[] = []; let numberOfContentCurators = 0;
+    const contentCurators = await this.entriesByIds<CuratorId, Curator>(this.api.query.contentWorkingGroup.curatorById);
+    const currentLeadId = (await this.api.query.contentWorkingGroup.currentLeadId() as Option<LeadId>).unwrapOr(null)?.toNumber();
+
+    contentCurators.forEach(([curatorId, curator]) => {
+      const stakeId = curator.role_stake_profile.isSome ? curator.role_stake_profile.unwrap().stake_id : null;
+      const rewardId = curator.reward_relationship.unwrapOr(null);
+
+      if (curator.is_active) {
+        numberOfContentCurators += 1;
+
+        if (stakeId) {
+          stakeIds.push(stakeId);
+        }
+
+        if (rewardId) {
+          rewardIds.push(rewardId);
+        }
+      }
+    });
+
+    return {
+      stakeIds,
+      rewardIds,
+      numberOfContentCurators,
+      contentCuratorLeadNumber: currentLeadId ? 1 : 0
+    };
+  }
+
+  private async contentCuratorStakeAndRewards (stakeIds: StakeId[], rewardIds: RewardRelationshipId[]) {
+    let totalContentCuratorStake = 0;
+    let contentCuratorRewardsPerBlock = 0;
+
+    (await this.api.query.stake.stakes.multi<Stake>(stakeIds)).forEach((stake) => {
+      totalContentCuratorStake += stake.value.toNumber();
+    });
+    (await this.api.query.recurringRewards.rewardRelationships.multi<RewardRelationship>(rewardIds)).map((rewardRelationship) => {
+      const amount = rewardRelationship.amount_per_payout.toNumber();
+      const payoutInterval = rewardRelationship.payout_interval.isSome
+        ? rewardRelationship.payout_interval.unwrap().toNumber()
+        : null;
+
+      if (amount && payoutInterval) {
+        contentCuratorRewardsPerBlock += amount / payoutInterval;
+      }
+    });
+
+    return {
+      totalContentCuratorStake,
+      contentCuratorRewardsPerBlock
+    };
+  }
+
+  async getContentCuratorData () {
+    const { stakeIds, rewardIds, numberOfContentCurators, contentCuratorLeadNumber } = await this.contentCuratorSizeAndIds();
+    const { totalContentCuratorStake, contentCuratorRewardsPerBlock } = await this.contentCuratorStakeAndRewards(stakeIds, rewardIds);
+
+    return {
+      numberOfContentCurators,
+      contentCuratorLeadNumber,
+      totalContentCuratorStake,
+      contentCuratorRewardsPerWeek: contentCuratorRewardsPerBlock * 100800
+    };
+  }
+
+  async validatorSizeAndStake () {
+    const validatorIds = await this.api.query.session.validators();
+    const currentEra = (await this.api.query.staking.currentEra()).unwrapOr(null);
+    let totalValidatorStake = 0; let numberOfNominators = 0;
+
+    if (currentEra !== null) {
+      const validatorStakeData = await this.api.query.staking.erasStakers.multi<Exposure>(
+        validatorIds.map((validatorId) => [currentEra, validatorId])
+      );
+
+      validatorStakeData.forEach((data) => {
+        if (!data.total.isEmpty) {
+          totalValidatorStake += data.total.toNumber();
+        }
+
+        if (!data.others.isEmpty) {
+          numberOfNominators += data.others.length;
+        }
+      });
+    }
+
+    return {
+      numberOfValidators: validatorIds.length,
+      numberOfNominators,
+      totalValidatorStake
+    };
+  }
+
+  async getValidatorData () {
+    const totalIssuance = (await this.api.query.balances.totalIssuance()).toNumber();
+    const { numberOfValidators, numberOfNominators, totalValidatorStake } = await this.validatorSizeAndStake();
+    const validatorRewardsPerEra = calculateValidatorsRewardsPerEra(totalValidatorStake, totalIssuance);
+
+    return {
+      totalIssuance,
+      numberOfValidators,
+      numberOfNominators,
+      totalValidatorStake,
+      validatorRewardsPerWeek: validatorRewardsPerEra * 168 // Assuming 1 era = 1h
+    };
+  }
+
+  async getTokenomicsData (): Promise<TokenomicsData> {
+    const { numberOfCouncilMembers, totalCouncilRewardsInOneWeek, totalCouncilStake } = await this.getCouncilData();
+    const { numberOfStorageProviders, storageProviderLeadNumber, totalStorageProviderStake, totalStorageProviderLeadStake, storageProviderLeadRewardsPerWeek, storageProviderRewardsPerWeek } = await this.getStorageProviderData();
+    const { numberOfContentCurators, contentCuratorLeadNumber, totalContentCuratorStake, contentCuratorRewardsPerWeek } = await this.getContentCuratorData();
+    const { numberOfValidators, numberOfNominators, totalValidatorStake, validatorRewardsPerWeek, totalIssuance } = await this.getValidatorData();
+    const currentlyStakedTokens = totalCouncilStake + totalStorageProviderStake + totalStorageProviderLeadStake + totalContentCuratorStake + totalValidatorStake;
+    const totalWeeklySpending = totalCouncilRewardsInOneWeek + storageProviderRewardsPerWeek + storageProviderLeadRewardsPerWeek + contentCuratorRewardsPerWeek + validatorRewardsPerWeek;
+    const totalNumberOfActors = numberOfCouncilMembers + numberOfStorageProviders + storageProviderLeadNumber + numberOfContentCurators + contentCuratorLeadNumber + numberOfValidators;
+
+    return {
+      totalIssuance,
+      currentlyStakedTokens,
+      totalWeeklySpending,
+      totalNumberOfActors,
+      validators: {
+        number: numberOfValidators,
+        nominators: {
+          number: numberOfNominators
+        },
+        rewardsPerWeek: validatorRewardsPerWeek,
+        rewardsShare: validatorRewardsPerWeek / totalWeeklySpending,
+        totalStake: totalValidatorStake,
+        stakeShare: totalValidatorStake / currentlyStakedTokens
+      },
+      council: {
+        number: numberOfCouncilMembers,
+        rewardsPerWeek: totalCouncilRewardsInOneWeek,
+        rewardsShare: totalCouncilRewardsInOneWeek / totalWeeklySpending,
+        totalStake: totalCouncilStake,
+        stakeShare: totalCouncilStake / currentlyStakedTokens
+      },
+      storageProviders: {
+        number: numberOfStorageProviders,
+        totalStake: totalStorageProviderStake,
+        stakeShare: totalStorageProviderStake / currentlyStakedTokens,
+        rewardsPerWeek: storageProviderRewardsPerWeek,
+        rewardsShare: storageProviderRewardsPerWeek / totalWeeklySpending,
+        lead: {
+          number: storageProviderLeadNumber,
+          totalStake: totalStorageProviderLeadStake,
+          stakeShare: totalStorageProviderLeadStake / currentlyStakedTokens,
+          rewardsPerWeek: storageProviderLeadRewardsPerWeek,
+          rewardsShare: storageProviderLeadRewardsPerWeek / totalWeeklySpending
+        }
+      },
+      contentCurators: {
+        number: numberOfContentCurators,
+        contentCuratorLead: contentCuratorLeadNumber,
+        rewardsPerWeek: contentCuratorRewardsPerWeek,
+        rewardsShare: contentCuratorRewardsPerWeek / totalWeeklySpending,
+        totalStake: totalContentCuratorStake,
+        stakeShare: totalContentCuratorStake / currentlyStakedTokens
+      }
+    };
+  }
+}

+ 53 - 0
pioneer/packages/joy-utils/src/types/tokenomics.ts

@@ -0,0 +1,53 @@
+export type TokenomicsData = {
+  totalIssuance: number;
+  currentlyStakedTokens: number;
+  totalWeeklySpending: number;
+  totalNumberOfActors: number;
+  validators: {
+    number: number;
+    nominators: {
+      number: number;
+    };
+    rewardsPerWeek: number;
+    rewardsShare: number;
+    totalStake: number;
+    stakeShare: number;
+  };
+  council: {
+    number: number;
+    rewardsPerWeek: number;
+    rewardsShare: number;
+    totalStake: number;
+    stakeShare: number;
+  };
+  storageProviders: {
+    number: number;
+    totalStake: number;
+    stakeShare: number;
+    rewardsPerWeek: number;
+    rewardsShare: number;
+    lead: {
+      number: number;
+      totalStake: number;
+      stakeShare: number;
+      rewardsPerWeek: number;
+      rewardsShare: number;
+    };
+  };
+  contentCurators: {
+    number: number;
+    contentCuratorLead: number;
+    rewardsPerWeek: number;
+    rewardsShare: number;
+    totalStake: number;
+    stakeShare: number;
+  };
+}
+
+export type StatusServerData = {
+  dollarPool: {
+    size: number;
+    replenishAmount: number;
+  };
+  price: string;
+};

+ 67 - 0
pioneer/packages/react-components/src/Chart/PieChart.tsx

@@ -0,0 +1,67 @@
+// Copyright 2017-2019 @polkadot/react-components authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { BareProps } from '../types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Pie } from 'react-chartjs-2';
+import { bnToBn } from '@polkadot/util';
+
+interface Value {
+  colors: string[];
+  label: string;
+  value: number | BN;
+}
+
+interface Props extends BareProps {
+  size?: number;
+  values: Value[];
+}
+
+interface Options {
+  colorNormal: string[];
+  colorHover: string[];
+  data: number[];
+  labels: string[];
+}
+
+export default function PieChart ({ className, style, values }: Props): React.ReactElement<Props> {
+  const options: Options = {
+    colorNormal: [],
+    colorHover: [],
+    data: [],
+    labels: []
+  };
+
+  values.forEach(({ colors: [normalColor = '#00f', hoverColor], label, value }): void => {
+    options.colorNormal.push(normalColor);
+    options.colorHover.push(hoverColor || normalColor);
+    options.data.push(bnToBn(value).toNumber());
+    options.labels.push(label);
+  });
+
+  return (
+    <div
+      className={className}
+    >
+      <Pie
+        legend={{
+          display: false
+        }}
+        options={{
+          maintainAspectRatio: false
+        }}
+        data={{
+          labels: options.labels,
+          datasets: [{
+            data: options.data,
+            backgroundColor: options.colorNormal,
+            hoverBackgroundColor: options.colorHover
+          }]
+        }}
+      />
+    </div>
+  );
+}

+ 3 - 1
pioneer/tsconfig.json

@@ -88,7 +88,9 @@
       "@polkadot/react-query": [ "packages/react-query/src" ],
       "@polkadot/react-query/*": [ "packages/react-query/src/*" ],
       "@polkadot/react-signer": [ "packages/react-signer/src" ],
-      "@polkadot/react-signer/*": [ "packages/react-signer/src/*" ]
+      "@polkadot/react-signer/*": [ "packages/react-signer/src/*" ],
+      "@polkadot/joy-tokenomics": [ "packages/joy-tokenomics/src" ],
+      "@polkadot/joy-tokenomics/*": [ "packages/joy-tokenomics/src/*" ]
     },
     "skipLibCheck": true,
     "typeRoots": [