/** * autopayout.js * * Claim and distribute validator staking rewards for your stakers * * https://github.com/Colm3na/polkadot-auto-payout * * Author: Mario Pino | @mariopino:matrix.org */ const DELAY = 10; // s const BigNumber = require("bignumber.js"); const { ApiPromise, WsProvider } = require("@polkadot/api"); const { types } = require("@joystream/types"); const keyring = require("@polkadot/ui-keyring").default; keyring.initKeyring({ isDevelopment: false, }); const fs = require("fs"); const prompts = require("prompts"); const yargs = require("yargs"); const config = require("./config.js"); const argv = yargs .scriptName("autopayout.js") .option("account", { alias: "a", description: "Account json file path", type: "string", }) .option("password", { alias: "p", description: "Account password, or stdin if this is not set", type: "string", }) .option("validator", { alias: "v", description: "Validator address", type: "string", }) .option("log", { alias: "l", description: "log (append) to autopayout.log file", type: "string", }) .usage( "node autopayout.js -a keystores/account.json -p password -v validator_stash_address" ) .help() .alias("help", "h") .version() .alias("version", "V").argv; // Exported account json file param const accountJSON = argv.account || config.accountJSON; // Password param let password = argv.password || config.password; // Validator address param const validator = argv.validator || config.validator; // Logging to file param const log = config.log || argv.log; // Node websocket const wsProvider = config.nodeWS; const main = async () => { console.log( "\n\x1b[45m\x1b[1m Substrate auto payout \x1b[0m", "by ColmenaLabs_SVQ https://colmenalabs.org\n", "(https://github.com/Colm3na/substrate-auto-payout)\n" ); let raw; try { raw = fs.readFileSync(accountJSON, { encoding: "utf-8" }); } catch (err) { console.log(`\x1b[31m\x1b[1mError! Can't open ${accountJSON}\x1b[0m\n`); process.exit(1); } const account = JSON.parse(raw); const address = account.address; if (!validator) { console.log(`\x1b[31m\x1b[1mError! Empty validator stash address\x1b[0m\n`); process.exit(1); } else { console.log(`\x1b[1m -> Validator stash address is\x1b[0m`, validator); } // Prompt user to enter password if (!password) { const response = await prompts({ type: "password", name: "password", message: `Enter password for ${address}:`, }); password = response.password; } if (password) { console.log(`\x1b[1m -> Importing account\x1b[0m`, address); const signer = keyring.restoreAccount(account, password); signer.decodePkcs8(password); // Connect to node console.log(`\x1b[1m -> Connecting to\x1b[0m`, wsProvider); const provider = new WsProvider(wsProvider); const api = await ApiPromise.create({ provider, types }); // Get session progress info const chainActiveEra = await api.query.staking.activeEra(); const activeEra = JSON.parse(JSON.stringify(chainActiveEra)).index; console.log(`\x1b[1m -> Active era is ${activeEra}\x1b[0m`); console.log(`\x1b[1m -> Fetching validators`); const validatorsRaw = await api.query.session.validators(); const validators = validatorsRaw.map((v) => v.toHuman()); console.log(`\x1b[1m -> Fetching staking info`); const claimedRewards = {}; await Promise.all( validators.map(async (v) => { const stakingInfo = await api.derive.staking.account(v); claimedRewards[v] = await stakingInfo.stakingLedger.claimedRewards; }) ); let transactions = []; let unclaimedRewards = []; let era = activeEra - 360; console.log(`\x1b[1m -> Processing eras`); for (era; era < activeEra; era++) { const eraPoints = await api.query.staking.erasRewardPoints(era); const eraValidators = Object.keys(eraPoints.individual.toHuman()); validators.map((validator) => { if ( eraValidators.includes(validator) && !claimedRewards[validator].includes(era) ) { transactions.push(api.tx.staking.payoutStakers(validator, era)); unclaimedRewards.push(era); } }); } // Claim rewards if (transactions.length > 0) { console.log(`\x1b[1m -> Unclaimed eras: ${unclaimedRewards.length}`); processTransactions(api, signer, address, transactions); } else { console.log(`Nothing to do. Exiting.`); process.exit(0, `Nothing to do. Exiting.`); } } }; const processTransactions = async (api, signer, address, transactions) => { const nonce = (await api.derive.balances.account(address)).accountNonce; let left = transactions; try { const hash = await api.tx.utility .batch(transactions.slice(0, 40)) .signAndSend(signer, { nonce }); console.log(`\n\x1b[32m\x1b[1mSuccess! \x1b[37${hash.toString()}\x1b[0m\n`); if (log) fs.appendFileSync(log, `${new Date()}: ${hash.toString()}\n`); left = transactions.slice(40); } catch (e) { console.log(`Transaction failed:`, e.message); } if (left.length) { console.log(`${left.length} unprocessed transactions, waiting ${DELAY}s`); setTimeout( () => processTransactions(api, signer, address, left), DELAY * 1000 ); } else process.exit(0); }; try { main(); } catch (error) { console.error(error); }