소스 검색

Merge pull request #29 from freakstatic/council-report

KPI 1.2 - Council Report Generator (Tokenomics + Network Report)
Martin 4 년 전
부모
커밋
112a84acfe

+ 5 - 0
council/report-generator/.gitignore

@@ -0,0 +1,5 @@
+.idea/*
+lib/*
+node_modules
+yarn.lock
+report.md

+ 20 - 0
council/report-generator/README.md

@@ -0,0 +1,20 @@
+# Council Report Generator
+
+This scripts collects some information from Joystream chain. \
+It was created to allow the council to generate a report in the finish of the council round. \
+It takes some minutes to complete the report, multiple runs, with the same block range, will be quicker since it has a "cache" system for the block events.  
+
+ ## Setup
+ ```
+ yarn && yarn build
+ ```
+
+ ## Usage
+ ```
+node lib/generator.js <start block> <end block> 
+ ```
+
+## Example
+ ```
+node lib/generator.js 57601 234038 
+ ```

+ 3 - 0
council/report-generator/cache/.gitignore

@@ -0,0 +1,3 @@
+*
+*/
+!.gitignore

+ 24 - 0
council/report-generator/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "report-generator",
+  "version": "0.1.0",
+  "main": "lib/index.js",
+  "license": "MIT",
+  "scripts": {
+    "build": "tsc --build tsconfig.json",
+    "status": "node lib/status"
+  },
+  "dependencies": {
+    "@joystream/types": "^0.13.1",
+    "@polkadot/api": "1.26.1",
+    "@polkadot/keyring": "^3.0.1",
+    "@polkadot/types": "1.26.1",
+    "@polkadot/util": "^3.0.1",
+    "@polkadot/util-crypto": "^3.0.1",
+    "@types/bn.js": "^4.11.5",
+    "bn.js": "^4.11.8"
+  },
+  "devDependencies": {
+    "@polkadot/ts": "^0.1.56",
+    "typescript": "^3.9.7"
+  }
+}

+ 84 - 0
council/report-generator/report-template.md

@@ -0,0 +1,84 @@
+# Tokenomics + Network Report
+This is a report which explains the current state of the Joystream network in numbers. It pulls figures from the chain and tries to provide a basic level of information about the network, tokens and more. 
+
+## 1.0 Basic Information
+* Block range: {startBlock} - {endBlock}
+* Date Range: {dateStart} - {dateEnd}
+* Council session #: {councilRound}
+
+## 2.0 Tokenomics
+### 2.1 Token generation breakdown
+| Property            | Start Block | End Block | % Change |
+|---------------------|--------------|--------------|----------|
+| Total Tokens Minted |  {startIssuance} | {endIssuance} | {percNewIssuance} |
+
+| Property            | Value        |
+|---------------------|--------------|
+| Total Tokens Burned | {newTokensBurn} | 
+| Validator Role      |  {newValidatorRewards}            | 
+| Council Role        | {newCouncilRewards}             | 
+| Storage Role        | {newStorageProviderReward}             | 
+| Curator Role        | {newCuratorRewards}             | 
+
+
+
+### 2.2 Mints 
+| Property                    | Start Block           | End Block | % Change |
+|-----------------------------|-----------------------|--------------|----------|
+| Council Mint Total Minted   | {startCouncilMinted}  |  {endCouncilMinted} |{percNewCouncilMinted}          |
+| Curator Mint Total Minted   |  {startCuratorMinted} | {endCuratorMinted}| {percCuratorMinted}          |
+| Storage Mint Total Minted   |  {startStorageMinted} |  {endStorageMinted}            |  {percStorageMinted}        |
+
+## 3.0 Council
+* Council session #: {councilRound}
+* Number of council members: {councilMembers}
+* Total number of proposals: {newProposals}
+* Total number of Approved proposals: {newApprovedProposals}
+
+### 3.1 Elections
+| Property                    | Start Block  |
+|-----------------------------|--------------|
+| Total Applicants            |{electionApplicants}              |
+| Total Applicant Stake       |{electionApplicantsStakes}              |
+| Total Votes                 |{electionVotes}             |
+
+## 4 Roles
+### 4.1 Validator Information
+* Block generation time (average): {avgBlockProduction}
+
+| Property                    | Start Block | End Block | % Change |
+|-----------------------------|--------------|--------------|----------|
+| Number of Validators       |  {startValidators} | {endValidators} | {percValidators} |
+| Validator Total Stake       | {startValidatorsStake} | {endValidatorsStake} | {percNewValidatorsStake} |
+
+
+### 4.2 Storage Role
+| Property                | Start Block | End Block | % Change |
+|-------------------------|--------------|--------------|----------|
+| Number of Storage Workers | {startStorageProviders}  |  {endStorageProviders} | {percNewStorageProviders} |
+| Total Storage Stake (workers + lead)  | {startStorageProvidersStake} |  {endStorageProvidersStake} | {percNewStorageProviderStake} |
+
+### 4.3 Curator Role
+| Property                | Start Block | End Block | % Change |
+|-------------------------|--------------|--------------|----------|
+| Number of Curators      | {startCurators} | {endCurators} | {percNewCurators} |
+
+## 5.0 User Generated Content
+### 5.1 Membership Information
+| Property          | Start Block | End Block | % Change |
+|-------------------|--------------|--------------|----------|
+| Number of members | {startMembers}|  {endMembers} | {percNewMembers} |
+
+### 5.2 Media & Uploads
+| Property                | Start Block | End Block | % Change |
+|-------------------------|--------------|--------------|----------|
+| Number of uploads       | {startMedia} | {endMedia}  |  {percNewMedia} |
+| Size of content         |  {startUsedSpace} |  {endUsedSpace} | {percNewUsedSpace}          |
+| Number of channels      |  {startChannels} | {endChannels} | {percNewChannels} |
+
+### 5.3 Forum Activity
+| Property          | Start Block | End Block | % Change |
+|-------------------|--------------|--------------|----------|
+| Number of categories | {startCategories} | {endCategories} | {perNewCategories}         |
+| Number of threads    | {startThreads}| {endThreads} | {percNewThreads}         |
+| Number of posts      | {startPosts} | {endPosts}            |  {percNewPosts}        |

+ 118 - 0
council/report-generator/report-template.md.old

