bot.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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 cats: number[] = [0, 0];
  56. const channels: number[] = [0, 0];
  57. const posts: number[] = [0, 0];
  58. const threads: number[] = [0, 0];
  59. let proposals: Proposals = { last: 0, current: 0, active: [], executing: [] };
  60. let lastProposalUpdate = 0;
  61. if (opts.channel) channels[0] = await get.currentChannelId(api);
  62. if (opts.forum) {
  63. posts[0] = await get.currentPostId(api);
  64. cats[0] = await get.currentCategoryId(api);
  65. threads[0] = await get.currentThreadId(api);
  66. }
  67. if (opts.proposals) {
  68. proposals.last = await get.proposalCount(api);
  69. proposals.active = await get.activeProposals(api, proposals.last);
  70. }
  71. const getReward = async (era: number) =>
  72. Number(await api.query.staking.erasValidatorReward(era));
  73. log(`Subscribed to ${chain} on ${node} v${version}`);
  74. api.rpc.chain.subscribeNewHeads(
  75. async (header: Header): Promise<void> => {
  76. // current block
  77. const id = header.number.toNumber();
  78. if (lastBlock.id === id) return;
  79. const timestamp = (await api.query.timestamp.now()).toNumber();
  80. const duration = timestamp - lastBlock.timestamp;
  81. // update validators and nominators every era
  82. const era = Number(await api.query.staking.currentEra());
  83. if (era > lastEra) {
  84. vals = (await api.query.session.validators()).length;
  85. stake = Number(await api.query.staking.erasTotalStake(era));
  86. issued = Number(await api.query.balances.totalIssuance());
  87. reward = (await getReward(era - 1)) || (await getReward(era - 2));
  88. // nominator count
  89. noms = 0;
  90. const nominators: { [key: string]: number } = {};
  91. const stashes = (await api.derive.staking.stashes())
  92. .map((s) => String(s))
  93. .map(async (v) => {
  94. const stakers = await api.query.staking.erasStakers(era, v);
  95. stakers.others.forEach(
  96. (n: { who: AccountId }) => nominators[String(n.who)]++
  97. );
  98. noms = Object.keys(nominators).length;
  99. });
  100. lastEra = era;
  101. }
  102. const block: Block = {
  103. id,
  104. timestamp,
  105. duration,
  106. stake,
  107. noms,
  108. vals,
  109. reward,
  110. issued,
  111. };
  112. blocks = blocks.concat(block);
  113. // heartbeat
  114. if (timestamp > lastHeartbeat + heartbeat) {
  115. const time = passedTime(lastHeartbeat, timestamp);
  116. blocks = announce.heartbeat(api, blocks, time, proposals, sendMessage);
  117. lastHeartbeat = block.timestamp;
  118. }
  119. // announcements
  120. if (opts.council && block.id > lastBlock.id)
  121. council = await announce.council(api, council, block.id, sendMessage);
  122. if (opts.channel) {
  123. channels[1] = await get.currentChannelId(api);
  124. if (channels[1] > channels[0])
  125. channels[0] = await announce.channels(api, channels, sendMessage);
  126. }
  127. if (opts.proposals) {
  128. proposals.current = await get.proposalCount(api);
  129. if (
  130. proposals.current > proposals.last ||
  131. (timestamp > lastProposalUpdate + 60000 * proposalDelay &&
  132. (proposals.active || proposals.executing))
  133. ) {
  134. proposals = await announce.proposals(api, proposals, id, sendMessage);
  135. lastProposalUpdate = timestamp;
  136. }
  137. }
  138. if (opts.forum) {
  139. cats[1] = await get.currentCategoryId(api);
  140. cats[0] = await announce.categories(api, cats, sendMessage);
  141. posts[1] = await get.currentPostId(api);
  142. posts[0] = await announce.posts(api, posts, sendMessage);
  143. }
  144. printStatus(opts, { block: id, cats, chain, posts, proposals });
  145. lastBlock = block
  146. }
  147. );
  148. };
  149. main().catch(() => exit(log));