import { ApiPromise , Keyring } from '@polkadot/api' ; import { SubmittableExtrinsic } from '@polkadot/api/submittable/types' ; import { Vec } from '@polkadot/types/codec' ; import { Codec , ISubmittableResult } from '@polkadot/types/types' ; import { cryptoWaitReady } from '@polkadot/util-crypto' ; import BN from 'bn.js' ; import { log } from './logger' ; const DEBUG = process . env . PAYOUTS_DEBUG ; const MAX_CALLS = 6 ; export interface ServiceArgs { api : ApiPromise ; suri : string ; stashes : string [ ] ; eraDepth : number ; } param collectionOptions returns export async function collectPayouts ( { api , suri , stashes , eraDepth , } ) { const payouts = await listPendingPayouts ( { stashes , eraDepth , api , } ) ; if ( ! payouts || ! payouts . length ) { log . info ( 'No payouts to claim' ) ; return ; } log . info ( `Transactions are being created. This may take some time if there are many unclaimed eras.` ) ; await signAndSendTxs ( api , payouts , suri ) ; } export async function listPendingPayouts ( { api , stashes , eraDepth , } ) { const activeInfoOpt = await api . query . staking . activeEra ( ) ; if ( activeInfoOpt . isNone ) { log . warn ( 'ActiveEra is None, pending payouts could not be fetched.' ) ; return null ; } const currEra = activeInfoOpt . unwrap ( ) . index . toNumber ( ) ; const validatorStashes = [ ] ; for ( const stash of stashes ) { const maybeNominations = await api . query . staking . nominators ( stash ) ; if ( maybeNominations . isSome ) { const targets = maybeNominations . unwrap ( ) . targets . map ( ( a ) => a . toHuman ( ) ) ; DEBUG && log . debug ( `Nominator address detected: ${ stash }. Adding its targets: ${ targets . join ( ', ' ) }` ) ; validatorStashes . push ( ... targets ) ; } else { DEBUG && log . debug ( `Validator address detected: ${ stash }` ) ; validatorStashes . push ( stash ) ; } } const payouts = [ ] ; for ( const stash of validatorStashes ) { const controllerOpt = await api . query . staking . bonded ( stash ) ; if ( controllerOpt . isNone ) { log . warn ( `${ stash } is not a valid stash address.` ) ; continue ; } const controller = controllerOpt . unwrap ( ) ; const ledgerOpt = await api . query . staking . ledger ( controller ) ; if ( ledgerOpt . isNone ) { log . warn ( `Staking ledger for ${ stash } was not found.` ) ; continue ; } const ledger = ledgerOpt . unwrap ( ) ; const { claimedRewards } = ledger ; const lastEra = claimedRewards [ claimedRewards . length - 1 ] ?. toNumber ( ) ; if ( ! lastEra ) { continue ; } for ( let e = lastEra - eraDepth ; e < lastEra ; e += 1 ) { if ( claimedRewards . includes ( e ) ) { continue ; } if ( await isValidatingInEra ( api , stash , e ) ) { const payoutStakes = api . tx . staking . payoutStakers ( stash , e ) ; payouts . push ( payoutStakes ) ; } } for ( let e = lastEra + 1 ; e < currEra ; e += 1 ) { if ( await isValidatingInEra ( api , stash , e ) ) { const payoutStakes = api . tx . staking . payoutStakers ( stash , e ) ; payouts . push ( payoutStakes ) ; } } } payouts . length && log . info ( `The following unclaimed payouts where found: \n${ payouts . map ( ( { method : { section , method , args } } ) => `${ section }.${ method }(${ args . map ? args . map ( ( a ) => `${ a . toHuman ( ) }` ) . join ( ', ' ) : args })` ) . join ( '\n' ) }` ) && log . info ( `Total of ${ payouts . length } unclaimed payouts.` ) ; return payouts ; } async function isValidatingInEra ( api , stash , eraToCheck ) { try { const exposure = await api . query . staking . erasStakers ( eraToCheck , stash ) ; return exposure . total . toBn ( ) . gtn ( 0 ) ; } catch { return false ; } } async function signAndSendTxs ( api , payouts , suri ) { await cryptoWaitReady ( ) ; const keyring = new Keyring ( ) ; const signingKeys = keyring . createFromUri ( suri , { } , 'sr25519' ) ; DEBUG && log . debug ( `Sender address: ${ keyring . encodeAddress ( signingKeys . address , api . registry . chainSS58 ) }` ) ; const maxExtrinsicMaybeOpt = api . consts . system . blockWeights . perClass . normal . maxExtrinsic ; const maxExtrinsic = maxExtrinsicMaybeOpt . isSome ? maxExtrinsicMaybeOpt . unwrap ( ) . toBn ( ) : maxExtrinsicMaybeOpt . toBn ( ) ; const byMaxCalls = payouts . reduce ( ( byMaxCalls , tx , idx ) => { if ( idx % MAX_CALLS === 0 ) { byMaxCalls . push ( [ ] ) ; } byMaxCalls [ byMaxCalls . length - 1 ] . push ( tx ) ; return byMaxCalls ; } , [ ] as SubmittableExtrinsic < 'promise' > [ ] [ ] ) ; const txs = [ ] ; let iters = 0 ; while ( byMaxCalls . length ) { const calls = byMaxCalls . shift ( ) ; if ( ! calls ) { break ; } let toHeavy = true ; while ( toHeavy ) { const batch = api . tx . utility . batch ( calls ) ; const { weight } = await batch . paymentInfo ( signingKeys ) ; if ( weight . gte ( maxExtrinsic ) ) { const removeTx = calls . pop ( ) ; if ( ! removeTx ) { toHeavy = false ; } else if ( ! byMaxCalls [ byMaxCalls . length - 1 ] || byMaxCalls [ byMaxCalls . length - 1 ] . length >= MAX_CALLS ) { byMaxCalls . push ( [ removeTx ] ) ; } else { byMaxCalls [ byMaxCalls . length - 1 ] . push ( removeTx ) ; } } else { toHeavy = false ; } iters += 1 ; DEBUG && log . debug ( `Current iteration of batch creation loop: ${ iters }.` + `Note: if this just keeps increasing file a bug report at` + `https://github.com/canontech/staking-payouts/issues` ) ; } if ( calls . length == 1 ) { txs . push ( calls [ 0 ] ) ; } else if ( calls . length > 1 ) { txs . push ( api . tx . utility . batch ( calls ) ) ; } } DEBUG && log . debug ( `Calls per tx ${ txs . map ( ( t ) => t . method . method . toLowerCase ( ) == 'batch' ? ( t . args [ 0 ] as Vec < Codec > ) . length : 1 ) . toString ( ) }` ) ; log . info ( `Getting ready to send ${ txs . length } transactions.` ) ; for ( const [ i , tx ] of txs . entries ( ) ) { log . info ( `Sending ${ tx . method . section }.${ tx . method . method } (tx ${ i + 1 }/${ txs . length })` ) ; DEBUG && tx . method . method . toLowerCase ( ) === 'batch' && log . debug ( `${ tx . method . section }.${ tx . method . method } has ${ ( tx . method . args [ 0 ] as unknown as [ ] ) ?. length } calls` ) ; try { const res = await tx . signAndSend ( signingKeys , { nonce : - 1 } ) ; log . info ( `Node response to tx: ${ res . toString ( ) }` ) ; } catch ( e ) { log . error ( `Tx failed to sign and send (tx ${ i + 1 }/${ txs . length })` ) ; throw e ; } } }