tokenomics.ts 29 KB

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