App.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import React from "react";
  2. import "bootstrap/dist/css/bootstrap.min.css";
  3. import "./index.css";
  4. import { Modals, Routes, Loading, Footer, Status } from "./components";
  5. import * as get from "./lib/getters";
  6. import { bootstrap, getTokenomics, queryJstats } from "./lib/queries";
  7. import { getMints, updateOpenings, updateWorkers } from "./lib/groups";
  8. import {
  9. updateElection,
  10. getCouncilApplicants,
  11. getCouncilSize,
  12. getVotes,
  13. } from "./lib/election";
  14. import {
  15. getStashes,
  16. getNominators,
  17. getValidators,
  18. getValidatorStakes,
  19. getEraRewardPoints,
  20. getLastReward,
  21. getTotalStake,
  22. } from "./lib/validators";
  23. import { apiLocation, wsLocation, historyDepth } from "./config";
  24. import { initialState } from "./state";
  25. import axios from "axios";
  26. // types
  27. import { Api, IState } from "./types";
  28. import { types } from "@joystream/types";
  29. import { ApiPromise, WsProvider } from "@polkadot/api";
  30. import { Header } from "@polkadot/types/interfaces";
  31. interface IProps {}
  32. class App extends React.Component<IProps, IState> {
  33. initializeSocket() {
  34. socket.on("disconnect", () => setTimeout(this.initializeSocket, 1000));
  35. socket.on("connect", () => {
  36. if (!socket.id) return console.log("no websocket connection");
  37. console.log("my socketId:", socket.id);
  38. socket.emit("get posts", this.state.posts.length);
  39. });
  40. socket.on("posts", (posts: Post[]) => {
  41. console.log(`received ${posts.length} posts`);
  42. this.setState({ posts });
  43. });
  44. }
  45. // sync via joystream-api
  46. async updateStatus(api: ApiPromise, id: number): Promise<Status> {
  47. console.debug(`#${id}: Updating status`);
  48. this.updateActiveProposals();
  49. getMints(api).then((mints) => this.save(`mints`, mints));
  50. getTokenomics().then((tokenomics) => this.save(`tokenomics`, tokenomics));
  51. let { status, councils } = this.state;
  52. status.election = await updateElection(api);
  53. if (status.election?.stage) this.getElectionStatus(api);
  54. councils.forEach((c) => {
  55. if (c?.round > status.council) status.council = c;
  56. });
  57. let hash: string = await api.rpc.chain.getBlockHash(1);
  58. if (hash)
  59. status.startTime = (await api.query.timestamp.now.at(hash)).toNumber();
  60. const nextMemberId = await await api.query.members.nextMemberId();
  61. status.members = nextMemberId - 1;
  62. status.proposals = await get.proposalCount(api);
  63. status.posts = await get.currentPostId(api);
  64. status.threads = await get.currentThreadId(api);
  65. status.categories = await get.currentCategoryId(api);
  66. status.proposalPosts = await api.query.proposalsDiscussion.postCount();
  67. await this.updateEra(api, status.era).then(async (era) => {
  68. status.era = era;
  69. status.lastReward = await getLastReward(api, era);
  70. status.validatorStake = await getTotalStake(api, era);
  71. this.save("status", status);
  72. });
  73. return status;
  74. }
  75. async getElectionStatus(api: ApiPromise): Promise<IElectionState> {
  76. getCouncilSize(api).then((councilSize) => {
  77. let election = this.state.election;
  78. election.councilSize = councilSize;
  79. this.save("election", election);
  80. });
  81. getVotes(api).then((votes) => {
  82. let election = this.state.election;
  83. election.votes = votes;
  84. this.save("election", election);
  85. });
  86. getCouncilApplicants(api).then((applicants) => {
  87. let election = this.state.election;
  88. election.applicants = applicants;
  89. this.save("election", election);
  90. });
  91. }
  92. updateActiveProposals() {
  93. const active = this.state.proposals.filter((p) => p.result === "Pending");
  94. if (!active.length) return;
  95. const s = active.length > 1 ? `s` : ``;
  96. console.log(`Updating ${active.length} active proposal${s}`);
  97. active.forEach(async (a) => {
  98. const { data } = await axios.get(`${apiLocation}/v2/proposals/${a.id}`);
  99. if (!data || data.error)
  100. return console.error(`failed to fetch proposal from API`);
  101. this.save(
  102. "proposals",
  103. this.state.proposals.map((p) => (p.id === a.id ? data : p))
  104. );
  105. });
  106. }
  107. async updateEra(api: Api, old: number) {
  108. const { status, validators } = this.state;
  109. const era = Number(await api.query.staking.currentEra());
  110. if (era === old) return era;
  111. this.updateWorkingGroups(api);
  112. this.updateValidatorPoints(api, status.era);
  113. if (era > status.era || !validators.length) this.updateValidators(api);
  114. return era;
  115. }
  116. async updateWorkingGroups(api: ApiPromise) {
  117. const { members, openings, workers } = this.state;
  118. updateWorkers(api, workers, members).then((workers) => {
  119. this.save("workers", workers);
  120. updateOpenings(api, openings, members).then((openings) =>
  121. this.save("openings", openings)
  122. );
  123. });
  124. return this.save("council", await api.query.council.activeCouncil());
  125. }
  126. updateValidators(api: ApiPromise) {
  127. getValidators(api).then((validators) => {
  128. this.save("validators", validators);
  129. getNominators(api).then((nominators) => {
  130. this.save("nominators", nominators);
  131. getStashes(api).then((stashes) => {
  132. this.save("stashes", stashes);
  133. const { status, members } = this.state;
  134. const { era } = status;
  135. getValidatorStakes(api, era, stashes, members, this.save).then(
  136. (stakes) => this.save("stakes", stakes)
  137. );
  138. });
  139. });
  140. });
  141. }
  142. async updateValidatorPoints(api: ApiPromise, currentEra: number) {
  143. let points = this.state.rewardPoints;
  144. const updateTotal = (eraTotals) => {
  145. let total = 0;
  146. Object.keys(eraTotals).forEach((era) => (total += eraTotals[era]));
  147. return total;
  148. };
  149. for (let era = currentEra; era > currentEra - historyDepth; --era) {
  150. if (era < currentEra && points.eraTotals[era]) continue;
  151. getEraRewardPoints(api, era).then((eraPoints) => {
  152. console.debug(`era ${era}: ${eraPoints.total} points`);
  153. points.eraTotals[era] = eraPoints.total;
  154. points.total = updateTotal(points.eraTotals);
  155. Object.keys(eraPoints.individual).forEach((validator: string) => {
  156. if (!points.validators[validator]) points.validators[validator] = {};
  157. points.validators[validator][era] = eraPoints.individual[validator];
  158. });
  159. this.save("rewardPoints", points);
  160. });
  161. }
  162. }
  163. async updateCouncils() {
  164. queryJstats(`v1/councils`).then((councils) => {
  165. this.save(`councils`, councils);
  166. // TODO OPTIMIZE find max round
  167. let council = { round: 0 };
  168. councils.forEach((c) => {
  169. if (c.round > council.round) council = c;
  170. });
  171. let { status } = this.state;
  172. status.council = council; // needed by dashboard
  173. this.save("status", status);
  174. });
  175. }
  176. // interface interactions
  177. toggleStar(account: string) {
  178. let { stars } = this.state;
  179. stars[account] = !stars[account];
  180. this.save("stars", stars);
  181. }
  182. toggleEditKpi(editKpi) {
  183. this.setState({ editKpi });
  184. }
  185. toggleShowStatus() {
  186. this.setState({ showStatus: !this.state.showStatus });
  187. }
  188. toggleFooter() {
  189. this.setState({ hideFooter: !this.state.hideFooter });
  190. }
  191. getMember(handle: string) {
  192. const { members } = this.state;
  193. const member = members.find((m) => m.handle === handle);
  194. if (member) return member;
  195. return members.find((m) => m.rootKey === handle);
  196. }
  197. render() {
  198. const { connected, fetching, loading, hideFooter } = this.state;
  199. if (loading) return <Loading />;
  200. return (
  201. <>
  202. <Routes
  203. toggleEditKpi={this.toggleEditKpi}
  204. toggleFooter={this.toggleFooter}
  205. toggleStar={this.toggleStar}
  206. getMember={this.getMember}
  207. {...this.state}
  208. />
  209. <Modals
  210. toggleEditKpi={this.toggleEditKpi}
  211. toggleShowStatus={this.toggleShowStatus}
  212. {...this.state}
  213. />
  214. <Footer show={!hideFooter} toggleHide={this.toggleFooter} />
  215. <Status
  216. toggleShowStatus={this.toggleShowStatus}
  217. connected={connected}
  218. fetching={fetching}
  219. />
  220. </>
  221. );
  222. }
  223. // startup from bottom up
  224. joyApi() {
  225. console.debug(`Connecting to ${wsLocation}`);
  226. const provider = new WsProvider(wsLocation);
  227. ApiPromise.create({ provider, types }).then(async (api) => {
  228. await api.isReady;
  229. console.log(`Connected to ${wsLocation}`);
  230. this.setState({ connected: true });
  231. this.updateWorkingGroups(api);
  232. api.rpc.chain.subscribeNewHeads(async (header: Header) => {
  233. let { blocks, status } = this.state;
  234. const id = header.number.toNumber();
  235. const isEven = id / 50 === Math.floor(id / 50);
  236. if (isEven || status.block?.id + 50 < id) this.updateStatus(api, id);
  237. if (blocks.find((b) => b.id === id)) return;
  238. const timestamp = (await api.query.timestamp.now()).toNumber();
  239. const duration = status.block
  240. ? timestamp - status.block.timestamp
  241. : 6000;
  242. status.block = { id, timestamp, duration };
  243. this.save("status", status);
  244. blocks = blocks.filter((i) => i.id !== id).concat(status.block);
  245. this.setState({ blocks });
  246. });
  247. });
  248. }
  249. save(key: string, data: any) {
  250. this.setState({ [key]: data });
  251. const value = JSON.stringify(data);
  252. try {
  253. localStorage.setItem(key, value);
  254. } catch (e) {
  255. const size = value.length / 1024;
  256. console.warn(`Failed to save ${key} (${size.toFixed()} KB)`, e.message);
  257. }
  258. return data;
  259. }
  260. load(key: string) {
  261. try {
  262. const data = localStorage.getItem(key);
  263. if (!data) return;
  264. const size = data.length;
  265. if (size > 10240)
  266. console.debug(` -${key}: ${(size / 1024).toFixed(1)} KB`);
  267. this.setState({ [key]: JSON.parse(data) });
  268. return JSON.parse(data);
  269. } catch (e) {
  270. console.warn(`Failed to load ${key}`, e);
  271. }
  272. }
  273. async loadData() {
  274. console.debug(`Loading data`);
  275. "status members assets providers councils council election workers categories channels proposals posts threads mints openings tokenomics transactions reports validators nominators staches stakes rewardPoints stars"
  276. .split(" ")
  277. .map((key) => this.load(key));
  278. getTokenomics().then((tokenomics) => this.save(`tokenomics`, tokenomics));
  279. bootstrap(this.save); // axios requests
  280. this.updateCouncils();
  281. }
  282. componentDidMount() {
  283. this.loadData(); // local storage + bootstrap
  284. this.joyApi(); // joystream rpc connection
  285. //this.initializeSocket() // jsstats socket.io
  286. }
  287. constructor(props: IProps) {
  288. super(props);
  289. this.state = initialState;
  290. this.save = this.save.bind(this);
  291. this.load = this.load.bind(this);
  292. this.toggleEditKpi = this.toggleEditKpi.bind(this);
  293. this.toggleStar = this.toggleStar.bind(this);
  294. this.toggleFooter = this.toggleFooter.bind(this);
  295. this.toggleShowStatus = this.toggleShowStatus.bind(this);
  296. this.getMember = this.getMember.bind(this);
  297. }
  298. }
  299. export default App;