autopayout-validators.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. /**
  2. * autopayout-validators.js
  3. *
  4. * Claim and distribute validator staking rewards for your stakers
  5. *
  6. * Accepts several validators in config.js file
  7. *
  8. * https://github.com/Colm3na/polkadot-auto-payout
  9. *
  10. * Author: Mario Pino | @mariopino:matrix.org
  11. */
  12. const BigNumber = require('bignumber.js');
  13. const { ApiPromise, WsProvider } = require('@polkadot/api');
  14. const keyring = require('@polkadot/ui-keyring').default;
  15. keyring.initKeyring({
  16. isDevelopment: false,
  17. });
  18. const fs = require('fs');
  19. const prompts = require('prompts');
  20. const yargs = require('yargs');
  21. const config = require('./config.js');
  22. const argv = yargs
  23. .scriptName("autopayout.js")
  24. .option('account', {
  25. alias: 'a',
  26. description: 'Account json file path',
  27. type: 'string',
  28. })
  29. .option('password', {
  30. alias: 'p',
  31. description: 'Account password, or stdin if this is not set',
  32. type: 'string',
  33. })
  34. .option('log', {
  35. alias: 'l',
  36. description: 'log (append) to autopayout.log file',
  37. type: 'boolean',
  38. })
  39. .usage("node autopayout.js -c keystores/account.json -p password -v validator_stash_address")
  40. .help()
  41. .alias('help', 'h')
  42. .version()
  43. .alias('version', 'V')
  44. .argv;
  45. // Exported account json file param
  46. const accountJSON = argv.account || config.accountJSON;
  47. // Password param
  48. let password = argv.password || config.password;
  49. // Logging to file param
  50. const log = argv.log || config.log;
  51. // Node websocket
  52. const wsProvider = config.nodeWS;
  53. const main = async () => {
  54. console.log("\n\x1b[45m\x1b[1m Substrate auto payout \x1b[0m\n");
  55. console.log("\x1b[1m - Check source at https://github.com/Colm3na/substrate-auto-payout\x1b[0m");
  56. console.log("\x1b[32m\x1b[1m - Made with love from ColmenaLabs_SVQ https://colmenalabs.org/\x1b[0m\n");
  57. let raw;
  58. try {
  59. raw = fs.readFileSync(accountJSON, { encoding: 'utf-8' });
  60. } catch(err) {
  61. console.log(`\x1b[31m\x1b[1mError! Can't open ${accountJSON}\x1b[0m\n`);
  62. process.exit(1);
  63. }
  64. const account = JSON.parse(raw);
  65. const address = account.address;
  66. // Prompt user to enter password
  67. if (!password) {
  68. const response = await prompts({
  69. type: 'password',
  70. name: 'password',
  71. message: `Enter password for ${address}:`
  72. });
  73. password = response.password;
  74. }
  75. if (password) {
  76. console.log(`\x1b[1m -> Importing account\x1b[0m`, address);
  77. const signer = keyring.restoreAccount(account, password);
  78. signer.decodePkcs8(password);
  79. // Connect to node
  80. console.log(`\x1b[1m -> Connecting to\x1b[0m`, wsProvider);
  81. const provider = new WsProvider(wsProvider);
  82. const api = await ApiPromise.create({ provider });
  83. // Check account balance
  84. const accountBalance = await api.query.system.account(address)
  85. const totalBalance = accountBalance.data.free
  86. const freeBalance = BigNumber(totalBalance.toString()).minus(
  87. accountBalance.data.miscFrozen.toString()
  88. )
  89. if (freeBalance === 0) {
  90. console.log(`\x1b[31m\x1b[1mError! Account ${address} doesn't have free funds\x1b[0m\n`);
  91. process.exit(1);
  92. }
  93. console.log(`\x1b[1m -> Account ${address} free balance is ${(new BigNumber(freeBalance).div(new BigNumber(10).pow(config.decimalPlaces))).toFixed(3)} ${config.denom}\x1b[0m`);
  94. // Get session progress info
  95. const chainActiveEra = await api.query.staking.activeEra();
  96. const activeEra = JSON.parse(JSON.stringify(chainActiveEra)).index;
  97. console.log(`\x1b[1m -> Active era is ${activeEra}\x1b[0m`);
  98. let transactions = [];
  99. for (let index = 0; index < config.validators.length; index++) {
  100. const validator = config.validators[index];
  101. let unclaimedRewards = [];
  102. let era = activeEra - 84;
  103. const stakingInfo = await api.derive.staking.account(validator);
  104. const claimedRewards = stakingInfo.stakingLedger.claimedRewards;
  105. console.log(`\x1b[1m -> Claimed eras for validator ${validator}: ${JSON.stringify(claimedRewards)}\x1b[0m`);
  106. for (era; era < activeEra; era++) {
  107. const eraPoints = await api.query.staking.erasRewardPoints(era);
  108. const eraValidators = Object.keys(eraPoints.individual.toHuman());
  109. if (eraValidators.includes(validator) && !claimedRewards.includes(era)) {
  110. transactions.push(api.tx.staking.payoutStakers(validator, era));
  111. unclaimedRewards.push(era);
  112. }
  113. }
  114. console.log(`\x1b[1m -> Unclaimed eras for validator ${validator}: ${JSON.stringify(unclaimedRewards)}\x1b[0m`);
  115. }
  116. if (transactions.length > 0) {
  117. // Claim rewards tx
  118. const nonce = (await api.derive.balances.account(address)).accountNonce
  119. const hash = await api.tx.utility.batch(transactions).signAndSend(signer, { nonce });
  120. console.log(`\n\x1b[32m\x1b[1mSuccess! \x1b[37mCheck tx in PolkaScan: https://polkascan.io/kusama/transaction/${hash.toString()}\x1b[0m\n`);
  121. if (log) {
  122. fs.appendFileSync(`autopayout.log`, `${new Date()} - Claimed rewards, transaction hash is ${hash.toString()}\n`);
  123. }
  124. } else {
  125. console.log(`\n\x1b[33m\x1b[1mWarning! There's no unclaimed rewards, exiting!\x1b[0m\n`);
  126. if (log) {
  127. fs.appendFileSync(`autopayout.log`, `${new Date()} - There's no unclaimed rewards\n`);
  128. }
  129. }
  130. process.exit(0);
  131. }
  132. }
  133. try {
  134. main();
  135. } catch (error) {
  136. console.error(error);
  137. }