StatisticsCollector.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  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. Exchange,
  16. Media,
  17. MintStatistics,
  18. StatisticsData,
  19. ValidatorReward, WorkersInfo
  20. } from "./StatisticsData";
  21. import {u32, Vec} from "@polkadot/types";
  22. import {ElectionStake, SealedVote, Seats} from "@joystream/types/council";
  23. import {Mint, MintId} from "@joystream/types/mint";
  24. import {ContentId, DataObject} from "@joystream/types/media";
  25. import {RoleParameters} from "@joystream/types/roles";
  26. import {Entity, EntityId} from "@joystream/types/versioned-store";
  27. import Option from "@polkadot/types/codec/Option";
  28. import Linkage from "@polkadot/types/codec/Linkage";
  29. import {PostId, ThreadId} from "@joystream/types/common";
  30. import {CategoryId} from "@joystream/types/forum";
  31. import {Event} from "@polkadot/types/interfaces/system/types";
  32. import number from "@polkadot/util/is/number";
  33. import toNumber from "@polkadot/util/hex/toNumber";
  34. import {
  35. ProposalStatus,
  36. FinalizationData,
  37. ProposalDecisionStatus,
  38. Finalized,
  39. IProposalStatus, Approved
  40. } from "@joystream/types/proposals";
  41. import {MemberId} from "@joystream/types/members";
  42. import {RewardRelationship, RewardRelationshipId} from "@joystream/types/recurring-rewards";
  43. import {StorageProviderId, WorkerId, Worker, RoleStakeProfile} from "@joystream/types/working-group";
  44. import workingGroup from "@joystream/types/src/working-group/index";
  45. import {Stake} from "@joystream/types/stake";
  46. import {ChannelId} from "@joystream/types/content-working-group";
  47. const fsSync = require('fs');
  48. const fs = fsSync.promises;
  49. const BURN_ADDRESS = '5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu';
  50. const COUNCIL_ROUND_OFFSET = 5;
  51. const PROVIDER_URL = "ws://localhost:9944";
  52. const CACHE_FOLDER = "cache";
  53. const WORKER_ID_OFFSET = 1;
  54. export class StatisticsCollector {
  55. private api?: ApiPromise;
  56. private blocksEventsCache: Map<number, CacheEvent[]>;
  57. private statistics: StatisticsData;
  58. constructor() {
  59. this.blocksEventsCache = new Map<number, CacheEvent[]>();
  60. this.statistics = new StatisticsData();
  61. }
  62. async getStatistics(startBlock: number, endBlock: number): Promise<StatisticsData> {
  63. this.api = await StatisticsCollector.connectApi();
  64. let startHash = (await this.api.rpc.chain.getBlockHash(startBlock)) as Hash;
  65. let endHash = (await this.api.rpc.chain.getBlockHash(endBlock)) as Hash;
  66. this.statistics.startBlock = startBlock;
  67. this.statistics.endBlock = endBlock;
  68. this.statistics.newBlocks = endBlock - startBlock;
  69. this.statistics.percNewBlocks = StatisticsCollector.convertToPercentage(startBlock, endBlock);
  70. await this.buildBlocksEventCache(startBlock, endBlock);
  71. await this.fillBasicInfo(startHash, endHash);
  72. await this.fillTokenGenerationInfo(startBlock, endBlock, startHash, endHash);
  73. await this.fillMintsInfo(startHash, endHash);
  74. await this.fillCouncilInfo(startHash, endHash);
  75. await this.fillCouncilElectionInfo(startBlock);
  76. await this.fillValidatorInfo(startHash, endHash);
  77. await this.fillStorageProviderInfo(startBlock, endBlock, startHash, endHash);
  78. await this.fillCuratorInfo(startHash, endHash);
  79. await this.fillMembershipInfo(startHash, endHash);
  80. await this.fillMediaUploadInfo(startHash, endHash);
  81. await this.fillForumInfo(startHash, endHash);
  82. this.api.disconnect();
  83. return this.statistics;
  84. //
  85. // if (statistics.electionVotes) {
  86. // statistics.avgVotePerApplicant = statistics.electionVotes / statistics.electionApplicants;
  87. // } else {
  88. // statistics.avgVotePerApplicant = 0;
  89. // }
  90. //
  91. //
  92. //
  93. //
  94. //
  95. //
  96. //
  97. // let startNrStakes = await this.api.query.stake.stakesCreated.at(startHash) as StakeId;
  98. // let endNrStakes = await this.api.query.stake.stakesCreated.at(endHash) as StakeId;
  99. // statistics.newStakes = endNrStakes.toNumber() - startNrStakes.toNumber();
  100. //
  101. // for (let i = startNrStakes.toNumber(); i < endNrStakes.toNumber(); ++i) {
  102. // let stakeResult = await this.api.query.stake.stakes(i) as unknown as [Stake, Linkage<StakeId>];
  103. // let stake = stakeResult[0] as Stake;
  104. //
  105. // statistics.totalNewStakeValue += stake.value ? stake.value.toNumber() : 0;
  106. // }
  107. //
  108. // // let startBurnedTokens = await this.api.query.balances.freeBalance.at(startHash, BURN_ADDRESS) as Balance;
  109. // // let endBurnedTokens = await this.api.query.balances.freeBalance.at(endHash, BURN_ADDRESS) as Balance;
  110. // //
  111. // // statistics.totalBurned = endBurnedTokens.toNumber() - startBurnedTokens.toNumber();
  112. //
  113. //
  114. //
  115. // let newMedia = endMedias.filter((endMedia) => {
  116. // return !startMedias.some((startMedia) => startMedia.id == endMedia.id);
  117. // });
  118. //
  119. // statistics.newMedia = newMedia.length;
  120. // statistics.totalMedia = endMedias.length;
  121. // statistics.percNewMedia = this.convertToPercentage(statistics.newMedia, statistics.totalMedia);
  122. //
  123. // let startDataObjects = await this.api.query.dataDirectory.knownContentIds.at(startHash) as Vec<ContentId>;
  124. // let startUsedSpace = await this.computeUsedSpaceInBytes(api, startDataObjects);
  125. //
  126. // let endDataObjects = await this.api.query.dataDirectory.knownContentIds.at(endHash) as Vec<ContentId>;
  127. // let endUsedSpace = await this.computeUsedSpaceInBytes(api, endDataObjects);
  128. //
  129. // statistics.newUsedSpace = endUsedSpace - startUsedSpace;
  130. // statistics.totalUsedSpace = endUsedSpace;
  131. // statistics.percNewUsedSpace = this.convertToPercentage(statistics.newUsedSpace, statistics.totalUsedSpace);
  132. //
  133. // statistics.avgNewSizePerContent = Number((statistics.newUsedSpace / statistics.newMedia).toFixed(2));
  134. // statistics.totalAvgSizePerContent = Number((statistics.totalUsedSpace / statistics.totalMedia).toFixed(2));
  135. // statistics.percAvgSizePerContent = this.convertToPercentage(statistics.avgNewSizePerContent, statistics.totalAvgSizePerContent);
  136. //
  137. // //
  138. // // for (let startMedia of startMedias) {
  139. // // let deleted = !endMedias.some((endMedia) => {
  140. // // return endMedia.id == startMedia.id;
  141. // // })
  142. // // if (deleted) {
  143. // // ++statistics.deletedMedia;
  144. // // }
  145. // // }
  146. //
  147. //
  148. //
  149. // for (let i = startNrProposals.toNumber(); i < endNrProposals.toNumber(); ++i) {
  150. // let proposalNumber = i - 1;
  151. // let proposalDetails = await this.api.query.proposalsCodex.proposalDetailsByProposalId.at(endHash, proposalNumber) as ProposalDetails;
  152. // switch (proposalDetails.type) {
  153. // case ProposalTypes.Text:
  154. // ++statistics.newTextProposals;
  155. // break;
  156. //
  157. // case ProposalTypes.RuntimeUpgrade:
  158. // ++statistics.newRuntimeUpgradeProposal;
  159. // break;
  160. //
  161. // case ProposalTypes.SetElectionParameters:
  162. // ++statistics.newSetElectionParametersProposal;
  163. // break;
  164. //
  165. // case ProposalTypes.Spending:
  166. // ++statistics.newSpendingProposal;
  167. // break;
  168. //
  169. // case ProposalTypes.SetLead:
  170. // ++statistics.newSetLeadProposal;
  171. // break;
  172. //
  173. // case ProposalTypes.SetContentWorkingGroupMintCapacity:
  174. // ++statistics.newSetContentWorkingGroupMintCapacityProposal;
  175. // break;
  176. //
  177. // case ProposalTypes.EvictStorageProvider:
  178. // ++statistics.newEvictStorageProviderProposal;
  179. // break;
  180. //
  181. // case ProposalTypes.SetValidatorCount:
  182. // ++statistics.newSetValidatorCountProposal;
  183. // break;
  184. //
  185. // case ProposalTypes.SetStorageRoleParameters:
  186. // ++statistics.newSetStorageRoleParametersProposal;
  187. // break;
  188. // }
  189. // }
  190. //
  191. // let validatorRewards: ValidatorReward[] = [];
  192. // let exchangesCollection: Exchange[] = [];
  193. // let promises = [];
  194. //
  195. // console.time('extractValidatorsRewards');
  196. // for (let i = startBlock; i < endBlock; ++i) {
  197. // let promise = (async () => {
  198. // const blockHash: Hash = await this.api.rpc.chain.getBlockHash(i);
  199. // const events = await this.api.query.system.events.at(blockHash) as Vec<EventRecord>;
  200. // let rewards = await this.extractValidatorsRewards(api, i, events);
  201. // if (rewards.length) {
  202. // validatorRewards = validatorRewards.concat(rewards);
  203. // }
  204. // let exchanges = this.extractExchanges(i, events);
  205. // if (exchanges.length) {
  206. // exchangesCollection = exchangesCollection.concat(exchanges);
  207. // }
  208. //
  209. // })();
  210. // promises.push(promise);
  211. // }
  212. // await Promise.all(promises);
  213. // console.timeEnd('extractValidatorsRewards');
  214. //
  215. // statistics.newValidatorReward = validatorRewards.map((validatorReward) => validatorReward.sharedReward).reduce((a, b) => a + b);
  216. // let avgValidators = validatorRewards.map((validatorReward) => validatorReward.validators).reduce((a, b) => a + b) / validatorRewards.length;
  217. // statistics.avgValidators = Number(avgValidators.toFixed(2));
  218. //
  219. // statistics.newTokensBurn = exchangesCollection.map((exchange) => exchange.amount).reduce((a, b) => a + b);
  220. //
  221. // statistics.newStorageProviderReward = await this.computeStorageRewards(api, startBlock, endBlock);
  222. //
  223. // this.api.disconnect();
  224. // return statistics;
  225. }
  226. async fillBasicInfo(startHash: Hash, endHash: Hash) {
  227. let startDate = (await this.api.query.timestamp.now.at(startHash)) as Moment;
  228. let endDate = (await this.api.query.timestamp.now.at(endHash)) as Moment;
  229. this.statistics.dateStart = new Date(startDate.toNumber()).toLocaleDateString("en-US");
  230. this.statistics.dateEnd = new Date(endDate.toNumber()).toLocaleDateString("en-US");
  231. }
  232. async fillTokenGenerationInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  233. this.statistics.startIssuance = (await this.api.query.balances.totalIssuance.at(startHash) as Balance).toNumber();
  234. this.statistics.endIssuance = (await this.api.query.balances.totalIssuance.at(endHash) as Balance).toNumber();
  235. this.statistics.newIssuance = this.statistics.endIssuance - this.statistics.startIssuance;
  236. this.statistics.percNewIssuance = StatisticsCollector.convertToPercentage(this.statistics.startIssuance, this.statistics.endIssuance);
  237. for (let [key, blockEvents] of this.blocksEventsCache) {
  238. let validatorRewards = blockEvents.filter((event) => {
  239. return event.section == "staking" && event.method == "Reward";
  240. });
  241. for (let validatorReward of validatorRewards) {
  242. this.statistics.newValidatorRewards += Number(validatorReward.data[1]);
  243. }
  244. let transfers = blockEvents.filter((event) => {
  245. return event.section == "balances" && event.method == "Transfer";
  246. });
  247. for (let transfer of transfers) {
  248. let receiver = transfer.data[1] as AccountId;
  249. let amount = transfer.data[2] as Balance;
  250. if (receiver.toString() == BURN_ADDRESS) {
  251. this.statistics.newTokensBurn = Number(amount);
  252. }
  253. }
  254. }
  255. let roundNrBlocks = endBlock - startBlock;
  256. this.statistics.newCouncilRewards = await this.computeCouncilReward(roundNrBlocks, endHash);
  257. this.statistics.newCouncilRewards = Number(this.statistics.newCouncilRewards.toFixed(2));
  258. this.statistics.newCuratorRewards = await this.computeCuratorsReward(roundNrBlocks, startHash, endHash);
  259. this.statistics.newCuratorRewards = Number(this.statistics.newCuratorRewards.toFixed(2));
  260. }
  261. async computeCouncilReward(roundNrBlocks: number, endHash: Hash): Promise<number> {
  262. const payoutInterval = Number((await this.api.query.council.payoutInterval.at(endHash) as Option<BlockNumber>).unwrapOr(0));
  263. const amountPerPayout = (await this.api.query.council.amountPerPayout.at(endHash) as BalanceOf).toNumber();
  264. const announcing_period = (await this.api.query.councilElection.announcingPeriod.at(endHash)) as BlockNumber;
  265. const voting_period = (await this.api.query.councilElection.votingPeriod.at(endHash)) as BlockNumber;
  266. const revealing_period = (await this.api.query.councilElection.revealingPeriod.at(endHash)) as BlockNumber;
  267. const new_term_duration = (await this.api.query.councilElection.newTermDuration.at(endHash)) as BlockNumber;
  268. const termDuration = new_term_duration.toNumber();
  269. const votingPeriod = voting_period.toNumber();
  270. const revealingPeriod = revealing_period.toNumber();
  271. const announcingPeriod = announcing_period.toNumber();
  272. const nrCouncilMembers = (await this.api.query.council.activeCouncil.at(endHash) as Seats).length
  273. const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
  274. ? (amountPerPayout * nrCouncilMembers) / payoutInterval
  275. : 0;
  276. const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
  277. const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
  278. return avgCouncilRewardPerBlock * roundNrBlocks;
  279. }
  280. async computeStorageProviderReward(roundNrBlocks: number, startHash: Hash, endHash: Hash): Promise<WorkersInfo> {
  281. let nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber();
  282. let info = new WorkersInfo();
  283. for (let i = 0; i < nextWorkerId; ++i) {
  284. let worker = await this.api.query.storageWorkingGroup.workerById(i) as Worker;
  285. if (worker.role_stake_profile.isSome) {
  286. let roleStakeProfile = worker.role_stake_profile.unwrap();
  287. let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
  288. info.startStake += stake.value.toNumber();
  289. }
  290. }
  291. nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
  292. let rewardRelationshipIds = Array<RewardRelationshipId>();
  293. for (let i = 0; i < nextWorkerId; ++i) {
  294. let worker = await this.api.query.storageWorkingGroup.workerById(i) as Worker;
  295. if (worker.reward_relationship.isSome) {
  296. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  297. }
  298. if (worker.role_stake_profile.isSome) {
  299. let roleStakeProfile = worker.role_stake_profile.unwrap();
  300. let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
  301. info.endStake += stake.value.toNumber();
  302. }
  303. }
  304. info.rewards = await this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  305. info.endNrOfWorkers = nextWorkerId - WORKER_ID_OFFSET;
  306. return info;
  307. }
  308. async computeCuratorsReward(roundNrBlocks: number, startHash: Hash, endHash: Hash) {
  309. let nextCuratorId = (await this.api.query.contentWorkingGroup.nextCuratorId.at(endHash) as WorkerId).toNumber();
  310. let rewardRelationshipIds = Array<RewardRelationshipId>();
  311. for (let i = 0; i < nextCuratorId; ++i) {
  312. let worker = await this.api.query.contentWorkingGroup.curatorById(i) as Worker;
  313. if (worker.reward_relationship.isSome) {
  314. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  315. }
  316. }
  317. return this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  318. }
  319. async computeReward(roundNrBlocks: number, rewardRelationshipIds: RewardRelationshipId[], hash: Hash) {
  320. let recurringRewards = await Promise.all(rewardRelationshipIds.map(async (rewardRelationshipId) => {
  321. return await this.api.query.recurringRewards.rewardRelationships.at(hash, rewardRelationshipId) as RewardRelationship;
  322. }));
  323. let rewardPerBlock = 0;
  324. for (let recurringReward of recurringRewards) {
  325. const amount = recurringReward.amount_per_payout.toNumber();
  326. const payoutInterval = recurringReward.payout_interval.unwrapOr(null);
  327. if (amount && payoutInterval) {
  328. rewardPerBlock += amount / payoutInterval;
  329. }
  330. }
  331. return rewardPerBlock * roundNrBlocks;
  332. }
  333. async fillMintsInfo(startHash: Hash, endHash: Hash) {
  334. let startNrMints = parseInt((await this.api.query.minting.mintsCreated.at(startHash)).toString());
  335. let endNrMints = parseInt((await this.api.query.minting.mintsCreated.at(endHash)).toString());
  336. this.statistics.newMints = endNrMints - startNrMints;
  337. // statistics.startMinted = 0;
  338. // statistics.endMinted = 0;
  339. for (let i = 0; i < startNrMints; ++i) {
  340. let startMint = (await this.api.query.minting.mints.at(startHash, i)) as Mint;
  341. // if (!startMint) {
  342. // continue;
  343. // }
  344. let endMint = (await this.api.query.minting.mints.at(endHash, i)) as Mint;
  345. // let = endMintResult[0];
  346. // if (!endMint) {
  347. // continue;
  348. // }
  349. let startMintTotal = parseInt(startMint.getField("total_minted").toString());
  350. let endMintTotal = parseInt(endMint.getField("total_minted").toString());
  351. // statistics.startMinted += startMintTotal;
  352. this.statistics.totalMinted += endMintTotal - startMintTotal;
  353. this.statistics.totalMintCapacityIncrease += parseInt(endMint.getField("capacity").toString()) - parseInt(startMint.getField("capacity").toString());
  354. }
  355. for (let i = startNrMints; i < endNrMints; ++i) {
  356. let endMintResult = ((await this.api.query.minting.mints.at(endHash, i)) as unknown) as [Mint, Linkage<MintId>];
  357. let endMint = endMintResult[0] as Mint;
  358. if (!endMint) {
  359. return;
  360. }
  361. this.statistics.totalMinted = parseInt(endMint.getField("total_minted").toString());
  362. }
  363. let councilMint = (await this.api.query.council.councilMint.at(endHash)) as MintId;
  364. let councilMintStatistics = await this.computeMintInfo(councilMint, startHash, endHash);
  365. this.statistics.startCouncilMinted = councilMintStatistics.startMinted;
  366. this.statistics.endCouncilMinted = councilMintStatistics.endMinted;
  367. this.statistics.newCouncilMinted = councilMintStatistics.diffMinted;
  368. this.statistics.percNewCouncilMinted = councilMintStatistics.percMinted;
  369. 6
  370. let curatorMint = (await this.api.query.contentWorkingGroup.mint.at(endHash)) as MintId;
  371. let curatorMintStatistics = await this.computeMintInfo(curatorMint, startHash, endHash);
  372. this.statistics.startCuratorMinted = curatorMintStatistics.startMinted;
  373. this.statistics.endCuratorMinted = curatorMintStatistics.endMinted;
  374. this.statistics.newCuratorMinted = curatorMintStatistics.diffMinted;
  375. this.statistics.percCuratorMinted = curatorMintStatistics.percMinted;
  376. let storageProviderMint = (await this.api.query.storageWorkingGroup.mint.at(endHash)) as MintId;
  377. let storageProviderMintStatistics = await this.computeMintInfo(storageProviderMint, startHash, endHash);
  378. this.statistics.startStorageMinted = storageProviderMintStatistics.startMinted;
  379. this.statistics.endStorageMinted = storageProviderMintStatistics.endMinted;
  380. this.statistics.newStorageMinted = storageProviderMintStatistics.diffMinted;
  381. this.statistics.percStorageMinted = storageProviderMintStatistics.percMinted;
  382. }
  383. async computeMintInfo(mintId: MintId, startHash: Hash, endHash: Hash): Promise<MintStatistics> {
  384. // if (mintId.toString() == "0") {
  385. // return new MintStatistics(0, 0, 0);
  386. // }
  387. let startMint = await this.api.query.minting.mints.at(startHash, mintId) as Mint;
  388. // let startMint = startMintResult[0] as unknown as Mint;
  389. // if (!startMint) {
  390. // return new MintStatistics(0, 0, 0);
  391. // }
  392. let endMint = await this.api.query.minting.mints.at(endHash, mintId) as Mint;
  393. // let endMint = endMintResult[0] as unknown as Mint;
  394. // if (!endMint) {
  395. // return new MintStatistics(0, 0, 0);
  396. // }
  397. let mintStatistics = new MintStatistics();
  398. mintStatistics.startMinted = parseInt(startMint.getField('total_minted').toString());
  399. mintStatistics.endMinted = parseInt(endMint.getField('total_minted').toString());
  400. mintStatistics.diffMinted = mintStatistics.endMinted - mintStatistics.startMinted;
  401. mintStatistics.percMinted = StatisticsCollector.convertToPercentage(mintStatistics.startMinted, mintStatistics.endMinted);
  402. return mintStatistics;
  403. }
  404. async fillCouncilInfo(startHash: Hash, endHash: Hash) {
  405. this.statistics.councilRound = (await this.api.query.councilElection.round.at(startHash) as u32).toNumber();
  406. this.statistics.councilMembers = (await this.api.query.councilElection.councilSize.at(startHash) as u32).toNumber();
  407. let startNrProposals = await this.api.query.proposalsEngine.proposalCount.at(startHash) as u32;
  408. let endNrProposals = await this.api.query.proposalsEngine.proposalCount.at(endHash) as u32;
  409. this.statistics.newProposals = endNrProposals.toNumber() - startNrProposals.toNumber();
  410. let approvedProposals = new Set();
  411. for (let [key, blockEvents] of this.blocksEventsCache) {
  412. for (let event of blockEvents) {
  413. if (event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated") {
  414. let statusUpdateData = event.data[1] as any;
  415. let finalizeData = statusUpdateData.Finalized as any
  416. if (finalizeData && finalizeData.proposalStatus.Approved) {
  417. approvedProposals.add(Number(event.data[0]));
  418. }
  419. }
  420. }
  421. }
  422. this.statistics.newApprovedProposals = approvedProposals.size;
  423. }
  424. async fillCouncilElectionInfo(startBlock: number) {
  425. let startBlockHash = await this.api.rpc.chain.getBlockHash(startBlock);
  426. let events = await this.api.query.system.events.at(startBlockHash) as Vec<EventRecord>;
  427. let isStartBlockFirstCouncilBlock = events.some((event) => {
  428. return event.event.section == "councilElection" && event.event.method == "CouncilElected";
  429. });
  430. if (!isStartBlockFirstCouncilBlock) {
  431. console.warn('The given start block is not the first block of the council round so council election information will be empty');
  432. return;
  433. }
  434. let previousCouncilRoundLastBlock = startBlock - 1;
  435. let previousCouncilRoundLastBlockHash = await this.api.rpc.chain.getBlockHash(previousCouncilRoundLastBlock);
  436. let applicants = await this.api.query.councilElection.applicants.at(previousCouncilRoundLastBlockHash) as Vec<AccountId>;
  437. this.statistics.electionApplicants = applicants.length;
  438. for (let applicant of applicants) {
  439. let applicantStakes = await this.api.query.councilElection.applicantStakes.at(previousCouncilRoundLastBlockHash, applicant) as unknown as ElectionStake;
  440. this.statistics.electionApplicantsStakes += applicantStakes.new.toNumber();
  441. }
  442. // let seats = await this.api.query.council.activeCouncil.at(startBlockHash) as Seats;
  443. //TODO: Find a more accurate way of getting the votes
  444. const votes = await this.api.query.councilElection.commitments.at(previousCouncilRoundLastBlockHash) as Vec<Hash>;
  445. this.statistics.electionVotes = votes.length;
  446. }
  447. async fillValidatorInfo(startHash: Hash, endHash: Hash) {
  448. let startTimestamp = await this.api.query.timestamp.now.at(startHash) as unknown as Moment;
  449. let endTimestamp = await this.api.query.timestamp.now.at(endHash) as unknown as Moment;
  450. let avgBlockProduction = (((endTimestamp.toNumber() - startTimestamp.toNumber())
  451. / 1000) / this.statistics.newBlocks);
  452. this.statistics.avgBlockProduction = Number(avgBlockProduction.toFixed(2));
  453. this.statistics.startValidators = (await this.api.query.staking.validatorCount.at(startHash) as u32).toNumber();
  454. this.statistics.endValidators = (await this.api.query.staking.validatorCount.at(endHash) as u32).toNumber();
  455. this.statistics.percValidators = StatisticsCollector.convertToPercentage(this.statistics.startValidators, this.statistics.endValidators);
  456. const startEra = await this.api.query.staking.currentEra.at(startHash) as Option<EraIndex>;
  457. this.statistics.startValidatorsStake = (await this.api.query.staking.erasTotalStake.at(startHash, startEra.unwrap())).toNumber();
  458. const endEra = await this.api.query.staking.currentEra.at(endHash) as Option<EraIndex>;
  459. this.statistics.endValidatorsStake = (await this.api.query.staking.erasTotalStake.at(endHash, endEra.unwrap())).toNumber();
  460. this.statistics.percNewValidatorsStake = StatisticsCollector.convertToPercentage(this.statistics.startValidatorsStake, this.statistics.endValidatorsStake);
  461. }
  462. async fillStorageProviderInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  463. let roundNrBlocks = endBlock - startBlock;
  464. let storageProvidersRewards = await this.computeStorageProviderReward(roundNrBlocks, startHash, endHash);
  465. this.statistics.newStorageProviderReward = storageProvidersRewards.rewards;
  466. this.statistics.newStorageProviderReward = Number(this.statistics.newStorageProviderReward.toFixed(2));
  467. this.statistics.startStorageProvidersStake = storageProvidersRewards.startStake;
  468. this.statistics.endStorageProvidersStake = storageProvidersRewards.endStake;
  469. this.statistics.percNewStorageProviderStake = StatisticsCollector.convertToPercentage(this.statistics.startStorageProvidersStake, this.statistics.endStorageProvidersStake);
  470. this.statistics.startStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
  471. this.statistics.endStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
  472. this.statistics.percNewStorageProviders = StatisticsCollector.convertToPercentage(this.statistics.startStorageProviders, this.statistics.endStorageProviders);
  473. }
  474. async fillCuratorInfo(startHash: Hash, endHash: Hash) {
  475. this.statistics.startCurators = (await this.api.query.contentWorkingGroup.nextCuratorId.at(startHash));
  476. this.statistics.endCurators = (await this.api.query.contentWorkingGroup.nextCuratorId.at(endHash));
  477. this.statistics.percNewCurators = StatisticsCollector.convertToPercentage(this.statistics.startCurators, this.statistics.endCurators);
  478. }
  479. async fillMembershipInfo(startHash: Hash, endHash: Hash) {
  480. this.statistics.startMembers = (await this.api.query.members.nextMemberId.at(startHash) as MemberId).toNumber();
  481. this.statistics.endMembers = (await this.api.query.members.nextMemberId.at(endHash) as MemberId).toNumber();
  482. this.statistics.newMembers = this.statistics.endMembers - this.statistics.startMembers;
  483. this.statistics.percNewMembers = StatisticsCollector.convertToPercentage(this.statistics.startMembers, this.statistics.endMembers);
  484. }
  485. async fillMediaUploadInfo(startHash: Hash, endHash: Hash) {
  486. let startMedias = await this.getMedia(startHash);
  487. let endMedias = await this.getMedia(endHash);
  488. this.statistics.startMedia = startMedias.length;
  489. this.statistics.endMedia = endMedias.length;
  490. this.statistics.percNewMedia = StatisticsCollector.convertToPercentage(this.statistics.startMedia, this.statistics.endMedia);
  491. this.statistics.startChannels = (await this.api.query.contentWorkingGroup.nextChannelId.at(startHash) as ChannelId).toNumber();
  492. this.statistics.endChannels = (await this.api.query.contentWorkingGroup.nextChannelId.at(endHash) as ChannelId).toNumber();
  493. this.statistics.percNewChannels = StatisticsCollector.convertToPercentage(this.statistics.startChannels, this.statistics.endChannels);
  494. let startDataObjects = await this.api.query.dataDirectory.knownContentIds.at(startHash) as Vec<ContentId>;
  495. this.statistics.startUsedSpace = await this.computeUsedSpaceInBytes(startDataObjects);
  496. let endDataObjects = await this.api.query.dataDirectory.knownContentIds.at(endHash) as Vec<ContentId>;
  497. this.statistics.endUsedSpace = await this.computeUsedSpaceInBytes(endDataObjects);
  498. this.statistics.percNewUsedSpace = StatisticsCollector.convertToPercentage(this.statistics.startUsedSpace, this.statistics.endUsedSpace);
  499. }
  500. async fillForumInfo(startHash: Hash, endHash: Hash) {
  501. let startPostId = await this.api.query.forum.nextPostId.at(startHash) as PostId;
  502. let endPostId = await this.api.query.forum.nextPostId.at(endHash) as PostId;
  503. this.statistics.startPosts = startPostId.toNumber();
  504. this.statistics.endPosts = endPostId.toNumber();
  505. this.statistics.newPosts = this.statistics.endPosts - this.statistics.startPosts;
  506. this.statistics.percNewPosts = StatisticsCollector.convertToPercentage(this.statistics.startPosts, this.statistics.endPosts);
  507. let startThreadId = ((await this.api.query.forum.nextThreadId.at(startHash)) as unknown) as ThreadId;
  508. let endThreadId = ((await this.api.query.forum.nextThreadId.at(endHash)) as unknown) as ThreadId;
  509. this.statistics.startThreads = startThreadId.toNumber();
  510. this.statistics.endThreads = endThreadId.toNumber();
  511. this.statistics.newThreads = this.statistics.endThreads - this.statistics.startThreads;
  512. this.statistics.percNewThreads = StatisticsCollector.convertToPercentage(this.statistics.startThreads, this.statistics.endThreads);
  513. let startCategoryId = (await this.api.query.forum.nextCategoryId.at(startHash)) as CategoryId;
  514. let endCategoryId = (await this.api.query.forum.nextCategoryId.at(endHash)) as CategoryId;
  515. this.statistics.startCategories = startCategoryId.toNumber();
  516. this.statistics.endCategories = endCategoryId.toNumber();
  517. this.statistics.newCategories = this.statistics.endCategories - this.statistics.startCategories;
  518. this.statistics.perNewCategories = StatisticsCollector.convertToPercentage(this.statistics.startCategories, this.statistics.endCategories);
  519. }
  520. static convertToPercentage(previousValue: number, newValue: number): number {
  521. if (previousValue == 0) {
  522. return 0;
  523. }
  524. return Number((newValue * 100 / previousValue - 100).toFixed(2));
  525. }
  526. async computeUsedSpaceInBytes(contentIds: Vec<ContentId>) {
  527. let space = 0;
  528. for (let contentId of contentIds) {
  529. let dataObject = (await this.api.query.dataDirectory.dataObjectByContentId(contentId)) as Option<DataObject>;
  530. space += dataObject.unwrap().size_in_bytes.toNumber();
  531. }
  532. return space;
  533. }
  534. async getMedia(blockHash: Hash) {
  535. let nrEntities = ((await this.api.query.versionedStore.nextEntityId.at(blockHash)) as EntityId).toNumber();
  536. let medias: Media[] = [];
  537. for (let i = 0; i < nrEntities; ++i) {
  538. let entity = await this.api.query.versionedStore.entityById.at(blockHash, i) as Entity;
  539. if (entity.class_id.toNumber() != 7 || entity.entity_values.isEmpty) {
  540. continue;
  541. }
  542. let title = entity.entity_values[0].value.toString();
  543. medias.push(new Media(entity.id.toNumber(), title));
  544. }
  545. return medias;
  546. }
  547. async buildBlocksEventCache(startBlock: number, endBlock: number) {
  548. let cacheFile = CACHE_FOLDER + '/' + startBlock + '-' + endBlock + '.json';
  549. let exists = await fs.access(cacheFile, fsSync.constants.R_OK).then(() => true)
  550. .catch(() => false);
  551. // let exists = false;
  552. if (!exists) {
  553. console.log('Building events cache...');
  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. this.blocksEventsCache.set(i, cacheEvents);
  563. }
  564. console.log('\nFinish events cache...');
  565. await fs.writeFile(cacheFile, JSON.stringify(Array.from(this.blocksEventsCache.entries()), null, 2));
  566. } else {
  567. console.log('Cache file found, loading it...');
  568. let fileData = await fs.readFile(cacheFile);
  569. this.blocksEventsCache = new Map(JSON.parse(fileData));
  570. console.log('Cache file loaded...');
  571. }
  572. }
  573. static async connectApi(): Promise<ApiPromise> {
  574. // const provider = new WsProvider('wss://testnet.joystream.org:9944');
  575. const provider = new WsProvider(PROVIDER_URL);
  576. // Create the API and wait until ready
  577. return await ApiPromise.create({provider, types});
  578. }
  579. }