123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- /**
- * 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);
- }
|