@@ -0,0 +1,118 @@
+# Tokenomics + Network Report
+This is a report which explains the current state of the Joystream network in numbers. It pulls figures from the chain and tries to provide a basic level of information about the network, tokens and more. 
+
+## 1.0 Basic Information
+* Date Range: {dateStart} - {dateEnd}
+* Council session #: {councilRound}
+* Starting block: {startBlock}
+* Block range: {startBlock} - {endBlock}
+
+### 1.1 Block Generation Information
+| Property                        | This Session | All Sessions | % Change |
+|---------------------------------|--------------|--------------|----------|
+| Number of blocks                | {newBlocks}       | {endBlock}   |   {percNewBlocks}       |
+| Block generation time (average) | {avgBlockProduction}|              |          |
+| Number of nodes (average)       |                     |              |          |
+
+### 1.2 Token + USD Information
+| Property       | This Session | All Sessions | % Change |
+|----------------|--------------|--------------|----------|
+| Token Issuance | {newIssuance}   | {totalIssuance}| {percNewIssuance}|
+| Token Burn     | {newTokensBurn} |              |          |
+| USD Backing    |                 |              |          |
+
+### 1.3 Membership Information
+| Property          | This Session | All Sessions | % Change |
+|-------------------|--------------|--------------|----------|
+| Number of members |{newMembers}  |{totalMembers}|{percNewMembers}|
+
+## 2.0 Tokenomics
+### 2.1 Token generation breakdown
+| Property                    | This Session | All Sessions | % Change |
+|-----------------------------|--------------|--------------|----------|
+| Total Tokens Minted         |{totalMinted}            |              |          |
+| Validator Role              |{newValidatorReward}|              |          |
+| Storage Role                |{newStorageProviderReward}              |              |          |
+| Council Role                |              |              |          |
+
+### 2.2 Mints 
+| Property                  | This Session | All Sessions | % Change |
+|---------------------------|--------------|--------------|----------|
+| Council Mint Total Minted |{newCouncilMinted}|              |          |
+| Curator Mint Total Minted |{newCuratorMinted}|              |          |
+
+## 3.0 Council
+* Council session #: {councilRound}
+* Number of council members: {councilMembers}
+* Total number of proposals: {newProposals}
+### 3.1 Elections
+| Property                    | This Session | All Sessions | % Change |
+|-----------------------------|--------------|--------------|----------|
+| Total Applicants            |{electionApplicants}      |{electionAvgApplicants}||
+| Total Applicant Stake       |{electionApplicantsStakes}|              |          |
+| Total Votes                 |{electionVotes}           |              |          |
+| Avg Votes per Applicant     |{avgVotePerApplicant}     |              |          |
+
+### 3.2 Proposals
+| Proposal Type                           | # of proposals during this session | Total number of proposal type |
+|-----------------------------------------|------------------------------------|-------------------------------|
+| Text                                    | {newTextProposals}                              |                               |
+| Runtime Upgrade                         | {newRuntimeUpgradeProposal}                    |                               |
+| Set Election Parameters                 | {newSetElectionParametersProposal}              |                               |
+| Spending                                | {newSpendingProposal}                           |                               |
+| Set Lead                                | {newSetLeadProposal}                            |                               |
+| Set Content Working Group Mint Capacity | {newSetContentWorkingGroupMintCapacityProposal} |                               |
+| Evict Storage Provider                  | {newEvictStorageProviderProposal}               |                               |
+| Set Validator Count                     | {newSetValidatorCountProposal}                  |                               |
+| Set Storage Role Parameters             | {newSetStorageRoleParametersProposal}           |                               |
+* Average time for proposal vote success:
+* Average overall time for proposal vote success:
+
+## 4 Roles
+### 4.1 Validator Information
+| Property                    | This Session | All Sessions | % Change |
+|-----------------------------|--------------|--------------|----------|
+| Number of validators        | {avgValidators}             |              |          |
+| Validator total stake       |              |              |          |
+| Average stake per validator |              |              |          |
+| Tokens generated by validator role |{newValidatorReward}            |              |          |
+
+### 4.2 Storage Role
+| Property                | This Session | All Sessions | % Change |
+|-------------------------|--------------|--------------|----------|
+| Number of storage nodes |              |              |          |
+| Content storage size    |              |              |          |
+| Total storage stake     |              |              |          |
+| Average storage stake   |              |              |          |
+| Storage Role Reward (/24h)   |              |              |          |
+
+### 4.3 Curator Role
+| Property                | This Session | All Sessions | % Change |
+|-------------------------|--------------|--------------|----------|
+| Curator roles filled     |              |              |          |
+
+
+## 5.0 User Generated Content
+### 5.1 Media & Uploads
+| Property                | This Session | All Sessions | % Change |
+|-------------------------|--------------|--------------|----------|
+| Number of uploads       | {newMedia}       |{totalMedia} | {percNewMedia} |
+| Size of content         | {newUsedSpace} | {totalUsedSpace} | {percNewUsedSpace} |
+| Average size of content | {avgNewSizePerContent} |  {totalAvgSizePerContent} | {percAvgSizePerContent}|
+| Number of channels      | {newChannels} | {totalChannels} | {percNewChannels} |
+| Avg. uploads per channel      |              |              |          |
+
+### 5.2 Forum Activity
+| Property          | This Session | All Sessions | % Change |
+|-------------------|--------------|--------------|----------|
+| Number of threads | {newThreads} |{totalThreads}| {percNewThreads}|
+| Number of posts   | {newPosts}   |{totalPosts}  | {percNewPosts}|
+
+## 6 Todo / Ideas
+* Video duration
+* KPIs
+* Unique channels
+* Verified channels
+* Censored channels
+* Forum posts by subcategory
+* Total staked across platform

+ 674 - 0
council/report-generator/src/StatisticsCollector.ts

