Browse Source

Merge pull request #485 from traumschule/council-report-script

merge council minutes scripts
mochet 3 years ago
parent
commit
5adc216dfa

+ 0 - 5
contributions/tech/council-report-generator/cli/.gitignore

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

+ 0 - 25
contributions/tech/council-report-generator/cli/package.json

@@ -1,25 +0,0 @@
-{
-  "name": "council-report-generator",
-  "version": "0.1.0",
-  "license": "GPL-3.0-only",
-  "scripts": {
-    "index": "ts-node src/index.ts"
-  },
-  "dependencies": {
-    "@joystream/types": "^0.16.1",
-    "@polkadot/api": "4.2.1",
-    "@polkadot/keyring": "^6.0.5",
-    "@polkadot/types": "4.2.1",
-    "@polkadot/util": "^6.0.5",
-    "@polkadot/util-crypto": "^6.0.5",
-    "@types/bn.js": "^4.11.5",
-    "bn.js": "^4.11.8",
-    "express": "^4.17.1"
-  },
-  "devDependencies": {
-    "@polkadot/ts": "^0.3.62",
-    "@types/node": "^16.4.0",
-    "ts-node": "^10.1.0",
-    "typescript": "^3.9.7"
-  }
-}

+ 0 - 83
contributions/tech/council-report-generator/cli/src/index.ts

@@ -1,83 +0,0 @@
-// @ts-check
-
-import { ApiPromise, WsProvider } from "@polkadot/api";
-import { types as joyTypes } from "@joystream/types";
-import { Hash, Moment } from "@polkadot/types/interfaces";
-import {
-  BlockRange,
-  CouncilMemberInfo,
-  CouncilRoundInfo,
-  ProposalFailedReason,
-  ProposalInfo,
-  ProposalStatus,
-  ProposalType,
-  ReportData,
-} from "./types";
-import { Seats } from "@joystream/types/council";
-import { MemberId, Membership } from "@joystream/types/members";
-import { StorageKey, u32, U32, Vec } from "@polkadot/types";
-import { Mint, MintId } from "@joystream/types/mint";
-import { ProposalDetailsOf, ProposalOf } from "@joystream/types/augment/types";
-
-import { generateReportData } from "./report-functions";
-
-const fsSync = require("fs");
-const fs = fsSync.promises;
-
-const PROPOSAL_URL = "https://testnet.joystream.org/#/proposals/";
-const ELECTION_OFFSET = 1;
-
-async function main() {
-  const args = process.argv.slice(2);
-  if (args.length != 2) {
-    console.error("Usage: [start bock number] [end block number]");
-    process.exit(1);
-  }
-
-  const startCouncilBlock = Number(args[0]);
-  const endCouncilBlock = Number(args[1]);
-
-  const provider = new WsProvider("ws://localhost:9944");
-
-  const api = new ApiPromise({ provider, types: joyTypes });
-  await api.isReady;
-
-  const startHash = (await api.rpc.chain.getBlockHash(
-    startCouncilBlock
-  )) as Hash;
-  const endHash = (await api.rpc.chain.getBlockHash(endCouncilBlock)) as Hash;
-  const blockRange = new BlockRange(
-    startCouncilBlock,
-    startHash,
-    endCouncilBlock,
-    endHash
-  );
-
-  const reportData = await generateReportData(api, blockRange);
-  const reportGenerationResult = await generateReport(reportData);
-  await fs.writeFile("report.md", reportGenerationResult);
-
-  api.disconnect();
-}
-
-async function generateReport(data: ReportData) {
-  try {
-    let fileData = await fs.readFile(__dirname + "/../report-template.md", {
-      encoding: "utf8",
-    });
-
-    let entries = Object.entries(data);
-
-    for (let entry of entries) {
-      let regex = new RegExp("{" + entry[0] + "}", "g");
-      fileData = fileData.replace(regex, entry[1].toString());
-    }
-
-    return fileData;
-  } catch (e) {
-    console.error(e);
-  }
-  return "";
-}
-
-main();

+ 1 - 1
contributions/tech/report-generator/README.md

@@ -1,4 +1,4 @@
-# Tokenomics Report Generator
+# Council + 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. \

+ 0 - 118
contributions/tech/report-generator/report-template.md.old

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

+ 102 - 85
contributions/tech/council-report-generator/cli/src/report-functions.ts → contributions/tech/report-generator/src/council.ts

