ghost-portal/packages/react-hooks/src/useSessionRewards.tsx
2019-11-16 20:21:46 +01:00

157 lines
5.7 KiB
TypeScript

// 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<SessionRewards[]> {
const results = await api.rpc.state.queryStorage([api.query.session.currentIndex.key()], fromHash, toHash);
const headers = await Promise.all(
results.map(({ block }): Promise<Header> => api.rpc.chain.getHeader(block))
);
const events: EventRecord[][] = await Promise.all(
results.map(({ block }): Promise<EventRecord[]> =>
(api.query.system.events.at(block) as unknown as Promise<EventRecord[]>)
.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<Serialized[]>('hooks:sessionSlashes');
const [filtered, setFiltered] = useState<SessionRewards[]>([]);
useEffect((): void => {
let workQueue = fromJSON(getCache() || []);
setImmediate((): void => {
api.isReady.then(async (): Promise<void> => {
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;
}