@@ -0,0 +1,674 @@
+import {ApiPromise, WsProvider} from "@polkadot/api";
+import {types} from '@joystream/types'
+import {
+    AccountId,
+    Balance,
+    BalanceOf,
+    BlockNumber,
+    EraIndex,
+    EventRecord,
+    Hash,
+    Moment
+} from "@polkadot/types/interfaces";
+
+import {
+    CacheEvent,
+    Exchange,
+    Media,
+    MintStatistics,
+    StatisticsData,
+    ValidatorReward, WorkersInfo
+} from "./StatisticsData";
+
+import {u32, Vec} from "@polkadot/types";
+import {ElectionStake, SealedVote, Seats} from "@joystream/types/council";
+import {Mint, MintId} from "@joystream/types/mint";
+import {ContentId, DataObject} from "@joystream/types/media";
+import {RoleParameters} from "@joystream/types/roles";
+import {Entity, EntityId} from "@joystream/types/versioned-store";
+import Option from "@polkadot/types/codec/Option";
+import Linkage from "@polkadot/types/codec/Linkage";
+import {PostId, ThreadId} from "@joystream/types/common";
+import {CategoryId} from "@joystream/types/forum";
+import {Event} from "@polkadot/types/interfaces/system/types";
+import number from "@polkadot/util/is/number";
+import toNumber from "@polkadot/util/hex/toNumber";
+import {
+    ProposalStatus,
+    FinalizationData,
+    ProposalDecisionStatus,
+    Finalized,
+    IProposalStatus, Approved
+} from "@joystream/types/proposals";
+import {MemberId} from "@joystream/types/members";
+import {RewardRelationship, RewardRelationshipId} from "@joystream/types/recurring-rewards";
+import {StorageProviderId, WorkerId, Worker, RoleStakeProfile} from "@joystream/types/working-group";
+import workingGroup from "@joystream/types/src/working-group/index";
+import {Stake} from "@joystream/types/stake";
+import {ChannelId} from "@joystream/types/content-working-group";
+
+const fsSync = require('fs');
+const fs = fsSync.promises;
+
+const BURN_ADDRESS = '5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu';
+
+const COUNCIL_ROUND_OFFSET = 5;
+const PROVIDER_URL = "ws://localhost:9944";
+
+const CACHE_FOLDER = "cache";
+const WORKER_ID_OFFSET = 1;
+
+export class StatisticsCollector {
+
+    private api?: ApiPromise;
+    private blocksEventsCache: Map<number, CacheEvent[]>;
+    private statistics: StatisticsData;
+
+    constructor() {
+        this.blocksEventsCache = new Map<number, CacheEvent[]>();
+        this.statistics = new StatisticsData();
+    }
+
+    async getStatistics(startBlock: number, endBlock: number): Promise<StatisticsData> {
+        this.api = await StatisticsCollector.connectApi();
+
+        let startHash = (await this.api.rpc.chain.getBlockHash(startBlock)) as Hash;
+        let endHash = (await this.api.rpc.chain.getBlockHash(endBlock)) as Hash;
+
+        this.statistics.startBlock = startBlock;
+        this.statistics.endBlock = endBlock;
+        this.statistics.newBlocks = endBlock - startBlock;
+        this.statistics.percNewBlocks = StatisticsCollector.convertToPercentage(startBlock, endBlock);
+        await this.buildBlocksEventCache(startBlock, endBlock);
+        await this.fillBasicInfo(startHash, endHash);
+        await this.fillTokenGenerationInfo(startBlock, endBlock, startHash, endHash);
+        await this.fillMintsInfo(startHash, endHash);
+        await this.fillCouncilInfo(startHash, endHash);
+        await this.fillCouncilElectionInfo(startBlock);
+        await this.fillValidatorInfo(startHash, endHash);
+        await this.fillStorageProviderInfo(startBlock, endBlock, startHash, endHash);
+        await this.fillCuratorInfo(startHash, endHash);
+        await this.fillMembershipInfo(startHash, endHash);
+        await this.fillMediaUploadInfo(startHash, endHash);
+        await this.fillForumInfo(startHash, endHash);
+        this.api.disconnect();
+        return this.statistics;
+
+
+        //
+        // if (statistics.electionVotes) {
+        //     statistics.avgVotePerApplicant = statistics.electionVotes / statistics.electionApplicants;
+        // } else {
+        //     statistics.avgVotePerApplicant = 0;
+        // }
+        //
+
+        //
+
+        //
+        //
+
+        //
+
+        //
+        //
+        // let startNrStakes = await this.api.query.stake.stakesCreated.at(startHash) as StakeId;
+        // let endNrStakes = await this.api.query.stake.stakesCreated.at(endHash) as StakeId;
+        // statistics.newStakes = endNrStakes.toNumber() - startNrStakes.toNumber();
+        //
+        // for (let i = startNrStakes.toNumber(); i < endNrStakes.toNumber(); ++i) {
+        //     let stakeResult = await this.api.query.stake.stakes(i) as unknown as [Stake, Linkage<StakeId>];
+        //     let stake = stakeResult[0] as Stake;
+        //
+        //     statistics.totalNewStakeValue += stake.value ? stake.value.toNumber() : 0;
+        // }
+        //
+        // // let startBurnedTokens = await this.api.query.balances.freeBalance.at(startHash, BURN_ADDRESS) as Balance;
+        // // let endBurnedTokens = await this.api.query.balances.freeBalance.at(endHash, BURN_ADDRESS) as Balance;
+        // //
+        // // statistics.totalBurned = endBurnedTokens.toNumber() - startBurnedTokens.toNumber();
+        //
+
+        //
+
+        //
+        // let newMedia = endMedias.filter((endMedia) => {
+        //     return !startMedias.some((startMedia) => startMedia.id == endMedia.id);
+        // });
+        //
+        // statistics.newMedia = newMedia.length;
+        // statistics.totalMedia = endMedias.length;
+        // statistics.percNewMedia = this.convertToPercentage(statistics.newMedia, statistics.totalMedia);
+        //
+        // let startDataObjects = await this.api.query.dataDirectory.knownContentIds.at(startHash) as Vec<ContentId>;
+        // let startUsedSpace = await this.computeUsedSpaceInBytes(api, startDataObjects);
+        //
+        // let endDataObjects = await this.api.query.dataDirectory.knownContentIds.at(endHash) as Vec<ContentId>;
+        // let endUsedSpace = await this.computeUsedSpaceInBytes(api, endDataObjects);
+        //
+        // statistics.newUsedSpace = endUsedSpace - startUsedSpace;
+        // statistics.totalUsedSpace = endUsedSpace;
+        // statistics.percNewUsedSpace = this.convertToPercentage(statistics.newUsedSpace, statistics.totalUsedSpace);
+        //
+        // statistics.avgNewSizePerContent = Number((statistics.newUsedSpace / statistics.newMedia).toFixed(2));
+        // statistics.totalAvgSizePerContent = Number((statistics.totalUsedSpace / statistics.totalMedia).toFixed(2));
+        // statistics.percAvgSizePerContent = this.convertToPercentage(statistics.avgNewSizePerContent, statistics.totalAvgSizePerContent);
+        //
+        // //
+        // // for (let startMedia of startMedias) {
+        // //     let deleted = !endMedias.some((endMedia) => {
+        // //         return endMedia.id == startMedia.id;
+        // //     })
+        // //     if (deleted) {
+        // //         ++statistics.deletedMedia;
+        // //     }
+        // // }
+        //
+        //
+
+        //
+        // for (let i = startNrProposals.toNumber(); i < endNrProposals.toNumber(); ++i) {
+        //     let proposalNumber = i - 1;
+        //     let proposalDetails = await this.api.query.proposalsCodex.proposalDetailsByProposalId.at(endHash, proposalNumber) as ProposalDetails;
+        //     switch (proposalDetails.type) {
+        //         case ProposalTypes.Text:
+        //             ++statistics.newTextProposals;
+        //             break;
+        //
+        //         case ProposalTypes.RuntimeUpgrade:
+        //             ++statistics.newRuntimeUpgradeProposal;
+        //             break;
+        //
+        //         case ProposalTypes.SetElectionParameters:
+        //             ++statistics.newSetElectionParametersProposal;
+        //             break;
+        //
+        //         case ProposalTypes.Spending:
+        //             ++statistics.newSpendingProposal;
+        //             break;
+        //
+        //         case ProposalTypes.SetLead:
+        //             ++statistics.newSetLeadProposal;
+        //             break;
+        //
+        //         case ProposalTypes.SetContentWorkingGroupMintCapacity:
+        //             ++statistics.newSetContentWorkingGroupMintCapacityProposal;
+        //             break;
+        //
+        //         case ProposalTypes.EvictStorageProvider:
+        //             ++statistics.newEvictStorageProviderProposal;
+        //             break;
+        //
+        //         case ProposalTypes.SetValidatorCount:
+        //             ++statistics.newSetValidatorCountProposal;
+        //             break;
+        //
+        //         case ProposalTypes.SetStorageRoleParameters:
+        //             ++statistics.newSetStorageRoleParametersProposal;
+        //             break;
+        //     }
+        // }
+        //
+        // let validatorRewards: ValidatorReward[] = [];
+        // let exchangesCollection: Exchange[] = [];
+        // let promises = [];
+        //
+        // console.time('extractValidatorsRewards');
+        // for (let i = startBlock; i < endBlock; ++i) {
+        //     let promise = (async () => {
+        //         const blockHash: Hash = await this.api.rpc.chain.getBlockHash(i);
+        //         const events = await this.api.query.system.events.at(blockHash) as Vec<EventRecord>;
+        //         let rewards = await this.extractValidatorsRewards(api, i, events);
+        //         if (rewards.length) {
+        //             validatorRewards = validatorRewards.concat(rewards);
+        //         }
+        //         let exchanges = this.extractExchanges(i, events);
+        //         if (exchanges.length) {
+        //             exchangesCollection = exchangesCollection.concat(exchanges);
+        //         }
+        //
+        //     })();
+        //     promises.push(promise);
+        // }
+        // await Promise.all(promises);
+        // console.timeEnd('extractValidatorsRewards');
+        //
+        // statistics.newValidatorReward = validatorRewards.map((validatorReward) => validatorReward.sharedReward).reduce((a, b) => a + b);
+        // let avgValidators = validatorRewards.map((validatorReward) => validatorReward.validators).reduce((a, b) => a + b) / validatorRewards.length;
+        // statistics.avgValidators = Number(avgValidators.toFixed(2));
+        //
+        // statistics.newTokensBurn = exchangesCollection.map((exchange) => exchange.amount).reduce((a, b) => a + b);
+        //
+        // statistics.newStorageProviderReward = await this.computeStorageRewards(api, startBlock, endBlock);
+        //
+        // this.api.disconnect();
+        // return statistics;
+    }
+
+    async fillBasicInfo(startHash: Hash, endHash: Hash) {
+        let startDate = (await this.api.query.timestamp.now.at(startHash)) as Moment;
+        let endDate = (await this.api.query.timestamp.now.at(endHash)) as Moment;
+        this.statistics.dateStart = new Date(startDate.toNumber()).toLocaleDateString("en-US");
+        this.statistics.dateEnd = new Date(endDate.toNumber()).toLocaleDateString("en-US");
+    }
+
+    async fillTokenGenerationInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
+        this.statistics.startIssuance = (await this.api.query.balances.totalIssuance.at(startHash) as Balance).toNumber();
+        this.statistics.endIssuance = (await this.api.query.balances.totalIssuance.at(endHash) as Balance).toNumber();
+        this.statistics.newIssuance = this.statistics.endIssuance - this.statistics.startIssuance;
+        this.statistics.percNewIssuance = StatisticsCollector.convertToPercentage(this.statistics.startIssuance, this.statistics.endIssuance);
+
+        for (let [key, blockEvents] of this.blocksEventsCache) {
+            let validatorRewards = blockEvents.filter((event) => {
+                return event.section == "staking" && event.method == "Reward";
+            });
+            for (let validatorReward of validatorRewards) {
+                this.statistics.newValidatorRewards += Number(validatorReward.data[1]);
+            }
+
+            let transfers = blockEvents.filter((event) => {
+                return event.section == "balances" && event.method == "Transfer";
+            });
+            for (let transfer of transfers) {
+                let receiver = transfer.data[1] as AccountId;
+                let amount = transfer.data[2] as Balance;
+                if (receiver.toString() == BURN_ADDRESS) {
+                    this.statistics.newTokensBurn = Number(amount);
+                }
+            }
+        }
+        let roundNrBlocks = endBlock - startBlock;
+        this.statistics.newCouncilRewards = await this.computeCouncilReward(roundNrBlocks, endHash);
+        this.statistics.newCouncilRewards = Number(this.statistics.newCouncilRewards.toFixed(2));
+
+        this.statistics.newCuratorRewards = await this.computeCuratorsReward(roundNrBlocks, startHash, endHash);
+        this.statistics.newCuratorRewards = Number(this.statistics.newCuratorRewards.toFixed(2));
+    }
+
+    async computeCouncilReward(roundNrBlocks: number, endHash: Hash): Promise<number> {
+        const payoutInterval = Number((await this.api.query.council.payoutInterval.at(endHash) as Option<BlockNumber>).unwrapOr(0));
+        const amountPerPayout = (await this.api.query.council.amountPerPayout.at(endHash) as BalanceOf).toNumber();
+
+        const announcing_period = (await this.api.query.councilElection.announcingPeriod.at(endHash)) as BlockNumber;
+        const voting_period = (await this.api.query.councilElection.votingPeriod.at(endHash)) as BlockNumber;
+        const revealing_period = (await this.api.query.councilElection.revealingPeriod.at(endHash)) as BlockNumber;
+        const new_term_duration = (await this.api.query.councilElection.newTermDuration.at(endHash)) as BlockNumber;
+
+        const termDuration = new_term_duration.toNumber();
+        const votingPeriod = voting_period.toNumber();
+        const revealingPeriod = revealing_period.toNumber();
+        const announcingPeriod = announcing_period.toNumber();
+
+        const nrCouncilMembers = (await this.api.query.council.activeCouncil.at(endHash) as Seats).length
+        const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
+            ? (amountPerPayout * nrCouncilMembers) / payoutInterval
+            : 0;
+
+        const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
+        const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
+
+        return avgCouncilRewardPerBlock * roundNrBlocks;
+    }
+
+    async computeStorageProviderReward(roundNrBlocks: number, startHash: Hash, endHash: Hash): Promise<WorkersInfo> {
+        let nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber();
+        let info = new WorkersInfo();
+        for (let i = 0; i < nextWorkerId; ++i) {
+            let worker = await this.api.query.storageWorkingGroup.workerById(i) as Worker;
+            if (worker.role_stake_profile.isSome) {
+                let roleStakeProfile = worker.role_stake_profile.unwrap();
+                let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
+                info.startStake += stake.value.toNumber();
+            }
+        }
+
+        nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
+        let rewardRelationshipIds = Array<RewardRelationshipId>();
+
+        for (let i = 0; i < nextWorkerId; ++i) {
+            let worker = await this.api.query.storageWorkingGroup.workerById(i) as Worker;
+            if (worker.reward_relationship.isSome) {
+                rewardRelationshipIds.push(worker.reward_relationship.unwrap());
+            }
+            if (worker.role_stake_profile.isSome) {
+                let roleStakeProfile = worker.role_stake_profile.unwrap();
+                let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
+                info.endStake += stake.value.toNumber();
+            }
+        }
+        info.rewards = await this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
+        info.endNrOfWorkers = nextWorkerId - WORKER_ID_OFFSET;
+        return info;
+    }
+
+    async computeCuratorsReward(roundNrBlocks: number, startHash: Hash, endHash: Hash) {
+        let nextCuratorId = (await this.api.query.contentWorkingGroup.nextCuratorId.at(endHash) as WorkerId).toNumber();
+
+        let rewardRelationshipIds = Array<RewardRelationshipId>();
+        for (let i = 0; i < nextCuratorId; ++i) {
+            let worker = await this.api.query.contentWorkingGroup.curatorById(i) as Worker;
+            if (worker.reward_relationship.isSome) {
+                rewardRelationshipIds.push(worker.reward_relationship.unwrap());
+            }
+        }
+        return this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
+    }
+
+    async computeReward(roundNrBlocks: number, rewardRelationshipIds: RewardRelationshipId[], hash: Hash) {
+        let recurringRewards = await Promise.all(rewardRelationshipIds.map(async (rewardRelationshipId) => {
+            return await this.api.query.recurringRewards.rewardRelationships.at(hash, rewardRelationshipId) as RewardRelationship;
+        }));
+
+        let rewardPerBlock = 0;
+        for (let recurringReward of recurringRewards) {
+            const amount = recurringReward.amount_per_payout.toNumber();
+            const payoutInterval = recurringReward.payout_interval.unwrapOr(null);
+
+            if (amount && payoutInterval) {
+                rewardPerBlock += amount / payoutInterval;
+            }
+
+        }
+        return rewardPerBlock * roundNrBlocks;
+    }
+
+    async fillMintsInfo(startHash: Hash, endHash: Hash) {
+        let startNrMints = parseInt((await this.api.query.minting.mintsCreated.at(startHash)).toString());
+        let endNrMints = parseInt((await this.api.query.minting.mintsCreated.at(endHash)).toString());
+
+        this.statistics.newMints = endNrMints - startNrMints;
+        // statistics.startMinted = 0;
+        // statistics.endMinted = 0;
+        for (let i = 0; i < startNrMints; ++i) {
+            let startMint = (await this.api.query.minting.mints.at(startHash, i)) as Mint;
+            // if (!startMint) {
+            //     continue;
+            // }
+
+            let endMint = (await this.api.query.minting.mints.at(endHash, i)) as Mint;
+            // let  = endMintResult[0];
+            // if (!endMint) {
+            //     continue;
+            // }
+
+            let startMintTotal = parseInt(startMint.getField("total_minted").toString());
+            let endMintTotal = parseInt(endMint.getField("total_minted").toString());
+
+            // statistics.startMinted += startMintTotal;
+
+            this.statistics.totalMinted += endMintTotal - startMintTotal;
+            this.statistics.totalMintCapacityIncrease += parseInt(endMint.getField("capacity").toString()) - parseInt(startMint.getField("capacity").toString());
+        }
+
+        for (let i = startNrMints; i < endNrMints; ++i) {
+            let endMintResult = ((await this.api.query.minting.mints.at(endHash, i)) as unknown) as [Mint, Linkage<MintId>];
+
+            let endMint = endMintResult[0] as Mint;
+            if (!endMint) {
+                return;
+            }
+            this.statistics.totalMinted = parseInt(endMint.getField("total_minted").toString());
+        }
+
+        let councilMint = (await this.api.query.council.councilMint.at(endHash)) as MintId;
+        let councilMintStatistics = await this.computeMintInfo(councilMint, startHash, endHash);
+
+        this.statistics.startCouncilMinted = councilMintStatistics.startMinted;
+        this.statistics.endCouncilMinted = councilMintStatistics.endMinted;
+        this.statistics.newCouncilMinted = councilMintStatistics.diffMinted;
+        this.statistics.percNewCouncilMinted = councilMintStatistics.percMinted;
+        6
+        let curatorMint = (await this.api.query.contentWorkingGroup.mint.at(endHash)) as MintId;
+        let curatorMintStatistics = await this.computeMintInfo(curatorMint, startHash, endHash);
+        this.statistics.startCuratorMinted = curatorMintStatistics.startMinted;
+        this.statistics.endCuratorMinted = curatorMintStatistics.endMinted;
+        this.statistics.newCuratorMinted = curatorMintStatistics.diffMinted;
+        this.statistics.percCuratorMinted = curatorMintStatistics.percMinted;
+
+        let storageProviderMint = (await this.api.query.storageWorkingGroup.mint.at(endHash)) as MintId;
+        let storageProviderMintStatistics = await this.computeMintInfo(storageProviderMint, startHash, endHash);
+        this.statistics.startStorageMinted = storageProviderMintStatistics.startMinted;
+        this.statistics.endStorageMinted = storageProviderMintStatistics.endMinted;
+        this.statistics.newStorageMinted = storageProviderMintStatistics.diffMinted;
+        this.statistics.percStorageMinted = storageProviderMintStatistics.percMinted;
+    }
+
+
+    async computeMintInfo(mintId: MintId, startHash: Hash, endHash: Hash): Promise<MintStatistics> {
+        // if (mintId.toString() == "0") {
+        //     return new MintStatistics(0, 0, 0);
+        // }
+        let startMint = await this.api.query.minting.mints.at(startHash, mintId) as Mint;
+        // let startMint = startMintResult[0] as unknown as Mint;
+        // if (!startMint) {
+        //     return new MintStatistics(0, 0, 0);
+        // }
+
+        let endMint = await this.api.query.minting.mints.at(endHash, mintId) as Mint;
+        // let endMint = endMintResult[0] as unknown as Mint;
+        // if (!endMint) {
+        //     return new MintStatistics(0, 0, 0);
+        // }
+
+        let mintStatistics = new MintStatistics();
+        mintStatistics.startMinted = parseInt(startMint.getField('total_minted').toString());
+        mintStatistics.endMinted = parseInt(endMint.getField('total_minted').toString());
+        mintStatistics.diffMinted = mintStatistics.endMinted - mintStatistics.startMinted;
+        mintStatistics.percMinted = StatisticsCollector.convertToPercentage(mintStatistics.startMinted, mintStatistics.endMinted);
+        return mintStatistics;
+    }
+
+    async fillCouncilInfo(startHash: Hash, endHash: Hash) {
+        this.statistics.councilRound = (await this.api.query.councilElection.round.at(startHash) as u32).toNumber();
+        this.statistics.councilMembers = (await this.api.query.councilElection.councilSize.at(startHash) as u32).toNumber();
+        let startNrProposals = await this.api.query.proposalsEngine.proposalCount.at(startHash) as u32;
+        let endNrProposals = await this.api.query.proposalsEngine.proposalCount.at(endHash) as u32;
+        this.statistics.newProposals = endNrProposals.toNumber() - startNrProposals.toNumber();
+
+        let approvedProposals = new Set();
+        for (let [key, blockEvents] of this.blocksEventsCache) {
+            for (let event of blockEvents) {
+                if (event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated") {
+                    let statusUpdateData = event.data[1] as any;
+                    let finalizeData = statusUpdateData.Finalized as any
+                    if (finalizeData && finalizeData.proposalStatus.Approved) {
+                        approvedProposals.add(Number(event.data[0]));
+                    }
+
+                }
+            }
+        }
+
+        this.statistics.newApprovedProposals = approvedProposals.size;
+    }
+
+    async fillCouncilElectionInfo(startBlock: number) {
+
+        let startBlockHash = await this.api.rpc.chain.getBlockHash(startBlock);
+        let events = await this.api.query.system.events.at(startBlockHash) as Vec<EventRecord>;
+        let isStartBlockFirstCouncilBlock = events.some((event) => {
+            return event.event.section == "councilElection" && event.event.method == "CouncilElected";
+        });
+
+        if (!isStartBlockFirstCouncilBlock) {
+            console.warn('The given start block is not the first block of the council round so council election information will be empty');
+            return;
+        }
+        let previousCouncilRoundLastBlock = startBlock - 1;
+        let previousCouncilRoundLastBlockHash = await this.api.rpc.chain.getBlockHash(previousCouncilRoundLastBlock);
+
+        let applicants = await this.api.query.councilElection.applicants.at(previousCouncilRoundLastBlockHash) as Vec<AccountId>;
+        this.statistics.electionApplicants = applicants.length;
+        for (let applicant of applicants) {
+            let applicantStakes = await this.api.query.councilElection.applicantStakes.at(previousCouncilRoundLastBlockHash, applicant) as unknown as ElectionStake;
+            this.statistics.electionApplicantsStakes += applicantStakes.new.toNumber();
+        }
+        // let seats = await this.api.query.council.activeCouncil.at(startBlockHash) as Seats;
+        //TODO: Find a more accurate way of getting the votes
+        const votes = await this.api.query.councilElection.commitments.at(previousCouncilRoundLastBlockHash) as Vec<Hash>;
+        this.statistics.electionVotes = votes.length;
+    }
+
+    async fillValidatorInfo(startHash: Hash, endHash: Hash) {
+        let startTimestamp = await this.api.query.timestamp.now.at(startHash) as unknown as Moment;
+        let endTimestamp = await this.api.query.timestamp.now.at(endHash) as unknown as Moment;
+        let avgBlockProduction = (((endTimestamp.toNumber() - startTimestamp.toNumber())
+            / 1000) / this.statistics.newBlocks);
+        this.statistics.avgBlockProduction = Number(avgBlockProduction.toFixed(2));
+
+        this.statistics.startValidators = (await this.api.query.staking.validatorCount.at(startHash) as u32).toNumber();
+        this.statistics.endValidators = (await this.api.query.staking.validatorCount.at(endHash) as u32).toNumber();
+        this.statistics.percValidators = StatisticsCollector.convertToPercentage(this.statistics.startValidators, this.statistics.endValidators);
+
+        const startEra = await this.api.query.staking.currentEra.at(startHash) as Option<EraIndex>;
+        this.statistics.startValidatorsStake = (await this.api.query.staking.erasTotalStake.at(startHash, startEra.unwrap())).toNumber();
+
+        const endEra = await this.api.query.staking.currentEra.at(endHash) as Option<EraIndex>;
+        this.statistics.endValidatorsStake = (await this.api.query.staking.erasTotalStake.at(endHash, endEra.unwrap())).toNumber();
+
+        this.statistics.percNewValidatorsStake = StatisticsCollector.convertToPercentage(this.statistics.startValidatorsStake, this.statistics.endValidatorsStake);
+    }
+
+    async fillStorageProviderInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
+        let roundNrBlocks = endBlock - startBlock;
+
+        let storageProvidersRewards = await this.computeStorageProviderReward(roundNrBlocks, startHash, endHash);
+        this.statistics.newStorageProviderReward = storageProvidersRewards.rewards;
+        this.statistics.newStorageProviderReward = Number(this.statistics.newStorageProviderReward.toFixed(2));
+
+        this.statistics.startStorageProvidersStake = storageProvidersRewards.startStake;
+        this.statistics.endStorageProvidersStake = storageProvidersRewards.endStake;
+        this.statistics.percNewStorageProviderStake = StatisticsCollector.convertToPercentage(this.statistics.startStorageProvidersStake, this.statistics.endStorageProvidersStake);
+
+        this.statistics.startStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
+        this.statistics.endStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
+        this.statistics.percNewStorageProviders = StatisticsCollector.convertToPercentage(this.statistics.startStorageProviders, this.statistics.endStorageProviders);
+
+    }
+
+    async fillCuratorInfo(startHash: Hash, endHash: Hash) {
+        this.statistics.startCurators = (await this.api.query.contentWorkingGroup.nextCuratorId.at(startHash));
+        this.statistics.endCurators = (await this.api.query.contentWorkingGroup.nextCuratorId.at(endHash));
+        this.statistics.percNewCurators = StatisticsCollector.convertToPercentage(this.statistics.startCurators, this.statistics.endCurators);
+    }
+
+    async fillMembershipInfo(startHash: Hash, endHash: Hash) {
+        this.statistics.startMembers = (await this.api.query.members.nextMemberId.at(startHash) as MemberId).toNumber();
+        this.statistics.endMembers = (await this.api.query.members.nextMemberId.at(endHash) as MemberId).toNumber();
+        this.statistics.newMembers = this.statistics.endMembers - this.statistics.startMembers;
+        this.statistics.percNewMembers = StatisticsCollector.convertToPercentage(this.statistics.startMembers, this.statistics.endMembers);
+    }
+
+    async fillMediaUploadInfo(startHash: Hash, endHash: Hash) {
+        let startMedias = await this.getMedia(startHash);
+        let endMedias = await this.getMedia(endHash);
+
+        this.statistics.startMedia = startMedias.length;
+        this.statistics.endMedia = endMedias.length;
+        this.statistics.percNewMedia = StatisticsCollector.convertToPercentage(this.statistics.startMedia, this.statistics.endMedia);
+
+        this.statistics.startChannels = (await this.api.query.contentWorkingGroup.nextChannelId.at(startHash) as ChannelId).toNumber();
+        this.statistics.endChannels = (await this.api.query.contentWorkingGroup.nextChannelId.at(endHash) as ChannelId).toNumber();
+        this.statistics.percNewChannels = StatisticsCollector.convertToPercentage(this.statistics.startChannels, this.statistics.endChannels);
+
+        let startDataObjects = await this.api.query.dataDirectory.knownContentIds.at(startHash) as Vec<ContentId>;
+        this.statistics.startUsedSpace = await this.computeUsedSpaceInBytes(startDataObjects);
+
+        let endDataObjects = await this.api.query.dataDirectory.knownContentIds.at(endHash) as Vec<ContentId>;
+        this.statistics.endUsedSpace = await this.computeUsedSpaceInBytes(endDataObjects);
+        this.statistics.percNewUsedSpace = StatisticsCollector.convertToPercentage(this.statistics.startUsedSpace, this.statistics.endUsedSpace);
+    }
+
+    async fillForumInfo(startHash: Hash, endHash: Hash) {
+        let startPostId = await this.api.query.forum.nextPostId.at(startHash) as PostId;
+        let endPostId = await this.api.query.forum.nextPostId.at(endHash) as PostId;
+        this.statistics.startPosts = startPostId.toNumber();
+        this.statistics.endPosts = endPostId.toNumber();
+        this.statistics.newPosts = this.statistics.endPosts - this.statistics.startPosts;
+        this.statistics.percNewPosts = StatisticsCollector.convertToPercentage(this.statistics.startPosts, this.statistics.endPosts);
+
+        let startThreadId = ((await this.api.query.forum.nextThreadId.at(startHash)) as unknown) as ThreadId;
+        let endThreadId = ((await this.api.query.forum.nextThreadId.at(endHash)) as unknown) as ThreadId;
+        this.statistics.startThreads = startThreadId.toNumber();
+        this.statistics.endThreads = endThreadId.toNumber();
+        this.statistics.newThreads = this.statistics.endThreads - this.statistics.startThreads;
+        this.statistics.percNewThreads = StatisticsCollector.convertToPercentage(this.statistics.startThreads, this.statistics.endThreads);
+
+        let startCategoryId = (await this.api.query.forum.nextCategoryId.at(startHash)) as CategoryId;
+        let endCategoryId = (await this.api.query.forum.nextCategoryId.at(endHash)) as CategoryId;
+        this.statistics.startCategories = startCategoryId.toNumber();
+        this.statistics.endCategories = endCategoryId.toNumber();
+        this.statistics.newCategories = this.statistics.endCategories - this.statistics.startCategories;
+        this.statistics.perNewCategories = StatisticsCollector.convertToPercentage(this.statistics.startCategories, this.statistics.endCategories);
+    }
+
+    static convertToPercentage(previousValue: number, newValue: number): number {
+        if (previousValue == 0) {
+            return 0;
+        }
+        return Number((newValue * 100 / previousValue - 100).toFixed(2));
+    }
+
+    async computeUsedSpaceInBytes(contentIds: Vec<ContentId>) {
+        let space = 0;
+        for (let contentId of contentIds) {
+            let dataObject = (await this.api.query.dataDirectory.dataObjectByContentId(contentId)) as Option<DataObject>;
+            space += dataObject.unwrap().size_in_bytes.toNumber();
+        }
+        return space;
+    }
+
+    async getMedia(blockHash: Hash) {
+        let nrEntities = ((await this.api.query.versionedStore.nextEntityId.at(blockHash)) as EntityId).toNumber();
+
+        let medias: Media[] = [];
+        for (let i = 0; i < nrEntities; ++i) {
+            let entity = await this.api.query.versionedStore.entityById.at(blockHash, i) as Entity;
+
+            if (entity.class_id.toNumber() != 7 || entity.entity_values.isEmpty) {
+                continue;
+            }
+
+            let title = entity.entity_values[0].value.toString();
+
+            medias.push(new Media(entity.id.toNumber(), title));
+        }
+        return medias;
+    }
+
+    async buildBlocksEventCache(startBlock: number, endBlock: number) {
+        let cacheFile = CACHE_FOLDER + '/' + startBlock + '-' + endBlock + '.json';
+        let exists = await fs.access(cacheFile, fsSync.constants.R_OK).then(() => true)
+            .catch(() => false);
+        // let exists = false;
+        if (!exists) {
+            console.log('Building events cache...');
+            for (let i = startBlock; i < endBlock; ++i) {
+                process.stdout.write('\rCaching block: ' + i + ' until ' + endBlock);
+                const blockHash: Hash = await this.api.rpc.chain.getBlockHash(i);
+                let eventRecord = await this.api.query.system.events.at(blockHash) as Vec<EventRecord>;
+                let cacheEvents = new Array<CacheEvent>();
+                for (let event of eventRecord) {
+                    cacheEvents.push(new CacheEvent(event.event.section, event.event.method, event.event.data));
+                }
+                this.blocksEventsCache.set(i, cacheEvents);
+            }
+
+            console.log('\nFinish events cache...');
+            await fs.writeFile(cacheFile, JSON.stringify(Array.from(this.blocksEventsCache.entries()), null, 2));
+        } else {
+            console.log('Cache file found, loading it...');
+            let fileData = await fs.readFile(cacheFile);
+            this.blocksEventsCache = new Map(JSON.parse(fileData));
+            console.log('Cache file loaded...');
+        }
+    }
+
+    static async connectApi(): Promise<ApiPromise> {
+        // const provider = new WsProvider('wss://testnet.joystream.org:9944');
+        const provider = new WsProvider(PROVIDER_URL);
+
+        // Create the API and wait until ready
+        return await ApiPromise.create({provider, types});
+    }
+}

