import { ApiPromise } from "@polkadot/api"; // types import { AccountId, Balance, BalanceOf, BlockNumber, EventRecord, Hash, } from "@polkadot/types/interfaces"; import { Config, MintStatistics, Statistics, WorkersInfo } from "./types/tokenomics"; import { CacheEvent, Bounty, WorkerReward, SpendingProposal, StatusData, } from "./lib/types"; import { Option, u32, Vec } from "@polkadot/types"; import { ElectionStake, SealedVote, Seats } from "@joystream/types/council"; import { Mint, MintId } from "@joystream/types/mint"; import { ContentId, DataObject } from "@joystream/types/media"; import { CategoryId } from "@joystream/types/forum"; import { MemberId, Membership } from "@joystream/types/members"; import { Proposal, ProposalId, SpendingParams, } from "@joystream/types/proposals"; import { RewardRelationship, RewardRelationshipId, } from "@joystream/types/recurring-rewards"; import { Stake } from "@joystream/types/stake"; import { Worker, WorkerId } from "@joystream/types/working-group"; import { ProposalDetails, ProposalOf } from "@joystream/types/augment/types"; import * as constants from "constants"; import axios from "axios"; // lib import { eventStats, getPercent, getTotalMinted, momentToString } from "./lib"; import { connectApi, getBlock, getBlockHash, getHead, getTimestamp, getIssuance, getEra, getEraStake, getEvents, getCouncil, getCouncilRound, getCouncilSize, getCouncilApplicants, getCouncilApplicantStakes, getCouncilCommitments, getCouncilPayoutInterval, getCouncilPayout, getCouncilElectionDurations, getNextWorker, getWorkers, getWorkerReward, getStake, getCouncilMint, getMintsCreated, getMint, getGroupMint, getNextMember, getMember, getNextPost, getNextThread, getNextCategory, getProposalCount, getProposalInfo, getProposalDetails, getValidatorCount, getValidators, getNextEntity, getNextChannel, getNextVideo, getEntity, getDataObject, getDataObjects, } from "./lib/api"; import { filterMethods, getWorkerRewards, getWorkerRow, getBurnedTokens, getFinalizedSpendingProposals, getActiveValidators, getValidatorsRewards, } from "./lib/rewards"; const fsSync = require("fs"); const fs = fsSync.promises; const parse = require("csv-parse/lib/sync"); export class StatisticsCollector { private api?: ApiPromise; private blocksEventsCache: Map; private statistics: Statistics; constructor() { this.blocksEventsCache = new Map(); this.statistics = new Statistics(); } saveStats(data: any) { Object.keys(data).map((key: string) => (this.statistics[key] = data[key])); } filterCache( filterEvent: (event: CacheEvent) => boolean ): [number, CacheEvent[]][] { const blocks: [number, CacheEvent[]][] = []; for (let block of this.blocksEventsCache) { const [key, events] = block; const filtered = events.filter((event) => filterEvent(event)); if (filtered.length) blocks.push([key, filtered]); } return blocks; } async getStats( startBlock: number, endBlock: number, config: Config ): Promise { const { cacheDir, providerUrl, statusUrl } = config; this.api = await connectApi(providerUrl); const aboveHead = endBlock - Number(await getHead(this.api)); if (aboveHead > 0) { console.log(`End Block is above our Head, wait ${aboveHead} blocks.`); return this.statistics; } let startHash: Hash = await getBlockHash(this.api, startBlock); let endHash: Hash = await getBlockHash(this.api, endBlock); let dateStart = momentToString(await getTimestamp(this.api, startHash)); let dateEnd = momentToString(await getTimestamp(this.api, endHash)); this.saveStats({ dateStart, dateEnd, startBlock, endBlock, newBlocks: endBlock - startBlock, percNewBlocks: getPercent(startBlock, endBlock), }); // run long running tasks in parallel first await Promise.all([ this.buildBlocksEventCache(startBlock, endBlock, cacheDir).then(() => this.fillStats(startBlock, endBlock, startHash, endHash, config) ), this.getFiatEvents(startBlock, endBlock, statusUrl), this.fillMediaUploadInfo(startHash, endHash), ]); this.api.disconnect(); return this.statistics; } fillStats( startBlock: number, endBlock: number, startHash: Hash, endHash: Hash, config: Config ): Promise { eventStats(this.blocksEventsCache); // print event stats return Promise.all([ this.fillTokenInfo(startBlock, endBlock, startHash, endHash, config), this.fillMintsInfo(startHash, endHash), this.fillCouncilInfo(startHash, endHash, config.councilRoundOffset), this.fillCouncilElectionInfo(startBlock), this.fillValidatorInfo(startHash, endHash), this.fillStorageProviderInfo(startBlock, endBlock, startHash, endHash), this.fillCuratorInfo(startHash, endHash), this.fillOperationsInfo(startBlock, endBlock, startHash, endHash), this.fillMembershipInfo(startHash, endHash), this.fillForumInfo(startHash, endHash), ]); } async getApprovedBounties(file: string): Promise { try { await fs.access(file, constants.R_OK); } catch { console.warn("File with spending proposal categories not found: ${file}"); } const fileContent = await fs.readFile(file); const proposals = parse(fileContent).slice(1); console.log(`Loaded ${proposals.length} proposals.`); return proposals .filter( (line: string[]) => line[0] === "Antioch" && line[3] === "Approved" && line[8] === "Bounties" ) .map((bounty: string[]) => { return new Bounty( bounty[0], Number(bounty[1]), bounty[2], bounty[3], Number(bounty[4]), Number(bounty[5]) ); }); } fillSudoSetBalance() { let balancesSetByRoot = 0; this.filterCache(filterMethods.sudoSetBalance).map(([block, events]) => events.forEach(({ data }) => { balancesSetByRoot += Number(data[1]); }) ); this.saveStats({ balancesSetByRoot }); } async fillTokenInfo( startBlock: number, endBlock: number, startHash: Hash, endHash: Hash, config: Config ): Promise { const { burnAddress } = config; const proposalsFile = config.repoDir + config.spendingCategoriesFile; const startIssuance = (await getIssuance(this.api, startHash)).toNumber(); const endIssuance = (await getIssuance(this.api, endHash)).toNumber(); const burnEvents = this.filterCache(filterMethods.getBurnedTokens); this.saveStats({ startIssuance, endIssuance, newIssuance: endIssuance - startIssuance, percNewIssuance: getPercent(startIssuance, endIssuance), newTokensBurn: await getBurnedTokens(burnAddress, burnEvents), }); this.fillSudoSetBalance(); // bounties const bounties = await this.getApprovedBounties(proposalsFile); const blocks = this.filterCache(filterMethods.finalizedSpendingProposals); const spendingProposals: SpendingProposal[] = await getFinalizedSpendingProposals(this.api, blocks); let bountiesTotalPaid = 0; for (let bounty of bounties) { const bountySpendingProposal = spendingProposals.find( (spendingProposal) => spendingProposal.id == bounty.proposalId ); if (bountySpendingProposal) bountiesTotalPaid += bountySpendingProposal.amount; } if (!bountiesTotalPaid) { console.warn( `No bounties in selected period. Need to update ${proposalsFile}?\nLooking for spending proposals titled "bounty":` ); for (const { title, amount } of spendingProposals) { if (!title.toLowerCase().includes("bounty")) continue; bountiesTotalPaid += amount; console.log(` - ${title}: ${amount}`); } } this.saveStats({ bountiesTotalPaid }); const spendingProposalsTotal = spendingProposals.reduce( (n, p) => n + p.amount, 0 ); const newCouncilRewards = await this.computeCouncilReward( endBlock - startBlock, endHash ); const newCuratorInfo = await this.computeWorkingGroupReward( startHash, endHash, "contentDirectory" ); this.saveStats({ spendingProposalsTotal, newCouncilRewards: newCouncilRewards.toFixed(2), newCuratorRewards: newCuratorInfo.rewards.toFixed(2), }); } async getMintInfo( api: ApiPromise, mintId: MintId, startHash: Hash, endHash: Hash ): Promise { const startMint: Mint = await getMint(api, startHash, mintId); const endMint: Mint = await getMint(api, endHash, mintId); let stats = new MintStatistics(); stats.startMinted = getTotalMinted(startMint); stats.endMinted = getTotalMinted(endMint); stats.diffMinted = stats.endMinted - stats.startMinted; stats.percMinted = getPercent(stats.startMinted, stats.endMinted); return stats; } async computeCouncilReward( roundNrBlocks: number, endHash: Hash ): Promise { const payoutInterval = Number( ( (await getCouncilPayoutInterval( this.api, endHash )) as Option ).unwrapOr(0) ); const amountPerPayout = ( (await getCouncilPayout(this.api, endHash)) as BalanceOf ).toNumber(); const [ announcingPeriod, votingPeriod, revealingPeriod, termDuration, ]: number[] = await getCouncilElectionDurations(this.api, endHash); const nrCouncilMembers = ((await getCouncil(this.api, endHash)) as Seats) .length; const totalCouncilRewardsPerBlock = amountPerPayout && payoutInterval ? (amountPerPayout * nrCouncilMembers) / payoutInterval : 0; const councilTermDurationRatio = termDuration / (termDuration + votingPeriod + revealingPeriod + announcingPeriod); const avgCouncilRewardPerBlock = councilTermDurationRatio * totalCouncilRewardsPerBlock; return avgCouncilRewardPerBlock * roundNrBlocks; } // Summarize stakes and rewards at start and end async computeWorkingGroupReward( startHash: Hash, endHash: Hash, workingGroup: string ): Promise { const group = workingGroup + "WorkingGroup"; let info = new WorkersInfo(); // stakes at start const workersStart: WorkerReward[] = await getWorkerRewards( this.api, group, startHash ); workersStart.forEach(({ stake }) => { if (stake) info.startStake += stake.value.toNumber(); }); // stakes at end const workersEnd: WorkerReward[] = await getWorkerRewards( this.api, group, endHash ); let workers = ``; workersEnd.forEach(async (worker) => { if (worker.stake) info.endStake += worker.stake.value.toNumber(); if (!worker.reward) return; let earnedBefore = 0; const hired = workersStart.find((w) => w.id === worker.id); if (hired) earnedBefore = hired.reward.total_reward_received.toNumber(); workers += getWorkerRow(worker, earnedBefore); }); const groupTag = workingGroup === `storage` ? `storageProviders` : workingGroup === `contentDirectory` ? `curators` : workingGroup === `operations` ? `operations` : ``; if (workers.length) { const header = `| # | Member | Status | tJOY / Block | M tJOY Term | M tJOY total |\n|--|--|--|--|--|--|\n`; this.saveStats({ [groupTag]: header + workers }); } else this.saveStats({ [groupTag]: `` }); const mintId = await getGroupMint(this.api, group); const mintStart: Mint = await getMint(this.api, startHash, mintId); const mintEnd: Mint = await getMint(this.api, endHash, mintId); const totalMinted = (m: Mint) => Number(m.total_minted); info.rewards = totalMinted(mintEnd) - totalMinted(mintStart); info.endNrOfWorkers = workersEnd.length; return info; } async computeGroupMintStats( [label, tag]: string[], startHash: Hash, endHash: Hash ) { const group = label + "WorkingGroup"; const mint = await getGroupMint(this.api, group); const info = await this.getMintInfo(this.api, mint, startHash, endHash); let stats: { [key: string]: number } = {}; stats[`start${tag}Minted`] = info.startMinted; stats[`end${tag}Minted`] = info.endMinted; stats[`new${tag}Minted`] = info.diffMinted; stats[`perc${tag}Minted`] = info.percMinted; this.saveStats(stats); } async fillMintsInfo(startHash: Hash, endHash: Hash): Promise { const startNrMints = await getMintsCreated(this.api, startHash); const endNrMints = await getMintsCreated(this.api, endHash); const newMints = endNrMints - startNrMints; // calcuate sum of all mints let totalMinted = 0; let totalMintCapacityIncrease = 0; // summarize old mints for (let i = 0; i < startNrMints; ++i) { const startMint: Mint = await getMint(this.api, startHash, i); const endMint: Mint = await getMint(this.api, endHash, i); const startMintTotal = getTotalMinted(startMint); const endMintTotal = getTotalMinted(endMint); totalMinted += endMintTotal - startMintTotal; totalMintCapacityIncrease += parseInt(endMint.getField("capacity").toString()) - parseInt(startMint.getField("capacity").toString()); } // summarize new mints for (let i = startNrMints; i < endNrMints; ++i) { const endMint: Mint = await getMint(this.api, endHash, i); if (endMint) totalMinted += getTotalMinted(endMint); } this.saveStats({ newMints, totalMinted, totalMintCapacityIncrease }); // council const councilInfo = await this.getMintInfo( this.api, await getCouncilMint(this.api, endHash), startHash, endHash ); this.saveStats({ startCouncilMinted: councilInfo.startMinted, endCouncilMinted: councilInfo.endMinted, newCouncilMinted: councilInfo.diffMinted, percNewCouncilMinted: councilInfo.percMinted, }); // working groups const groups = [ ["contentDirectory", "Curator"], ["storage", "Storage"], ["operations", "Operations"], ].forEach((group) => this.computeGroupMintStats(group, startHash, endHash)); } async fillCouncilInfo( startHash: Hash, endHash: Hash, councilRoundOffset: number ): Promise { const round = await getCouncilRound(this.api, startHash); const startNrProposals = await getProposalCount(this.api, startHash); const endNrProposals = await getProposalCount(this.api, endHash); let approvedProposals = new Set(); for (let [key, blockEvents] of this.blocksEventsCache) { for (let event of blockEvents) { if ( event.section == "proposalsEngine" && event.method == "ProposalStatusUpdated" ) { let statusUpdateData = event.data[1] as any; let finalizeData = statusUpdateData.finalized as any; if (finalizeData && finalizeData.proposalStatus.approved) { approvedProposals.add(Number(event.data[0])); } } } } this.saveStats({ councilRound: round - councilRoundOffset, councilMembers: await getCouncilSize(this.api, startHash), newProposals: endNrProposals - startNrProposals, newApprovedProposals: approvedProposals.size, }); } async fillCouncilElectionInfo(startBlock: number): Promise { let startBlockHash = await getBlockHash(this.api, startBlock); let events: Vec = await getEvents(this.api, startBlockHash); let isStartBlockFirstCouncilBlock = events.some( ({ event }) => event.section == "councilElection" && event.method == "CouncilElected" ); if (!isStartBlockFirstCouncilBlock) return console.warn( "Note: The given start block is not the first block of the council round so council election information will be empty" ); let lastBlockHash = await getBlockHash(this.api, startBlock - 1); let applicants: Vec = await getCouncilApplicants( this.api, lastBlockHash ); let electionApplicantsStakes = 0; for (let applicant of applicants) { const applicantStakes: ElectionStake = await getCouncilApplicantStakes( this.api, lastBlockHash, applicant ); electionApplicantsStakes += applicantStakes.new.toNumber(); } // let seats = await getCouncil(this.api,startBlockHash) as Seats; //TODO: Find a more accurate way of getting the votes const votes: Vec = await getCouncilCommitments( this.api, lastBlockHash ); this.saveStats({ electionApplicants: applicants.length, electionApplicantsStakes, electionVotes: votes.length, }); } async fillValidatorInfo(startHash: Hash, endHash: Hash): Promise { const startTimestamp: number = await getTimestamp(this.api, startHash); const endTimestamp: number = await getTimestamp(this.api, endHash); const blocks = this.statistics.newBlocks; const avgBlockProduction = (endTimestamp - startTimestamp) / 1000 / blocks; const maxStartValidators = await getValidatorCount(this.api, startHash); const startValidators = await getActiveValidators(this.api, startHash); const maxEndValidators = await getValidatorCount(this.api, endHash); const endValidators = await getActiveValidators(this.api, endHash, true); const startEra: number = await getEra(this.api, startHash); const endEra: number = await getEra(this.api, endHash); const startStake = await getEraStake(this.api, startHash, startEra); const endStake = await getEraStake(this.api, endHash, endEra); this.saveStats({ avgBlockProduction: Number(avgBlockProduction.toFixed(2)), startValidators: startValidators.length + " / " + maxStartValidators, endValidators: endValidators.length + " / " + maxEndValidators, percValidators: getPercent(startValidators.length, endValidators.length), startValidatorsStake: startStake, endValidatorsStake: endStake, percNewValidatorsStake: getPercent(startStake, endStake), newValidatorRewards: await getValidatorsRewards( this.filterCache(filterMethods.newValidatorsRewards) ), }); } async fillStorageProviderInfo( startBlock: number, endBlock: number, startHash: Hash, endHash: Hash ): Promise { let storageProvidersRewards = await this.computeWorkingGroupReward( startHash, endHash, "storage" ); const newStorageProviderReward = Number( storageProvidersRewards.rewards.toFixed(2) ); const startStorageProvidersStake = storageProvidersRewards.startStake; const endStorageProvidersStake = storageProvidersRewards.endStake; const group = "storageWorkingGroup"; const startStorageProviders = await getWorkers(this.api, group, startHash); const endStorageProviders = await getWorkers(this.api, group, endHash); this.saveStats({ newStorageProviderReward, startStorageProvidersStake, endStorageProvidersStake, percNewStorageProviderStake: getPercent( startStorageProvidersStake, endStorageProvidersStake ), startStorageProviders, endStorageProviders, percNewStorageProviders: getPercent( startStorageProviders, endStorageProviders ), }); } async fillCuratorInfo(startHash: Hash, endHash: Hash): Promise { const group = "contentDirectoryWorkingGroup"; const startCurators = await getWorkers(this.api, group, startHash); const endCurators = await getWorkers(this.api, group, endHash); this.saveStats({ startCurators, endCurators, percNewCurators: getPercent(startCurators, endCurators), }); } async fillOperationsInfo( startBlock: number, endBlock: number, startHash: Hash, endHash: Hash ): Promise { const operationsRewards = await this.computeWorkingGroupReward( startHash, endHash, "operations" ); const newOperationsReward = operationsRewards.rewards.toFixed(2); const startOperationsStake = operationsRewards.startStake; const endOperationsStake = operationsRewards.endStake; const group = "operationsWorkingGroup"; const startWorkers = await getWorkers(this.api, group, startHash); const endWorkers = await getWorkers(this.api, group, endHash); this.saveStats({ newOperationsReward: Number(newOperationsReward), startOperationsWorkers: startWorkers, endOperationsWorkers: endWorkers, percNewOperationsWorkers: getPercent(startWorkers, endWorkers), startOperationsStake, endOperationsStake, percNewOperationstake: getPercent( startOperationsStake, endOperationsStake ), }); } async fillMembershipInfo(startHash: Hash, endHash: Hash): Promise { const startMembers = await getNextMember(this.api, startHash); const endMembers = await getNextMember(this.api, endHash); this.saveStats({ startMembers, endMembers, newMembers: endMembers - startMembers, percNewMembers: getPercent(startMembers, endMembers), }); } async fillMediaUploadInfo(startHash: Hash, endHash: Hash): Promise { console.log(`Collecting Media stats`); const startMedia = Number(await getNextVideo(this.api, startHash)); const endMedia = Number(await getNextVideo(this.api, endHash)); const startChannels = Number(await getNextChannel(this.api, startHash)); const endChannels = Number(await getNextChannel(this.api, endHash)); // count size let startUsedSpace = 0; let endUsedSpace = 0; const startBlock = await getBlock(this.api, startHash); const endBlock = await getBlock(this.api, endHash); getDataObjects(this.api).then((dataObjects: Map) => { for (let [key, dataObject] of dataObjects) { const added = dataObject.added_at.block.toNumber(); const start = startBlock.block.header.number.toNumber(); const end = endBlock.block.header.number.toNumber(); if (added < start) startUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024; if (added < end) endUsedSpace += dataObject.size_in_bytes.toNumber() / 1024 / 1024; } if (!startUsedSpace || !endUsedSpace) console.log(`space start, end`, startUsedSpace, endUsedSpace); this.saveStats({ startMedia, endMedia, percNewMedia: getPercent(startMedia, endMedia), startChannels, endChannels, percNewChannels: getPercent(startChannels, endChannels), startUsedSpace: Number(startUsedSpace.toFixed(2)), endUsedSpace: Number(endUsedSpace.toFixed(2)), percNewUsedSpace: getPercent(startUsedSpace, endUsedSpace), }); }); } async fillForumInfo(startHash: Hash, endHash: Hash): Promise { const startPosts = await getNextPost(this.api, startHash); const endPosts = await getNextPost(this.api, endHash); const startThreads = await getNextThread(this.api, startHash); const endThreads = await getNextThread(this.api, endHash); const startCategories = await getNextCategory(this.api, startHash); const endCategories = await getNextCategory(this.api, endHash); this.saveStats({ startPosts, endPosts, newPosts: endPosts - startPosts, percNewPosts: getPercent(startPosts, endPosts), startThreads, endThreads, newThreads: endThreads - startThreads, percNewThreads: getPercent(startThreads, endThreads), startCategories, endCategories, newCategories: endCategories - startCategories, perNewCategories: getPercent(startCategories, endCategories), }); } async getFiatEvents( startBlockHeight: number, endBlockHeight: number, statusUrl: string ) { let sumerGenesis = new Date("2021-04-07T18:20:54.000Z"); console.log("Fetching fiat events...."); await axios.get(statusUrl).then(({ data }) => { const { burns, exchanges, dollarPoolChanges } = data as StatusData; console.log("# Exchanges"); let filteredExchanges = exchanges.filter( (exchange) => exchange.blockHeight >= startBlockHeight && exchange.blockHeight <= endBlockHeight && new Date(exchange.date) > sumerGenesis ); for (let filteredExchange of filteredExchanges) { console.log( `Block: ${filteredExchange.blockHeight}, USD: ${filteredExchange.amountUSD}` ); } let filteredBurns = burns.filter( (burn: any) => burn.blockHeight >= startBlockHeight && burn.blockHeight <= endBlockHeight && new Date(burn.date) > sumerGenesis ); if (filteredBurns.length) { console.log("# Burns"); filteredBurns.forEach(({ blockHeight, amount }) => console.log(`Block: ${blockHeight}, tJOY: ${amount}`) ); } console.log("# Dollar Pool Changes"); const allDollarPoolChanges = dollarPoolChanges.filter( (dollarPoolChange: any) => dollarPoolChange.blockHeight >= startBlockHeight && dollarPoolChange.blockHeight <= endBlockHeight && new Date(dollarPoolChange.blockTime) > sumerGenesis ); const filteredDollarPoolChanges = dollarPoolChanges.filter( (dollarPoolChange: any) => dollarPoolChange.blockHeight >= startBlockHeight && dollarPoolChange.blockHeight <= endBlockHeight && dollarPoolChange.change > 0 && new Date(dollarPoolChange.blockTime) > sumerGenesis ); let dollarPoolRefills = ``; if (filteredDollarPoolChanges.length > 0) { dollarPoolRefills = "| Refill, USD | Reason | Block # |\n|---------------------|--------------|--------------|\n"; filteredDollarPoolChanges.forEach(({ blockHeight, change, reason }) => { console.log( `Block: ${blockHeight}, USD: ${change}, Reason: ${reason}` ); dollarPoolRefills += `| ${change} | ${reason} | ${blockHeight} |\n`; }); } // calculate inflation let startTermExchangeRate = 0; let endTermExchangeRate = 0; if (filteredExchanges.length) { const lastExchangeEvent = filteredExchanges[filteredExchanges.length - 1]; startTermExchangeRate = filteredExchanges[0].price * 1000000; endTermExchangeRate = lastExchangeEvent.price * 1000000; } else { startTermExchangeRate = filteredDollarPoolChanges[0].rateAfter * 1000000; const lastEvent = filteredDollarPoolChanges[filteredDollarPoolChanges.length - 1]; endTermExchangeRate = lastEvent.rateAfter * 1000000; } let inflationPct = getPercent(endTermExchangeRate, startTermExchangeRate); console.log( "# USD / 1M tJOY Rate\n", `@ Term start (block #${startBlockHeight}: ${startTermExchangeRate}\n`, `@ Term end (block #${endBlockHeight}: ${endTermExchangeRate}\n`, `Inflation: ${inflationPct}` ); const startDollarPool = allDollarPoolChanges[0].change > 0 ? allDollarPoolChanges[0].valueAfter - allDollarPoolChanges[0].change : allDollarPoolChanges[0].valueAfter; const endDollarEvent = allDollarPoolChanges[allDollarPoolChanges.length - 1]; const endDollarPool = endDollarEvent.valueAfter; const dollarPoolPctChange = getPercent(startDollarPool, endDollarPool); this.saveStats({ startTermExchangeRate: startTermExchangeRate.toFixed(2), endTermExchangeRate: endTermExchangeRate.toFixed(2), inflationPct, startDollarPool: startDollarPool.toFixed(2), endDollarPool: endDollarPool.toFixed(2), dollarPoolPctChange, dollarPoolRefills, }); }); } async buildBlocksEventCache( startBlock: number, endBlock: number, cacheDir: string ): Promise { const cacheFile = `${cacheDir}/${startBlock}-${endBlock}.json`; const exists = await fs .access(cacheFile, fsSync.constants.R_OK) .then(() => true) .catch(() => false); if (!exists) { console.log("Building events cache..."); let blocksEvents = new Map(); for (let i = startBlock; i < endBlock; ++i) { process.stdout.write("\rCaching block: " + i + " until " + endBlock); const blockHash: Hash = await getBlockHash(this.api, i); let eventRecord: EventRecord[] = []; try { eventRecord = await getEvents(this.api, blockHash); } catch (e) { console.warn(`Failed to get events.`, e); } let cacheEvents = new Array(); for (let { event } of eventRecord) { if (!event) { console.warn(`empty event record`); continue; } cacheEvents.push( new CacheEvent(event.section, event.method, event.data) ); } blocksEvents.set(i, cacheEvents); } console.log("\nFinish events cache..."); const json = JSON.stringify(Array.from(blocksEvents.entries()), null, 2); fsSync.writeFileSync(cacheFile, json); this.blocksEventsCache = new Map(JSON.parse(json)); } else { console.log("Cache file found, loading it..."); let fileData = await fs.readFile(cacheFile); this.blocksEventsCache = new Map(JSON.parse(fileData)); } } }