StatisticsCollector.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  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, Channel
  20. } from "./StatisticsData";
  21. import {Option, 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 Linkage from "@polkadot/types/codec/Linkage";
  26. import {PostId, ThreadId} from "@joystream/types/common";
  27. import {CategoryId} from "@joystream/types/forum";
  28. import {MemberId} from "@joystream/types/members";
  29. import {RewardRelationship, RewardRelationshipId} from "@joystream/types/recurring-rewards";
  30. import workingGroup from "@joystream/types/src/working-group/index";
  31. import {Stake} from "@joystream/types/stake";
  32. import {ChannelId} from "@joystream/types/content-working-group";
  33. import {WorkerId} from "@joystream/types/working-group";
  34. import {Entity, EntityId, PropertyType} from "@joystream/types/content-directory";
  35. import {WorkerOf} from "@joystream/types/augment-codec/all";
  36. const fsSync = require('fs');
  37. const fs = fsSync.promises;
  38. const BURN_ADDRESS = '5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu';
  39. const COUNCIL_ROUND_OFFSET = 0;
  40. const PROVIDER_URL = "ws://localhost:9944";
  41. const CACHE_FOLDER = "cache";
  42. const WORKER_ID_OFFSET = 0;
  43. const VIDEO_CLASS_iD = 10;
  44. const CHANNEL_CLASS_iD = 1;
  45. export class StatisticsCollector {
  46. private api?: ApiPromise;
  47. private blocksEventsCache: Map<number, CacheEvent[]>;
  48. private statistics: StatisticsData;
  49. constructor() {
  50. this.blocksEventsCache = new Map<number, CacheEvent[]>();
  51. this.statistics = new StatisticsData();
  52. }
  53. async getStatistics(startBlock: number, endBlock: number): Promise<StatisticsData> {
  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.fillMembershipInfo(startHash, endHash);
  71. await this.fillMediaUploadInfo(startHash, endHash);
  72. await this.fillForumInfo(startHash, endHash);
  73. await this.api.disconnect();
  74. return this.statistics;
  75. }
  76. async fillBasicInfo(startHash: Hash, endHash: Hash) {
  77. let startDate = (await this.api.query.timestamp.now.at(startHash)) as Moment;
  78. let endDate = (await this.api.query.timestamp.now.at(endHash)) as Moment;
  79. this.statistics.dateStart = new Date(startDate.toNumber()).toLocaleDateString("en-US");
  80. this.statistics.dateEnd = new Date(endDate.toNumber()).toLocaleDateString("en-US");
  81. }
  82. async fillTokenGenerationInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  83. this.statistics.startIssuance = (await this.api.query.balances.totalIssuance.at(startHash) as Balance).toNumber();
  84. this.statistics.endIssuance = (await this.api.query.balances.totalIssuance.at(endHash) as Balance).toNumber();
  85. this.statistics.newIssuance = this.statistics.endIssuance - this.statistics.startIssuance;
  86. this.statistics.percNewIssuance = StatisticsCollector.convertToPercentage(this.statistics.startIssuance, this.statistics.endIssuance);
  87. for (let [key, blockEvents] of this.blocksEventsCache) {
  88. let validatorRewards = blockEvents.filter((event) => {
  89. return event.section == "staking" && event.method == "Reward";
  90. });
  91. for (let validatorReward of validatorRewards) {
  92. this.statistics.newValidatorRewards += Number(validatorReward.data[1]);
  93. }
  94. let transfers = blockEvents.filter((event) => {
  95. return event.section == "balances" && event.method == "Transfer";
  96. });
  97. for (let transfer of transfers) {
  98. let receiver = transfer.data[1] as AccountId;
  99. let amount = transfer.data[2] as Balance;
  100. if (receiver.toString() == BURN_ADDRESS) {
  101. this.statistics.newTokensBurn = Number(amount);
  102. }
  103. }
  104. }
  105. let roundNrBlocks = endBlock - startBlock;
  106. this.statistics.newCouncilRewards = await this.computeCouncilReward(roundNrBlocks, endHash);
  107. this.statistics.newCouncilRewards = Number(this.statistics.newCouncilRewards.toFixed(2));
  108. this.statistics.newCuratorRewards = await this.computeCuratorsReward(roundNrBlocks, startHash, endHash);
  109. this.statistics.newCuratorRewards = Number(this.statistics.newCuratorRewards.toFixed(2));
  110. }
  111. async computeCouncilReward(roundNrBlocks: number, endHash: Hash): Promise<number> {
  112. const payoutInterval = Number((await this.api.query.council.payoutInterval.at(endHash) as Option<BlockNumber>).unwrapOr(0));
  113. const amountPerPayout = (await this.api.query.council.amountPerPayout.at(endHash) as BalanceOf).toNumber();
  114. const announcing_period = (await this.api.query.councilElection.announcingPeriod.at(endHash)) as BlockNumber;
  115. const voting_period = (await this.api.query.councilElection.votingPeriod.at(endHash)) as BlockNumber;
  116. const revealing_period = (await this.api.query.councilElection.revealingPeriod.at(endHash)) as BlockNumber;
  117. const new_term_duration = (await this.api.query.councilElection.newTermDuration.at(endHash)) as BlockNumber;
  118. const termDuration = new_term_duration.toNumber();
  119. const votingPeriod = voting_period.toNumber();
  120. const revealingPeriod = revealing_period.toNumber();
  121. const announcingPeriod = announcing_period.toNumber();
  122. const nrCouncilMembers = (await this.api.query.council.activeCouncil.at(endHash) as Seats).length
  123. const totalCouncilRewardsPerBlock = (amountPerPayout && payoutInterval)
  124. ? (amountPerPayout * nrCouncilMembers) / payoutInterval
  125. : 0;
  126. const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
  127. const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock;
  128. return avgCouncilRewardPerBlock * roundNrBlocks;
  129. }
  130. async computeStorageProviderReward(roundNrBlocks: number, startHash: Hash, endHash: Hash): Promise<WorkersInfo> {
  131. let nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber();
  132. let info = new WorkersInfo();
  133. for (let i = 0; i < nextWorkerId; ++i) {
  134. let worker = await this.api.query.storageWorkingGroup.workerById(i) as WorkerOf;
  135. if (worker.role_stake_profile.isSome) {
  136. let roleStakeProfile = worker.role_stake_profile.unwrap();
  137. let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
  138. info.startStake += stake.value.toNumber();
  139. }
  140. }
  141. nextWorkerId = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
  142. let rewardRelationshipIds = Array<RewardRelationshipId>();
  143. for (let i = 0; i < nextWorkerId; ++i) {
  144. let worker = await this.api.query.storageWorkingGroup.workerById(i) as WorkerOf;
  145. if (worker.reward_relationship.isSome) {
  146. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  147. }
  148. if (worker.role_stake_profile.isSome) {
  149. let roleStakeProfile = worker.role_stake_profile.unwrap();
  150. let stake = await this.api.query.stake.stakes(roleStakeProfile.stake_id) as Stake;
  151. info.endStake += stake.value.toNumber();
  152. }
  153. }
  154. info.rewards = await this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  155. info.endNrOfWorkers = nextWorkerId - WORKER_ID_OFFSET;
  156. return info;
  157. }
  158. async computeCuratorsReward(roundNrBlocks: number, startHash: Hash, endHash: Hash) {
  159. let nextCuratorId = (await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber();
  160. let rewardRelationshipIds = Array<RewardRelationshipId>();
  161. for (let i = 0; i < nextCuratorId; ++i) {
  162. let worker = await this.api.query.contentDirectoryWorkingGroup.workerById(i) as WorkerOf;
  163. if (worker.reward_relationship.isSome) {
  164. rewardRelationshipIds.push(worker.reward_relationship.unwrap());
  165. }
  166. }
  167. return this.computeReward(roundNrBlocks, rewardRelationshipIds, endHash);
  168. }
  169. async computeReward(roundNrBlocks: number, rewardRelationshipIds: RewardRelationshipId[], hash: Hash) {
  170. let recurringRewards = await Promise.all(rewardRelationshipIds.map(async (rewardRelationshipId) => {
  171. return await this.api.query.recurringRewards.rewardRelationships.at(hash, rewardRelationshipId) as RewardRelationship;
  172. }));
  173. let rewardPerBlock = 0;
  174. for (let recurringReward of recurringRewards) {
  175. const amount = recurringReward.amount_per_payout.toNumber();
  176. const payoutInterval = recurringReward.payout_interval.unwrapOr(null);
  177. if (amount && payoutInterval) {
  178. rewardPerBlock += amount / payoutInterval;
  179. }
  180. }
  181. return rewardPerBlock * roundNrBlocks;
  182. }
  183. async fillMintsInfo(startHash: Hash, endHash: Hash) {
  184. let startNrMints = parseInt((await this.api.query.minting.mintsCreated.at(startHash)).toString());
  185. let endNrMints = parseInt((await this.api.query.minting.mintsCreated.at(endHash)).toString());
  186. this.statistics.newMints = endNrMints - startNrMints;
  187. // statistics.startMinted = 0;
  188. // statistics.endMinted = 0;
  189. for (let i = 0; i < startNrMints; ++i) {
  190. let startMint = (await this.api.query.minting.mints.at(startHash, i)) as Mint;
  191. // if (!startMint) {
  192. // continue;
  193. // }
  194. let endMint = (await this.api.query.minting.mints.at(endHash, i)) as Mint;
  195. // let = endMintResult[0];
  196. // if (!endMint) {
  197. // continue;
  198. // }
  199. let startMintTotal = parseInt(startMint.getField("total_minted").toString());
  200. let endMintTotal = parseInt(endMint.getField("total_minted").toString());
  201. // statistics.startMinted += startMintTotal;
  202. this.statistics.totalMinted += endMintTotal - startMintTotal;
  203. this.statistics.totalMintCapacityIncrease += parseInt(endMint.getField("capacity").toString()) - parseInt(startMint.getField("capacity").toString());
  204. }
  205. for (let i = startNrMints; i < endNrMints; ++i) {
  206. let endMint = await this.api.query.minting.mints.at(endHash, i) as Mint;
  207. if (!endMint) {
  208. return;
  209. }
  210. this.statistics.totalMinted = parseInt(endMint.getField("total_minted").toString());
  211. }
  212. let councilMint = (await this.api.query.council.councilMint.at(endHash)) as MintId;
  213. let councilMintStatistics = await this.computeMintInfo(councilMint, startHash, endHash);
  214. this.statistics.startCouncilMinted = councilMintStatistics.startMinted;
  215. this.statistics.endCouncilMinted = councilMintStatistics.endMinted;
  216. this.statistics.newCouncilMinted = councilMintStatistics.diffMinted;
  217. this.statistics.percNewCouncilMinted = councilMintStatistics.percMinted;
  218. 6
  219. let curatorMint = (await this.api.query.contentDirectoryWorkingGroup.mint.at(endHash)) as MintId;
  220. let curatorMintStatistics = await this.computeMintInfo(curatorMint, startHash, endHash);
  221. this.statistics.startCuratorMinted = curatorMintStatistics.startMinted;
  222. this.statistics.endCuratorMinted = curatorMintStatistics.endMinted;
  223. this.statistics.newCuratorMinted = curatorMintStatistics.diffMinted;
  224. this.statistics.percCuratorMinted = curatorMintStatistics.percMinted;
  225. let storageProviderMint = (await this.api.query.storageWorkingGroup.mint.at(endHash)) as MintId;
  226. let storageProviderMintStatistics = await this.computeMintInfo(storageProviderMint, startHash, endHash);
  227. this.statistics.startStorageMinted = storageProviderMintStatistics.startMinted;
  228. this.statistics.endStorageMinted = storageProviderMintStatistics.endMinted;
  229. this.statistics.newStorageMinted = storageProviderMintStatistics.diffMinted;
  230. this.statistics.percStorageMinted = storageProviderMintStatistics.percMinted;
  231. }
  232. async computeMintInfo(mintId: MintId, startHash: Hash, endHash: Hash): Promise<MintStatistics> {
  233. // if (mintId.toString() == "0") {
  234. // return new MintStatistics(0, 0, 0);
  235. // }
  236. let startMint = await this.api.query.minting.mints.at(startHash, mintId) as Mint;
  237. // let startMint = startMintResult[0] as unknown as Mint;
  238. // if (!startMint) {
  239. // return new MintStatistics(0, 0, 0);
  240. // }
  241. let endMint = await this.api.query.minting.mints.at(endHash, mintId) as Mint;
  242. // let endMint = endMintResult[0] as unknown as Mint;
  243. // if (!endMint) {
  244. // return new MintStatistics(0, 0, 0);
  245. // }
  246. let mintStatistics = new MintStatistics();
  247. mintStatistics.startMinted = parseInt(startMint.getField('total_minted').toString());
  248. mintStatistics.endMinted = parseInt(endMint.getField('total_minted').toString());
  249. mintStatistics.diffMinted = mintStatistics.endMinted - mintStatistics.startMinted;
  250. mintStatistics.percMinted = StatisticsCollector.convertToPercentage(mintStatistics.startMinted, mintStatistics.endMinted);
  251. return mintStatistics;
  252. }
  253. async fillCouncilInfo(startHash: Hash, endHash: Hash) {
  254. this.statistics.councilRound = (await this.api.query.councilElection.round.at(startHash) as u32).toNumber();
  255. this.statistics.councilMembers = (await this.api.query.councilElection.councilSize.at(startHash) as u32).toNumber();
  256. let startNrProposals = await this.api.query.proposalsEngine.proposalCount.at(startHash) as u32;
  257. let endNrProposals = await this.api.query.proposalsEngine.proposalCount.at(endHash) as u32;
  258. this.statistics.newProposals = endNrProposals.toNumber() - startNrProposals.toNumber();
  259. let approvedProposals = new Set();
  260. for (let [key, blockEvents] of this.blocksEventsCache) {
  261. for (let event of blockEvents) {
  262. if (event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated") {
  263. let statusUpdateData = event.data[1] as any;
  264. let finalizeData = statusUpdateData.finalized as any
  265. if (finalizeData && finalizeData.proposalStatus.approved) {
  266. approvedProposals.add(Number(event.data[0]));
  267. }
  268. }
  269. }
  270. }
  271. this.statistics.newApprovedProposals = approvedProposals.size;
  272. }
  273. async fillCouncilElectionInfo(startBlock: number) {
  274. let startBlockHash = await this.api.rpc.chain.getBlockHash(startBlock);
  275. let events = await this.api.query.system.events.at(startBlockHash) as Vec<EventRecord>;
  276. let isStartBlockFirstCouncilBlock = events.some((event) => {
  277. return event.event.section == "councilElection" && event.event.method == "CouncilElected";
  278. });
  279. if (!isStartBlockFirstCouncilBlock) {
  280. console.warn('Note: The given start block is not the first block of the council round so council election information will be empty');
  281. return;
  282. }
  283. let previousCouncilRoundLastBlock = startBlock - 1;
  284. let previousCouncilRoundLastBlockHash = await this.api.rpc.chain.getBlockHash(previousCouncilRoundLastBlock);
  285. let applicants = await this.api.query.councilElection.applicants.at(previousCouncilRoundLastBlockHash) as Vec<AccountId>;
  286. this.statistics.electionApplicants = applicants.length;
  287. for (let applicant of applicants) {
  288. let applicantStakes = await this.api.query.councilElection.applicantStakes.at(previousCouncilRoundLastBlockHash, applicant) as unknown as ElectionStake;
  289. this.statistics.electionApplicantsStakes += applicantStakes.new.toNumber();
  290. }
  291. // let seats = await this.api.query.council.activeCouncil.at(startBlockHash) as Seats;
  292. //TODO: Find a more accurate way of getting the votes
  293. const votes = await this.api.query.councilElection.commitments.at(previousCouncilRoundLastBlockHash) as Vec<Hash>;
  294. this.statistics.electionVotes = votes.length;
  295. }
  296. async fillValidatorInfo(startHash: Hash, endHash: Hash) {
  297. let startTimestamp = await this.api.query.timestamp.now.at(startHash) as unknown as Moment;
  298. let endTimestamp = await this.api.query.timestamp.now.at(endHash) as unknown as Moment;
  299. let avgBlockProduction = (((endTimestamp.toNumber() - startTimestamp.toNumber())
  300. / 1000) / this.statistics.newBlocks);
  301. this.statistics.avgBlockProduction = Number(avgBlockProduction.toFixed(2));
  302. let maxStartValidators = (await this.api.query.staking.validatorCount.at(startHash) as u32).toNumber();
  303. let startValidators = await this.findActiveValidators(startHash, false);
  304. this.statistics.startValidators = startValidators.length + " / " + maxStartValidators;
  305. let maxEndValidators = (await this.api.query.staking.validatorCount.at(endHash) as u32).toNumber();
  306. let endValidators = await this.findActiveValidators(endHash, true);
  307. this.statistics.endValidators = endValidators.length + " / " + maxEndValidators;
  308. this.statistics.percValidators = StatisticsCollector.convertToPercentage(startValidators.length, endValidators.length);
  309. const startEra = await this.api.query.staking.currentEra.at(startHash) as Option<EraIndex>;
  310. this.statistics.startValidatorsStake = (await this.api.query.staking.erasTotalStake.at(startHash, startEra.unwrap())).toNumber();
  311. const endEra = await this.api.query.staking.currentEra.at(endHash) as Option<EraIndex>;
  312. this.statistics.endValidatorsStake = (await this.api.query.staking.erasTotalStake.at(endHash, endEra.unwrap())).toNumber();
  313. this.statistics.percNewValidatorsStake = StatisticsCollector.convertToPercentage(this.statistics.startValidatorsStake, this.statistics.endValidatorsStake);
  314. }
  315. async findActiveValidators(hash: Hash, searchPreviousBlocks: boolean): Promise<AccountId[]> {
  316. const block = await this.api.rpc.chain.getBlock(hash);
  317. let currentBlockNr = block.block.header.number.toNumber();
  318. let activeValidators;
  319. do {
  320. let currentHash = (await this.api.rpc.chain.getBlockHash(currentBlockNr)) as Hash;
  321. let allValidators = await this.api.query.staking.snapshotValidators.at(currentHash) as Option<Vec<AccountId>>;
  322. if (!allValidators.isEmpty) {
  323. let max = (await this.api.query.staking.validatorCount.at(currentHash)).toNumber();
  324. activeValidators = Array.from(allValidators.unwrap()).slice(0, max);
  325. }
  326. if (searchPreviousBlocks) {
  327. --currentBlockNr;
  328. } else {
  329. ++currentBlockNr;
  330. }
  331. } while (activeValidators == undefined);
  332. return activeValidators;
  333. }
  334. async fillStorageProviderInfo(startBlock: number, endBlock: number, startHash: Hash, endHash: Hash) {
  335. let roundNrBlocks = endBlock - startBlock;
  336. let storageProvidersRewards = await this.computeStorageProviderReward(roundNrBlocks, startHash, endHash);
  337. this.statistics.newStorageProviderReward = storageProvidersRewards.rewards;
  338. this.statistics.newStorageProviderReward = Number(this.statistics.newStorageProviderReward.toFixed(2));
  339. this.statistics.startStorageProvidersStake = storageProvidersRewards.startStake;
  340. this.statistics.endStorageProvidersStake = storageProvidersRewards.endStake;
  341. this.statistics.percNewStorageProviderStake = StatisticsCollector.convertToPercentage(this.statistics.startStorageProvidersStake, this.statistics.endStorageProvidersStake);
  342. this.statistics.startStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(startHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
  343. this.statistics.endStorageProviders = (await this.api.query.storageWorkingGroup.nextWorkerId.at(endHash) as WorkerId).toNumber() - WORKER_ID_OFFSET;
  344. this.statistics.percNewStorageProviders = StatisticsCollector.convertToPercentage(this.statistics.startStorageProviders, this.statistics.endStorageProviders);
  345. }
  346. async fillCuratorInfo(startHash: Hash, endHash: Hash) {
  347. this.statistics.startCurators = (await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(startHash));
  348. this.statistics.endCurators = (await this.api.query.contentDirectoryWorkingGroup.nextWorkerId.at(endHash));
  349. this.statistics.percNewCurators = StatisticsCollector.convertToPercentage(this.statistics.startCurators, this.statistics.endCurators);
  350. }
  351. async fillMembershipInfo(startHash: Hash, endHash: Hash) {
  352. this.statistics.startMembers = (await this.api.query.members.nextMemberId.at(startHash) as MemberId).toNumber();
  353. this.statistics.endMembers = (await this.api.query.members.nextMemberId.at(endHash) as MemberId).toNumber();
  354. this.statistics.newMembers = this.statistics.endMembers - this.statistics.startMembers;
  355. this.statistics.percNewMembers = StatisticsCollector.convertToPercentage(this.statistics.startMembers, this.statistics.endMembers);
  356. }
  357. async fillMediaUploadInfo(startHash: Hash, endHash: Hash) {
  358. let startEntites = await this.getEntities(startHash);
  359. let endEntities = await this.getEntities(endHash);
  360. let startVideos = await this.parseVideos(startEntites);
  361. let endVideos = await this.parseVideos(endEntities);
  362. this.statistics.startMedia = startVideos.length;
  363. this.statistics.endMedia = endVideos.length;
  364. this.statistics.percNewMedia = StatisticsCollector.convertToPercentage(this.statistics.startMedia, this.statistics.endMedia);
  365. let startChannels = await this.parseChannels(startEntites);
  366. let endChannels = await this.parseChannels(endEntities);
  367. this.statistics.startChannels = startChannels.length;
  368. this.statistics.endChannels = endChannels.length;
  369. this.statistics.percNewChannels = StatisticsCollector.convertToPercentage(this.statistics.startChannels, this.statistics.endChannels);
  370. let startDataObjects = await this.api.query.dataDirectory.knownContentIds.at(startHash) as Vec<ContentId>;
  371. this.statistics.startUsedSpace = Number((await this.computeUsedSpaceInMbs(startDataObjects)).toFixed(2));
  372. let endDataObjects = await this.api.query.dataDirectory.knownContentIds.at(endHash) as Vec<ContentId>;
  373. this.statistics.endUsedSpace = Number((await this.computeUsedSpaceInMbs(endDataObjects)).toFixed(2));
  374. this.statistics.percNewUsedSpace = StatisticsCollector.convertToPercentage(this.statistics.startUsedSpace, this.statistics.endUsedSpace);
  375. }
  376. async fillForumInfo(startHash: Hash, endHash: Hash) {
  377. let startPostId = await this.api.query.forum.nextPostId.at(startHash) as PostId;
  378. let endPostId = await this.api.query.forum.nextPostId.at(endHash) as PostId;
  379. this.statistics.startPosts = startPostId.toNumber();
  380. this.statistics.endPosts = endPostId.toNumber();
  381. this.statistics.newPosts = this.statistics.endPosts - this.statistics.startPosts;
  382. this.statistics.percNewPosts = StatisticsCollector.convertToPercentage(this.statistics.startPosts, this.statistics.endPosts);
  383. let startThreadId = ((await this.api.query.forum.nextThreadId.at(startHash)) as unknown) as ThreadId;
  384. let endThreadId = ((await this.api.query.forum.nextThreadId.at(endHash)) as unknown) as ThreadId;
  385. this.statistics.startThreads = startThreadId.toNumber();
  386. this.statistics.endThreads = endThreadId.toNumber();
  387. this.statistics.newThreads = this.statistics.endThreads - this.statistics.startThreads;
  388. this.statistics.percNewThreads = StatisticsCollector.convertToPercentage(this.statistics.startThreads, this.statistics.endThreads);
  389. let startCategoryId = (await this.api.query.forum.nextCategoryId.at(startHash)) as CategoryId;
  390. let endCategoryId = (await this.api.query.forum.nextCategoryId.at(endHash)) as CategoryId;
  391. this.statistics.startCategories = startCategoryId.toNumber();
  392. this.statistics.endCategories = endCategoryId.toNumber();
  393. this.statistics.newCategories = this.statistics.endCategories - this.statistics.startCategories;
  394. this.statistics.perNewCategories = StatisticsCollector.convertToPercentage(this.statistics.startCategories, this.statistics.endCategories);
  395. }
  396. static convertToPercentage(previousValue: number, newValue: number): number {
  397. if (previousValue == 0) {
  398. return newValue > 0 ? Infinity : 0;
  399. }
  400. return Number((newValue * 100 / previousValue - 100).toFixed(2));
  401. }
  402. async computeUsedSpaceInMbs(contentIds: Vec<ContentId>) {
  403. let space = 0;
  404. for (let contentId of contentIds) {
  405. let dataObject = (await this.api.query.dataDirectory.dataObjectByContentId(contentId)) as Option<DataObject>;
  406. space += dataObject.unwrap().size_in_bytes.toNumber();
  407. }
  408. return space / 1024 / 1024;
  409. }
  410. async parseVideos(entities: Map<number, Entity>) {
  411. let videos: Media[] = [];
  412. for (let [key, entity] of entities) {
  413. if (entity.class_id.toNumber() != VIDEO_CLASS_iD || entity.values.isEmpty) {
  414. continue;
  415. }
  416. let values = Array.from(entity.getField('values').entries());
  417. if (values.length < 2 || values[2].length < 1) {
  418. continue;
  419. }
  420. let title = values[2][1].getValue().toString();
  421. videos.push(new Media(key, title));
  422. }
  423. return videos;
  424. }
  425. async parseChannels(entities: Map<number, Entity>) {
  426. let channels: Channel[] = [];
  427. for (let [key, entity] of entities) {
  428. if (entity.class_id.toNumber() != CHANNEL_CLASS_iD || entity.values.isEmpty) {
  429. continue;
  430. }
  431. let values = Array.from(entity.getField('values').entries());
  432. let title = values[0][1].getValue().toString();
  433. channels.push(new Channel(key, title));
  434. }
  435. return channels;
  436. }
  437. async getEntities(blockHash: Hash) {
  438. let nrEntities = ((await this.api.query.contentDirectory.nextEntityId.at(blockHash)) as EntityId).toNumber();
  439. let entities = new Map<number, Entity>();
  440. for (let i = 0; i < nrEntities; ++i) {
  441. let entity = await this.api.query.contentDirectory.entityById.at(blockHash, i) as Entity;
  442. entities.set(i, entity);
  443. }
  444. return entities;
  445. }
  446. async buildBlocksEventCache(startBlock: number, endBlock: number) {
  447. let cacheFile = CACHE_FOLDER + '/' + startBlock + '-' + endBlock + '.json';
  448. let exists = await fs.access(cacheFile, fsSync.constants.R_OK).then(() => true)
  449. .catch(() => false);
  450. // let exists = false;
  451. if (!exists) {
  452. console.log('Building events cache...');
  453. for (let i = startBlock; i < endBlock; ++i) {
  454. process.stdout.write('\rCaching block: ' + i + ' until ' + endBlock);
  455. const blockHash: Hash = await this.api.rpc.chain.getBlockHash(i);
  456. let eventRecord = await this.api.query.system.events.at(blockHash) as Vec<EventRecord>;
  457. let cacheEvents = new Array<CacheEvent>();
  458. for (let event of eventRecord) {
  459. cacheEvents.push(new CacheEvent(event.event.section, event.event.method, event.event.data));
  460. }
  461. this.blocksEventsCache.set(i, cacheEvents);
  462. }
  463. console.log('\nFinish events cache...');
  464. await fs.writeFile(cacheFile, JSON.stringify(Array.from(this.blocksEventsCache.entries()), null, 2));
  465. } else {
  466. console.log('Cache file found, loading it...');
  467. let fileData = await fs.readFile(cacheFile);
  468. this.blocksEventsCache = new Map(JSON.parse(fileData));
  469. console.log('Cache file loaded...');
  470. }
  471. }
  472. static async connectApi(): Promise<ApiPromise> {
  473. // const provider = new WsProvider('wss://testnet.joystream.org:9944');
  474. const provider = new WsProvider(PROVIDER_URL);
  475. // Create the API and wait until ready
  476. return await ApiPromise.create({provider, types});
  477. }
  478. }