+ 206 - 0
council/report-generator/src/StatisticsData.ts

@@ -0,0 +1,206 @@
+import {Vec} from "@polkadot/types";
+import {EventRecord} from "@polkadot/types/interfaces";
+import {EventData} from "@polkadot/types/generic/Event";
+
+export class StatisticsData {
+    councilRound: number = 0;
+    councilMembers: number = 0;
+
+    electionApplicants: number = 0;
+    electionAvgApplicants: number = 0;
+    perElectionApplicants: number = 0;
+
+    electionApplicantsStakes: number = 0;
+    electionVotes: number = 0;
+    avgVotePerApplicant: number = 0;
+
+    dateStart: string = "";
+    dateEnd: string = "";
+
+    startBlock: number = 0;
+    endBlock: number = 0;
+    percNewBlocks: number = 0;
+
+    startMembers: number = 0;
+    endMembers: number = 0;
+    newMembers: number = 0;
+    percNewMembers: number = 0;
+
+    newBlocks: number = 0;
+    avgBlockProduction: number = 0;
+
+
+    startThreads: number = 0;
+    endThreads: number = 0;
+    newThreads: number = 0;
+    totalThreads: number = 0;
+    percNewThreads: number = 0;
+
+    startPosts: number = 0;
+    // endPosts: number = 0;
+    newPosts: number = 0;
+    endPosts: number = 0;
+    percNewPosts: number = 0;
+
+    startCategories: number = 0;
+    endCategories: number = 0;
+    newCategories: number = 0;
+    perNewCategories: number = 0;
+
+    newProposals: number = 0;
+    newApprovedProposals: number = 0;
+
+    startChannels: number = 0;
+    newChannels: number = 0;
+    endChannels: number = 0;
+    percNewChannels: number = 0;
+
+    startMedia: number = 0;
+    newMedia: number = 0;
+    endMedia: number = 0;
+    percNewMedia: number = 0;
+
+    deletedMedia: number = 0;
+    newMints: number = 0;
+
+    startMinted: number = 0;
+    totalMinted: number = 0;
+    percMinted: number = 0;
+    endMinted: number = 0;
+
+    totalMintCapacityIncrease: number = 0;
+
+    startCouncilMinted: number = 0;
+    endCouncilMinted: number = 0;
+    newCouncilMinted: number = 0;
+    percNewCouncilMinted: number = 0;
+
+    startCuratorMinted: number = 0;
+    endCuratorMinted: number = 0;
+    newCuratorMinted: number = 0;
+    percCuratorMinted: number = 0;
+
+    startStorageMinted: number = 0;
+    endStorageMinted: number = 0;
+    newStorageMinted: number = 0;
+    percStorageMinted: number = 0;
+
+    startIssuance: number = 0;
+    endIssuance: number = 0;
+    newIssuance: number = 0;
+    percNewIssuance: number = 0;
+
+    newTokensBurn: number = 0;
+    newValidatorRewards: number = 0;
+    avgValidators: number = 0;
+    startValidators: number = 0;
+    endValidators: number = 0;
+    percValidators: number = 0;
+    startValidatorsStake: number = 0;
+    endValidatorsStake: number = 0;
+    percNewValidatorsStake: number = 0;
+
+    startStorageProviders: number = 0;
+    endStorageProviders: number = 0;
+    percNewStorageProviders: number = 0;
+    newStorageProviderReward: number = 0;
+    startStorageProvidersStake: number = 0;
+    endStorageProvidersStake: number = 0;
+    percNewStorageProviderStake: number = 0;
+
+    newCouncilRewards: number = 0;
+
+    startCurators: number = 0;
+    endCurators: number = 0;
+    percNewCurators: number = 0;
+    newCuratorRewards: number = 0;
+
+    startUsedSpace: number = 0;
+    newUsedSpace: number = 0;
+    endUsedSpace: number = 0;
+    percNewUsedSpace: number = 0;
+
+    avgNewSizePerContent: number = 0;
+    totalAvgSizePerContent: number = 0;
+    percAvgSizePerContent: number = 0;
+
+    newStakes: number = 0;
+    totalNewStakeValue: number = 0;
+
+    newTextProposals: number = 0;
+    newRuntimeUpgradeProposal: number = 0;
+    newSetElectionParametersProposal: number = 0;
+    newSpendingProposal: number = 0;
+    newSetLeadProposal: number = 0;
+    newSetContentWorkingGroupMintCapacityProposal: number = 0;
+    newEvictStorageProviderProposal: number = 0;
+    newSetValidatorCountProposal: number = 0;
+    newSetStorageRoleParametersProposal: number = 0;
+
+
+    constructor() {
+    }
+
+}
+
+export class ValidatorReward {
+    sharedReward: number = 0;
+    remainingReward: number = 0;
+    validators: number = 0;
+    slotStake: number = 0;
+    blockNumber: number = 0;
+}
+
+export class WorkersInfo {
+    rewards: number = 0;
+    startStake: number = 0;
+    endStake: number = 0;
+    startNrOfWorkers: number = 0;
+    endNrOfWorkers: number = 0;
+}
+
+export class Exchange {
+    sender: string = "";
+    amount: number = 0;
+    fees: number = 0;
+    blockNumber: number = 0;
+}
+
+export enum ProposalTypes {
+    Text = "Text",
+    RuntimeUpgrade = "RuntimeUpgrade",
+    SetElectionParameters = "SetElectionParameters",
+    Spending = "Spending",
+    SetLead = "SetLead",
+    SetContentWorkingGroupMintCapacity = "SetContentWorkingGroupMintCapacity",
+    EvictStorageProvider = "EvictStorageProvider",
+    SetValidatorCount = "SetValidatorCount",
+    SetStorageRoleParameters = "SetStorageRoleParameters",
+}
+
+export class MintStatistics {
+    startMinted: number;
+    endMinted: number;
+    diffMinted: number;
+    percMinted: number;
+
+    constructor(startMinted: number = 0, endMinted: number = 0, diffMinted: number = 0, percMinted: number = 0) {
+        this.startMinted = startMinted;
+        this.endMinted = endMinted;
+        this.diffMinted = diffMinted;
+        this.percMinted = percMinted;
+    }
+
+}
+
+export class Media {
+
+    constructor(public id: number, public title: string) {
+    }
+}
+
+export class CacheEvent {
+
+    constructor(public section: string, public method: string, public data: EventData) {
+    }
+}

