// Copyright 2017-2019 @polkadot/react-hooks authors & contributors // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. import { Balance, BlockNumber, EventRecord, Hash, Header } from '@polkadot/types/interfaces'; import { Slash, SessionRewards } from './types'; import BN from 'bn.js'; import { useEffect, useState } from 'react'; import { ApiPromise } from '@polkadot/api'; import { createType } from '@polkadot/types'; import { bnMax, u8aToU8a } from '@polkadot/util'; import useApi from './useApi'; import useCacheKey from './useCacheKey'; interface SerializedSlash { accountId: string; amount: string; } interface Serialized { blockHash: string; blockNumber: string; isEventsEmpty: boolean; reward: string; sessionIndex: string; slashes: SerializedSlash[]; } function fromJSON (sessions: Serialized[]): SessionRewards[] { return sessions.map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): SessionRewards => ({ blockHash: createType('Hash', blockHash), blockNumber: createType('BlockNumber', blockNumber), isEventsEmpty, reward: createType('Balance', reward), sessionIndex: createType('SessionIndex', sessionIndex), slashes: slashes.map(({ accountId, amount }): Slash => ({ accountId: createType('AccountId', accountId), amount: createType('Balance', amount) })) })); } function toJSON (sessions: SessionRewards[], maxSessions: number): Serialized[] { return sessions.map(({ blockHash, blockNumber, isEventsEmpty, reward, sessionIndex, slashes }): Serialized => ({ blockHash: blockHash.toHex(), blockNumber: blockNumber.toHex(), isEventsEmpty, reward: reward.toHex(), sessionIndex: sessionIndex.toHex(), slashes: slashes.map(({ accountId, amount }): SerializedSlash => ({ accountId: accountId.toString(), amount: amount.toHex() })) })).slice(-maxSessions); } function mergeResults (sessions: SessionRewards[], newSessions: SessionRewards[]): SessionRewards[] { const tmp = sessions .concat(newSessions) .sort((a, b): number => a.blockNumber.cmp(b.blockNumber)); return tmp.filter(({ sessionIndex }, index): boolean => index === 0 // for the first, always use it ? true // if the prev has the same sessionIndex, ignore this one : !tmp[index - 1].sessionIndex.eq(sessionIndex) ); } async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise { const results = await api.rpc.state.queryStorage([api.query.session.currentIndex.key()], fromHash, toHash); const headers = await Promise.all( results.map(({ block }): Promise
=> api.rpc.chain.getHeader(block)) ); const events: EventRecord[][] = await Promise.all( results.map(({ block }): Promise => (api.query.system.events.at(block) as unknown as Promise) .then((records): EventRecord[] => records.filter(({ event: { section } }): boolean => section === 'staking') ) .catch((): EventRecord[] => []) // may throw, update metadata for old ) ); const slashes: Slash[][] = events.map((info): Slash[] => info .filter(({ event: { method } }): boolean => method === 'Slash') .map(({ event: { data: [accountId, amount] } }): Slash => ({ accountId: accountId as any, amount: amount as any })) ); const rewards: (Balance | undefined)[] = events.map((info): Balance | undefined => { const rewards = info.filter(({ event: { method } }): boolean => method === 'Reward'); return rewards[0]?.event?.data[0] as Balance; }); return results.map(({ changes: [[, value]] }, index): SessionRewards => ({ blockHash: headers[index].hash, blockNumber: headers[index].number.unwrap(), isEventsEmpty: events[index].length === 0, reward: rewards[index] || createType('Balance'), sessionIndex: createType('SessionIndex', u8aToU8a( value.isSome ? value.unwrap() : new Uint8Array([]) )), slashes: slashes[index] })); } export default function useSessionRewards (maxSessions: number): SessionRewards[] { const { api } = useApi(); const [getCache, setCache] = useCacheKey('hooks:sessionSlashes'); const [filtered, setFiltered] = useState([]); useEffect((): void => { let workQueue = fromJSON(getCache() || []); setImmediate((): void => { api.isReady.then(async (): Promise => { const maxSessionsStore = maxSessions + 1; // assuming first is a bust const sessionLength = (api.consts.babe?.epochDuration as BlockNumber || new BN(500)); const count = Math.min(sessionLength.muln(maxSessionsStore).divn(10).toNumber(), 10000); const bestHeader = await api.rpc.chain.getHeader(); let toHash = bestHeader.hash; let toNumber = bestHeader.number.unwrap().toBn(); let fromHash = api.genesisHash; let fromNumber = bnMax(toNumber.subn(count), new BN(1)); while (true) { fromHash = await api.rpc.chain.getBlockHash(fromNumber as any); const newQueue = await loadSome(api, fromHash, toHash); workQueue = mergeResults(workQueue, newQueue); toHash = fromHash; toNumber = fromNumber; fromNumber = bnMax(toNumber.subn(count), new BN(1)); setCache(toJSON(workQueue, maxSessionsStore)); setFiltered(workQueue.slice(-maxSessions)); const lastNumber = workQueue[workQueue.length - 1]?.blockNumber; if (!lastNumber || fromNumber.eqn(1) || ((workQueue.length >= maxSessionsStore) && fromNumber.lt(lastNumber))) { break; } } }); }); }, []); return filtered; }