@@ -8,7 +8,7 @@ import {
   ProposalStatus,
   ProposalType,
   ReportData,
-} from "./types";
+} from "./types/council";
 import { StorageKey, U32, u32, Vec } from "@polkadot/types";
 import { Seats } from "@joystream/types/council";
 import { MemberId, Membership } from "@joystream/types/members";
@@ -286,6 +286,102 @@ async function getCouncilMembersInfo(
   return councilRoundInfo;
 }
 
+async function getProposal(
+  api: ApiPromise,
+  range: BlockRange,
+  id: number
+): Promise<ProposalInfo | undefined> {
+  const proposal = (await api.query.proposalsEngine.proposals.at(
+    range.endBlockHash,
+    id
+  )) as ProposalOf;
+  if (proposal.createdAt?.toBigInt() < range.startBlockHeight) {
+    return null;
+  }
+
+  let proposalInfo = new ProposalInfo();
+  proposalInfo.id = id;
+  proposalInfo.name = proposal.title?.toString();
+  try {
+    const proposer = (await api.query.members.membershipById(
+      proposal.proposerId
+    )) as Membership;
+    proposalInfo.creatorUsername = proposer.handle.toString();
+  } catch (e) {
+    proposalInfo.creatorUsername = ``;
+    console.error(`Failed to fetch proposer: ${e.message}`);
+  }
+
+  if (proposal.status.isFinalized) {
+    const finalizedData = proposal.status.asFinalized;
+
+    if (finalizedData.proposalStatus.isCanceled) {
+      proposalInfo.status = ProposalStatus.Cancelled;
+    } else if (finalizedData.proposalStatus.isExpired) {
+      proposalInfo.status = ProposalStatus.Expired;
+    } else if (finalizedData.proposalStatus.isRejected) {
+      proposalInfo.status = ProposalStatus.Rejected;
+    } else if (finalizedData.proposalStatus.isApproved) {
+      let approvedData = finalizedData.proposalStatus.asApproved;
+      if (approvedData.isExecuted) {
+        proposalInfo.status = ProposalStatus.Executed;
+      } else if (approvedData.isPendingExecution) {
+        proposalInfo.status = ProposalStatus.PendingExecution;
+      } else if (approvedData.isExecutionFailed) {
+        proposalInfo.status = ProposalStatus.ExecutionFailed;
+        let executionFailedData = approvedData.asExecutionFailed;
+        if (executionFailedData.error.toString() == "NotEnoughCapacity") {
+          proposalInfo.failedReason = ProposalFailedReason.NotEnoughCapacity;
+        } else {
+          proposalInfo.failedReason = ProposalFailedReason.ExecutionFailed;
+        }
+      }
+    } else if (finalizedData.proposalStatus.isSlashed) {
+      proposalInfo.status = ProposalStatus.Slashed;
+    }
+
+    proposalInfo.blocksToFinalized =
+      Number(proposal.status.asFinalized.finalizedAt.toBigInt()) -
+      Number(proposal.createdAt.toBigInt());
+
+    const proposalByVoters =
+      await api.query.proposalsEngine.voteExistsByProposalByVoter.entries(id);
+
+    for (let proposalByVoter of proposalByVoters) {
+      let key = proposalByVoter[0] as StorageKey;
+      let memberId = key.args[1] as MemberId;
+      let member = (await api.query.members.membershipById(
+        memberId
+      )) as Membership;
+      proposalInfo.votersUsernames.push(member.handle.toString());
+    }
+  }
+
+  let proposalDetails =
+    (await api.query.proposalsCodex.proposalDetailsByProposalId(
+      id
+    )) as ProposalDetailsOf;
+  let typeString = proposalDetails.type as keyof typeof ProposalType;
+  proposalInfo.type = ProposalType[typeString];
+
+  if (proposalInfo.type == ProposalType.Spending) {
+    let spendingData = proposalDetails.asSpending;
+    let accountId = spendingData[1];
+    proposalInfo.paymentAmount = Number(spendingData[0].toBigInt());
+    let paymentDestinationMemberId =
+      await api.query.members.memberIdsByControllerAccountId(accountId);
+    if (!paymentDestinationMemberId.isEmpty) {
+      let paymentDestinationMembership =
+        (await api.query.members.membershipById(
+          paymentDestinationMemberId
+        )) as Membership;
+      proposalInfo.paymentDestinationMemberUsername =
+        paymentDestinationMembership.handle.toString();
+    }
+  }
+  return proposalInfo;
+}
+
 async function getProposals(api: ApiPromise, range: BlockRange) {
   let startProposalCount = Number(
     (
@@ -304,91 +400,12 @@ async function getProposals(api: ApiPromise, range: BlockRange) {
 
   let proposals = new Array<ProposalInfo>();
   for (let i = startProposalCount - 1; i <= endProposalCount; i++) {
-    let proposal = (await api.query.proposalsEngine.proposals.at(
-      range.endBlockHash,
-      i
-    )) as ProposalOf;
-    if (proposal.createdAt.toBigInt() < range.startBlockHeight) {
-      continue;
+    try {
+      const proposal = await getProposal(api, range, i);
+      if (proposal) proposals.push(proposal);
+    } catch (e) {
+      console.error(`Failed to fetch proposal ${i}: ${e.message}`);
     }
-
-    let proposer = (await api.query.members.membershipById(
-      proposal.proposerId
-    )) as Membership;
-    let proposalInfo = new ProposalInfo();
-    proposalInfo.id = i;
-    proposalInfo.name = proposal.title.toString();
-    proposalInfo.creatorUsername = proposer.handle.toString();
-
-    if (proposal.status.isFinalized) {
-      const finalizedData = proposal.status.asFinalized;
-
-      if (finalizedData.proposalStatus.isCanceled) {
-        proposalInfo.status = ProposalStatus.Cancelled;
-      } else if (finalizedData.proposalStatus.isExpired) {
-        proposalInfo.status = ProposalStatus.Expired;
-      } else if (finalizedData.proposalStatus.isRejected) {
-        proposalInfo.status = ProposalStatus.Rejected;
-      } else if (finalizedData.proposalStatus.isApproved) {
-        let approvedData = finalizedData.proposalStatus.asApproved;
-        if (approvedData.isExecuted) {
-          proposalInfo.status = ProposalStatus.Executed;
-        } else if (approvedData.isPendingExecution) {
-          proposalInfo.status = ProposalStatus.PendingExecution;
-        } else if (approvedData.isExecutionFailed) {
-          proposalInfo.status = ProposalStatus.ExecutionFailed;
-          let executionFailedData = approvedData.asExecutionFailed;
-          if (executionFailedData.error.toString() == "NotEnoughCapacity") {
-            proposalInfo.failedReason = ProposalFailedReason.NotEnoughCapacity;
-          } else {
-            proposalInfo.failedReason = ProposalFailedReason.ExecutionFailed;
-          }
-        }
-      } else if (finalizedData.proposalStatus.isSlashed) {
-        proposalInfo.status = ProposalStatus.Slashed;
-      }
-
-      proposalInfo.blocksToFinalized =
-        Number(proposal.status.asFinalized.finalizedAt.toBigInt()) -
-        Number(proposal.createdAt.toBigInt());
-
-      const proposalByVoters =
-        await api.query.proposalsEngine.voteExistsByProposalByVoter.entries(i);
-
-      for (let proposalByVoter of proposalByVoters) {
-        let key = proposalByVoter[0] as StorageKey;
-        let memberId = key.args[1] as MemberId;
-        let member = (await api.query.members.membershipById(
-          memberId
-        )) as Membership;
-        proposalInfo.votersUsernames.push(member.handle.toString());
-      }
-    }
-
-    let proposalDetails =
-      (await api.query.proposalsCodex.proposalDetailsByProposalId(
-        i
-      )) as ProposalDetailsOf;
-    let typeString = proposalDetails.type as keyof typeof ProposalType;
-    proposalInfo.type = ProposalType[typeString];
-
-    if (proposalInfo.type == ProposalType.Spending) {
-      let spendingData = proposalDetails.asSpending;
-      let accountId = spendingData[1];
-      proposalInfo.paymentAmount = Number(spendingData[0].toBigInt());
-      let paymentDestinationMemberId =
-        await api.query.members.memberIdsByControllerAccountId(accountId);
-      if (!paymentDestinationMemberId.isEmpty) {
-        let paymentDestinationMembership =
-          (await api.query.members.membershipById(
-            paymentDestinationMemberId
-          )) as Membership;
-        proposalInfo.paymentDestinationMemberUsername =
-          paymentDestinationMembership.handle.toString();
-      }
-    }
-
-    proposals.push(proposalInfo);
   }
 
   return proposals;

+ 101 - 23
contributions/tech/report-generator/src/generator.ts

@@ -1,16 +1,40 @@
 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 { ApiPromise } from "@polkadot/api";
+import { connectApi, getHead, getCouncils, getCouncilRound } from "./lib/api";
+import { StatisticsCollector } from "./tokenomics";
+import { generateReportData } from "./council";
+import { Config } from "./types/tokenomics";
+
+// types
 import { Round } from "./lib/types";
-import { Config } from "./types";
+import { types as joyTypes } from "@joystream/types";
+import { Hash, Moment } from "@polkadot/types/interfaces";
+import {
+  BlockRange,
+  CouncilMemberInfo,
+  CouncilRoundInfo,
+  ProposalFailedReason,
+  ProposalInfo,
+  ProposalStatus,
+  ProposalType,
+  ReportData,
+} from "./types/council";
+import { Seats } from "@joystream/types/council";
+import { MemberId, Membership } from "@joystream/types/members";
+import { StorageKey, u32, U32, Vec } from "@polkadot/types";
+import { Mint, MintId } from "@joystream/types/mint";
+import { ProposalDetailsOf, ProposalOf } from "@joystream/types/augment/types";
 
 const CONFIG: Config = {
   repoDir: __dirname + "/../../../../",
   reportsDir: "council/tokenomics",
   spendingCategoriesFile: "governance/spending_proposal_categories.csv",
-  templateFile: __dirname + "/../report-template.md",
+  councilTemplate: __dirname + "/../templates/council.md",
+  tokenomicsTemplate: __dirname + "/../templates/tokenomics.md",
   providerUrl: "ws://127.0.0.1:9944",
+  proposalUrl: "https://testnet.joystream.org/#/proposals/",
   statusUrl: "https://status.joystream.org/status/",
   burnAddress: "5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu",
   cacheDir: "cache",
@@ -19,31 +43,62 @@ const CONFIG: Config = {
   channelClassId: 1,
 };
 
-async function main(config: Config) {
-  const { templateFile } = config;
-  const args = process.argv.slice(2);
-  if (args.length < 2) return updateReports(config, Number(args[0]));
+const councilReport = async (
+  startBlock: number,
+  endBlock: number,
+  config: Config
+) => {
+  const { repoDir, councilTemplate, providerUrl } = config;
+  const api: ApiPromise = await connectApi(providerUrl);
+  await api.isReady;
 
-  const startBlock = Number(args[0]);
-  const endBlock = Number(args[1]);
+  // council report
+  const startHash: Hash = await api.rpc.chain.getBlockHash(startBlock);
+  const endHash: Hash = await api.rpc.chain.getBlockHash(endBlock);
+  const blockRange = new BlockRange(startBlock, startHash, endBlock, endHash);
 
-  if (isNaN(startBlock) || isNaN(endBlock) || startBlock >= endBlock) {
-    console.error("Invalid block range.");
-    process.exit(1);
-  } else generateReport(startBlock, endBlock, config);
-}
+  const data = await generateReportData(api, blockRange);
+  const report = await generateCouncilReport(data, councilTemplate);
+
+  const term = data.councilTerm;
+  const version = startBlock < 717987 ? "antioch" : "sumer";
+  const versionStr = version[0].toUpperCase() + version.slice(1);
+  const filename = `council/reports/${version}-reports/${versionStr}_Council${term}_Report.md`;
+  fs.writeFileSync(repoDir + filename, report);
+  console.log(`-> Wrote ${filename}`);
+
+  api.disconnect();
+};
+
+const generateCouncilReport = async (
+  data: ReportData,
+  templateFile: string
+) => {
+  try {
+    let fileData = await fs.readFileSync(templateFile, "utf8");
+    let entries = Object.entries(data);
+
+    for (let entry of entries) {
+      let regex = new RegExp("{" + entry[0] + "}", "g");
+      fileData = fileData.replace(regex, entry[1].toString());
+    }
+    return fileData;
+  } catch (e) {
+    console.error(e);
+    return "";
+  }
+};
 
-const generateReport = async (
+const tokenomicsReport = async (
   startBlock: number,
   endBlock: number,
   config: Config
 ): Promise<boolean> => {
-  const { templateFile, repoDir, reportsDir } = config;
-  let fileData = fs.readFileSync(templateFile, "utf8");
+  const { tokenomicsTemplate, repoDir, reportsDir } = config;
+  let fileData = fs.readFileSync(tokenomicsTemplate, "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;
 
@@ -62,9 +117,9 @@ const generateReport = async (
 };
 
 const updateReports = async (config: Config, round?: number) => {
-  const { templateFile, providerUrl } = config;
+  const { providerUrl } = config;
   console.debug(`Connecting to ${providerUrl}`);
-  const api = await connectApi(providerUrl);
+  const api: ApiPromise = await connectApi(providerUrl);
   await api.isReady;
 
   console.log(`-> Fetching councils`);
@@ -74,7 +129,7 @@ const updateReports = async (config: Config, round?: number) => {
     if (round === null || isNaN(round)) {
       console.log(`-> Updating reports`);
       await Promise.all(
-        councils.map(({ start, end }) => generateReport(start, end, config))
+        councils.map(({ start, end }) => createReports(start, end, config))
       );
     } else {
       const council = councils.find((c) => c.round === round);
@@ -82,10 +137,33 @@ const updateReports = async (config: Config, round?: number) => {
       console.log(
         `-> Updating round ${round} (${council.start}-${council.end})`
       );
-      await generateReport(council.start, council.end, config);
+      await createReports(council.start, council.end, config);
     }
     process.exit();
   });
 };
 
+const createReports = (
+  startBlock: number,
+  endBlock: number,
+  config: Config
+) => {
+  councilReport(startBlock, endBlock, config);
+  return tokenomicsReport(startBlock, endBlock, config);
+};
+
+const main = async (config: Config) => {
+  const args = process.argv.slice(2);
+  if (args.length < 2) return updateReports(config, Number(args[0]));
+
+  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);
+  }
+
+  createReports(startBlock, endBlock, config);
+};
 main(CONFIG);

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

@@ -1 +1 @@
-Subproject commit b66ce1329015715d32d7d3f873a411401d691583
+Subproject commit 000bb8b8661b3c6afb6049cba2c8e24f44f78a9f

+ 2 - 2
contributions/tech/report-generator/src/StatisticsCollector.ts → contributions/tech/report-generator/src/tokenomics.ts

@@ -9,7 +9,7 @@ import {
   EventRecord,
   Hash,
 } from "@polkadot/types/interfaces";
-import { Config, MintStatistics, Statistics, WorkersInfo } from "./types";
+import { Config, MintStatistics, Statistics, WorkersInfo } from "./types/tokenomics";
 import {
   CacheEvent,
   Bounty,
@@ -804,7 +804,7 @@ export class StatisticsCollector {
         endTermExchangeRate = lastExchangeEvent.price * 1000000;
       } else {
         startTermExchangeRate =
-          filteredDollarPoolChanges[0].valueAfter * 1000000;
+          filteredDollarPoolChanges[0].rateAfter * 1000000;
         const lastEvent =
           filteredDollarPoolChanges[filteredDollarPoolChanges.length - 1];
         endTermExchangeRate = lastEvent.rateAfter * 1000000;

+ 0 - 0
contributions/tech/council-report-generator/cli/src/types.ts → contributions/tech/report-generator/src/types/council.ts


+ 13 - 11
contributions/tech/report-generator/src/types.ts → contributions/tech/report-generator/src/types/tokenomics.ts

@@ -1,15 +1,17 @@
 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,
+  repoDir: string;
+  reportsDir: string;
+  spendingCategoriesFile: string;
+  councilTemplate: string;
+  tokenomicsTemplate: string;
+  providerUrl: string;
+  proposalUrl: string;
+  statusUrl: string;
+  burnAddress: string;
+  cacheDir: string;
+  councilRoundOffset: number;
+  videoClassId: number;
+  channelClassId: number;
 }
 
 export class Statistics {

+ 3 - 2
contributions/tech/council-report-generator/cli/report-template.md → contributions/tech/report-generator/templates/council.md

@@ -74,7 +74,7 @@ Failed Proposals
 
 {proposalBreakdown}
 
-### 2.4 - Select threads & events
+### 2.4 - Selected Threads & Events
 N/A
 
 ### 2.5 - Working Group Spotchecks
@@ -83,7 +83,7 @@ N/A
 - Operations Role Spot Check: N/A
 
 ## 3 - Review
-### 3.1 - Workflow, Performance, Challenged & Thinking
+### 3.1 - Workflow, Performance, Challenges & Thoughts
 * N/A
 
 ## 4 - Obligations
@@ -117,6 +117,7 @@ Council obligations are payments or items that carry through council sessions. T
 ## 5 - Report changelog
 - [October, November 2021](https://github.com/Joystream/community-repo/pull/428)
     - proposals refactor, show amount for spending proposals
+    - merge council report script into tokenomics generator ([PR #485](https://github.com/Joystream/community-repo/pull/485))
 
 - 04.11.2020
     - added working group review section

+ 0 - 0
contributions/tech/report-generator/report-template.md → contributions/tech/report-generator/templates/tokenomics.md