+ 85 - 0
council/report-generator/src/block-interval.ts

@@ -0,0 +1,85 @@
+// import {ApiPromise, WsProvider} from "@polkadot/api";
+// import {Hash, Header} from "@polkadot/types/interfaces/runtime";
+// import { types } from '@joystream/types'
+//
+// async function main() {
+//     let startDate = new Date(2020, 4, 20, 13, 0);
+//     console.log(startDate);
+//     let endDate = new Date(2020, 4, 29, 23, 59);
+//
+//     // Initialise the provider to connect to the local node
+//     const provider = new WsProvider('wss://rome-rpc-endpoint.joystream.org:9944');
+//
+//     // Create the API and wait until ready
+//     const api = await ApiPromise.create({provider, types});
+//
+//     let blockInterval = await getBlockInterval(api, startDate.getTime(), endDate.getTime());
+//     console.log(blockInterval);
+// }
+//
+// async function getBlockInterval(api: ApiPromise, startTimestamp: number, endTimestamp: number) {
+//
+//     let approximateStartBlockHash = await getApproximatedBlockHash(api, startTimestamp);
+//     let startBlock = await adjustApproximatedBlockHash(api, startTimestamp, approximateStartBlockHash);
+//
+//     let approximateEndBlockHash = await getApproximatedBlockHash(api, endTimestamp);
+//     let endBlock = await adjustApproximatedBlockHash(api, endTimestamp, approximateEndBlockHash);
+//
+//     let startBlockHeader = await api.rpc.chain.getHeader(startBlock) as Header;
+//     let endBlockHeader = await api.rpc.chain.getHeader(endBlock) as Header;
+//
+//     return {
+//         'startBlock':
+//             startBlockHeader.number.unwrap().toNumber(),
+//         'endBlock':
+//             endBlockHeader.number.unwrap().toNumber()
+//     };
+// }
+//
+// async function getApproximatedBlockHash(api: ApiPromise, timestampToFound: number): Promise<Hash> {
+//     let lastHeader = await api.rpc.chain.getHeader();
+//     let lastHash = lastHeader.hash.toString();
+//     let lastTimestamp = parseInt((await api.query.timestamp.now.at(lastHash)).toString());
+//
+//     let prevousBlockHash = lastHeader.parentHash;
+//     let previousBlockTimestamp = parseInt((await api.query.timestamp.now.at(prevousBlockHash)).toString());
+//
+//     let secondsPerBlock = lastTimestamp - previousBlockTimestamp;
+//
+//     let blocksDiff = Math.floor((lastTimestamp - timestampToFound) / secondsPerBlock);
+//     let lastBlockNumber = lastHeader.number.unwrap();
+//     let approximatedBlockNr = lastBlockNumber.toNumber() - blocksDiff;
+//     return await api.rpc.chain.getBlockHash(approximatedBlockNr);
+// }
+//
+// async function adjustApproximatedBlockHash(api: ApiPromise, timestamp: number, hash: Hash) {
+//     let approximatedBlockTimestamp = parseInt((await api.query.timestamp.now.at(hash)).toString());
+//
+//     if (timestamp == approximatedBlockTimestamp) {
+//         return hash;
+//     }
+//
+//     let step = 1;
+//     if (timestamp < approximatedBlockTimestamp) {
+//         step = -1;
+//     }
+//
+//     let approximatedBlockHeader = await api.rpc.chain.getHeader(hash);
+//     let blockNumber = approximatedBlockHeader.number.unwrap().toNumber();
+//     let lastHashFound = hash;
+//     do {
+//         blockNumber += step;
+//         let nextBlockHash = await api.rpc.chain.getBlockHash(blockNumber);
+//         let nextBlockTimeStamp = parseInt((await api.query.timestamp.now.at(nextBlockHash)).toString());
+//
+//         if (Math.abs(approximatedBlockTimestamp - timestamp) < Math.abs(nextBlockTimeStamp - timestamp)) {
+//             return lastHashFound;
+//         }
+//
+//         approximatedBlockTimestamp = nextBlockTimeStamp;
+//         lastHashFound = nextBlockHash;
+//
+//     } while (true);
+// }
+//
+// main();

