bot.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import TelegramBot from "node-telegram-bot-api";
  2. import { token, chatid, heartbeat, proposalDelay, wsLocation } from "../config";
  3. // types
  4. import { Block, Council, Options, Proposals } from "./types";
  5. import { types } from "@joystream/types";
  6. import { ApiPromise, WsProvider } from "@polkadot/api";
  7. import { AccountId, Header } from "@polkadot/types/interfaces";
  8. // functions
  9. import * as announce from "./lib/announcements";
  10. import * as get from "./lib/getters";
  11. import { parseArgs, printStatus, passedTime, exit } from "./lib/util";
  12. import moment from "moment";
  13. const opts: Options = parseArgs(process.argv.slice(2));
  14. const log = (msg: string): void | number => opts.verbose && console.log(msg);
  15. process.env.NTBA_FIX_319 ||
  16. log("TL;DR: Set NTBA_FIX_319 to hide this warning.");
  17. const bot = token ? new TelegramBot(token, { polling: true }) : null;
  18. let lastHeartbeat: number = moment().valueOf();
  19. const sendMessage = (msg: string) => {
  20. if (msg === "") return;
  21. try {
  22. if (bot) bot.sendMessage(chatid, msg, { parse_mode: "HTML" });
  23. else console.log(msg);
  24. } catch (e) {
  25. console.log(`Failed to send message: ${e}`);
  26. }
  27. };
  28. const main = async () => {
  29. const provider = new WsProvider(wsLocation);
  30. const api = await ApiPromise.create({ provider, types });
  31. await api.isReady;
  32. const [chain, node, version] = await Promise.all([
  33. String(await api.rpc.system.chain()),
  34. api.rpc.system.name(),
  35. api.rpc.system.version(),
  36. ]);
  37. let council: Council = { round: 0, last: "" };
  38. let blocks: Block[] = [];
  39. let lastEra = 0;
  40. let lastBlock: Block = {
  41. id: 0,
  42. duration: 6000,
  43. timestamp: lastHeartbeat,
  44. stake: 0,
  45. noms: 0,
  46. vals: 0,
  47. issued: 0,
  48. reward: 0,
  49. };
  50. let issued = 0;
  51. let reward = 0;
  52. let stake = 0;
  53. let vals = 0;
  54. let noms = 0;
  55. const channels: number[] = [0, 0];
  56. const posts: number[] = [0, 0];
  57. const threads: number[] = [0, 0];
  58. let proposals: Proposals = { last: 0, current: 0, active: [], executing: [] };
  59. let lastProposalUpdate = 0;
  60. if (opts.channel) channels[0] = await get.currentChannelId(api);
  61. if (opts.forum) {
  62. posts[0] = await get.currentPostId(api);
  63. threads[0] = await get.currentThreadId(api);
  64. }
  65. if (opts.proposals) {
  66. proposals.last = await get.proposalCount(api);
  67. proposals.active = await get.activeProposals(api, proposals.last);
  68. }
  69. const getReward = async (era: number) =>
  70. Number(await api.query.staking.erasValidatorReward(era));
  71. log(`Subscribed to ${chain} on ${node} v${version}`);
  72. api.rpc.chain.subscribeNewHeads(
  73. async (header: Header): Promise<void> => {
  74. // current block
  75. const id = header.number.toNumber();
  76. if (lastBlock.id === id) return;
  77. const timestamp = (await api.query.timestamp.now()).toNumber();
  78. const duration = timestamp - lastBlock.timestamp;
  79. // update validators and nominators every era
  80. const era = Number(await api.query.staking.currentEra());
  81. if (era > lastEra) {
  82. vals = (await api.query.session.validators()).length;
  83. stake = Number(await api.query.staking.erasTotalStake(era));
  84. issued = Number(await api.query.balances.totalIssuance());
  85. reward = (await getReward(era - 1)) || (await getReward(era - 2));
  86. // nominator count
  87. noms = 0;
  88. const nominators: { [key: string]: number } = {};
  89. const stashes = (await api.derive.staking.stashes())
  90. .map((s) => String(s))
  91. .map(async (v) => {
  92. const stakers = await api.query.staking.erasStakers(era, v);
  93. stakers.others.forEach(
  94. (n: { who: AccountId }) => nominators[String(n.who)]++
  95. );
  96. noms = Object.keys(nominators).length;
  97. });
  98. lastEra = era;
  99. }
  100. const block: Block = {
  101. id,
  102. timestamp,
  103. duration,
  104. stake,
  105. noms,
  106. vals,
  107. reward,
  108. issued,
  109. };
  110. blocks = blocks.concat(block);
  111. // heartbeat
  112. if (timestamp > lastHeartbeat + heartbeat) {
  113. const time = passedTime(lastHeartbeat, timestamp);
  114. blocks = announce.heartbeat(api, blocks, time, proposals, sendMessage);
  115. lastHeartbeat = block.timestamp;
  116. }
  117. // announcements
  118. if (opts.council && block.id > lastBlock.id)
  119. council = await announce.council(api, council, block.id, sendMessage);
  120. if (opts.channel) {
  121. channels[1] = await get.currentChannelId(api);
  122. if (channels[1] > channels[0])
  123. channels[0] = await announce.channels(api, channels, sendMessage);
  124. }
  125. if (opts.proposals) {
  126. proposals.current = await get.proposalCount(api);
  127. if (
  128. proposals.current > proposals.last ||
  129. (timestamp > lastProposalUpdate + 60000 * proposalDelay &&
  130. (proposals.active || proposals.executing))
  131. ) {
  132. proposals = await announce.proposals(api, proposals, id, sendMessage);
  133. lastProposalUpdate = timestamp;
  134. }
  135. }
  136. if (opts.forum) {
  137. posts[1] = await get.currentPostId(api);
  138. posts[0] = await announce.posts(api, posts, sendMessage);
  139. }
  140. printStatus(opts, { block: id, chain, posts, proposals });
  141. lastBlock = block
  142. }
  143. );
  144. };
  145. main().catch(() => exit(log));