StatisticsCollector.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  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,
  19. Channel,
  20. SpendingProposals,
  21. Bounty,
  22. WorkerReward,
  23. } from "./types";
  24. import { Option, u32, Vec } from "@polkadot/types";
  25. import { ElectionStake, SealedVote, Seats } from "@joystream/types/council";
  26. import { Mint, MintId } from "@joystream/types/mint";
  27. import { ContentId, DataObject } from "@joystream/types/media";
  28. import { ChannelId, PostId, ThreadId } from "@joystream/types/common";
  29. import { CategoryId } from "@joystream/types/forum";
  30. import { MemberId, Membership } from "@joystream/types/members";
  31. import {
  32. RewardRelationship,
  33. RewardRelationshipId,
  34. } from "@joystream/types/recurring-rewards";
  35. import { Stake } from "@joystream/types/stake";
  36. import { WorkerId } from "@joystream/types/working-group";
  37. import {
  38. Entity,
  39. EntityId,
  40. PropertyType,
  41. } from "@joystream/types/content-directory";
  42. import {
  43. ProposalId,
  44. Video,
  45. VideoId,
  46. WorkerOf,
  47. } from "@joystream/types/augment-codec/all";
  48. import { ProposalDetails, ProposalOf } from "@joystream/types/augment/types";
  49. import { SpendingParams } from "@joystream/types/proposals";
  50. import * as constants from "constants";
  51. import { eventStats, getPercent, getTotalMinted, momentToString } from "./lib";
  52. import {
  53. getBlock,
  54. getBlockHash,
  55. getTimestamp,
  56. getIssuance,
  57. getEra,
  58. getEraStake,
  59. getEvents,
  60. getCouncil,
  61. getCouncilRound,
  62. getCouncilSize,
  63. getCouncilApplicants,
  64. getCouncilApplicantStakes,
  65. getCouncilCommitments,
  66. getCouncilPayoutInterval,
  67. getCouncilPayout,
  68. getCouncilPeriods,
  69. getNextWorker,
  70. getWorker,
  71. getWorkers,
  72. getWorkerReward,
  73. getStake,
  74. getCouncilMint,
  75. getMintsCreated,
  76. getMint,
  77. getGroupMint,
  78. getNextMember,
  79. getMember,
  80. getNextPost,
  81. getNextThread,
  82. getNextCategory,
  83. getProposalCount,
  84. getProposalInfo,
  85. getProposalDetails,
  86. getValidatorCount,
  87. getValidators,
  88. getNextEntity,
  89. getNextChannel,
  90. getNextVideo,
  91. getEntity,
  92. getDataObject,
  93. getDataObjects,
  94. } from "./lib/api";
  95. import {
  96. filterMethods,
  97. getWorkerRewards,
  98. getBurnedTokens,
  99. getMintInfo,
  100. getActiveValidators,
  101. getValidatorsRewards,
  102. } from "./lib/rewards";
  103. const fsSync = require("fs");
  104. const fs = fsSync.promises;
  105. const parse = require("csv-parse/lib/sync");
  106. const BURN_ADDRESS = "5D5PhZQNJzcJXVBxwJxZcsutjKPqUPydrvpu6HeiBfMaeKQu";
  107. const COUNCIL_ROUND_OFFSET = 2;
  108. const PROVIDER_URL = "ws://localhost:9944";
  109. const CACHE_FOLDER = "cache";
  110. const VIDEO_CLASS_iD = 10;
  111. const CHANNEL_CLASS_iD = 1;
  112. const SPENDING_CATEGORIES_FILE_NAME = "spending_proposal_categories";
  113. export class StatisticsCollector {
  114. private api?: ApiPromise;
  115. private blocksEventsCache: Map<number, CacheEvent[]>;
  116. private statistics: Statistics;
  117. constructor() {
  118. this.blocksEventsCache = new Map<number, CacheEvent[]>();
  119. this.statistics = new Statistics();
  120. }
  121. saveStats(data: any) {
  122. Object.keys(data).map((key: string) => (this.statistics[key] = data[key]));
  123. }
  124. filterCache(
  125. filterEvent: (event: CacheEvent) => boolean
  126. ): [number, CacheEvent[]][] {
  127. const blocks: [number, CacheEvent[]][] = [];
  128. for (let block of this.blocksEventsCache) {
  129. const [key, events] = block;
  130. const filtered = events.filter((event) => filterEvent(event));
  131. if (filtered.length) blocks.push([key, filtered]);
  132. }
  133. return blocks;
  134. }
  135. async getStatistics(
  136. startBlock: number,
  137. endBlock: number
  138. ): Promise<Statistics> {
  139. this.api = await StatisticsCollector.connectApi();
  140. let startHash: Hash = await getBlockHash(this.api, startBlock);
  141. let endHash: Hash = await getBlockHash(this.api, endBlock);
  142. let startDate: Moment = await getTimestamp(this.api, startHash);
  143. let endDate: Moment = await getTimestamp(this.api, endHash);
  144. this.saveStats({
  145. dateStart: momentToString(startDate),
  146. dateEnd: momentToString(endDate),
  147. startBlock,
  148. endBlock,
  149. newBlocks: endBlock - startBlock,
  150. percNewBlocks: getPercent(startBlock, endBlock),
  151. });
  152. await this.buildBlocksEventCache(startBlock, endBlock);
  153. eventStats(this.blocksEventsCache);
  154. await this.fillTokenGenerationInfo(
  155. startBlock,
  156. endBlock,
  157. startHash,
  158. endHash
  159. );
  160. await this.fillMintsInfo(startHash, endHash);
  161. await this.fillCouncilInfo(startHash, endHash);
  162. await this.fillCouncilElectionInfo(startBlock);
  163. await this.fillValidatorInfo(startHash, endHash);
  164. await this.fillStorageProviderInfo(
  165. startBlock,
  166. endBlock,
  167. startHash,
  168. endHash
  169. );
  170. await this.fillCuratorInfo(startHash, endHash);
  171. await this.fillOperationsInfo(startBlock, endBlock, startHash, endHash);
  172. await this.fillMembershipInfo(startHash, endHash);
  173. await this.fillMediaUploadInfo(startHash, endHash);
  174. await this.fillForumInfo(startHash, endHash);
  175. this.api.disconnect();
  176. return this.statistics;
  177. }
  178. async getApprovedBounties(): Promise<Bounty[]> {
  179. let bountiesFilePath = `${__dirname}/../${SPENDING_CATEGORIES_FILE_NAME}.csv`;
  180. try {
  181. await fs.access(bountiesFilePath, constants.R_OK);
  182. } catch {
  183. throw new Error("Bounties CSV file not found");
  184. }
  185. const fileContent = await fs.readFile(bountiesFilePath);
  186. let rawBounties = parse(fileContent);
  187. rawBounties.shift();
  188. rawBounties = rawBounties.filter((line: string[]) => line[8] == "Bounties");
  189. let bounties = rawBounties.map((rawBounty: any) => {
  190. return new Bounty(
  191. rawBounty[0],
  192. rawBounty[1],
  193. rawBounty[2],
  194. rawBounty[3],
  195. rawBounty[4],
  196. rawBounty[5]
  197. );
  198. });
  199. return bounties.filter(
  200. (bounty: Bounty) =>
  201. bounty.status == "Approved" && bounty.testnet == "Antioch"
  202. );
  203. }
  204. async getFinalizedSpendingProposals(): Promise<Array<SpendingProposals>> {
  205. let spendingProposals = new Array<SpendingProposals>();
  206. for (let [key, blockEvents] of this.blocksEventsCache) {
  207. let proposalEvents = blockEvents.filter(
  208. ({ section, method }) =>
  209. section === "proposalsEngine" && method === "ProposalStatusUpdated"
  210. );
  211. for (let proposalEvent of proposalEvents) {
  212. let statusUpdateData = proposalEvent.data[1] as any;
  213. const finalizedAt = statusUpdateData.finalized.finalizedAt;
  214. if (!(statusUpdateData.finalized && finalizedAt)) continue;
  215. const id: ProposalId = proposalEvent.data[0] as any;
  216. const proposalInfo: ProposalOf = await getProposalInfo(this.api, id);
  217. const finalizedData = proposalInfo.status.asFinalized;
  218. const proposalDetail: ProposalDetails = await getProposalDetails(
  219. this.api,
  220. id
  221. );
  222. if (
  223. !finalizedData.proposalStatus.isApproved ||
  224. !proposalDetail.isSpending
  225. )
  226. continue;
  227. let spendingParams = proposalDetail.asSpending;
  228. if (!spendingProposals.some((proposal) => proposal.id == +id)) {
  229. const title = proposalInfo.title.toString();
  230. const amount = +spendingParams[0];
  231. const proposal = new SpendingProposals(+id, title, amount);
  232. spendingProposals.push(proposal);
  233. }
  234. }
  235. }
  236. return spendingProposals;
  237. }
  238. async fillTokenGenerationInfo(
  239. startBlock: number,
  240. endBlock: number,
  241. startHash: Hash,
  242. endHash: Hash
  243. ): Promise<void> {
  244. const startIssuance = (await getIssuance(this.api, startHash)).toNumber();
  245. const endIssuance = (await getIssuance(this.api, endHash)).toNumber();
  246. this.saveStats({
  247. startIssuance,
  248. endIssuance,
  249. newIssuance: endIssuance - startIssuance,
  250. percNewIssuance: getPercent(startIssuance, endIssuance),
  251. newTokensBurn: await getBurnedTokens(
  252. BURN_ADDRESS,
  253. this.filterCache(filterMethods.getBurnedTokens)
  254. ),
  255. });
  256. // bounties
  257. const bounties = await this.getApprovedBounties();
  258. let spendingProposals: SpendingProposals[] =
  259. await this.getFinalizedSpendingProposals();
  260. let bountiesTotalPaid = 0;
  261. if (bounties) {
  262. for (let bounty of bounties) {
  263. const bountySpendingProposal = spendingProposals.find(
  264. (spendingProposal) => spendingProposal.id == bounty.proposalId
  265. );
  266. if (bountySpendingProposal)
  267. bountiesTotalPaid += bountySpendingProposal.spentAmount;
  268. }
  269. this.saveStats({ bountiesTotalPaid });
  270. }
  271. if (!bountiesTotalPaid) {
  272. console.warn(
  273. "No bounties found in " +
  274. SPENDING_CATEGORIES_FILE_NAME +
  275. ", trying to find spending proposals of bounties, please check the values!..."
  276. );
  277. for (const spendingProposal of spendingProposals) {
  278. if (spendingProposal.title.toLowerCase().includes("bounty")) {
  279. bountiesTotalPaid += spendingProposal.spentAmount;
  280. }
  281. }
  282. this.saveStats({ bountiesTotalPaid });
  283. }
  284. let roundNrBlocks = endBlock - startBlock;
  285. const spendingProposalsTotal = spendingProposals.reduce(
  286. (n, p) => n + p.spentAmount,
  287. 0
  288. );
  289. const newCouncilRewards = await this.computeCouncilReward(
  290. roundNrBlocks,
  291. endHash
  292. );
  293. const newCuratorInfo = await this.computeWorkingGroupReward(
  294. roundNrBlocks,
  295. startHash,
  296. endHash,
  297. "contentDirectory"
  298. );
  299. this.saveStats({
  300. spendingProposalsTotal,
  301. newCouncilRewards: newCouncilRewards.toFixed(2),
  302. newCuratorRewards: newCuratorInfo.rewards.toFixed(2),
  303. });
  304. }
  305. async computeCouncilReward(
  306. roundNrBlocks: number,
  307. endHash: Hash
  308. ): Promise<number> {
  309. const payoutInterval = Number(
  310. (
  311. (await getCouncilPayoutInterval(
  312. this.api,
  313. endHash
  314. )) as Option<BlockNumber>
  315. ).unwrapOr(0)
  316. );
  317. const amountPerPayout = (
  318. (await getCouncilPayout(this.api, endHash)) as BalanceOf
  319. ).toNumber();
  320. const [announcingPeriod, votingPeriod, revealingPeriod, termDuration] =
  321. await Promise.all(getCouncilPeriods(this.api, endHash));
  322. const nrCouncilMembers = ((await getCouncil(this.api, endHash)) as Seats)
  323. .length;
  324. const totalCouncilRewardsPerBlock =
  325. amountPerPayout && payoutInterval
  326. ? (amountPerPayout * nrCouncilMembers) / payoutInterval
  327. : 0;
  328. const councilTermDurationRatio =
  329. termDuration /
  330. (termDuration + votingPeriod + revealingPeriod + announcingPeriod);
  331. const avgCouncilRewardPerBlock =
  332. councilTermDurationRatio * totalCouncilRewardsPerBlock;
  333. return avgCouncilRewardPerBlock * roundNrBlocks;
  334. }
  335. // Summarize stakes and rewards at start and end
  336. async computeWorkingGroupReward(
  337. roundNrBlocks: number,
  338. startHash: Hash,
  339. endHash: Hash,
  340. workingGroup: string
  341. ): Promise<WorkersInfo> {
  342. const group = workingGroup + "WorkingGroup";
  343. let info = new WorkersInfo();
  344. // stakes at start
  345. const workersStart: WorkerReward[] = await getWorkerRewards(
  346. this.api,
  347. group,
  348. startHash
  349. );
  350. workersStart.forEach(({ stake }) => {
  351. if (stake) info.startStake += stake.value.toNumber();
  352. });
  353. // stakes at end
  354. const workersEnd: WorkerReward[] = await getWorkerRewards(
  355. this.api,
  356. group,
  357. endHash
  358. );
  359. workersEnd.forEach(({ stake }) => {
  360. if (stake) info.endStake += stake.value.toNumber();
  361. });
  362. info.rewards = await this.computeReward(
  363. roundNrBlocks,
  364. workersEnd.filter((w) => w.reward).map((w) => w.reward)
  365. );
  366. info.endNrOfWorkers = workersEnd.length;
  367. return info;
  368. }
  369. async computeReward(
  370. roundNrBlocks: number,
  371. recurringRewards: RewardRelationship[]
  372. ): Promise<number> {
  373. let rewardPerBlock = 0;
  374. recurringRewards.forEach((recurringReward: RewardRelationship) => {
  375. if (!recurringReward) return;
  376. const amount = recurringReward.amount_per_payout.toNumber();
  377. const payoutInterval = Number(recurringReward.payout_interval);
  378. if (amount && payoutInterval) rewardPerBlock += amount / payoutInterval;
  379. });
  380. return rewardPerBlock * roundNrBlocks;
  381. }
  382. async computeGroupMintStats(
  383. [label, tag]: string[],
  384. startHash: Hash,
  385. endHash: Hash
  386. ) {
  387. const group = label + "WorkingGroup";
  388. const mint = await getGroupMint(this.api, group, endHash);
  389. const info = await getMintInfo(this.api, mint, startHash, endHash);
  390. let stats: { [key: string]: number } = {};
  391. stats[`start${tag}Minted`] = info.startMinted;
  392. stats[`end${tag}Minted`] = info.endMinted;
  393. stats[`new${tag}Minted`] = info.diffMinted;
  394. stats[`perc${tag}Minted`] = info.percMinted;
  395. this.saveStats(stats);
  396. }
  397. async fillMintsInfo(startHash: Hash, endHash: Hash): Promise<void> {
  398. const startNrMints = await getMintsCreated(this.api, startHash);
  399. const endNrMints = await getMintsCreated(this.api, endHash);
  400. const newMints = endNrMints - startNrMints;
  401. // calcuate sum of all mints
  402. let totalMinted = 0;
  403. let totalMintCapacityIncrease = 0;
  404. // summarize old mints
  405. for (let i = 0; i < startNrMints; ++i) {
  406. const startMint: Mint = await getMint(this.api, startHash, i);
  407. const endMint: Mint = await getMint(this.api, endHash, i);
  408. const startMintTotal = getTotalMinted(startMint);
  409. const endMintTotal = getTotalMinted(endMint);
  410. totalMinted += endMintTotal - startMintTotal;
  411. totalMintCapacityIncrease +=
  412. parseInt(endMint.getField("capacity").toString()) -
  413. parseInt(startMint.getField("capacity").toString());
  414. }
  415. // summarize new mints
  416. for (let i = startNrMints; i < endNrMints; ++i) {
  417. const endMint: Mint = await getMint(this.api, endHash, i);
  418. if (endMint) totalMinted += getTotalMinted(endMint);
  419. }
  420. this.saveStats({ newMints, totalMinted, totalMintCapacityIncrease });
  421. // council
  422. const councilInfo = await getMintInfo(
  423. this.api,
  424. await getCouncilMint(this.api, endHash),
  425. startHash,
  426. endHash
  427. );
  428. this.saveStats({
  429. startCouncilMinted: councilInfo.startMinted,
  430. endCouncilMinted: councilInfo.endMinted,
  431. newCouncilMinted: councilInfo.diffMinted,
  432. percNewCouncilMinted: councilInfo.percMinted,
  433. });
  434. // working groups
  435. const groups = [
  436. ["contentDirectory", "Curator"],
  437. ["storage", "Storage"],
  438. ["operations", "Operations"],
  439. ].forEach((group) => this.computeGroupMintStats(group, startHash, endHash));
  440. }
  441. async fillCouncilInfo(startHash: Hash, endHash: Hash): Promise<void> {
  442. const round = await getCouncilRound(this.api, startHash);
  443. const startNrProposals = await getProposalCount(this.api, startHash);
  444. const endNrProposals = await getProposalCount(this.api, endHash);
  445. let approvedProposals = new Set();
  446. for (let [key, blockEvents] of this.blocksEventsCache) {
  447. for (let event of blockEvents) {
  448. if (
  449. event.section == "proposalsEngine" &&
  450. event.method == "ProposalStatusUpdated"
  451. ) {
  452. let statusUpdateData = event.data[1] as any;
  453. let finalizeData = statusUpdateData.finalized as any;
  454. if (finalizeData && finalizeData.proposalStatus.approved) {
  455. approvedProposals.add(Number(event.data[0]));
  456. }
  457. }
  458. }
  459. }
  460. this.saveStats({
  461. councilRound: round - COUNCIL_ROUND_OFFSET, // TODO repeated elections?
  462. councilMembers: await getCouncilSize(this.api, startHash),
  463. newProposals: endNrProposals - startNrProposals,
  464. newApprovedProposals: approvedProposals.size,
  465. });
  466. }
  467. async fillCouncilElectionInfo(startBlock: number): Promise<void> {
  468. let startBlockHash = await getBlockHash(this.api, startBlock);
  469. let events: Vec<EventRecord> = await getEvents(this.api, startBlockHash);
  470. let isStartBlockFirstCouncilBlock = events.some(
  471. ({ event }) =>
  472. event.section == "councilElection" && event.method == "CouncilElected"
  473. );
  474. if (!isStartBlockFirstCouncilBlock)
  475. return console.warn(
  476. "Note: The given start block is not the first block of the council round so council election information will be empty"
  477. );
  478. let lastBlockHash = await getBlockHash(this.api, startBlock - 1);
  479. let applicants: Vec<AccountId> = await getCouncilApplicants(
  480. this.api,
  481. lastBlockHash
  482. );
  483. let electionApplicantsStakes = 0;
  484. for (let applicant of applicants) {
  485. const applicantStakes: ElectionStake = await getCouncilApplicantStakes(
  486. this.api,
  487. lastBlockHash,
  488. applicant
  489. );
  490. electionApplicantsStakes += applicantStakes.new.toNumber();
  491. }
  492. // let seats = await getCouncil(this.api,startBlockHash) as Seats;
  493. //TODO: Find a more accurate way of getting the votes
  494. const votes: Vec<Hash> = await getCouncilCommitments(
  495. this.api,
  496. lastBlockHash
  497. );
  498. this.saveStats({
  499. electionApplicants: applicants.length,
  500. electionApplicantsStakes,
  501. electionVotes: votes.length,
  502. });
  503. }
  504. async fillValidatorInfo(startHash: Hash, endHash: Hash): Promise<void> {
  505. let startTimestamp: Moment = await getTimestamp(this.api, startHash);
  506. let endTimestamp: Moment = await getTimestamp(this.api, endHash);
  507. let avgBlockProduction =
  508. (endTimestamp.toNumber() - startTimestamp.toNumber()) /
  509. 1000 /
  510. this.statistics.newBlocks;
  511. const maxStartValidators = await getValidatorCount(this.api, startHash);
  512. const startValidators = await getActiveValidators(this.api, startHash);
  513. const maxEndValidators = await getValidatorCount(this.api, endHash);
  514. const endValidators = await getActiveValidators(this.api, endHash, true);
  515. const startEra: Option<EraIndex> = await getEra(this.api, startHash);
  516. const endEra: Option<EraIndex> = await getEra(this.api, endHash);
  517. this.saveStats({
  518. avgBlockProduction: Number(avgBlockProduction.toFixed(2)),
  519. startValidators: startValidators.length + " / " + maxStartValidators,
  520. endValidators: endValidators.length + " / " + maxEndValidators,
  521. percValidators: getPercent(startValidators.length, endValidators.length),
  522. startValidatorsStake: await getEraStake(
  523. this.api,
  524. startHash,
  525. startEra.unwrap()
  526. ),
  527. endValidatorsStake: await getEraStake(this.api, endHash, endEra.unwrap()),
  528. percNewValidatorsStake: getPercent(
  529. this.statistics.startValidatorsStake,
  530. this.statistics.endValidatorsStake
  531. ),
  532. newValidatorRewards: await getValidatorsRewards(
  533. this.filterCache(filterMethods.newValidatorsRewards)
  534. ),
  535. });
  536. }
  537. async fillStorageProviderInfo(
  538. startBlock: number,
  539. endBlock: number,
  540. startHash: Hash,
  541. endHash: Hash
  542. ): Promise<void> {
  543. let roundNrBlocks = endBlock - startBlock;
  544. let storageProvidersRewards = await this.computeWorkingGroupReward(
  545. roundNrBlocks,
  546. startHash,
  547. endHash,
  548. "storage"
  549. );
  550. const newStorageProviderReward = Number(
  551. storageProvidersRewards.rewards.toFixed(2)
  552. );
  553. const startStorageProvidersStake = storageProvidersRewards.startStake;
  554. const endStorageProvidersStake = storageProvidersRewards.endStake;
  555. const group = "storageWorkingGroup";
  556. const startStorageProviders = await getWorkers(this.api, group, startHash);
  557. const endStorageProviders = await getWorkers(this.api, group, endHash);
  558. let storageProviders = "";
  559. const nextWorkerId = await getNextWorker(this.api, group, endHash);
  560. for (let i = 0; i < nextWorkerId; ++i) {
  561. const provider: WorkerOf = await getWorker(this.api, group, endHash, i);
  562. if (!provider.is_active) continue;
  563. const id = provider.member_id;
  564. const { handle, root_account } = await getMember(this.api, endHash, id);
  565. storageProviders += `@${handle} | (${root_account}) \n`;
  566. }
  567. this.saveStats({
  568. newStorageProviderReward,
  569. startStorageProvidersStake,
  570. endStorageProvidersStake,
  571. percNewStorageProviderStake: getPercent(
  572. startStorageProvidersStake,
  573. endStorageProvidersStake
  574. ),
  575. startStorageProviders,
  576. endStorageProviders,
  577. percNewStorageProviders: getPercent(
  578. startStorageProviders,
  579. endStorageProviders
  580. ),
  581. storageProviders,
  582. });
  583. }
  584. async fillCuratorInfo(startHash: Hash, endHash: Hash): Promise<void> {
  585. const group = "contentDirectoryWorkingGroup";
  586. const startCurators = await getWorkers(this.api, group, startHash);
  587. const endCurators = await getWorkers(this.api, group, endHash);
  588. let nextCuratorId = await getNextWorker(this.api, group, endHash);
  589. let curators = "";
  590. for (let i = 0; i < nextCuratorId; ++i) {
  591. const curator: WorkerOf = await getWorker(this.api, group, endHash, i);
  592. if (!curator.is_active) continue;
  593. const id = curator.member_id;
  594. const { handle, root_account } = await getMember(this.api, endHash, id);
  595. curators += `@${handle} | (${root_account}) \n`;
  596. }
  597. this.saveStats({
  598. startCurators,
  599. endCurators,
  600. percNewCurators: getPercent(
  601. this.statistics.startCurators,
  602. this.statistics.endCurators
  603. ),
  604. curators,
  605. });
  606. }
  607. async fillOperationsInfo(
  608. startBlock: number,
  609. endBlock: number,
  610. startHash: Hash,
  611. endHash: Hash
  612. ): Promise<void> {
  613. const roundNrBlocks = endBlock - startBlock;
  614. const operationsRewards = await this.computeWorkingGroupReward(
  615. roundNrBlocks,
  616. startHash,
  617. endHash,
  618. "operations"
  619. );
  620. const newOperationsReward = operationsRewards.rewards.toFixed(2);
  621. const startOperationsStake = operationsRewards.startStake;
  622. const endOperationsStake = operationsRewards.endStake;
  623. const group = "operationsWorkingGroup";
  624. const startWorkers = await getWorkers(this.api, group, startHash);
  625. const endWorkers = await getWorkers(this.api, group, endHash);
  626. let operations = "";
  627. let nextOperationsWorkerId = await getNextWorker(this.api, group, endHash);
  628. for (let i = 0; i < nextOperationsWorkerId; ++i) {
  629. let worker: WorkerOf = await getWorker(this.api, group, endHash, i);
  630. if (!worker.is_active) continue;
  631. const id = worker.member_id;
  632. const { handle, root_account } = await getMember(this.api, endHash, id);
  633. operations += `@${handle} | (${root_account}) \n`;
  634. }
  635. this.saveStats({
  636. operations,
  637. newOperationsReward: Number(newOperationsReward),
  638. startOperationsWorkers: startWorkers,
  639. endOperationsWorkers: endWorkers,
  640. percNewOperationsWorkers: getPercent(startWorkers, endWorkers),
  641. startOperationsStake,
  642. endOperationsStake,
  643. percNewOperationstake: getPercent(
  644. startOperationsStake,
  645. endOperationsStake
  646. ),
  647. });
  648. }
  649. async fillMembershipInfo(startHash: Hash, endHash: Hash): Promise<void> {
  650. const startMembers = await getNextMember(this.api, startHash);
  651. const endMembers = await getNextMember(this.api, endHash);
  652. this.saveStats({
  653. startMembers,
  654. endMembers,
  655. newMembers: endMembers - startMembers,
  656. percNewMembers: getPercent(startMembers, endMembers),
  657. });
  658. }
  659. async fillMediaUploadInfo(startHash: Hash, endHash: Hash): Promise<void> {
  660. const startMedia = await getNextVideo(this.api, startHash);
  661. const endMedia = await getNextVideo(this.api, endHash);
  662. const startChannels = await getNextChannel(this.api, startHash);
  663. const endChannels = await getNextChannel(this.api, endHash);
  664. // count size
  665. let startUsedSpace = 0;
  666. let endUsedSpace = 0;
  667. const startBlock = await getBlock(this.api, startHash);
  668. const endBlock = await getBlock(this.api, endHash);
  669. const dataObjects: Map<ContentId, DataObject> = await getDataObjects(
  670. this.api
  671. );
  672. for (let [key, dataObject] of dataObjects) {
  673. const added = dataObject.added_at.block.toNumber();
  674. const start = startBlock.block.header.number.toNumber();
  675. const end = endBlock.block.header.number.toNumber();
  676. if (added < start)
  677. startUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
  678. if (added < end)
  679. endUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024;
  680. }
  681. this.saveStats({
  682. startMedia,
  683. endMedia,
  684. percNewMedia: getPercent(startMedia, endMedia),
  685. startChannels,
  686. endChannels,
  687. percNewChannels: getPercent(startChannels, endChannels),
  688. startUsedSpace: Number(startUsedSpace.toFixed(2)),
  689. endUsedSpace: Number(endUsedSpace.toFixed(2)),
  690. percNewUsedSpace: getPercent(startUsedSpace, endUsedSpace),
  691. });
  692. }
  693. async fillForumInfo(startHash: Hash, endHash: Hash): Promise<void> {
  694. const startPosts = await getNextPost(this.api, startHash);
  695. const endPosts = await getNextPost(this.api, endHash);
  696. const startThreads = await getNextThread(this.api, startHash);
  697. const endThreads = await getNextThread(this.api, endHash);
  698. const startCategories = await getNextCategory(this.api, startHash);
  699. const endCategories = await getNextCategory(this.api, endHash);
  700. this.saveStats({
  701. startPosts,
  702. endPosts,
  703. newPosts: endPosts - startPosts,
  704. percNewPosts: getPercent(startPosts, endPosts),
  705. startThreads,
  706. endThreads,
  707. newThreads: endThreads - startThreads,
  708. percNewThreads: getPercent(startThreads, endThreads),
  709. startCategories,
  710. endCategories,
  711. newCategories: endCategories - startCategories,
  712. perNewCategories: getPercent(startCategories, endCategories),
  713. });
  714. }
  715. async buildBlocksEventCache(
  716. startBlock: number,
  717. endBlock: number
  718. ): Promise<void> {
  719. const cacheFile = `${CACHE_FOLDER}/${startBlock}-${endBlock}.json`;
  720. const exists = await fs
  721. .access(cacheFile, fsSync.constants.R_OK)
  722. .then(() => true)
  723. .catch(() => false);
  724. if (!exists) {
  725. console.log("Building events cache...");
  726. let blocksEvents = new Map<number, CacheEvent[]>();
  727. for (let i = startBlock; i < endBlock; ++i) {
  728. process.stdout.write("\rCaching block: " + i + " until " + endBlock);
  729. const blockHash: Hash = await getBlockHash(this.api, i);
  730. let eventRecord: Vec<EventRecord> = await getEvents(
  731. this.api,
  732. blockHash
  733. );
  734. let cacheEvents = new Array<CacheEvent>();
  735. for (let event of eventRecord) {
  736. cacheEvents.push(
  737. new CacheEvent(
  738. event.event.section,
  739. event.event.method,
  740. event.event.data
  741. )
  742. );
  743. }
  744. blocksEvents.set(i, cacheEvents);
  745. }
  746. console.log("\nFinish events cache...");
  747. const json = JSON.stringify(Array.from(blocksEvents.entries()), null, 2);
  748. fsSync.writeFileSync(cacheFile, json);
  749. this.blocksEventsCache = new Map(JSON.parse(json));
  750. } else {
  751. console.log("Cache file found, loading it...");
  752. let fileData = await fs.readFile(cacheFile);
  753. this.blocksEventsCache = new Map(JSON.parse(fileData));
  754. console.log("Cache file loaded...");
  755. }
  756. }
  757. static async connectApi(): Promise<ApiPromise> {
  758. const provider = new WsProvider(PROVIDER_URL);
  759. return await ApiPromise.create({ provider, types });
  760. }
  761. }