import { CronJob } from "cron" ; import { ApiPromise } from "@polkadot/api" ; import { bnToBn } from "@polkadot/util" ; import ApiHandler from "./ApiHandler" ; import ChainData from "./chaindata" ; import { Config , NominatorConfig } from "./config" ; import { FIFTY_KSM , KUSAMA_FOUR_DAYS_ERAS , POLKADOT_FOUR_DAYS_ERAS , SCOREKEEPER_CRON , TEN_PERCENT , TEN_THOUSAND_DOT , FIVE_THOUSAND_DOT , THREE_PERCENT , SIXTEEN_HOURS , TEN_KSM , BALANCE_BUFFER_PERCENT , BALANCE_BUFFER_AMOUNT , } from "./constants" ; import { checkAllValidateIntentions , OTV } from "./constraints" ; import Db from "./db" ; import logger from "./logger" ; import Nominator from "./nominator" ; import { CandidateData , ClaimerConfig , Stash } from "./types" ; import { formatAddress , getNow , sleep , addressUrl , toDecimals } from "./util" ; import { startActiveValidatorJob , startCancelCron , startEraPointsJob , startEraStatsJob , startExecutionJob , startInclusionJob , startLocationStatsJob , startMonitorJob , startRewardClaimJob , startScoreJob , startSessionKeyJob , startStaleNominationCron , startUnclaimedEraJob , startValidatityJob , startValidatorPrefJob , startCouncilJob , startSubscanJob , startDemocracyJob , } from "./cron" ; import Claimer from "./claimer" ; import { activeValidatorJob , eraPointsJob , inclusionJob , monitorJob , sessionKeyJob , unclaimedErasJob , validatorPrefJob , validityJob , } from "./jobs" ; import Monitor from "./monitor" ; import { Subscan } from "./subscan" ; import { asc } from "./score" ; type NominatorGroup = NominatorConfig [ ] ; type SpawnedNominatorGroup = Nominator [ ] ; export const autoNumNominations = async ( api , nominator , db ? ) : Promise < any > => { const chainType = await api . rpc . system . chain ( ) ; const denom = chainType . toString ( ) == "Polkadot" ? 10000000000 : 1000000000000 ; const stash = await nominator . stash ( ) ; if ( ! stash ) return 0 ; const stashQuery = await api . query . system . account ( stash ) ; const stashBal = ( parseFloat ( stashQuery . data . free ) + parseFloat ( stashQuery . data . reserved ) ) / denom ; const bufferedBalance = stashBal - Math . max ( BALANCE_BUFFER_PERCENT * stashBal , BALANCE_BUFFER_AMOUNT ) ; const query = await api . derive . staking . electedInfo ( ) ; const { info } = query ; const totalStakeAmounts = [ ] ; for ( const validator of info ) { const { exposure } = validator ; const { total , own , others } = exposure ; const formattedTotal = parseFloat ( total . toBigInt ( ) ) / denom ; if ( formattedTotal > 0 ) { totalStakeAmounts . push ( formattedTotal ) ; } } const sorted = totalStakeAmounts . sort ( ( a , b ) => a - b ) ; let sum = 0 ; let amount = 1 ; while ( sum < bufferedBalance ) { const offset = 5 ; const lowestNum = sorted . slice ( offset , offset + amount ) ; sum = lowestNum . reduce ( ( a , b ) => a + b , 0 ) ; if ( sum < bufferedBalance ) { amount ++ ; } else { amount -- ; const lowestNum = sorted . slice ( offset , offset + amount ) ; sum = lowestNum . reduce ( ( a , b ) => a + b , 0 ) ; break ; } } const adjustedNominationAmount = Math . min ( amount , 24 ) ; const newBondedAmount = ( 1 + BALANCE_BUFFER_PERCENT ) * sum ; const targetValStake = newBondedAmount / adjustedNominationAmount ; nominator . nominationNum = adjustedNominationAmount ; nominator . targetBond = newBondedAmount ; nominator . avgStake = targetValStake ; if ( db ) { await db . setNominatorAvgStake ( nominator . address , targetValStake ) ; } logger . info ( `{Scorekeeper::autoNom} stash: ${ stash } with balance ${ stashBal } should adjust balance to ${ newBondedAmount } and can elect ${ adjustedNominationAmount } validators, each having ~${ targetValStake } stake` ) ; return { nominationNum : adjustedNominationAmount , newBondedAmount : newBondedAmount , targetValStake : targetValStake , } ; } ; export default class ScoreKeeper { public handler : ApiHandler ; public bot : any ; public chaindata : ChainData ; public config : Config ; public constraints : OTV ; public currentEra = 0 ; public currentTargets : string [ ] ; public db : Db ; private rewardDestinationCache : string [ ] ; private candidateCache : any [ ] ; private isUpdatingEras = false ; private ending = false ; private nominating = false ; private nominatorGroups : Array < SpawnedNominatorGroup > = [ ] ; private claimer : Claimer ; private monitor : Monitor ; private subscan : Subscan ; constructor ( handler , db , config , bot = false ) { this . handler = handler ; this . db = db ; this . chaindata = new ChainData ( this . handler ) ; this . handler . on ( "reward" , async ( data ) => { const { stash , amount } = data ; if ( this . rewardDestinationCache && this . rewardDestinationCache . includes ( stash ) ) { logger . info ( `{scorekeeper::reward} ${ stash } claimed reward of ${ amount }. Updating eras....` ) ; const unclaimedEras = await this . chaindata . getUnclaimedEras ( stash , db ) ; await db . setUnclaimedEras ( stash , unclaimedEras ) ; await this . constraints . checkCandidateStash ( stash ) ; await this . constraints . scoreAllCandidates ( ) ; } for ( const nomGroup of this . nominatorGroups ) { for ( const nom of nomGroup ) { const nomStash = await nom . stash ( ) ; if ( ! nomStash ) continue ; if ( nomStash == stash ) { const activeEra = await this . chaindata . getActiveEraIndex ( ) ; await this . db . updateAccountingRecord ( nom . controller , nomStash , activeEra . toString ( ) , amount ) ; } } } } ) ; this . handler . on ( "someOffline" , async ( data ) => { const { offlineVals } = data ; const session = await this . chaindata . getSession ( ) ; for ( const val of offlineVals ) { const candidate = await this . db . getCandidate ( val ) ; if ( ! candidate ) return ; const reason = `${ candidate . name } had an offline event in session ${ session - 1 }` ; let alreadyFaulted = false ; for ( const fault of candidate . faultEvents ) { if ( fault . reason === reason ) { alreadyFaulted = true ; } } if ( alreadyFaulted ) continue ; logger . info ( `{ScoreKeeper::SomeOffline} ${ reason }` ) ; await this . botLog ( reason ) ; await this . db . pushFaultEvent ( candidate . stash , reason ) ; await this . dockPoints ( candidate . stash ) ; } } ) ; this . handler . on ( "newSession" , async ( data ) => { const { sessionIndex } = data ; logger . info ( `{Session::NewSession} New Session Event: ${ sessionIndex }` ) ; await checkAllValidateIntentions ( this . config , this . chaindata , this . db , this . candidateCache ) ; } ) ; this . config = config ; this . bot = bot ; this . constraints = new OTV ( this . handler , this . config . constraints . skipConnectionTime , this . config . constraints . skipIdentity , this . config . constraints . skipStakedDestination , this . config . constraints . skipClientUpgrade , this . config . constraints . skipUnclaimed , this . config . global . networkPrefix == 2 ? TEN_KSM : FIVE_THOUSAND_DOT , this . config . global . networkPrefix == 2 ? TEN_PERCENT : THREE_PERCENT , this . config . global . networkPrefix == 2 ? KUSAMA_FOUR_DAYS_ERAS : POLKADOT_FOUR_DAYS_ERAS , this . config , this . db ) ; this . monitor = new Monitor ( db , SIXTEEN_HOURS ) ; this . subscan = new Subscan ( this . config . subscan . baseV1Url , this . config . subscan . baseV2Url , this . config . global . networkPrefix == 2 ? Math . pow ( 10 , 12 ) : Math . pow ( 10 , 10 ) ) ; this . populateCandidates ( ) ; this . populateRewardDestinationCache ( ) ; } async populateCandidates ( ) { this . candidateCache = await this . db . allCandidates ( ) ; } async populateRewardDestinationCache ( ) { const allCandidates = await this . db . allCandidates ( ) ; const rewardAddresses = [ ] ; for ( const candidate of allCandidates ) { if ( ! ! candidate . rewardDestination && candidate . rewardDestination . length == 48 ) { rewardAddresses . push ( candidate . rewardDestination ) ; continue ; } rewardAddresses . push ( candidate . stash ) ; rewardAddresses . push ( candidate . controller ) ; } this . rewardDestinationCache = rewardAddresses ; } getAllNominatorGroups ( ) { return this . nominatorGroups ; } getAllNominatorControllers ( ) { const controllers = [ ] ; for ( const group of this . nominatorGroups ) { controllers . push ( ... group . map ( ( n ) => n . controller ) ) ; } return controllers ; } getNominatorGroupAtIndex ( index ) { if ( index < 0 || index >= this . nominatorGroups . length ) { throw new Error ( "Index out of bounds." ) ; } return this . nominatorGroups [ index ] ; } async botLog ( msg ) { if ( this . bot ) { await this . bot . sendMessage ( msg ) ; } } _spawn ( cfg , networkPrefix = 2 ) { return new Nominator ( this . handler , this . db , cfg , networkPrefix , this . bot ) ; } async addNominatorGroup ( nominatorGroup ) { let group = [ ] ; const now = getNow ( ) ; for ( const nomCfg of nominatorGroup ) { const nom = this . _spawn ( nomCfg , this . config . global . networkPrefix ) ; const api = await this . handler . getApi ( ) ; const ledger = await api . query . staking . ledger ( nom . controller ) ; if ( ! ledger ) { logger . info ( `{Scorekeeper::addNominatorGroup} ${ nom . controller } is not bonded, skipping...` ) ; continue ; } else { const stash = await nom . stash ( ) ; const payee = await nom . payee ( ) ; const [ bonded , err ] = await this . chaindata . getBondedAmount ( stash ) ; const proxy = nom . isProxy ? nom . address : "" ; const proxyDelay = nom . proxyDelay ; const { nominationNum , newBondedAmount , targetValStake } = await autoNumNominations ( api , nom , this . db ) ; await this . db . addNominator ( nom . controller , stash , proxy , bonded , now , proxyDelay , payee , targetValStake , nominationNum , newBondedAmount ) ; await this . db . newAccountingRecord ( stash , nom . controller ) ; group . push ( nom ) ; } } group = group . sort ( ( a , b ) => a . avgStake - b . avgStake ) ; this . nominatorGroups . push ( group ) ; const nominatorGroupString = ( await Promise . all ( group . map ( async ( n ) => { const stash = await n . stash ( ) ; const proxy = ( await n . _isProxy ) ? `/ ${ n . address }` : "" ; return `- ${ n . controller } / ${ stash } ${ proxy }` ; } ) ) ) . join ( "\n" ) ; const nominatorGroupStringHtml = ( await Promise . all ( group . map ( async ( n ) => { const stash = await n . stash ( ) ; const name = ( await this . db . getChainMetadata ( ) ) . name ; const decimals = name == "Kusama" ? 12 : 10 ; const [ rawBal , err ] = await this . chaindata . getBondedAmount ( stash ) ; const bal = toDecimals ( rawBal , decimals ) ; const sym = name == "Kusama" ? "KSM" : "DOT" ; const proxy = ( await n . _isProxy ) ? `/ ${ addressUrl ( n . address , this . config ) }` : "" ; return `- ${ addressUrl ( n . controller , this . config ) } / ${ addressUrl ( stash , this . config ) } (${ bal } ${ sym }) ${ proxy }` ; } ) ) ) . join ( "<br>" ) ; logger . info ( `Nominator group added! Nominator addresses (Controller / Stash / Proxy):\n${ nominatorGroupString }` ) ; await this . botLog ( `<h4>Nominator group added! Nominator addresses (Controller / Stash / Proxy):</h4><br> ${ nominatorGroupStringHtml }` ) ; const currentNominationsGroupStringHtml = ( await Promise . all ( group . map ( async ( n ) => { const stash = await n . stash ( ) ; const nominations = await this . db . getNominator ( stash ) ; const current = nominations . current . map ( ( val ) => { return `- ${ val . name }<br>` ; } ) ; return `- ${ addressUrl ( n . controller , this . config ) } / ${ addressUrl ( stash , this . config ) } <br> Current Nominations:<br> ${ current }` ; } ) ) ) . join ( "<br>" ) ; await this . botLog ( currentNominationsGroupStringHtml ) ; return true ; } async addClaimer ( claimerCfg ) { const claimer = new Claimer ( this . handler , this . db , claimerCfg , this . config . global . networkPrefix , this . bot ) ; this . claimer = claimer ; await this . botLog ( `<h4>Added Reward Claimer:</h4><br> - ${ this . claimer . address }` ) ; return true ; } async begin ( ) { logger . info ( `(Scorekeeper::begin) Starting Scorekeeper.` ) ; if ( this . config . scorekeeper . forceRound ) { logger . info ( `(Scorekeeper::begin) Force Round: ${ this . config . scorekeeper . forceRound } starting round....` ) ; await this . startRound ( ) ; } const scoreKeeperFrequency = this . config . cron . scorekeeper ? this . config . cron . scorekeeper : SCOREKEEPER_CRON ; const mainCron = new CronJob ( scoreKeeperFrequency , async ( ) => { logger . info ( `(Scorekeeper::mainCron) Running mainCron of Scorekeeper with frequency ${ scoreKeeperFrequency }` ) ; if ( this . ending ) { logger . info ( `(Scorekeeper::mainCron) ROUND IS CURRENTLY ENDING.` ) ; return ; } const [ activeEra , err ] = await this . chaindata . getActiveEraIndex ( ) ; if ( err ) { logger . warn ( `CRITICAL: ${ err }` ) ; return ; } const { lastNominatedEraIndex } = await this . db . getLastNominatedEraIndex ( ) ; const eraBuffer = this . config . global . networkPrefix == 0 ? 1 : 4 ; const isNominationRound = Number ( lastNominatedEraIndex ) <= activeEra - eraBuffer ; if ( isNominationRound ) { logger . info ( `(Scorekeeper::mainCron) Last nomination was in era ${ lastNominatedEraIndex }. Current era is ${ activeEra }. This is a nomination round.` ) ; if ( ! this . nominatorGroups ) { logger . info ( "(Scorekeeper::mainCron) No nominators spawned. Skipping round." ) ; return ; } if ( ! this . config . scorekeeper . nominating ) { logger . info ( "(Scorekeeper::mainCron) Nominating is disabled in the settings. Skipping round." ) ; return ; } const allCurrentTargets = [ ] ; for ( const nomGroup of this . nominatorGroups ) { for ( const nominator of nomGroup ) { const currentTargets = await this . db . getCurrentTargets ( nominator . controller ) ; allCurrentTargets . push ( currentTargets ) ; } } this . currentTargets = allCurrentTargets ; if ( ! this . currentTargets ) { logger . info ( "(Scorekeeper::mainCron) Current Targets is empty. Starting round." ) ; await this . startRound ( ) ; } else { logger . info ( `(Scorekeeper::mainCron) Current Targets: ${ this . currentTargets }. Ending round.` ) ; await this . endRound ( ) ; await this . startRound ( ) ; } } } ) ; const candidates = await this . db . allCandidates ( ) ; try { await startMonitorJob ( this . config , this . db , this . monitor ) ; await startValidatityJob ( this . config , this . db , this . constraints , this . chaindata , candidates ) ; await startEraPointsJob ( this . config , this . db , this . chaindata ) ; await startActiveValidatorJob ( this . config , this . db , this . chaindata ) ; await startInclusionJob ( this . config , this . db , this . chaindata ) ; await startSessionKeyJob ( this . config , this . db , this . chaindata ) ; await startUnclaimedEraJob ( this . config , this . db , this . chaindata ) ; await startValidatorPrefJob ( this . config , this . db , this . chaindata ) ; await startExecutionJob ( this . handler , this . nominatorGroups , this . config , this . db , this . bot ) ; await startCancelCron ( this . config , this . handler , this . db , this . nominatorGroups , this . chaindata , this . bot ) ; await startStaleNominationCron ( this . config , this . handler , this . db , this . nominatorGroups , this . chaindata , this . bot ) ; await startScoreJob ( this . config , this . constraints ) ; await startEraStatsJob ( this . db , this . config , this . chaindata ) ; await startLocationStatsJob ( this . config , this . db , this . chaindata ) ; await startCouncilJob ( this . config , this . db , this . chaindata ) ; startDemocracyJob ( this . config , this . db , this . chaindata ) ; } catch ( e ) { logger . info ( `{Scorekeeper::RunCron} There was an error running some cron jobs...` ) ; console . log ( e ) ; } mainCron . start ( ) ; } async startRound ( ) { if ( this . nominating ) return ; this . nominating = true ; const now = new Date ( ) . getTime ( ) ; this . currentEra = await this . _getCurrentEra ( ) ; logger . info ( `(Scorekeeper::startRound) New round starting at ${ now } for next Era ${ this . currentEra + 1 }` ) ; this . botLog ( `New round is starting! Era ${ this . currentEra } will begin new nominations.` ) ; const proxyTxs = await this . db . getAllDelayedTxs ( ) ; const NUM_NOMINATORS = 20 ; if ( proxyTxs . length >= NUM_NOMINATORS ) { const infoMsg = `(Scorekeeper::startRound) round was started with ${ proxyTxs . length } pending proxy txs. Skipping Round.` ; logger . info ( infoMsg ) ; this . botLog ( infoMsg ) ; return ; } const allCandidates = await this . db . allCandidates ( ) ; for ( const candidate of allCandidates ) { await this . constraints . checkCandidate ( candidate ) ; } await this . constraints . scoreCandidates ( allCandidates , this . db ) ; await sleep ( 6000 ) ; let validCandidates = allCandidates . filter ( ( candidate ) => candidate . valid ) ; validCandidates = await Promise . all ( validCandidates . map ( async ( candidate ) => { const score = await this . db . getValidatorScore ( candidate . stash ) ; const scoredCandidate = { name : candidate . name , stash : candidate . stash , total : score . total , } ; return scoredCandidate ; } ) ) ; validCandidates = validCandidates . sort ( ( a , b ) => { return b . total - a . total ; } ) ; logger . info ( `{Scorekeeper::startRound} number of all candidates: ${ allCandidates . length } valid candidates: ${ validCandidates . length }` ) ; const numValidatorsNominated = await this . _doNominations ( validCandidates , this . nominatorGroups ) ; if ( numValidatorsNominated > 0 ) { logger . info ( `{Scorekeeper::startRound} ${ numValidatorsNominated } nominated this round, setting last nominated era to ${ this . currentEra }` ) ; await this . db . setLastNominatedEraIndex ( this . currentEra ) ; } else { logger . info ( `{Scorekeeper::startRound} ${ numValidatorsNominated } nominated this round, lastNominatedEra not set...` ) ; } this . nominating = false ; return this . currentTargets ; } async _doNominations ( candidates , nominatorGroups = [ ] , dryRun = false ) { if ( candidates . length == 0 ) { logger . info ( `{ScoreKeeper::_doNominations} Candidates length was 0. Skipping nominations` ) ; return ; } const allTargets = candidates . map ( ( c ) => c . stash ) ; let counter = 0 ; for ( const nomGroup of nominatorGroups ) { const sortedNominators = nomGroup . sort ( ( a , b ) => a . avgStake - b . avgStake ) ; for ( const nominator of sortedNominators ) { const api = await this . chaindata . handler . getApi ( ) ; const denom = await this . chaindata . getDenom ( ) ; const autoNom = await autoNumNominations ( api , nominator , this . db ) ; const { nominationNum , newBondedAmount : formattedNewBondedAmount , targetValStake , } = autoNom ; const stash = await nominator . stash ( ) ; const [ currentBondedAmount , bondErr ] = await this . chaindata . getBondedAmount ( stash ) ; const newBondedAmount = formattedNewBondedAmount * denom ; logger . info ( `{Scorekeepr::_doNominations} ${ nominator . address } number of nominations: ${ nominationNum } newBondedAmount: ${ newBondedAmount } targetValStake: ${ targetValStake }` ) ; const balance = await this . chaindata . getBalance ( nominator . address ) ; const metadata = await this . db . getChainMetadata ( ) ; const network = metadata . name . toLowerCase ( ) ; const free = toDecimals ( Number ( balance . free ) , metadata . decimals ) ; if ( free < 0.5 ) { logger . info ( `{Scorekeeper::_doNominations} Nominator has low free balance: ${ free }` ) ; this . botLog ( `Nominator Account ${ addressUrl ( nominator . address , this . config ) } has low free balance: ${ free }` ) ; continue ; } const targets = allTargets . slice ( counter , counter + nominationNum ) ; counter = counter + nominationNum ; if ( targets . length == 0 ) { logger . info ( `{ScoreKeeper::_doNominations} targets length was 0. Skipping nominations` ) ; return ; } await nominator . adjustBond ( newBondedAmount , Number ( currentBondedAmount ) ) ; await sleep ( 10000 ) ; await nominator . nominate ( targets , dryRun || this . config . global . dryRun ) ; await sleep ( 16000 ) ; const targetsString = ( await Promise . all ( targets . map ( async ( target ) => { const name = ( await this . db . getCandidate ( target ) ) . name ; return `- ${ name } (${ target })` ; } ) ) ) . join ( "\n" ) ; if ( ! stash ) continue ; const name = ( await this . db . getChainMetadata ( ) ) . name ; const decimals = name == "Kusama" ? 12 : 10 ; const [ rawBal , err ] = await this . chaindata . getBondedAmount ( stash ) ; const bal = toDecimals ( rawBal , decimals ) ; const sym = name == "Kusama" ? "KSM" : "DOT" ; const targetsHtml = ( await Promise . all ( targets . map ( async ( target ) => { const name = ( await this . db . getCandidate ( target ) ) . name ; return `- ${ name } (${ addressUrl ( target , this . config ) })` ; } ) ) ) . join ( "<br>" ) ; logger . info ( `Nominator ${ stash } (${ bal } ${ sym }) / ${ nominator . controller } nominated:\n${ targetsString }` ) ; this . botLog ( `Nominator ${ addressUrl ( stash , this . config ) } (${ bal } ${ sym }) / 
          ${ addressUrl ( nominator . controller , this . config ) } nominated:<br>${ targetsHtml }` ) ; } } logger . info ( `(Scorekeeper::_doNominations) Number of Validators nominated this round: ${ counter }` ) ; this . botLog ( `${ counter } Validators nominated this round` ) ; this . currentTargets = allTargets . slice ( 0 , counter ) ; const nextTargets = allTargets . slice ( counter , allTargets . length ) ; const nextTargetsString = ( await Promise . all ( nextTargets . map ( async ( target ) => { const name = ( await this . db . getCandidate ( target ) ) . name ; return `- ${ name } (${ target })` ; } ) ) ) . join ( "\n" ) ; logger . info ( `Next targets: \n${ nextTargetsString }` ) ; return counter ; } async _getCurrentEra ( ) { const [ eraIndex , eraErr ] = await this . chaindata . getActiveEraIndex ( ) ; if ( eraErr ) { throw eraErr ; } return eraIndex ; } /**
     * Handles the ending of a Nomination round.
     */ async endRound ( ) { this . ending = true ; logger . info ( "(Scorekeeper::endRound) Ending round" ) ; const toProcess = new Map ( ) ; const { lastNominatedEraIndex : startEra } = await this . db . getLastNominatedEraIndex ( ) ; const [ activeEra , err ] = await this . chaindata . getActiveEraIndex ( ) ; if ( err ) { throw new Error ( `Error getting active era: ${ err }` ) ; } const chainType = await this . db . getChainMetadata ( ) ; logger . info ( `(Scorekeeper::endRound) finding validators that were active from era ${ startEra } to ${ activeEra }` ) ; const [ activeValidators , err2 ] = await this . chaindata . activeValidatorsInPeriod ( Number ( startEra ) , activeEra , chainType . name ) ; if ( err2 ) { throw new Error ( `Error getting active validators: ${ err2 }` ) ; } for ( const nomGroup of this . nominatorGroups ) { for ( const nominator of nomGroup ) { const current = await this . db . getCurrentTargets ( nominator . controller ) ; if ( ! current . length ) { logger . info ( `${ nominator . controller } is not nominating any targets.` ) ; continue ; } for ( const val of current ) { const candidate = await this . db . getCandidate ( val . stash ) ; if ( toProcess . has ( candidate . stash ) ) continue ; toProcess . set ( candidate . stash , candidate ) ; } } } const allCandidates = await this . db . allCandidates ( ) ; const validCandidates = allCandidates . filter ( ( candidate ) => candidate . valid ) ; for ( const candidate of validCandidates ) { if ( toProcess . has ( candidate . stash ) ) continue ; toProcess . set ( candidate . stash , candidate ) ; } const [ good , bad ] = await this . constraints . processCandidates ( new Set ( toProcess . values ( ) ) ) ; logger . info ( `{ScoreKeeper::endRound} Done processing Candidates. ${ good . size } good ${ bad . size } bad` ) ; for ( const goodOne of good . values ( ) ) { const { stash } = goodOne ; const wasActive = activeValidators . indexOf ( formatAddress ( stash , this . config ) ) !== - 1 ; if ( ! wasActive ) { logger . info ( `${ stash } was not active during eras ${ startEra } to ${ activeEra }` ) ; continue ; } const didRank = await this . db . pushRankEvent ( stash , startEra , activeEra ) ; if ( didRank ) await this . addPoint ( stash ) ; } for ( const badOne of bad . values ( ) ) { const { candidate , reason } = badOne ; const { stash } = candidate ; const didFault = await this . db . pushFaultEvent ( stash , reason ) ; if ( didFault ) await this . dockPoints ( stash ) ; } this . ending = false ; } async dockPoints ( stash ) { logger . info ( `(Scorekeeper::dockPoints) Stash ${ stash } did BAD, docking points` ) ; await this . db . dockPoints ( stash ) ; const candidate = await this . db . getCandidate ( stash ) ; this . botLog ( `${ candidate . name } docked points. New rank: ${ candidate . rank }` ) ; return true ; } async addPoint ( stash ) { logger . info ( `(Scorekeeper::addPoint) Stash ${ stash } did GOOD, adding points` ) ; await this . db . addPoint ( stash ) ; const candidate = await this . db . getCandidate ( stash ) ; this . botLog ( `${ candidate . name } did GOOD! Adding a point. New rank: ${ candidate . rank }` ) ; return true ; } }