+ 46 - 0
council/report-generator/src/generator.ts

@@ -0,0 +1,46 @@
+import {StatisticsCollector} from "./StatisticsCollector";
+
+const fs = require('fs').promises;
+
+async function main() {
+    const args = process.argv.slice(2);
+
+    if (args.length != 2) {
+        console.error('Usage: [start bock number] [end block number]');
+        process.exit(1);
+    }
+
+    const startBlock = Number(args[0]);
+    const endBlock = Number(args[1]);
+
+    if (isNaN(startBlock) || isNaN(endBlock) || startBlock >= endBlock) {
+        console.error('Invalid block range');
+        process.exit(1);
+    }
+
+    try {
+        let fileData = await fs.readFile(__dirname + '/../report-template.md', {
+            encoding: "utf8"
+        });
+        console.log('Getting report info...');
+        let staticCollecttor = new StatisticsCollector();
+        let statistics =  await staticCollecttor.getStatistics(startBlock, endBlock);
+        console.log('Writing info in the report...');
+
+        let entries = Object.entries(statistics);
+
+        for (let entry of entries){
+            let regex = new RegExp('{' + entry[0] + '}', "g");
+            fileData = fileData.replace(regex, entry[1].toString());
+        }
+
+        await fs.writeFile('report.md', fileData);
+        console.log('Report generated!');
+        process.exit(0);
+    }catch (e) {
+        console.error(e);
+    }
+
+}
+
+main();

+ 39 - 0
council/report-generator/tsconfig.json

@@ -0,0 +1,39 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "commonjs",
+    "strict": false,
+    "sourceMap": true,
+    "noImplicitAny": true,
+    "noUnusedLocals": false,
+    "noImplicitReturns": true,
+    "moduleResolution": "node",
+    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
+    "declaration": true,
+    "resolveJsonModule": true,
+    "types" : [
+      "node"
+    ],
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "paths": {
+      "@polkadot/types/augment": ["./node_modules/@joystream/types/augment-codec/augment-types.ts"]
+    },
+    "typeRoots": [
+      "./node_modules/@polkadot/ts",
+      "./node_modules/@types"
+    ],
+    "declarationDir": "lib",
+    "outDir": "lib"
+  },
+  "include": [
+    "src/*.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "**/*.spec.ts",
+    "**/*.d.ts"
+  ]
+}