autopayout.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * autopayout.js
  3. *
  4. * Claim and distribute validator staking rewards for your stakers
  5. *
  6. * https://github.com/Colm3na/polkadot-auto-payout
  7. *
  8. * Author: Mario Pino | @mariopino:matrix.org
  9. */
  10. const DELAY = 10; // s
  11. const BigNumber = require("bignumber.js");
  12. const { ApiPromise, WsProvider } = require("@polkadot/api");
  13. const { types } = require("@joystream/types");
  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("validator", {
  35. alias: "v",
  36. description: "Validator address",
  37. type: "string",
  38. })
  39. .option("log", {
  40. alias: "l",
  41. description: "log (append) to autopayout.log file",
  42. type: "string",
  43. })
  44. .usage(
  45. "node autopayout.js -a keystores/account.json -p password -v validator_stash_address"
  46. )
  47. .help()
  48. .alias("help", "h")
  49. .version()
  50. .alias("version", "V").argv;
  51. // Exported account json file param
  52. const accountJSON = argv.account || config.accountJSON;
  53. // Password param
  54. let password = argv.password || config.password;
  55. // Validator address param
  56. const validator = argv.validator || config.validator;
  57. // Logging to file param
  58. const log = config.log || argv.log;
  59. // Node websocket
  60. const wsProvider = config.nodeWS;
  61. const main = async () => {
  62. console.log(
  63. "\n\x1b[45m\x1b[1m Substrate auto payout \x1b[0m",
  64. "by ColmenaLabs_SVQ https://colmenalabs.org\n",
  65. "(https://github.com/Colm3na/substrate-auto-payout)\n"
  66. );
  67. let raw;
  68. try {
  69. raw = fs.readFileSync(accountJSON, { encoding: "utf-8" });
  70. } catch (err) {
  71. console.log(`\x1b[31m\x1b[1mError! Can't open ${accountJSON}\x1b[0m\n`);
  72. process.exit(1);
  73. }
  74. const account = JSON.parse(raw);
  75. const address = account.address;
  76. if (!validator) {
  77. console.log(`\x1b[31m\x1b[1mError! Empty validator stash address\x1b[0m\n`);
  78. process.exit(1);
  79. } else {
  80. console.log(`\x1b[1m -> Validator stash address is\x1b[0m`, validator);
  81. }
  82. // Prompt user to enter password
  83. if (!password) {
  84. const response = await prompts({
  85. type: "password",
  86. name: "password",
  87. message: `Enter password for ${address}:`,
  88. });
  89. password = response.password;
  90. }
  91. if (password) {
  92. console.log(`\x1b[1m -> Importing account\x1b[0m`, address);
  93. const signer = keyring.restoreAccount(account, password);
  94. signer.decodePkcs8(password);
  95. // Connect to node
  96. console.log(`\x1b[1m -> Connecting to\x1b[0m`, wsProvider);
  97. const provider = new WsProvider(wsProvider);
  98. const api = await ApiPromise.create({ provider, types });
  99. // Get session progress info
  100. const chainActiveEra = await api.query.staking.activeEra();
  101. const activeEra = JSON.parse(JSON.stringify(chainActiveEra)).index;
  102. console.log(`\x1b[1m -> Active era is ${activeEra}\x1b[0m`);
  103. console.log(`\x1b[1m -> Fetching validators`);
  104. const validatorsRaw = await api.query.session.validators();
  105. const validators = validatorsRaw.map((v) => v.toHuman());
  106. console.log(`\x1b[1m -> Fetching staking info`);
  107. const claimedRewards = {};
  108. await Promise.all(
  109. validators.map(async (v) => {
  110. const stakingInfo = await api.derive.staking.account(v);
  111. claimedRewards[v] = await stakingInfo.stakingLedger.claimedRewards;
  112. })
  113. );
  114. let transactions = [];
  115. let unclaimedRewards = [];
  116. let era = activeEra - 360;
  117. console.log(`\x1b[1m -> Processing eras`);
  118. for (era; era < activeEra; era++) {
  119. const eraPoints = await api.query.staking.erasRewardPoints(era);
  120. const eraValidators = Object.keys(eraPoints.individual.toHuman());
  121. validators.map((validator) => {
  122. if (
  123. eraValidators.includes(validator) &&
  124. !claimedRewards[validator].includes(era)
  125. ) {
  126. transactions.push(api.tx.staking.payoutStakers(validator, era));
  127. unclaimedRewards.push(era);
  128. }
  129. });
  130. }
  131. // Claim rewards
  132. if (transactions.length > 0) {
  133. console.log(`\x1b[1m -> Unclaimed eras: ${unclaimedRewards.length}`);
  134. processTransactions(api, signer, address, transactions);
  135. } else {
  136. console.log(`Nothing to do. Exiting.`);
  137. process.exit(0, `Nothing to do. Exiting.`);
  138. }
  139. }
  140. };
  141. const processTransactions = async (api, signer, address, transactions) => {
  142. const nonce = (await api.derive.balances.account(address)).accountNonce;
  143. let left = transactions;
  144. try {
  145. const hash = await api.tx.utility
  146. .batch(transactions.slice(0, 40))
  147. .signAndSend(signer, { nonce });
  148. console.log(`\n\x1b[32m\x1b[1mSuccess! \x1b[37${hash.toString()}\x1b[0m\n`);
  149. if (log) fs.appendFileSync(log, `${new Date()}: ${hash.toString()}\n`);
  150. left = transactions.slice(40);
  151. } catch (e) {
  152. console.log(`Transaction failed:`, e.message);
  153. }
  154. if (left.length) {
  155. console.log(`${left.length} unprocessed transactions, waiting ${DELAY}s`);
  156. setTimeout(
  157. () => processTransactions(api, signer, address, left),
  158. DELAY * 1000
  159. );
  160. } else process.exit(0);
  161. };
  162. try {
  163. main();
  164. } catch (error) {
  165. console.error(error);
  166. }