StatisticsCollector.ts 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. import {ApiPromise, WsProvider} from "@polkadot/api";
  2. import {types} from '@joystream/types'
  3. import {
  4. AccountId,
  5. Balance,
  6. BalanceOf,
  7. BlockNumber,
  8. EraIndex,
  9. EventRecord,
  10. Hash,
  11. Moment
  12. } from "@polkadot/types/interfaces";
  13. import {
  14. CacheEvent,
  15. Media,
  16. MintStatistics,
  17. Statistics,
  18. WorkersInfo, Channel, SpendingProposals, Bounty
  19. } from "./types";
  20. import {Option, u32, Vec} from "@polkadot/types";
  21. import {ElectionStake, SealedVote, Seats} from "@joystream/types/council";
  22. import {Mint, MintId} from "@joystream/types/mint";
  23. import {ContentId, DataObject} from "@joystream/types/media";
  24. import {ChannelId, PostId, ThreadId} from "@joystream/types/common";
  25. import {CategoryId} from "@joystream/types/forum";
  26. import {MemberId, Membership} from "@joystream/types/members";
  27. import {RewardRelationship, RewardRelationshipId} from "@joystream/types/recurring-rewards";
  28. import {Stake} from "@joystream/types/stake";
  29. import {WorkerId} from "@joystream/types/working-group";
  30. import {Entity, EntityId, PropertyType} from "@joystream/types/content-directory";
  31. import {ProposalId, Video, VideoId, WorkerOf, } from "@joystream/types/augment-codec/all";
  32. import {ProposalDetails, ProposalOf} from "@joystream/types/augment/types";
  33. import {SpendingParams} from "@joystream/types/proposals";
  34. import * as constants from "constants";
  35. const fsSync = require('fs');
  36. const fs = fsSync.promises;
  37. const parse = require('csv-parse/lib/sync');
  38. const BURN_ADDRESS = '5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu';
  39. const COUNCIL_ROUND_OFFSET = 2;
  40. const PROVIDER_URL = "ws://localhost:9944";
  41. const CACHE_FOLDER = "cache";
  42. const VIDEO_CLASS_iD = 10;
  43. const CHANNEL_CLASS_iD = 1;
  44. const SPENDING_PROPOSALS_CATEGORIES_FILE = __dirname + '/../../../documentation/spending_proposal_categories.csv';
  45. export class StatisticsCollector {
  46. private api?: ApiPromise;
  47. private blocksEventsCache: Map<number, CacheEvent[]>;
  48. private statistics: Statistics;
  49. constructor() {
  50. this.blocksEventsCache = new Map<number, CacheEvent[]>();
  51. this.statistics = new Statistics();
  52. }
  53. async getStatistics(startBlock: number, endBlock: number): Promise<Statistics> {
  54. this.api = await StatisticsCollector.connectApi();
  55. let startHash = (await this.api.rpc.chain.getBlockHash(startBlock)) as Hash;
  56. let endHash = (await this.api.rpc.chain.getBlockHash(endBlock)) as Hash;
  57. this.statistics.startBlock = startBlock;
  58. this.statistics.endBlock = endBlock;
  59. this.statistics.newBlocks = endBlock - startBlock;
  60. this.statistics.percNewBlocks = StatisticsCollector.convertToPercentage(startBlock, endBlock);
  61. await this.buildBlocksEventCache(startBlock, endBlock);
  62. await this.fillBasicInfo(startHash, endHash);
  63. await this.fillTokenGenerationInfo(startBlock, endBlock, startHash, endHash);
  64. await this.fillMintsInfo(startHash, endHash);
  65. await this.fillCouncilInfo(startHash, endHash);
  66. await this.fillCouncilElectionInfo(startBlock);
  67. await this.fillValidatorInfo(startHash, endHash);
  68. await this.fillStorageProviderInfo(startBlock, endBlock, startHash, endHash);
  69. await this.fillCuratorInfo(startHash, endHash);
  70. await this.fillOperationsInfo(startBlock, endBlock, startHash, endHash);
  71. await this.fillMembershipInfo(startHash, endHash);
  72. await this.fillMediaUploadInfo(startHash, endHash);
  73. await this.fillForumInfo(startHash, endHash);
  74. await this.api.disconnect();
  75. return this.statistics;
  76. }
  77. async getApprovedBounties() {
  78. try {
  79. await fs.access(SPENDING_PROPOSALS_CATEGORIES_FILE, constants.R_OK);
  80. } catch {
  81. console.warn('File with the spending proposal categories not found');
  82. return [];
  83. }
  84. const fileContent = await fs.readFile(SPENDING_PROPOSALS_CATEGORIES_FILE);
  85. let rawBounties = parse(fileContent);
  86. rawBounties.shift();
  87. rawBounties = rawBounties.filter((line: string[]) => line[8] == 'Bounties');
  88. let bounties = rawBounties.map((rawBounty: any) => {
  89. return new Bounty(rawBounty[0], rawBounty[1], rawBounty[2], rawBounty[3], rawBounty[4], rawBounty[5]);
  90. });
  91. return bounties.filter((bounty: Bounty) => bounty.status == "Approved" && bounty.testnet == "Antioch");
  92. }
  93. async fillValidatorsRewards() {
  94. for (let [key, blockEvents] of this.blocksEventsCache) {
  95. let validatorRewards = blockEvents.filter((event) => {
  96. return event.section == "staking" && event.method == "Reward";
  97. });
  98. for (let validatorReward of validatorRewards) {
  99. this.statistics.newValidatorRewards += Number(validatorReward.data[1]);
  100. }
  101. }
  102. }
  103. async computeTokensBurn(){
  104. let tokensBurned = 0;
  105. for (let [, blockEvents] of this.blocksEventsCache) {
  106. let transfers = blockEvents.filter((event) => {
  107. return event.section == "balances" && event.method == "Transfer";
  108. });
  109. for (let transfer of transfers) {
  110. let receiver = transfer.data[1] as AccountId;
  111. let amount = transfer.data[2] as Balance;
  112. if (receiver.toString() == BURN_ADDRESS) {
  113. tokensBurned += Number(amount);
  114. }
  115. }
  116. }
  117. return tokensBurned;
  118. }
  119. async getFinalizedSpendingProposals(endHash: Hash): Promise<Array<SpendingProposals>> {
  120. let spendingProposals = new Array<SpendingProposals>();
  121. for (let [key, blockEvents] of this.blocksEventsCache) {
  122. let proposalEvents = blockEvents.filter((event) => {
  123. return event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated";
  124. });
  125. for (let proposalEvent of proposalEvents) {
  126. let statusUpdateData = proposalEvent.data[1] as any;
  127. if (!(statusUpdateData.finalized && statusUpdateData.finalized.finalizedAt)) {
  128. continue;
  129. }
  130. let proposalId = proposalEvent.data[0] as ProposalId;
  131. let proposalInfo = await this.api.query.proposalsEngine.proposals.at(endHash, proposalId) as ProposalOf;
  132. const finalizedData = proposalInfo.status.asFinalized;
  133. let proposalDetail = await this.api.query.proposalsCodex.proposalDetailsByProposalId.at(endHash, proposalId) as ProposalDetails;
  134. if (!finalizedData.proposalStatus.isApproved || !proposalDetail.isSpending) {
  135. continue;
  136. }
  137. let approvedData = finalizedData.proposalStatus.asApproved;
  138. if (!approvedData.isExecuted) {
  139. continue;
  140. }
  141. let spendingParams = proposalDetail.asSpending;
  142. if (!spendingProposals.some(spendingProposal => (spendingProposal.id == Number(proposalId)))){
  143. spendingProposals.push(new SpendingProposals(Number(proposalId), proposalInfo.title.toString(), Number(spendingParams[0])));
  144. }
  145. }
  146. }
  147. return spendingProposals;
  148. }
  149. async fillBasicInfo(startHash: Hash, endHash: Hash) {
  150. let startDate = (await this.api.query.timestamp.now.at(startHash)) as Moment;
  151. let endDate = (await this.api.query.timestamp.now.at(endHash)) as Moment;
  152. this.statistics.dateStart = new Date(startDate.toNumber()).toLocaleDateString("en-US");
  153. this.statistics.dateEnd = new Date(endDate.toNumber()).toLocaleDateString("en-US");
  154. }
  155. async fillTokenGenerationInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  156. this.statistics.startIssuance = (await this.api.query.balances.totalIssuance.at(startHash) as Balance).toNumber();
  157. this.statistics.endIssuance = (await this.api.query.balances.totalIssuance.at(endHash) as Balance).toNumber();
  158. this.statistics.newIssuance = this.statistics.endIssuance - this.statistics.startIssuance;
  159. this.statistics.percNewIssuance = StatisticsCollector.convertToPercentage(this.statistics.startIssuance, this.statistics.endIssuance);
  160. this.statistics.newTokensBurn = await this.computeTokensBurn();
  161. let bounties = await this.getApprovedBounties();
  162. let spendingProposals = await this.getFinalizedSpendingProposals(endHash);
  163. this.statistics.bountiesTotalPaid = 0;
  164. if (bounties) {
  165. for (let bounty of bounties) {
  166. let bountySpendingProposal = spendingProposals.find((spendingProposal) => spendingProposal.id == bounty.proposalId);
  167. if (bountySpendingProposal) {
  168. this.statistics.bountiesTotalPaid += bountySpendingProposal.spentAmount;
  169. }
  170. }
  171. }
  172. if (!this.statistics.bountiesTotalPaid) {
  173. console.warn('No bounties found in ' + SPENDING_PROPOSALS_CATEGORIES_FILE +', trying to find spending proposals of bounties, please check the values!...');
  174. for (const spendingProposal of spendingProposals) {
  175. if (spendingProposal.title.toLowerCase().includes("bounty")) {
  176. this.statistics.bountiesTotalPaid += spendingProposal.spentAmount;
  177. }
  178. }
  179. }
  180. this.statistics.spendingProposalsTotal = spendingProposals.reduce((n, spendingProposal) => n + spendingProposal.spentAmount, 0);
  181. let roundNrBlocks = endBlock - startBlock;
  182. this.statistics.newCouncilRewards = await this.computeCouncilReward(roundNrBlocks, endHash);
  183. this.statistics.newCouncilRewards = Number(this.statistics.newCouncilRewards.toFixed(2));
  184. this.statistics.newCuratorRewards = await this.computeCuratorsReward(roundNrBlocks, startHash, endHash);
  185. this.statistics.newCuratorRewards = Number(this.statistics.newCuratorRewards.toFixed(2));
  186. }
  187. async computeCouncilReward(roundNrBlocks: number, endHash: Hash): Promise<number> {
  188. const payoutInterval = Number((await this.api.query.council.payoutInterval.at(endHash) as Option<BlockNumber>).unwrapOr(0));
  189. const amountPerPayout = (await this.api.query.council.amountPerPayout.at(endHash) as BalanceOf).toNumber();
  190. const announcing_period = (await this.api.query.councilElection.announcingPeriod.at(endHash)) as BlockNumber;
  191. const voting_period = (await this.api.query.councilElection.votingPeriod.at(endHash)) as BlockNumber;
  192. const revealing_period = (await this.api.query.councilElection.revealingPeriod.at(endHash)) as BlockNumber;
  193. const new_term_duration = (await this.api.query.councilElection.newTermDuration.at(endHash)) as BlockNumber;
  194. const termDuration = new_term_duration.toNumber();
  195. const votingPeriod = voting_period.toNumber();
  196. const revealingPeriod = revealing_period.toNumber();
  197. const announcingPeriod = announcing_period.toNumber();
  198. const nrCouncilMembers = (await this.api.query.council.activeCouncil.at(endHash) as Seats).length
  199. const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
  200. ? (amountPerPayout * nrCouncilMembers) / payoutInterval
  201. : 0;
  202. const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
  203. const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
  204. return avgCouncilRewardPerBlock * roundNrBlocks;
  205. }
  206. async computeWorkingGroupReward(roundNrBlocks: number, startHash: Hash, endHash: Hash, workingGroup: string): Promise<WorkersInfo> {
  207. let nextWorkerId = (await this.api.query[workingGroup + 'WorkingGroup'].nextWorkerId.at(startHash) as WorkerId).toNumber();
  208. let info = new WorkersInfo();
  209. for (let i = 0; i < nextWorkerId; ++i) {
  210. let worker = await this.api.query[workingGroup + 'WorkingGroup'].workerById.at(endHash, i) as WorkerOf;
  211. if (!worker.is_active) {
  212. continue;
  213. }
  214. if (worker.role_stake_profile.isSome) {
  215. let roleStakeProfile = worker.role_stake_profile.unwrap();
  216. let stake = await this.api.query.stake.stakes.at(endHash, roleStakeProfile.stake_id) as Stake;
  217. info.startStake += stake.value.toNumber();
  218. }
  219. }
  220. nextWorkerId = (await this.api.query[workingGroup + 'WorkingGroup'].nextWorkerId.at(endHash) as WorkerId).toNumber();
  221. let rewardRelationshipIds = Array<RewardRelationshipId>();
  222. for (let i = 0; i < nextWorkerId; ++i) {
  223. let worker = await this.api.query[workingGroup + 'WorkingGroup'].workerById.at(endHash, i) as WorkerOf;
  224. if (!worker.is_active) {
  225. continue;
  226. }
  227. if (worker.reward_relationship.isSome) {
  228. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  229. }
  230. if (worker.role_stake_profile.isSome) {
  231. let roleStakeProfile = worker.role_stake_profile.unwrap();
  232. let stake = await this.api.query.stake.stakes.at(endHash, roleStakeProfile.stake_id) as Stake;
  233. info.endStake += stake.value.toNumber();
  234. }
  235. }
  236. info.rewards = await this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  237. info.endNrOfWorkers = nextWorkerId;
  238. return info;
  239. }
  240. async computeCuratorsReward(roundNrBlocks: number, startHash: Hash, endHash: Hash) {
  241. let nextCuratorId = (await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
  242. let rewardRelationshipIds = Array<RewardRelationshipId>();
  243. for (let i = 0; i < nextCuratorId; ++i) {
  244. let worker = await this.api.query.contentDirectoryWorkingGroup.workerById.at(endHash, i) as WorkerOf;
  245. if (!worker.is_active) {
  246. continue;
  247. }
  248. if (worker.reward_relationship.isSome) {
  249. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  250. }
  251. }
  252. return this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  253. }
  254. async computeReward(roundNrBlocks: number, rewardRelationshipIds: RewardRelationshipId[], hash: Hash) {
  255. let recurringRewards = await Promise.all(rewardRelationshipIds.map(async (rewardRelationshipId) => {
  256. return await this.api.query.recurringRewards.rewardRelationships.at(hash, rewardRelationshipId) as RewardRelationship;
  257. }));
  258. let rewardPerBlock = 0;
  259. for (let recurringReward of recurringRewards) {
  260. const amount = recurringReward.amount_per_payout.toNumber();
  261. const payoutInterval = recurringReward.payout_interval.unwrapOr(null);
  262. if (amount && payoutInterval) {
  263. rewardPerBlock += amount / payoutInterval;
  264. }
  265. }
  266. return rewardPerBlock * roundNrBlocks;
  267. }
  268. async fillMintsInfo(startHash: Hash, endHash: Hash) {
  269. let startNrMints = parseInt((await this.api.query.minting.mintsCreated.at(startHash)).toString());
  270. let endNrMints = parseInt((await this.api.query.minting.mintsCreated.at(endHash)).toString());
  271. this.statistics.newMints = endNrMints - startNrMints;
  272. // statistics.startMinted = 0;
  273. // statistics.endMinted = 0;
  274. for (let i = 0; i < startNrMints; ++i) {
  275. let startMint = (await this.api.query.minting.mints.at(startHash, i)) as Mint;
  276. // if (!startMint) {
  277. // continue;
  278. // }
  279. let endMint = (await this.api.query.minting.mints.at(endHash, i)) as Mint;
  280. // let = endMintResult[0];
  281. // if (!endMint) {
  282. // continue;
  283. // }
  284. let startMintTotal = parseInt(startMint.getField("total_minted").toString());
  285. let endMintTotal = parseInt(endMint.getField("total_minted").toString());
  286. // statistics.startMinted += startMintTotal;
  287. this.statistics.totalMinted += endMintTotal - startMintTotal;
  288. this.statistics.totalMintCapacityIncrease += parseInt(endMint.getField("capacity").toString()) - parseInt(startMint.getField("capacity").toString());
  289. }
  290. for (let i = startNrMints; i < endNrMints; ++i) {
  291. let endMint = await this.api.query.minting.mints.at(endHash, i) as Mint;
  292. if (!endMint) {
  293. return;
  294. }
  295. this.statistics.totalMinted = parseInt(endMint.getField("total_minted").toString());
  296. }
  297. let councilMint = (await this.api.query.council.councilMint.at(endHash)) as MintId;
  298. let councilMintStatistics = await this.computeMintInfo(councilMint, startHash, endHash);
  299. this.statistics.startCouncilMinted = councilMintStatistics.startMinted;
  300. this.statistics.endCouncilMinted = councilMintStatistics.endMinted;
  301. this.statistics.newCouncilMinted = councilMintStatistics.diffMinted;
  302. this.statistics.percNewCouncilMinted = councilMintStatistics.percMinted;
  303. let curatorMint = (await this.api.query.contentDirectoryWorkingGroup.mint.at(endHash)) as MintId;
  304. let curatorMintStatistics = await this.computeMintInfo(curatorMint, startHash, endHash);
  305. this.statistics.startCuratorMinted = curatorMintStatistics.startMinted;
  306. this.statistics.endCuratorMinted = curatorMintStatistics.endMinted;
  307. this.statistics.newCuratorMinted = curatorMintStatistics.diffMinted;
  308. this.statistics.percCuratorMinted = curatorMintStatistics.percMinted;
  309. let storageProviderMint = (await this.api.query.storageWorkingGroup.mint.at(endHash)) as MintId;
  310. let storageProviderMintStatistics = await this.computeMintInfo(storageProviderMint, startHash, endHash);
  311. this.statistics.startStorageMinted = storageProviderMintStatistics.startMinted;
  312. this.statistics.endStorageMinted = storageProviderMintStatistics.endMinted;
  313. this.statistics.newStorageMinted = storageProviderMintStatistics.diffMinted;
  314. this.statistics.percStorageMinted = storageProviderMintStatistics.percMinted;
  315. let operationsProviderMint = (await this.api.query.operationsWorkingGroup.mint.at(endHash)) as MintId;
  316. let operationsProviderMintStatistics = await this.computeMintInfo(operationsProviderMint, startHash, endHash);
  317. this.statistics.startOperationsMinted = operationsProviderMintStatistics.startMinted;
  318. this.statistics.endOperationsMinted = operationsProviderMintStatistics.endMinted;
  319. this.statistics.newOperationsMinted = operationsProviderMintStatistics.diffMinted;
  320. this.statistics.percOperationsMinted = operationsProviderMintStatistics.percMinted;
  321. }
  322. async computeMintInfo(mintId: MintId, startHash: Hash, endHash: Hash): Promise<MintStatistics> {
  323. // if (mintId.toString() == "0") {
  324. // return new MintStatistics(0, 0, 0);
  325. // }
  326. let startMint = await this.api.query.minting.mints.at(startHash, mintId) as Mint;
  327. // let startMint = startMintResult[0] as unknown as Mint;
  328. // if (!startMint) {
  329. // return new MintStatistics(0, 0, 0);
  330. // }
  331. let endMint = await this.api.query.minting.mints.at(endHash, mintId) as Mint;
  332. // let endMint = endMintResult[0] as unknown as Mint;
  333. // if (!endMint) {
  334. // return new MintStatistics(0, 0, 0);
  335. // }
  336. let mintStatistics = new MintStatistics();
  337. mintStatistics.startMinted = parseInt(startMint.getField('total_minted').toString());
  338. mintStatistics.endMinted = parseInt(endMint.getField('total_minted').toString());
  339. mintStatistics.diffMinted = mintStatistics.endMinted - mintStatistics.startMinted;
  340. mintStatistics.percMinted = StatisticsCollector.convertToPercentage(mintStatistics.startMinted, mintStatistics.endMinted);
  341. return mintStatistics;
  342. }
  343. async fillCouncilInfo(startHash: Hash, endHash: Hash) {
  344. this.statistics.councilRound = (await this.api.query.councilElection.round.at(startHash) as u32).toNumber() - COUNCIL_ROUND_OFFSET;
  345. this.statistics.councilMembers = (await this.api.query.councilElection.councilSize.at(startHash) as u32).toNumber();
  346. let startNrProposals = await this.api.query.proposalsEngine.proposalCount.at(startHash) as u32;
  347. let endNrProposals = await this.api.query.proposalsEngine.proposalCount.at(endHash) as u32;
  348. this.statistics.newProposals = endNrProposals.toNumber() - startNrProposals.toNumber();
  349. let approvedProposals = new Set();
  350. for (let [key, blockEvents] of this.blocksEventsCache) {
  351. for (let event of blockEvents) {
  352. if (event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated") {
  353. let statusUpdateData = event.data[1] as any;
  354. let finalizeData = statusUpdateData.finalized as any
  355. if (finalizeData && finalizeData.proposalStatus.approved) {
  356. approvedProposals.add(Number(event.data[0]));
  357. }
  358. }
  359. }
  360. }
  361. this.statistics.newApprovedProposals = approvedProposals.size;
  362. }
  363. async fillCouncilElectionInfo(startBlock: number) {
  364. let startBlockHash = await this.api.rpc.chain.getBlockHash(startBlock);
  365. let events = await this.api.query.system.events.at(startBlockHash) as Vec<EventRecord>;
  366. let isStartBlockFirstCouncilBlock = events.some((event) => {
  367. return event.event.section == "councilElection" && event.event.method == "CouncilElected";
  368. });
  369. if (!isStartBlockFirstCouncilBlock) {
  370. console.warn('Note: The given start block is not the first block of the council round so council election information will be empty');
  371. return;
  372. }
  373. let previousCouncilRoundLastBlock = startBlock - 1;
  374. let previousCouncilRoundLastBlockHash = await this.api.rpc.chain.getBlockHash(previousCouncilRoundLastBlock);
  375. let applicants = await this.api.query.councilElection.applicants.at(previousCouncilRoundLastBlockHash) as Vec<AccountId>;
  376. this.statistics.electionApplicants = applicants.length;
  377. for (let applicant of applicants) {
  378. let applicantStakes = await this.api.query.councilElection.applicantStakes.at(previousCouncilRoundLastBlockHash, applicant) as unknown as ElectionStake;
  379. this.statistics.electionApplicantsStakes += applicantStakes.new.toNumber();
  380. }
  381. // let seats = await this.api.query.council.activeCouncil.at(startBlockHash) as Seats;
  382. //TODO: Find a more accurate way of getting the votes
  383. const votes = await this.api.query.councilElection.commitments.at(previousCouncilRoundLastBlockHash) as Vec<Hash>;
  384. this.statistics.electionVotes = votes.length;
  385. }
  386. async fillValidatorInfo(startHash: Hash, endHash: Hash) {
  387. let startTimestamp = await this.api.query.timestamp.now.at(startHash) as unknown as Moment;
  388. let endTimestamp = await this.api.query.timestamp.now.at(endHash) as unknown as Moment;
  389. let avgBlockProduction = (((endTimestamp.toNumber() - startTimestamp.toNumber())
  390. / 1000) / this.statistics.newBlocks);
  391. this.statistics.avgBlockProduction = Number(avgBlockProduction.toFixed(2));
  392. let maxStartValidators = (await this.api.query.staking.validatorCount.at(startHash) as u32).toNumber();
  393. let startValidators = await this.findActiveValidators(startHash, false);
  394. this.statistics.startValidators = startValidators.length + " / " + maxStartValidators;
  395. let maxEndValidators = (await this.api.query.staking.validatorCount.at(endHash) as u32).toNumber();
  396. let endValidators = await this.findActiveValidators(endHash, true);
  397. this.statistics.endValidators = endValidators.length + " / " + maxEndValidators;
  398. this.statistics.percValidators = StatisticsCollector.convertToPercentage(startValidators.length, endValidators.length);
  399. const startEra = await this.api.query.staking.currentEra.at(startHash) as Option<EraIndex>;
  400. this.statistics.startValidatorsStake = (await this.api.query.staking.erasTotalStake.at(startHash, startEra.unwrap())).toNumber();
  401. const endEra = await this.api.query.staking.currentEra.at(endHash) as Option<EraIndex>;
  402. this.statistics.endValidatorsStake = (await this.api.query.staking.erasTotalStake.at(endHash, endEra.unwrap())).toNumber();
  403. this.statistics.percNewValidatorsStake = StatisticsCollector.convertToPercentage(this.statistics.startValidatorsStake, this.statistics.endValidatorsStake);
  404. await this.fillValidatorsRewards();
  405. }
  406. async findActiveValidators(hash: Hash, searchPreviousBlocks: boolean): Promise<AccountId[]> {
  407. const block = await this.api.rpc.chain.getBlock(hash);
  408. let currentBlockNr = block.block.header.number.toNumber();
  409. let activeValidators;
  410. do {
  411. let currentHash = (await this.api.rpc.chain.getBlockHash(currentBlockNr)) as Hash;
  412. let allValidators = await this.api.query.staking.snapshotValidators.at(currentHash) as Option<Vec<AccountId>>;
  413. if (!allValidators.isEmpty) {
  414. let max = (await this.api.query.staking.validatorCount.at(currentHash)).toNumber();
  415. activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
  416. }
  417. if (searchPreviousBlocks) {
  418. --currentBlockNr;
  419. } else {
  420. ++currentBlockNr;
  421. }
  422. } while (activeValidators == undefined);
  423. return activeValidators;
  424. }
  425. async fillStorageProviderInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  426. let roundNrBlocks = endBlock - startBlock;
  427. let storageProvidersRewards = await this.computeWorkingGroupReward(roundNrBlocks, startHash, endHash, 'storage');
  428. this.statistics.newStorageProviderReward = storageProvidersRewards.rewards;
  429. this.statistics.newStorageProviderReward = Number(this.statistics.newStorageProviderReward.toFixed(2));
  430. this.statistics.startStorageProvidersStake = storageProvidersRewards.startStake;
  431. this.statistics.endStorageProvidersStake = storageProvidersRewards.endStake;
  432. this.statistics.percNewStorageProviderStake = StatisticsCollector.convertToPercentage(this.statistics.startStorageProvidersStake, this.statistics.endStorageProvidersStake);
  433. this.statistics.startStorageProviders = await this.api.query.storageWorkingGroup.activeWorkerCount.at(startHash);
  434. this.statistics.endStorageProviders = await this.api.query.storageWorkingGroup.activeWorkerCount.at(endHash);
  435. this.statistics.percNewStorageProviders = StatisticsCollector.convertToPercentage(this.statistics.startStorageProviders, this.statistics.endStorageProviders);
  436. let nextWorkerId = Number(await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash));
  437. this.statistics.storageProviders = "";
  438. for (let i = 0; i < nextWorkerId; ++i) {
  439. let storageProvider = await this.api.query.storageWorkingGroup.workerById.at(endHash, i) as WorkerOf;
  440. if (!storageProvider.is_active) {
  441. continue;
  442. }
  443. let membership = await this.api.query.members.membershipById.at(endHash, storageProvider.member_id) as Membership;
  444. this.statistics.storageProviders += "@" + membership.handle + " | (" + membership.root_account + ") \n";
  445. }
  446. }
  447. async fillCuratorInfo(startHash: Hash, endHash: Hash) {
  448. this.statistics.startCurators = Number(await this.api.query.contentDirectoryWorkingGroup.activeWorkerCount.at(startHash));
  449. this.statistics.endCurators = Number(await this.api.query.contentDirectoryWorkingGroup.activeWorkerCount.at(endHash));
  450. this.statistics.percNewCurators = StatisticsCollector.convertToPercentage(this.statistics.startCurators, this.statistics.endCurators);
  451. let nextCuratorId = Number(await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash));
  452. this.statistics.curators = "";
  453. for (let i = 0; i < nextCuratorId; i++) {
  454. let worker = await this.api.query.contentDirectoryWorkingGroup.workerById.at(endHash, i) as WorkerOf;
  455. if (!worker.is_active) {
  456. continue;
  457. }
  458. let curatorMembership = await this.api.query.members.membershipById.at(endHash, worker.member_id) as Membership;
  459. this.statistics.curators += "@" + curatorMembership.handle + " | (" + curatorMembership.root_account + ") \n";
  460. }
  461. }
  462. async fillOperationsInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  463. let roundNrBlocks = endBlock - startBlock;
  464. let operationsRewards = await this.computeWorkingGroupReward(roundNrBlocks, startHash, endHash, 'operations');
  465. this.statistics.newOperationsReward = operationsRewards.rewards;
  466. this.statistics.newOperationsReward = Number(this.statistics.newOperationsReward.toFixed(2));
  467. this.statistics.startOperationsStake = operationsRewards.startStake;
  468. this.statistics.endOperationsStake = operationsRewards.endStake;
  469. this.statistics.percNewOperationstake = StatisticsCollector.convertToPercentage(this.statistics.startOperationsStake, this.statistics.endOperationsStake);
  470. this.statistics.startOperationsWorkers = Number(await this.api.query.operationsWorkingGroup.activeWorkerCount.at(startHash));
  471. this.statistics.endOperationsWorkers = Number(await this.api.query.operationsWorkingGroup.activeWorkerCount.at(endHash));
  472. this.statistics.percNewOperationsWorkers = StatisticsCollector.convertToPercentage(this.statistics.startOperationsWorkers, this.statistics.endOperationsWorkers);
  473. let nextOperationsWorkerId = Number(await this.api.query.operationsWorkingGroup.nextWorkerId.at(endHash));
  474. this.statistics.operations = "";
  475. for (let i = 0; i < nextOperationsWorkerId; i++) {
  476. let worker = await this.api.query.operationsWorkingGroup.workerById.at(endHash, i) as WorkerOf;
  477. if (!worker.is_active) {
  478. continue;
  479. }
  480. let operationMembership = await this.api.query.members.membershipById.at(endHash, worker.member_id) as Membership;
  481. this.statistics.operations += "@" + operationMembership.handle + " | (" + operationMembership.root_account + ") \n";
  482. }
  483. }
  484. async fillMembershipInfo(startHash: Hash, endHash: Hash) {
  485. this.statistics.startMembers = (await this.api.query.members.nextMemberId.at(startHash) as MemberId).toNumber();
  486. this.statistics.endMembers = (await this.api.query.members.nextMemberId.at(endHash) as MemberId).toNumber();
  487. this.statistics.newMembers = this.statistics.endMembers - this.statistics.startMembers;
  488. this.statistics.percNewMembers = StatisticsCollector.convertToPercentage(this.statistics.startMembers, this.statistics.endMembers);
  489. }
  490. async fillMediaUploadInfo(startHash: Hash, endHash: Hash) {
  491. let startVideos = (await this.api.query.content.nextVideoId.at(startHash) as VideoId).toNumber();
  492. let endVideos = (await this.api.query.content.nextVideoId.at(endHash) as VideoId).toNumber();
  493. this.statistics.startMedia = startVideos;
  494. this.statistics.endMedia = endVideos;
  495. this.statistics.percNewMedia = StatisticsCollector.convertToPercentage(this.statistics.startMedia, this.statistics.endMedia);
  496. let startChannels = (await this.api.query.content.nextChannelId.at(startHash) as ChannelId).toNumber();
  497. let endChannels = (await this.api.query.content.nextChannelId.at(endHash) as ChannelId).toNumber();
  498. this.statistics.startChannels = startChannels;
  499. this.statistics.endChannels = endChannels;
  500. this.statistics.percNewChannels = StatisticsCollector.convertToPercentage(this.statistics.startChannels, this.statistics.endChannels);
  501. let dataObjects = await this.api.query.dataDirectory.dataByContentId.entries() as unknown as Map<ContentId, DataObject>;
  502. let startObjects = new Map<ContentId, DataObject>();
  503. let endObjects = new Map<ContentId, DataObject>();
  504. const startBlock = await this.api.rpc.chain.getBlock(startHash);
  505. const endBlock = await this.api.rpc.chain.getBlock(endHash);
  506. for (let [key, dataObject] of dataObjects) {
  507. if (dataObject.added_at.block.toNumber() < startBlock.block.header.number.toNumber()) {
  508. startObjects.set(key, dataObject);
  509. this.statistics.startUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
  510. }
  511. if (dataObject.added_at.block.toNumber() < endBlock.block.header.number.toNumber()) {
  512. endObjects.set(key, dataObject);
  513. this.statistics.endUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
  514. }
  515. }
  516. this.statistics.startUsedSpace = Number(this.statistics.startUsedSpace.toFixed(2));
  517. this.statistics.endUsedSpace = Number(this.statistics.endUsedSpace.toFixed(2));
  518. this.statistics.percNewUsedSpace = StatisticsCollector.convertToPercentage(this.statistics.startUsedSpace, this.statistics.endUsedSpace);
  519. }
  520. async fillForumInfo(startHash: Hash, endHash: Hash) {
  521. let startPostId = await this.api.query.forum.nextPostId.at(startHash) as PostId;
  522. let endPostId = await this.api.query.forum.nextPostId.at(endHash) as PostId;
  523. this.statistics.startPosts = startPostId.toNumber();
  524. this.statistics.endPosts = endPostId.toNumber();
  525. this.statistics.newPosts = this.statistics.endPosts - this.statistics.startPosts;
  526. this.statistics.percNewPosts = StatisticsCollector.convertToPercentage(this.statistics.startPosts, this.statistics.endPosts);
  527. let startThreadId = ((await this.api.query.forum.nextThreadId.at(startHash)) as unknown) as ThreadId;
  528. let endThreadId = ((await this.api.query.forum.nextThreadId.at(endHash)) as unknown) as ThreadId;
  529. this.statistics.startThreads = startThreadId.toNumber();
  530. this.statistics.endThreads = endThreadId.toNumber();
  531. this.statistics.newThreads = this.statistics.endThreads - this.statistics.startThreads;
  532. this.statistics.percNewThreads = StatisticsCollector.convertToPercentage(this.statistics.startThreads, this.statistics.endThreads);
  533. let startCategoryId = (await this.api.query.forum.nextCategoryId.at(startHash)) as CategoryId;
  534. let endCategoryId = (await this.api.query.forum.nextCategoryId.at(endHash)) as CategoryId;
  535. this.statistics.startCategories = startCategoryId.toNumber();
  536. this.statistics.endCategories = endCategoryId.toNumber();
  537. this.statistics.newCategories = this.statistics.endCategories - this.statistics.startCategories;
  538. this.statistics.perNewCategories = StatisticsCollector.convertToPercentage(this.statistics.startCategories, this.statistics.endCategories);
  539. }
  540. static convertToPercentage(previousValue: number, newValue: number): number {
  541. if (previousValue == 0) {
  542. return newValue > 0 ? Infinity : 0;
  543. }
  544. return Number((newValue * 100 / previousValue - 100).toFixed(2));
  545. }
  546. async buildBlocksEventCache(startBlock: number, endBlock: number) {
  547. let cacheFile = CACHE_FOLDER + '/' + startBlock + '-' + endBlock + '.json';
  548. let exists = await fs.access(cacheFile, fsSync.constants.R_OK).then(() => true)
  549. .catch(() => false);
  550. // let exists = false;
  551. if (!exists) {
  552. console.log('Building events cache...');
  553. let blocksEvents = new Map<number, CacheEvent[]>();
  554. for (let i = startBlock; i < endBlock; ++i) {
  555. process.stdout.write('\rCaching block: ' + i + ' until ' + endBlock);
  556. const blockHash: Hash = await this.api.rpc.chain.getBlockHash(i);
  557. let eventRecord = await this.api.query.system.events.at(blockHash) as Vec<EventRecord>;
  558. let cacheEvents = new Array<CacheEvent>();
  559. for (let event of eventRecord) {
  560. cacheEvents.push(new CacheEvent(event.event.section, event.event.method, event.event.data));
  561. }
  562. blocksEvents.set(i, cacheEvents);
  563. }
  564. console.log('\nFinish events cache...');
  565. let jsonOutput = JSON.stringify(Array.from(blocksEvents.entries()), null, 2);
  566. await fs.writeFile(cacheFile, jsonOutput);
  567. this.blocksEventsCache = new Map(JSON.parse(jsonOutput));
  568. } else {
  569. console.log('Cache file found, loading it...');
  570. let fileData = await fs.readFile(cacheFile);
  571. this.blocksEventsCache = new Map(JSON.parse(fileData));
  572. console.log('Cache file loaded...');
  573. }
  574. }
  575. static async connectApi(): Promise<ApiPromise> {
  576. // const provider = new WsProvider('wss://testnet.joystream.org:9944');
  577. const provider = new WsProvider(PROVIDER_URL);
  578. // Create the API and wait until ready
  579. return await ApiPromise.create({provider, types});
  580. }
  581. }