Bladeren bron

Merge pull request #414 from traumschule/report-generator-refactor

Refactor tokenomics report generator
mochet 3 jaren geleden
bovenliggende
commit
e44691bafb

+ 0 - 1
.gitignore

@@ -1,5 +1,4 @@
 .DS_Store
 node_modules/
 dist/
-lib/
 .idea/*

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "lib"]
+	path = contributions/tech/report-generator/src/lib
+	url = https://git.joystreamstats.live/Operations/joystream-lib

+ 2 - 2
contributions/tech/report-generator/.gitignore

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

+ 27 - 13
contributions/tech/report-generator/README.md

@@ -1,20 +1,34 @@
-# Council Report Generator
+# Tokenomics 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
- ```
+## Setup
 
- ## Usage
- ```
-node lib/generator.js <start block> <end block> 
- ```
+`yarn`
 
-## Example
- ```
-node lib/generator.js 57601 234038 
- ```
+To update the submodule in `src/lib`:
+```
+git submodule init
+git submodule update
+```
+
+## Usage
+
+`yarn run report`
+
+OR
+
+`yarn build && node build/generator.js <start block> <end block>`
+
+## Examples
+
+- `yarn run report 30`
+- `node build/generator.js 57601 234038`
+
+# Contributors
+
+* [freakstatic](https://github.com/freakstatic)
+* [l1dev](https://git.joystreamstats.live/l1devx)
+* [isonar](https://github.com/singulart)

+ 4 - 2
contributions/tech/report-generator/package.json

@@ -1,9 +1,10 @@
 {
   "name": "report-generator",
   "version": "0.1.0",
-  "main": "lib/index.js",
+  "main": "build/index.js",
   "license": "MIT",
   "scripts": {
+    "report": "ts-node src/generator.ts",
     "build": "tsc --build tsconfig.json",
     "status": "node lib/status"
   },
@@ -19,7 +20,8 @@
     "@polkadot/wasm-crypto": "^4.0.2",
     "@types/bn.js": "^4.11.6",
     "bn.js": "^5.1.2",
-    "csv-parse": "^4.15.4"
+    "csv-parse": "^4.15.4",
+    "ts-node": "^10.3.0"
   },
   "devDependencies": {
     "@polkadot/ts": "^0.3.62",

+ 43 - 30
contributions/tech/report-generator/report-template.md

@@ -11,25 +11,41 @@ This is a report which explains the current state of the Joystream network in nu
 | Property            | Start Block | End Block | % Change |
 |---------------------|--------------|--------------|----------|
 | Total Tokens Minted |  {startIssuance} | {endIssuance} | {percNewIssuance} |
+| USD Pool |  {startDollarPool} | {endDollarPool} | {dollarPoolPctChange} |
 
 | Property            | Value        |
 |---------------------|--------------|
-| Total Tokens Burned | {newTokensBurn}           | 
-| Spending Proposals (Executed)   |  {spendingProposalsTotal}                  |
-| Bounties paid       |  {bountiesTotalPaid}                           |
-| Validator Role      |  {newValidatorRewards}            |  
-| Storage Role        | {newStorageProviderReward}             | 
-| Curator Role        | {newCuratorRewards}             |
-| Operations Role     | {newOperationsReward}          |
+| Total Tokens Burned | {newTokensBurn} |
+| Spending Proposals (Executed) | {spendingProposalsTotal} |
+| Bounties paid       | {bountiesTotalPaid} |
+| Validator Role      | {newValidatorRewards} |
+| Storage Role        | {newStorageProviderReward} |
+| Curator Role        | {newCuratorRewards} |
+| Operations Role     | {newOperationsReward} |
+
+### 2.2 Fiat Pool
+| Property            | Start Block, USD | End Block, USD | % Change |
+|---------------------|--------------|--------------|----------|
+| USD Pool | {startDollarPool} | {endDollarPool} | {dollarPoolPctChange} |
 
+{dollarPoolRefills}
 
-### 2.3 Mints 
+### 2.3 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}        |
-| Operations Mint Total Minted   |  {startOperationsMinted} |  {endOperationsMinted}            |  {percOperationsMinted}        |
+| Council Mint Total Minted   | {startCouncilMinted}  | {endCouncilMinted} |{percNewCouncilMinted} |
+| Curator Mint Total Minted   | {startCuratorMinted} | {endCuratorMinted} | {percCuratorMinted} |
+| Storage Mint Total Minted   | {startStorageMinted} | {endStorageMinted} | {percStorageMinted} |
+| Operations Mint Total Minted | {startOperationsMinted} | {endOperationsMinted} | {percOperationsMinted} |
+
+
+### 2.4 tJOY Inflation
+
+* Start Block Exchange Rate, USD/1M tJOY: {startTermExchangeRate}
+* End Block Exchange Rate, USD/1M tJOY: {endTermExchangeRate}
+* Inflation, %: {inflationPct}
+
+Negative value indicates deflation
 
 ## 3.0 Council
 * Council session #: {councilRound}
@@ -40,44 +56,41 @@ This is a report which explains the current state of the Joystream network in nu
 ### 3.1 Elections
 | Property                    | Start Block  |
 |-----------------------------|--------------|
-| Total Applicants            |{electionApplicants}              |
-| Total Applicant Stake       |{electionApplicantsStakes}              |
-| Total Votes                 |{electionVotes}             |
+| 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} |
+| 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} |   
+| Number of Storage Workers | {startStorageProviders} | {endStorageProviders} | {percNewStorageProviders} |
+| Total Storage Stake (workers + lead) | {startStorageProvidersStake} | {endStorageProvidersStake} | {percNewStorageProviderStake} |
 
-Role occupants:  
 {storageProviders}
 
 ### 4.3 Curator Role
 | Property                | Start Block | End Block | % Change |
 |-------------------------|--------------|--------------|----------|
-| Number of Curators      | {startCurators} | {endCurators} | {percNewCurators} |   
+| Number of Curators      | {startCurators} | {endCurators} | {percNewCurators} |
 
-Role occupants:  
 {curators}
 
 ### 4.4 Operations Role
 | Property                | Start Block | End Block | % Change |
 |-------------------------|--------------|--------------|----------|
 | Number of Operations Workers      | {startOperationsWorkers} | {endOperationsWorkers} | {percNewOperationsWorkers} |
-| Total Operations Stake (workers + lead)  | {startOperationsStake} |  {endOperationsStake} | {percNewOperationstake} |
+| Total Operations Stake (workers + lead) | {startOperationsStake} | {endOperationsStake} | {percNewOperationstake} |
 
-Role occupants:  
 {operations}
 
 ## 5.0 User Generated Content
@@ -90,12 +103,12 @@ Role occupants:
 | Property                | Start Block | End Block | % Change |
 |-------------------------|--------------|--------------|----------|
 | Number of uploads       | {startMedia} | {endMedia}  |  {percNewMedia} |
-| Size of content (MB)        |  {startUsedSpace} |  {endUsedSpace} | {percNewUsedSpace}          |
+| Size of content (MB)    |  {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}        |
+| Number of categories | {startCategories} | {endCategories} | {perNewCategories} |
+| Number of threads    | {startThreads} | {endThreads} | {percNewThreads} |
+| Number of posts      | {startPosts} | {endPosts} | {percNewPosts} |

+ 877 - 691
contributions/tech/report-generator/src/StatisticsCollector.ts

@@ -1,718 +1,904 @@
-import {ApiPromise, WsProvider} from "@polkadot/api";
-import {types} from '@joystream/types'
+import { ApiPromise } from "@polkadot/api";
+
+// types
 import {
-    AccountId,
-    Balance,
-    BalanceOf,
-    BlockNumber,
-    EraIndex,
-    EventRecord,
-    Hash,
-    Moment
+  AccountId,
+  Balance,
+  BalanceOf,
+  BlockNumber,
+  EventRecord,
+  Hash,
 } from "@polkadot/types/interfaces";
-
+import { Config, MintStatistics, Statistics, WorkersInfo } from "./types";
 import {
-    CacheEvent,
-    Media,
-    MintStatistics,
-    Statistics,
-    WorkersInfo, Channel, SpendingProposals, Bounty
-} from "./types";
-
-import {Option, 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 {ChannelId, PostId, ThreadId} from "@joystream/types/common";
-import {CategoryId} from "@joystream/types/forum";
-
-import {MemberId, Membership} from "@joystream/types/members";
-import {RewardRelationship, RewardRelationshipId} from "@joystream/types/recurring-rewards";
-
-import {Stake} from "@joystream/types/stake";
-
-import {WorkerId} from "@joystream/types/working-group";
-import {Entity, EntityId, PropertyType} from "@joystream/types/content-directory";
-import {ProposalId, Video, VideoId, WorkerOf, } from "@joystream/types/augment-codec/all";
-import {ProposalDetails, ProposalOf} from "@joystream/types/augment/types";
-import {SpendingParams} from "@joystream/types/proposals";
+  CacheEvent,
+  Bounty,
+  WorkerReward,
+  SpendingProposal,
+  StatusData,
+} from "./lib/types";
+
+import { Option, 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 { CategoryId } from "@joystream/types/forum";
+import { MemberId, Membership } from "@joystream/types/members";
+import {
+  Proposal,
+  ProposalId,
+  SpendingParams,
+} from "@joystream/types/proposals";
+import {
+  RewardRelationship,
+  RewardRelationshipId,
+} from "@joystream/types/recurring-rewards";
+import { Stake } from "@joystream/types/stake";
+import { Worker, WorkerId } from "@joystream/types/working-group";
+import { ProposalDetails, ProposalOf } from "@joystream/types/augment/types";
 import * as constants from "constants";
+import axios from "axios";
 
-const fsSync = require('fs');
-const fs = fsSync.promises;
-const parse = require('csv-parse/lib/sync');
-
-const BURN_ADDRESS = '5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu';
-
-const COUNCIL_ROUND_OFFSET = 2;
-const PROVIDER_URL = "ws://localhost:9944";
-
-const CACHE_FOLDER = "cache";
-
-const VIDEO_CLASS_iD = 10;
-const CHANNEL_CLASS_iD = 1;
+// lib
+import { eventStats, getPercent, getTotalMinted, momentToString } from "./lib";
+import {
+  connectApi,
+  getBlock,
+  getBlockHash,
+  getHead,
+  getTimestamp,
+  getIssuance,
+  getEra,
+  getEraStake,
+  getEvents,
+  getCouncil,
+  getCouncilRound,
+  getCouncilSize,
+  getCouncilApplicants,
+  getCouncilApplicantStakes,
+  getCouncilCommitments,
+  getCouncilPayoutInterval,
+  getCouncilPayout,
+  getCouncilElectionDurations,
+  getNextWorker,
+  getWorkers,
+  getWorkerReward,
+  getStake,
+  getCouncilMint,
+  getMintsCreated,
+  getMint,
+  getGroupMint,
+  getNextMember,
+  getMember,
+  getNextPost,
+  getNextThread,
+  getNextCategory,
+  getProposalCount,
+  getProposalInfo,
+  getProposalDetails,
+  getValidatorCount,
+  getValidators,
+  getNextEntity,
+  getNextChannel,
+  getNextVideo,
+  getEntity,
+  getDataObject,
+  getDataObjects,
+} from "./lib/api";
 
-const SPENDING_PROPOSALS_CATEGORIES_FILE = __dirname + '/../../../documentation/spending_proposal_categories.csv';
+import {
+  filterMethods,
+  getWorkerRewards,
+  getWorkerRow,
+  getBurnedTokens,
+  getFinalizedSpendingProposals,
+  getActiveValidators,
+  getValidatorsRewards,
+} from "./lib/rewards";
+
+const fsSync = require("fs");
+const fs = fsSync.promises;
+const parse = require("csv-parse/lib/sync");
 
 export class StatisticsCollector {
-
-    private api?: ApiPromise;
-    private blocksEventsCache: Map<number, CacheEvent[]>;
-    private statistics: Statistics;
-
-    constructor() {
-        this.blocksEventsCache = new Map<number, CacheEvent[]>();
-        this.statistics = new Statistics();
-    }
-
-    async getStatistics(startBlock: number, endBlock: number): Promise<Statistics> {
-        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.fillOperationsInfo(startBlock, endBlock, startHash, endHash);
-        await this.fillMembershipInfo(startHash, endHash);
-        await this.fillMediaUploadInfo(startHash, endHash);
-        await this.fillForumInfo(startHash, endHash);
-
-        await this.api.disconnect();
-        return this.statistics;
-    }
-
-    async getApprovedBounties() {
-        try {
-            await fs.access(SPENDING_PROPOSALS_CATEGORIES_FILE, constants.R_OK);
-        } catch {
-            console.warn('File with the spending proposal categories not found');
-            return [];
-        }
-
-        const fileContent = await fs.readFile(SPENDING_PROPOSALS_CATEGORIES_FILE);
-        let rawBounties = parse(fileContent);
-        rawBounties.shift();
-        rawBounties = rawBounties.filter((line: string[]) => line[8] == 'Bounties');
-
-        let bounties = rawBounties.map((rawBounty: any) => {
-            return new Bounty(rawBounty[0], rawBounty[1], rawBounty[2], rawBounty[3], rawBounty[4], rawBounty[5]);
-        });
-
-        return bounties.filter((bounty: Bounty) => bounty.status == "Approved" && bounty.testnet == "Antioch");
-    }
-
-
-    async fillValidatorsRewards() {
-        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]);
-            }
-        }
-    }
-
-    async computeTokensBurn(){
-        let tokensBurned = 0;
-        for (let [, blockEvents] of this.blocksEventsCache) {
-            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) {
-                    tokensBurned += Number(amount);
-                }
-            }
-        }
-        return tokensBurned;
+  private api?: ApiPromise;
+  private blocksEventsCache: Map<number, CacheEvent[]>;
+  private statistics: Statistics;
+
+  constructor() {
+    this.blocksEventsCache = new Map<number, CacheEvent[]>();
+    this.statistics = new Statistics();
+  }
+
+  saveStats(data: any) {
+    Object.keys(data).map((key: string) => (this.statistics[key] = data[key]));
+  }
+
+  filterCache(
+    filterEvent: (event: CacheEvent) => boolean
+  ): [number, CacheEvent[]][] {
+    const blocks: [number, CacheEvent[]][] = [];
+    for (let block of this.blocksEventsCache) {
+      const [key, events] = block;
+      const filtered = events.filter((event) => filterEvent(event));
+      if (filtered.length) blocks.push([key, filtered]);
     }
-
-    async getFinalizedSpendingProposals(endHash: Hash): Promise<Array<SpendingProposals>> {
-        let spendingProposals = new Array<SpendingProposals>();
-        for (let [key, blockEvents] of this.blocksEventsCache) {
-            let proposalEvents = blockEvents.filter((event) => {
-                return event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated";
-            });
-
-            for (let proposalEvent of proposalEvents) {
-                let statusUpdateData = proposalEvent.data[1] as any;
-                if (!(statusUpdateData.finalized && statusUpdateData.finalized.finalizedAt)) {
-                    continue;
-                }
-
-                let proposalId = proposalEvent.data[0] as ProposalId;
-                let proposalInfo = await this.api.query.proposalsEngine.proposals.at(endHash, proposalId) as ProposalOf;
-                const finalizedData = proposalInfo.status.asFinalized;
-
-                let proposalDetail = await this.api.query.proposalsCodex.proposalDetailsByProposalId.at(endHash, proposalId) as ProposalDetails;
-                if (!finalizedData.proposalStatus.isApproved || !proposalDetail.isSpending) {
-                    continue;
-                }
-
-                let approvedData = finalizedData.proposalStatus.asApproved;
-                if (!approvedData.isExecuted) {
-                    continue;
-                }
-
-                let spendingParams = proposalDetail.asSpending;
-                if (!spendingProposals.some(spendingProposal => (spendingProposal.id == Number(proposalId)))){
-                    spendingProposals.push(new SpendingProposals(Number(proposalId), proposalInfo.title.toString(), Number(spendingParams[0])));
-                }
-            }
-        }
-        return spendingProposals;
+    return blocks;
+  }
+
+  async getStats(
+    startBlock: number,
+    endBlock: number,
+    config: Config
+  ): Promise<Statistics> {
+    const { cacheDir, providerUrl, statusUrl } = config;
+    this.api = await connectApi(providerUrl);
+
+    const aboveHead = endBlock - Number(await getHead(this.api));
+    if (aboveHead > 0) {
+      console.log(`End Block is above our Head, wait ${aboveHead} blocks.`);
+      return this.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");
+    let startHash: Hash = await getBlockHash(this.api, startBlock);
+    let endHash: Hash = await getBlockHash(this.api, endBlock);
+
+    let dateStart = momentToString(await getTimestamp(this.api, startHash));
+    let dateEnd = momentToString(await getTimestamp(this.api, endHash));
+    this.saveStats({
+      dateStart,
+      dateEnd,
+      startBlock,
+      endBlock,
+      newBlocks: endBlock - startBlock,
+      percNewBlocks: getPercent(startBlock, endBlock),
+    });
+
+    // run long running tasks in parallel first
+    await Promise.all([
+      this.buildBlocksEventCache(startBlock, endBlock, cacheDir).then(() =>
+        this.fillStats(startBlock, endBlock, startHash, endHash, config)
+      ),
+      this.getFiatEvents(startBlock, endBlock, statusUrl),
+      this.fillMediaUploadInfo(startHash, endHash),
+    ]);
+    this.api.disconnect();
+    return this.statistics;
+  }
+
+  fillStats(
+    startBlock: number,
+    endBlock: number,
+    startHash: Hash,
+    endHash: Hash,
+    config: Config
+  ): Promise<void[]> {
+    eventStats(this.blocksEventsCache); // print event stats
+    return Promise.all([
+      this.fillTokenInfo(startBlock, endBlock, startHash, endHash, config),
+      this.fillMintsInfo(startHash, endHash),
+      this.fillCouncilInfo(startHash, endHash, config.councilRoundOffset),
+      this.fillCouncilElectionInfo(startBlock),
+      this.fillValidatorInfo(startHash, endHash),
+      this.fillStorageProviderInfo(startBlock, endBlock, startHash, endHash),
+      this.fillCuratorInfo(startHash, endHash),
+      this.fillOperationsInfo(startBlock, endBlock, startHash, endHash),
+      this.fillMembershipInfo(startHash, endHash),
+      this.fillForumInfo(startHash, endHash),
+    ]);
+  }
+
+  async getApprovedBounties(file: string): Promise<Bounty[]> {
+    try {
+      await fs.access(file, constants.R_OK);
+    } catch {
+      console.warn("File with spending proposal categories not found: ${file}");
     }
-
-    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);
-        this.statistics.newTokensBurn = await this.computeTokensBurn();
-
-        let bounties = await this.getApprovedBounties();
-        let spendingProposals = await this.getFinalizedSpendingProposals(endHash);
-
-        this.statistics.bountiesTotalPaid = 0;
-        if (bounties) {
-            for (let bounty of bounties) {
-                let bountySpendingProposal = spendingProposals.find((spendingProposal) => spendingProposal.id == bounty.proposalId);
-                if (bountySpendingProposal) {
-                    this.statistics.bountiesTotalPaid += bountySpendingProposal.spentAmount;
-                }
-            }
-        }
-
-        if (!this.statistics.bountiesTotalPaid) {
-            console.warn('No bounties found in ' + SPENDING_PROPOSALS_CATEGORIES_FILE +', trying to find spending proposals of bounties, please check the values!...');
-            for (const spendingProposal of spendingProposals) {
-                if (spendingProposal.title.toLowerCase().includes("bounty")) {
-                    this.statistics.bountiesTotalPaid += spendingProposal.spentAmount;
-                }
-            }
-        }
-
-        this.statistics.spendingProposalsTotal = spendingProposals.reduce((n, spendingProposal) => n + spendingProposal.spentAmount, 0);
-
-        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));
+    const fileContent = await fs.readFile(file);
+    const proposals = parse(fileContent).slice(1);
+    console.log(`Loaded ${proposals.length} proposals.`);
+    return proposals
+      .filter(
+        (line: string[]) =>
+          line[0] === "Antioch" &&
+          line[3] === "Approved" &&
+          line[8] === "Bounties"
+      )
+      .map((bounty: string[]) => {
+        return new Bounty(
+          bounty[0],
+          Number(bounty[1]),
+          bounty[2],
+          bounty[3],
+          Number(bounty[4]),
+          Number(bounty[5])
+        );
+      });
+  }
+
+  fillSudoSetBalance() {
+    let balancesSetByRoot = 0;
+    this.filterCache(filterMethods.sudoSetBalance).map(([block, events]) =>
+      events.forEach(({ data }) => {
+        balancesSetByRoot += Number(data[1]);
+      })
+    );
+    this.saveStats({ balancesSetByRoot });
+  }
+
+  async fillTokenInfo(
+    startBlock: number,
+    endBlock: number,
+    startHash: Hash,
+    endHash: Hash,
+    config: Config
+  ): Promise<void> {
+    const { burnAddress } = config;
+    const proposalsFile = config.repoDir + config.spendingCategoriesFile;
+    const startIssuance = (await getIssuance(this.api, startHash)).toNumber();
+    const endIssuance = (await getIssuance(this.api, endHash)).toNumber();
+    const burnEvents = this.filterCache(filterMethods.getBurnedTokens);
+    this.saveStats({
+      startIssuance,
+      endIssuance,
+      newIssuance: endIssuance - startIssuance,
+      percNewIssuance: getPercent(startIssuance, endIssuance),
+      newTokensBurn: await getBurnedTokens(burnAddress, burnEvents),
+    });
+    this.fillSudoSetBalance();
+
+    // bounties
+    const bounties = await this.getApprovedBounties(proposalsFile);
+    const blocks = this.filterCache(filterMethods.finalizedSpendingProposals);
+    const spendingProposals: SpendingProposal[] =
+      await getFinalizedSpendingProposals(this.api, blocks);
+
+    let bountiesTotalPaid = 0;
+    for (let bounty of bounties) {
+      const bountySpendingProposal = spendingProposals.find(
+        (spendingProposal) => spendingProposal.id == bounty.proposalId
+      );
+      if (bountySpendingProposal)
+        bountiesTotalPaid += bountySpendingProposal.amount;
     }
 
-    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;
+    if (!bountiesTotalPaid) {
+      console.warn(
+        `No bounties in selected period. Need to update ${proposalsFile}?\nLooking for spending proposals titled "bounty":`
+      );
+      for (const { title, amount } of spendingProposals) {
+        if (!title.toLowerCase().includes("bounty")) continue;
+        bountiesTotalPaid += amount;
+        console.log(` - ${title}: ${amount}`);
+      }
     }
-
-    async computeWorkingGroupReward(roundNrBlocks: number, startHash: Hash, endHash: Hash, workingGroup: string): Promise<WorkersInfo> {
-        let nextWorkerId = (await this.api.query[workingGroup + 'WorkingGroup'].nextWorkerId.at(startHash) as WorkerId).toNumber();
-        let info = new WorkersInfo();
-        for (let i = 0; i < nextWorkerId; ++i) {
-            let worker = await this.api.query[workingGroup + 'WorkingGroup'].workerById.at(endHash, i) as WorkerOf;
-
-            if (!worker.is_active) {
-                continue;
-            }
-
-            if (worker.role_stake_profile.isSome) {
-                let roleStakeProfile = worker.role_stake_profile.unwrap();
-                let stake = await this.api.query.stake.stakes.at(endHash, roleStakeProfile.stake_id) as Stake;
-                info.startStake += stake.value.toNumber();
-            }
-        }
-
-        nextWorkerId = (await this.api.query[workingGroup + 'WorkingGroup'].nextWorkerId.at(endHash) as WorkerId).toNumber();
-        let rewardRelationshipIds = Array<RewardRelationshipId>();
-
-        for (let i = 0; i < nextWorkerId; ++i) {
-            let worker = await this.api.query[workingGroup + 'WorkingGroup'].workerById.at(endHash, i) as WorkerOf;
-
-            if (!worker.is_active) {
-                continue;
-            }
-
-            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.at(endHash, roleStakeProfile.stake_id) as Stake;
-                info.endStake += stake.value.toNumber();
-            }
-        }
-        info.rewards = await this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
-        info.endNrOfWorkers = nextWorkerId;
-        return info;
+    this.saveStats({ bountiesTotalPaid });
+
+    let roundNrBlocks = endBlock - startBlock;
+    const spendingProposalsTotal = spendingProposals.reduce(
+      (n, p) => n + p.amount,
+      0
+    );
+    const newCouncilRewards = await this.computeCouncilReward(
+      roundNrBlocks,
+      endHash
+    );
+    const newCuratorInfo = await this.computeWorkingGroupReward(
+      roundNrBlocks,
+      startHash,
+      endHash,
+      "contentDirectory"
+    );
+
+    this.saveStats({
+      spendingProposalsTotal,
+      newCouncilRewards: newCouncilRewards.toFixed(2),
+      newCuratorRewards: newCuratorInfo.rewards.toFixed(2),
+    });
+  }
+
+  async getMintInfo(
+    api: ApiPromise,
+    mintId: MintId,
+    startHash: Hash,
+    endHash: Hash
+  ): Promise<MintStatistics> {
+    const startMint: Mint = await getMint(api, startHash, mintId);
+    const endMint: Mint = await getMint(api, endHash, mintId);
+    let stats = new MintStatistics();
+    stats.startMinted = getTotalMinted(startMint);
+    stats.endMinted = getTotalMinted(endMint);
+    stats.diffMinted = stats.endMinted - stats.startMinted;
+    stats.percMinted = getPercent(stats.startMinted, stats.endMinted);
+    return stats;
+  }
+
+  async computeCouncilReward(
+    roundNrBlocks: number,
+    endHash: Hash
+  ): Promise<number> {
+    const payoutInterval = Number(
+      (
+        (await getCouncilPayoutInterval(
+          this.api,
+          endHash
+        )) as Option<BlockNumber>
+      ).unwrapOr(0)
+    );
+    const amountPerPayout = (
+      (await getCouncilPayout(this.api, endHash)) as BalanceOf
+    ).toNumber();
+
+    const [
+      announcingPeriod,
+      votingPeriod,
+      revealingPeriod,
+      termDuration,
+    ]: number[] = await getCouncilElectionDurations(this.api, endHash);
+
+    const nrCouncilMembers = ((await getCouncil(this.api, 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;
+  }
+
+  // Summarize stakes and rewards at start and end
+  async computeWorkingGroupReward(
+    roundNrBlocks: number,
+    startHash: Hash,
+    endHash: Hash,
+    workingGroup: string
+  ): Promise<WorkersInfo> {
+    const group = workingGroup + "WorkingGroup";
+    let info = new WorkersInfo();
+
+    // stakes at start
+    const workersStart: WorkerReward[] = await getWorkerRewards(
+      this.api,
+      group,
+      startHash
+    );
+    workersStart.forEach(({ stake }) => {
+      if (stake) info.startStake += stake.value.toNumber();
+    });
+
+    // stakes at end
+    const workersEnd: WorkerReward[] = await getWorkerRewards(
+      this.api,
+      group,
+      endHash
+    );
+    let workers = ``;
+    workersEnd.forEach(async (worker) => {
+      if (worker.stake) info.endStake += worker.stake.value.toNumber();
+      if (!worker.reward) return;
+      let earnedBefore = 0;
+      const hired = workersStart.find((w) => w.id === worker.id);
+      if (hired) earnedBefore = hired.reward.total_reward_received.toNumber();
+      workers += getWorkerRow(worker, earnedBefore);
+    });
+    const groupTag =
+      workingGroup === `storage`
+        ? `storageProviders`
+        : workingGroup === `contentDirectory`
+        ? `curators`
+        : workingGroup === `operations`
+        ? `operations`
+        : ``;
+    if (workers.length) {
+      const header = `| # | Member | Status | tJOY / Block | M tJOY Term | M tJOY total |\n|--|--|--|--|--|--|\n`;
+      this.saveStats({ [groupTag]: header + workers });
+    } else this.saveStats({ [groupTag]: `` });
+
+    info.rewards = await this.computeReward(
+      roundNrBlocks,
+      workersEnd.filter((w) => w.reward).map((w) => w.reward)
+    );
+    info.endNrOfWorkers = workersEnd.length;
+    return info;
+  }
+
+  async computeReward(
+    roundNrBlocks: number,
+    recurringRewards: RewardRelationship[]
+  ): Promise<number> {
+    let rewardPerBlock = 0;
+    recurringRewards.forEach((recurringReward: RewardRelationship) => {
+      if (!recurringReward) return;
+      const amount = recurringReward.amount_per_payout.toNumber();
+      const payoutInterval = Number(recurringReward.payout_interval);
+      if (amount && payoutInterval) rewardPerBlock += amount / payoutInterval;
+    });
+    return rewardPerBlock * roundNrBlocks;
+  }
+
+  async computeGroupMintStats(
+    [label, tag]: string[],
+    startHash: Hash,
+    endHash: Hash
+  ) {
+    const group = label + "WorkingGroup";
+    const mint = await getGroupMint(this.api, group, endHash);
+    const info = await this.getMintInfo(this.api, mint, startHash, endHash);
+    let stats: { [key: string]: number } = {};
+    stats[`start${tag}Minted`] = info.startMinted;
+    stats[`end${tag}Minted`] = info.endMinted;
+    stats[`new${tag}Minted`] = info.diffMinted;
+    stats[`perc${tag}Minted`] = info.percMinted;
+    this.saveStats(stats);
+  }
+
+  async fillMintsInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    const startNrMints = await getMintsCreated(this.api, startHash);
+    const endNrMints = await getMintsCreated(this.api, endHash);
+    const newMints = endNrMints - startNrMints;
+
+    // calcuate sum of all mints
+    let totalMinted = 0;
+    let totalMintCapacityIncrease = 0;
+    // summarize old mints
+    for (let i = 0; i < startNrMints; ++i) {
+      const startMint: Mint = await getMint(this.api, startHash, i);
+      const endMint: Mint = await getMint(this.api, endHash, i);
+      const startMintTotal = getTotalMinted(startMint);
+      const endMintTotal = getTotalMinted(endMint);
+      totalMinted += endMintTotal - startMintTotal;
+      totalMintCapacityIncrease +=
+        parseInt(endMint.getField("capacity").toString()) -
+        parseInt(startMint.getField("capacity").toString());
     }
 
-    async computeCuratorsReward(roundNrBlocks: number, startHash: Hash, endHash: Hash) {
-        let nextCuratorId = (await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
-
-        let rewardRelationshipIds = Array<RewardRelationshipId>();
-        for (let i = 0; i < nextCuratorId; ++i) {
-            let worker = await this.api.query.contentDirectoryWorkingGroup.workerById.at(endHash, i) as WorkerOf;
-            if (!worker.is_active) {
-                continue;
-            }
-
-            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;
+    // summarize new mints
+    for (let i = startNrMints; i < endNrMints; ++i) {
+      const endMint: Mint = await getMint(this.api, endHash, i);
+      if (endMint) totalMinted += getTotalMinted(endMint);
     }
-
-    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 endMint = await this.api.query.minting.mints.at(endHash, i) as Mint;
-            if (!endMint) {
-                return;
-            }
-            this.statistics.totalMinted = parseInt(endMint.getField("total_minted").toString());
+    this.saveStats({ newMints, totalMinted, totalMintCapacityIncrease });
+
+    // council
+    const councilInfo = await this.getMintInfo(
+      this.api,
+      await getCouncilMint(this.api, endHash),
+      startHash,
+      endHash
+    );
+    this.saveStats({
+      startCouncilMinted: councilInfo.startMinted,
+      endCouncilMinted: councilInfo.endMinted,
+      newCouncilMinted: councilInfo.diffMinted,
+      percNewCouncilMinted: councilInfo.percMinted,
+    });
+    // working groups
+    const groups = [
+      ["contentDirectory", "Curator"],
+      ["storage", "Storage"],
+      ["operations", "Operations"],
+    ].forEach((group) => this.computeGroupMintStats(group, startHash, endHash));
+  }
+
+  async fillCouncilInfo(
+    startHash: Hash,
+    endHash: Hash,
+    councilRoundOffset: number
+  ): Promise<void> {
+    const round = await getCouncilRound(this.api, startHash);
+    const startNrProposals = await getProposalCount(this.api, startHash);
+    const endNrProposals = await getProposalCount(this.api, endHash);
+
+    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]));
+          }
         }
-
-        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;
-
-        let curatorMint = (await this.api.query.contentDirectoryWorkingGroup.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;
-
-        let operationsProviderMint = (await this.api.query.operationsWorkingGroup.mint.at(endHash)) as MintId;
-        let operationsProviderMintStatistics = await this.computeMintInfo(operationsProviderMint, startHash, endHash);
-        this.statistics.startOperationsMinted = operationsProviderMintStatistics.startMinted;
-        this.statistics.endOperationsMinted = operationsProviderMintStatistics.endMinted;
-        this.statistics.newOperationsMinted = operationsProviderMintStatistics.diffMinted;
-        this.statistics.percOperationsMinted = operationsProviderMintStatistics.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() - COUNCIL_ROUND_OFFSET;
-        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;
+    this.saveStats({
+      councilRound: round - councilRoundOffset,
+      councilMembers: await getCouncilSize(this.api, startHash),
+      newProposals: endNrProposals - startNrProposals,
+      newApprovedProposals: approvedProposals.size,
+    });
+  }
+
+  async fillCouncilElectionInfo(startBlock: number): Promise<void> {
+    let startBlockHash = await getBlockHash(this.api, startBlock);
+    let events: Vec<EventRecord> = await getEvents(this.api, startBlockHash);
+    let isStartBlockFirstCouncilBlock = events.some(
+      ({ event }) =>
+        event.section == "councilElection" && event.method == "CouncilElected"
+    );
+
+    if (!isStartBlockFirstCouncilBlock)
+      return console.warn(
+        "Note: The given start block is not the first block of the council round so council election information will be empty"
+      );
+
+    let lastBlockHash = await getBlockHash(this.api, startBlock - 1);
+    let applicants: Vec<AccountId> = await getCouncilApplicants(
+      this.api,
+      lastBlockHash
+    );
+    let electionApplicantsStakes = 0;
+    for (let applicant of applicants) {
+      const applicantStakes: ElectionStake = await getCouncilApplicantStakes(
+        this.api,
+        lastBlockHash,
+        applicant
+      );
+      electionApplicantsStakes += applicantStakes.new.toNumber();
     }
-
-    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";
+    // let seats = await getCouncil(this.api,startBlockHash) as Seats;
+    //TODO: Find a more accurate way of getting the votes
+    const votes: Vec<Hash> = await getCouncilCommitments(
+      this.api,
+      lastBlockHash
+    );
+
+    this.saveStats({
+      electionApplicants: applicants.length,
+      electionApplicantsStakes,
+      electionVotes: votes.length,
+    });
+  }
+
+  async fillValidatorInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    const startTimestamp: number = await getTimestamp(this.api, startHash);
+    const endTimestamp: number = await getTimestamp(this.api, endHash);
+    const blocks = this.statistics.newBlocks;
+    const avgBlockProduction = (endTimestamp - startTimestamp) / 1000 / blocks;
+    const maxStartValidators = await getValidatorCount(this.api, startHash);
+    const startValidators = await getActiveValidators(this.api, startHash);
+    const maxEndValidators = await getValidatorCount(this.api, endHash);
+    const endValidators = await getActiveValidators(this.api, endHash, true);
+    const startEra: number = await getEra(this.api, startHash);
+    const endEra: number = await getEra(this.api, endHash);
+
+    const startStake = await getEraStake(this.api, startHash, startEra);
+    const endStake = await getEraStake(this.api, endHash, endEra);
+
+    this.saveStats({
+      avgBlockProduction: Number(avgBlockProduction.toFixed(2)),
+      startValidators: startValidators.length + " / " + maxStartValidators,
+      endValidators: endValidators.length + " / " + maxEndValidators,
+      percValidators: getPercent(startValidators.length, endValidators.length),
+      startValidatorsStake: startStake,
+      endValidatorsStake: endStake,
+      percNewValidatorsStake: getPercent(startStake, endStake),
+      newValidatorRewards: await getValidatorsRewards(
+        this.filterCache(filterMethods.newValidatorsRewards)
+      ),
+    });
+  }
+
+  async fillStorageProviderInfo(
+    startBlock: number,
+    endBlock: number,
+    startHash: Hash,
+    endHash: Hash
+  ): Promise<void> {
+    let roundNrBlocks = endBlock - startBlock;
+    let storageProvidersRewards = await this.computeWorkingGroupReward(
+      roundNrBlocks,
+      startHash,
+      endHash,
+      "storage"
+    );
+    const newStorageProviderReward = Number(
+      storageProvidersRewards.rewards.toFixed(2)
+    );
+    const startStorageProvidersStake = storageProvidersRewards.startStake;
+    const endStorageProvidersStake = storageProvidersRewards.endStake;
+
+    const group = "storageWorkingGroup";
+    const startStorageProviders = await getWorkers(this.api, group, startHash);
+    const endStorageProviders = await getWorkers(this.api, group, endHash);
+
+    this.saveStats({
+      newStorageProviderReward,
+      startStorageProvidersStake,
+      endStorageProvidersStake,
+      percNewStorageProviderStake: getPercent(
+        startStorageProvidersStake,
+        endStorageProvidersStake
+      ),
+      startStorageProviders,
+      endStorageProviders,
+      percNewStorageProviders: getPercent(
+        startStorageProviders,
+        endStorageProviders
+      ),
+    });
+  }
+
+  async fillCuratorInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    const group = "contentDirectoryWorkingGroup";
+    const startCurators = await getWorkers(this.api, group, startHash);
+    const endCurators = await getWorkers(this.api, group, endHash);
+
+    this.saveStats({
+      startCurators,
+      endCurators,
+      percNewCurators: getPercent(startCurators, endCurators),
+    });
+  }
+
+  async fillOperationsInfo(
+    startBlock: number,
+    endBlock: number,
+    startHash: Hash,
+    endHash: Hash
+  ): Promise<void> {
+    const roundNrBlocks = endBlock - startBlock;
+    const operationsRewards = await this.computeWorkingGroupReward(
+      roundNrBlocks,
+      startHash,
+      endHash,
+      "operations"
+    );
+    const newOperationsReward = operationsRewards.rewards.toFixed(2);
+    const startOperationsStake = operationsRewards.startStake;
+    const endOperationsStake = operationsRewards.endStake;
+
+    const group = "operationsWorkingGroup";
+    const startWorkers = await getWorkers(this.api, group, startHash);
+    const endWorkers = await getWorkers(this.api, group, endHash);
+
+    this.saveStats({
+      newOperationsReward: Number(newOperationsReward),
+      startOperationsWorkers: startWorkers,
+      endOperationsWorkers: endWorkers,
+      percNewOperationsWorkers: getPercent(startWorkers, endWorkers),
+      startOperationsStake,
+      endOperationsStake,
+      percNewOperationstake: getPercent(
+        startOperationsStake,
+        endOperationsStake
+      ),
+    });
+  }
+
+  async fillMembershipInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    const startMembers = await getNextMember(this.api, startHash);
+    const endMembers = await getNextMember(this.api, endHash);
+    this.saveStats({
+      startMembers,
+      endMembers,
+      newMembers: endMembers - startMembers,
+      percNewMembers: getPercent(startMembers, endMembers),
+    });
+  }
+
+  async fillMediaUploadInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    console.log(`Collecting Media stats`);
+    const startMedia = Number(await getNextVideo(this.api, startHash));
+    const endMedia = Number(await getNextVideo(this.api, endHash));
+    const startChannels = Number(await getNextChannel(this.api, startHash));
+    const endChannels = Number(await getNextChannel(this.api, endHash));
+
+    // count size
+    let startUsedSpace = 0;
+    let endUsedSpace = 0;
+    const startBlock = await getBlock(this.api, startHash);
+    const endBlock = await getBlock(this.api, endHash);
+    getDataObjects(this.api).then((dataObjects: Map<ContentId, DataObject>) => {
+      for (let [key, dataObject] of dataObjects) {
+        const added = dataObject.added_at.block.toNumber();
+        const start = startBlock.block.header.number.toNumber();
+        const end = endBlock.block.header.number.toNumber();
+
+        if (added < start)
+          startUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
+        if (added < end)
+          endUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
+      }
+      if (!startUsedSpace || !endUsedSpace)
+        console.log(`space start, end`, startUsedSpace, endUsedSpace);
+      this.saveStats({
+        startMedia,
+        endMedia,
+        percNewMedia: getPercent(startMedia, endMedia),
+        startChannels,
+        endChannels,
+        percNewChannels: getPercent(startChannels, endChannels),
+        startUsedSpace: Number(startUsedSpace.toFixed(2)),
+        endUsedSpace: Number(endUsedSpace.toFixed(2)),
+        percNewUsedSpace: getPercent(startUsedSpace, endUsedSpace),
+      });
+    });
+  }
+
+  async fillForumInfo(startHash: Hash, endHash: Hash): Promise<void> {
+    const startPosts = await getNextPost(this.api, startHash);
+    const endPosts = await getNextPost(this.api, endHash);
+    const startThreads = await getNextThread(this.api, startHash);
+    const endThreads = await getNextThread(this.api, endHash);
+    const startCategories = await getNextCategory(this.api, startHash);
+    const endCategories = await getNextCategory(this.api, endHash);
+    this.saveStats({
+      startPosts,
+      endPosts,
+      newPosts: endPosts - startPosts,
+      percNewPosts: getPercent(startPosts, endPosts),
+      startThreads,
+      endThreads,
+      newThreads: endThreads - startThreads,
+      percNewThreads: getPercent(startThreads, endThreads),
+      startCategories,
+      endCategories,
+      newCategories: endCategories - startCategories,
+      perNewCategories: getPercent(startCategories, endCategories),
+    });
+  }
+
+  async getFiatEvents(
+    startBlockHeight: number,
+    endBlockHeight: number,
+    statusUrl: string
+  ) {
+    let sumerGenesis = new Date("2021-04-07T18:20:54.000Z");
+
+    console.log("Fetching fiat events....");
+    await axios.get(statusUrl).then((response: { data: StatusData }) => {
+      console.log("# Exchanges");
+      let filteredExchanges = response.data.exchanges.filter(
+        (exchange) =>
+          exchange.blockHeight >= startBlockHeight &&
+          exchange.blockHeight <= endBlockHeight &&
+          new Date(exchange.date) > sumerGenesis
+      );
+
+      for (let filteredExchange of filteredExchanges) {
+        console.log(
+          `Block: ${filteredExchange.blockHeight}, USD: ${filteredExchange.amountUSD}`
+        );
+      }
+
+      let filteredBurns = response.data.burns.filter(
+        (burn: any) =>
+          burn.blockHeight >= startBlockHeight &&
+          burn.blockHeight <= endBlockHeight &&
+          new Date(burn.date) > sumerGenesis
+      );
+      if (filteredBurns.length) {
+        console.log("# Burns");
+        filteredBurns.forEach(({ blockHeight, amount }) =>
+          console.log(`Block: ${blockHeight}, tJOY: ${amount}`)
+        );
+      }
+
+      console.log("# Dollar Pool Changes");
+      const allDollarPoolChanges = response.data.dollarPoolChanges.filter(
+        (dollarPoolChange: any) =>
+          dollarPoolChange.blockHeight >= startBlockHeight &&
+          dollarPoolChange.blockHeight <= endBlockHeight &&
+          new Date(dollarPoolChange.blockTime) > sumerGenesis
+      );
+      const filteredDollarPoolChanges = response.data.dollarPoolChanges.filter(
+        (dollarPoolChange: any) =>
+          dollarPoolChange.blockHeight >= startBlockHeight &&
+          dollarPoolChange.blockHeight <= endBlockHeight &&
+          dollarPoolChange.change > 0 &&
+          new Date(dollarPoolChange.blockTime) > sumerGenesis
+      );
+
+      let dollarPoolRefills = ``;
+      if (filteredDollarPoolChanges.length > 0) {
+        dollarPoolRefills =
+          "| Refill, USD | Reason | Block # |\n|---------------------|--------------|--------------|\n";
+        filteredDollarPoolChanges.forEach(({ blockHeight, change, reason }) => {
+          console.log(
+            `Block: ${blockHeight}, USD: ${change}, Reason: ${reason}`
+          );
+          dollarPoolRefills += `| ${change} | ${reason} | ${blockHeight} |\n`;
         });
-
-        if (!isStartBlockFirstCouncilBlock) {
-            console.warn('Note: 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));
-
-        let maxStartValidators = (await this.api.query.staking.validatorCount.at(startHash) as u32).toNumber();
-        let startValidators = await this.findActiveValidators(startHash, false);
-        this.statistics.startValidators = startValidators.length + " / " + maxStartValidators;
-
-        let maxEndValidators = (await this.api.query.staking.validatorCount.at(endHash) as u32).toNumber();
-        let endValidators = await this.findActiveValidators(endHash, true);
-        this.statistics.endValidators = endValidators.length + " / " + maxEndValidators;
-
-        this.statistics.percValidators = StatisticsCollector.convertToPercentage(startValidators.length, endValidators.length);
-
-        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);
-        await this.fillValidatorsRewards();
-    }
-
-    async findActiveValidators(hash: Hash, searchPreviousBlocks: boolean): Promise<AccountId[]> {
-        const block = await this.api.rpc.chain.getBlock(hash);
-
-        let currentBlockNr = block.block.header.number.toNumber();
-        let activeValidators;
-        do {
-            let currentHash = (await this.api.rpc.chain.getBlockHash(currentBlockNr)) as Hash;
-            let allValidators = await this.api.query.staking.snapshotValidators.at(currentHash) as Option<Vec<AccountId>>;
-            if (!allValidators.isEmpty) {
-                let max = (await this.api.query.staking.validatorCount.at(currentHash)).toNumber();
-                activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
-            }
-
-            if (searchPreviousBlocks) {
-                --currentBlockNr;
-            } else {
-                ++currentBlockNr;
-            }
-
-        } while (activeValidators == undefined);
-        return activeValidators;
-    }
-
-    async fillStorageProviderInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
-        let roundNrBlocks = endBlock - startBlock;
-
-        let storageProvidersRewards = await this.computeWorkingGroupReward(roundNrBlocks, startHash, endHash, 'storage');
-        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.activeWorkerCount.at(startHash);
-        this.statistics.endStorageProviders = await this.api.query.storageWorkingGroup.activeWorkerCount.at(endHash);
-        this.statistics.percNewStorageProviders = StatisticsCollector.convertToPercentage(this.statistics.startStorageProviders, this.statistics.endStorageProviders);
-
-        let nextWorkerId = Number(await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash));
-        this.statistics.storageProviders = "";
-        for (let i = 0; i < nextWorkerId; ++i) {
-            let storageProvider = await this.api.query.storageWorkingGroup.workerById.at(endHash, i) as WorkerOf;
-            if (!storageProvider.is_active) {
-                continue;
-            }
-
-            let membership = await this.api.query.members.membershipById.at(endHash, storageProvider.member_id) as Membership;
-            this.statistics.storageProviders += "@" + membership.handle + " | (" + membership.root_account + ")  \n";
-
-        }
-
-    }
-
-    async fillCuratorInfo(startHash: Hash, endHash: Hash) {
-        this.statistics.startCurators = Number(await this.api.query.contentDirectoryWorkingGroup.activeWorkerCount.at(startHash));
-        this.statistics.endCurators = Number(await this.api.query.contentDirectoryWorkingGroup.activeWorkerCount.at(endHash));
-        this.statistics.percNewCurators = StatisticsCollector.convertToPercentage(this.statistics.startCurators, this.statistics.endCurators);
-
-        let nextCuratorId = Number(await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash));
-        this.statistics.curators = "";
-
-        for (let i = 0; i < nextCuratorId; i++) {
-            let worker = await this.api.query.contentDirectoryWorkingGroup.workerById.at(endHash, i) as WorkerOf;
-            if (!worker.is_active) {
-                continue;
-            }
-
-            let curatorMembership = await this.api.query.members.membershipById.at(endHash, worker.member_id) as Membership;
-            this.statistics.curators += "@" + curatorMembership.handle + " | (" + curatorMembership.root_account + ")  \n";
-
-        }
-    }
-
-    async fillOperationsInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
-        let roundNrBlocks = endBlock - startBlock;
-
-        let operationsRewards = await this.computeWorkingGroupReward(roundNrBlocks, startHash, endHash, 'operations');
-        this.statistics.newOperationsReward = operationsRewards.rewards;
-        this.statistics.newOperationsReward = Number(this.statistics.newOperationsReward.toFixed(2));
-
-        this.statistics.startOperationsStake = operationsRewards.startStake;
-        this.statistics.endOperationsStake = operationsRewards.endStake;
-        this.statistics.percNewOperationstake = StatisticsCollector.convertToPercentage(this.statistics.startOperationsStake, this.statistics.endOperationsStake);
-
-        this.statistics.startOperationsWorkers = Number(await this.api.query.operationsWorkingGroup.activeWorkerCount.at(startHash));
-        this.statistics.endOperationsWorkers = Number(await this.api.query.operationsWorkingGroup.activeWorkerCount.at(endHash));
-        this.statistics.percNewOperationsWorkers = StatisticsCollector.convertToPercentage(this.statistics.startOperationsWorkers, this.statistics.endOperationsWorkers);
-
-        let nextOperationsWorkerId = Number(await this.api.query.operationsWorkingGroup.nextWorkerId.at(endHash));
-        this.statistics.operations = "";
-
-        for (let i = 0; i < nextOperationsWorkerId; i++) {
-            let worker = await this.api.query.operationsWorkingGroup.workerById.at(endHash, i) as WorkerOf;
-            if (!worker.is_active) {
-                continue;
-            }
-
-            let operationMembership = await this.api.query.members.membershipById.at(endHash, worker.member_id) as Membership;
-            this.statistics.operations += "@" + operationMembership.handle + " | (" + operationMembership.root_account + ")  \n";
-
-        }
-    }
-
-    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 startVideos = (await this.api.query.content.nextVideoId.at(startHash) as VideoId).toNumber();
-        let endVideos = (await this.api.query.content.nextVideoId.at(endHash) as VideoId).toNumber();
-
-        this.statistics.startMedia = startVideos;
-        this.statistics.endMedia = endVideos;
-        this.statistics.percNewMedia = StatisticsCollector.convertToPercentage(this.statistics.startMedia, this.statistics.endMedia);
-
-        let startChannels = (await this.api.query.content.nextChannelId.at(startHash) as ChannelId).toNumber();
-        let endChannels = (await this.api.query.content.nextChannelId.at(endHash) as ChannelId).toNumber();
-
-        this.statistics.startChannels = startChannels;
-        this.statistics.endChannels = endChannels;
-        this.statistics.percNewChannels = StatisticsCollector.convertToPercentage(this.statistics.startChannels, this.statistics.endChannels);
-
-        let dataObjects = await this.api.query.dataDirectory.dataByContentId.entries() as unknown as Map<ContentId, DataObject>;
-
-        let startObjects = new Map<ContentId, DataObject>();
-        let endObjects = new Map<ContentId, DataObject>();
-
-        const startBlock = await this.api.rpc.chain.getBlock(startHash);
-        const endBlock = await this.api.rpc.chain.getBlock(endHash);
-
-        for (let [key, dataObject] of dataObjects) {
-            if (dataObject.added_at.block.toNumber() < startBlock.block.header.number.toNumber()) {
-                startObjects.set(key, dataObject);
-                this.statistics.startUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
-            }
-
-            if (dataObject.added_at.block.toNumber() < endBlock.block.header.number.toNumber()) {
-                endObjects.set(key, dataObject);
-                this.statistics.endUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
-            }
-        }
-        this.statistics.startUsedSpace = Number(this.statistics.startUsedSpace.toFixed(2));
-        this.statistics.endUsedSpace = Number(this.statistics.endUsedSpace.toFixed(2));
-
-        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 newValue > 0 ? Infinity : 0;
+      }
+
+      // calculate inflation
+      let startTermExchangeRate = 0;
+      let endTermExchangeRate = 0;
+      if (filteredExchanges.length) {
+        const lastExchangeEvent =
+          filteredExchanges[filteredExchanges.length - 1];
+        startTermExchangeRate = filteredExchanges[0].price * 1000000;
+        endTermExchangeRate = lastExchangeEvent.price * 1000000;
+      } else {
+        startTermExchangeRate =
+          filteredDollarPoolChanges[0].valueAfter * 1000000;
+        const lastEvent =
+          filteredDollarPoolChanges[filteredDollarPoolChanges.length - 1];
+        endTermExchangeRate = lastEvent.rateAfter * 1000000;
+      }
+      let inflationPct = getPercent(endTermExchangeRate, startTermExchangeRate);
+      console.log(
+        "# USD / 1M tJOY Rate\n",
+        `@ Term start (block #${startBlockHeight}: ${startTermExchangeRate}\n`,
+        `@ Term end (block #${endBlockHeight}: ${endTermExchangeRate}\n`,
+        `Inflation: ${inflationPct}`
+      );
+
+      const startDollarPool =
+        allDollarPoolChanges[0].change > 0
+          ? allDollarPoolChanges[0].valueAfter - allDollarPoolChanges[0].change
+          : allDollarPoolChanges[0].valueAfter;
+      const endDollarEvent =
+        allDollarPoolChanges[allDollarPoolChanges.length - 1];
+      const endDollarPool = endDollarEvent.valueAfter;
+      const dollarPoolPctChange = getPercent(startDollarPool, endDollarPool);
+
+      this.saveStats({
+        startTermExchangeRate: startTermExchangeRate.toFixed(2),
+        endTermExchangeRate: endTermExchangeRate.toFixed(2),
+        inflationPct,
+        startDollarPool: startDollarPool.toFixed(2),
+        endDollarPool: endDollarPool.toFixed(2),
+        dollarPoolPctChange,
+        dollarPoolRefills,
+      });
+    });
+  }
+
+  async buildBlocksEventCache(
+    startBlock: number,
+    endBlock: number,
+    cacheDir: string
+  ): Promise<void> {
+    const cacheFile = `${cacheDir}/${startBlock}-${endBlock}.json`;
+    const exists = await fs
+      .access(cacheFile, fsSync.constants.R_OK)
+      .then(() => true)
+      .catch(() => false);
+    if (!exists) {
+      console.log("Building events cache...");
+      let blocksEvents = new Map<number, CacheEvent[]>();
+      for (let i = startBlock; i < endBlock; ++i) {
+        process.stdout.write("\rCaching block: " + i + " until " + endBlock);
+        const blockHash: Hash = await getBlockHash(this.api, i);
+        let eventRecord: EventRecord[] = [];
+        try {
+          eventRecord = await getEvents(this.api, blockHash);
+        } catch (e) {
+          console.warn(`Failed to get events.`, e);
         }
-        return Number((newValue * 100 / previousValue - 100).toFixed(2));
-    }
-
-    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...');
-            let blocksEvents = new Map<number, CacheEvent[]>();
-            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));
-                }
-                blocksEvents.set(i, cacheEvents);
-            }
-
-            console.log('\nFinish events cache...');
-            let jsonOutput = JSON.stringify(Array.from(blocksEvents.entries()), null, 2);
-            await fs.writeFile(cacheFile, jsonOutput);
-            this.blocksEventsCache = new Map(JSON.parse(jsonOutput));
-        } 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...');
+        let cacheEvents = new Array<CacheEvent>();
+        for (let { event } of eventRecord) {
+          if (!event) {
+            console.warn(`empty event record`);
+            continue;
+          }
+          cacheEvents.push(
+            new CacheEvent(event.section, event.method, event.data)
+          );
         }
+        blocksEvents.set(i, cacheEvents);
+      }
+
+      console.log("\nFinish events cache...");
+      const json = JSON.stringify(Array.from(blocksEvents.entries()), null, 2);
+      fsSync.writeFileSync(cacheFile, json);
+      this.blocksEventsCache = new Map(JSON.parse(json));
+    } else {
+      console.log("Cache file found, loading it...");
+      let fileData = await fs.readFile(cacheFile);
+      this.blocksEventsCache = new Map(JSON.parse(fileData));
     }
-
-    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});
-    }
+  }
 }

+ 83 - 38
contributions/tech/report-generator/src/generator.ts

@@ -1,46 +1,91 @@
-import {StatisticsCollector} from "./StatisticsCollector";
+import fs from "fs";
+const exec = require("util").promisify(require("child_process").exec);
+import { StatisticsCollector } from "./StatisticsCollector";
+import { connectApi, getHead, getCouncils } from "./lib/api";
+import { Round } from "./lib/types";
+import { Config } from "./types";
 
-const fs = require('fs').promises;
+const CONFIG: Config = {
+  repoDir: __dirname + "/../../../../",
+  reportsDir: "council/tokenomics",
+  spendingCategoriesFile: "governance/spending_proposal_categories.csv",
+  templateFile: __dirname + "/../report-template.md",
+  providerUrl: "ws://127.0.0.1:9944",
+  statusUrl: "https://status.joystream.org/status/",
+  burnAddress: "5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu",
+  cacheDir: "cache",
+  councilRoundOffset: 2,
+  videoClassId: 10,
+  channelClassId: 1,
+};
 
-async function main() {
-    const args = process.argv.slice(2);
+async function main(config: Config) {
+  const { templateFile } = config;
+  const args = process.argv.slice(2);
+  if (args.length < 2) return updateReports(config, Number(args[0]));
 
-    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]);
 
-    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);
+  } else generateReport(startBlock, endBlock, config);
+}
 
-    if (isNaN(startBlock) || isNaN(endBlock) || startBlock >= endBlock) {
-        console.error('Invalid block range');
-        process.exit(1);
-    }
+const generateReport = async (
+  startBlock: number,
+  endBlock: number,
+  config: Config
+): Promise<boolean> => {
+  const { templateFile, repoDir, reportsDir } = config;
+  let fileData = fs.readFileSync(templateFile, "utf8");
+  let statsCollecttor = new StatisticsCollector();
+  console.log(`-> Collecting stats from ${startBlock} to ${endBlock}`);
+  const stats = await statsCollecttor.getStats(startBlock, endBlock, config);
+  console.log(stats);
+  if (!stats.dateStart) return false;
+  const round = stats.councilRound || 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);
-    }
+  const fileName = `Council_Round${round}_${startBlock}-${endBlock}_Tokenomics_Report.md`;
+  // antioch was updated to sumer at 717987
+  const version = startBlock < 717987 ? "antioch-3" : "sumer-4";
+  const dir = `${repoDir}${reportsDir}/${version}`;
 
-}
+  console.log(`-> Writing report to ${fileName}`);
+  for (const entry of Object.entries(stats)) {
+    const regex = new RegExp("{" + entry[0] + "}", "g");
+    fileData = fileData.replace(regex, entry[1].toString());
+  }
+  fs.writeFileSync(`${dir}/${fileName}`, fileData);
+  return true;
+};
+
+const updateReports = async (config: Config, round?: number) => {
+  const { templateFile, providerUrl } = config;
+  console.debug(`Connecting to ${providerUrl}`);
+  const api = await connectApi(providerUrl);
+  await api.isReady;
+
+  console.log(`-> Fetching councils`);
+  const head = await getHead(api);
+  getCouncils(api, +head).then(async (councils: Round[]) => {
+    api.disconnect();
+    if (round === null || isNaN(round)) {
+      console.log(`-> Updating reports`);
+      await Promise.all(
+        councils.map(({ start, end }) => generateReport(start, end, config))
+      );
+    } else {
+      const council = councils.find((c) => c.round === round);
+      if (!council) return console.warn(`Round ${round} not found:`, councils);
+      console.log(
+        `-> Updating round ${round} (${council.start}-${council.end})`
+      );
+      await generateReport(council.start, council.end, config);
+    }
+    process.exit();
+  });
+};
 
-main();
+main(CONFIG);

+ 1 - 0
contributions/tech/report-generator/src/lib

@@ -0,0 +1 @@
+Subproject commit 7a0df3177dbb47836c7968af0e12e3f722425d5b

+ 208 - 215
contributions/tech/report-generator/src/types.ts

@@ -1,239 +1,232 @@
-import {GenericEventData} from "@polkadot/types/generic/Event";
+export interface Config {
+  repoDir: string,
+  reportsDir: string,
+  spendingCategoriesFile: string,
+  templateFile: string,
+  providerUrl: string,
+  statusUrl: string,
+  burnAddress: string,
+  cacheDir: string,
+  councilRoundOffset: number,
+  videoClassId: number,
+  channelClassId: number,
+}
 
 export class Statistics {
-    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;
-
-    startOperationsMinted: number = 0;
-    endOperationsMinted: number = 0;
-    newOperationsMinted: number = 0;
-    percOperationsMinted: number = 0;
-
-    startIssuance: number = 0;
-    endIssuance: number = 0;
-    newIssuance: number = 0;
-    percNewIssuance: number = 0;
-
-    newTokensBurn: number = 0;
-    newValidatorRewards: number = 0;
-    avgValidators: number = 0;
-    startValidators: string = "";
-    endValidators: string = "";
-    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;
-
-    startOperationsWorkers: number = 0;
-    endOperationsWorkers: number = 0;
-    percNewOperationsWorkers: number = 0;
-    newOperationsReward: number = 0;
-    startOperationsStake: number = 0;
-    endOperationsStake: number = 0;
-    percNewOperationstake: 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;
-
-    spendingProposalsTotal: number = 0;
-    bountiesTotalPaid: number = 0;
-
-    newSetLeadProposal: number = 0;
-    newSetContentWorkingGroupMintCapacityProposal: number = 0;
-    newEvictStorageProviderProposal: number = 0;
-    newSetValidatorCountProposal: number = 0;
-    newSetStorageRoleParametersProposal: number = 0;
-
-    storageProviders: string;
-    curators: string;
-    operations:string;
-
-    constructor() {
-    }
-
+  [key: string]: number | string;
+  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;
+  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;
+
+  startOperationsMinted: number = 0;
+  endOperationsMinted: number = 0;
+  newOperationsMinted: number = 0;
+  percOperationsMinted: number = 0;
+
+  startIssuance: number = 0;
+  endIssuance: number = 0;
+  newIssuance: number = 0;
+  percNewIssuance: number = 0;
+
+  newTokensBurn: number = 0;
+  newValidatorRewards: number = 0;
+  avgValidators: number = 0;
+  startValidators: string = "";
+  endValidators: string = "";
+  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;
+
+  startOperationsWorkers: number = 0;
+  endOperationsWorkers: number = 0;
+  percNewOperationsWorkers: number = 0;
+  newOperationsReward: number = 0;
+  startOperationsStake: number = 0;
+  endOperationsStake: number = 0;
+  percNewOperationstake: 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;
+
+  spendingProposalsTotal: number = 0;
+  bountiesTotalPaid: number = 0;
+
+  newSetLeadProposal: number = 0;
+  newSetContentWorkingGroupMintCapacityProposal: number = 0;
+  newEvictStorageProviderProposal: number = 0;
+  newSetValidatorCountProposal: number = 0;
+  newSetStorageRoleParametersProposal: number = 0;
+
+  storageProviders: string;
+  curators: string;
+  operations: string;
+
+  constructor() {}
 }
 
 export class ValidatorReward {
-    sharedReward: number = 0;
-    remainingReward: number = 0;
-    validators: number = 0;
-    slotStake: number = 0;
-    blockNumber: number = 0;
+  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;
+  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;
+  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 SpendingProposals {
-
-    constructor(public id: number, public title: string, public spentAmount: number) {
-    }
-
+  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;
-    }
-
+  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) {
-    }
+  constructor(public id: number, public title: string) {}
 }
 
 export class Channel {
-    constructor(public id: number, public title: string) {
-    }
-}
-
-export class Bounty {
-    constructor(public testnet: string, public proposalId: number, public title: string, public status: string, public amountAsked: number, public amountMinted: number) {
-    }
-}
-
-export class CacheEvent {
-
-    constructor(public section: string, public method: string, public data: GenericEventData) {
-    }
+  constructor(public id: number, public title: string) {}
 }

+ 3 - 3
contributions/tech/report-generator/tsconfig.json

@@ -25,8 +25,8 @@
       "./node_modules/@polkadot/ts",
       "./node_modules/@types"
     ],
-    "declarationDir": "lib",
-    "outDir": "lib"
+    "declarationDir": "build",
+    "outDir": "build"
   },
   "include": [
     "src/*.ts"
@@ -36,4 +36,4 @@
     "**/*.spec.ts",
     "**/*.d.ts"
   ]
-}
+}