App.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. import React from "react";
  2. import "bootstrap/dist/css/bootstrap.min.css";
  3. import "./index.css";
  4. import { Routes, Loading } from "./components";
  5. import * as get from "./lib/getters";
  6. import { domain, wsLocation } from "./config";
  7. import proposalPosts from "./proposalPosts";
  8. import axios from "axios";
  9. import { ProposalDetail } from "./types";
  10. import {
  11. Api,
  12. Block,
  13. Handles,
  14. IState,
  15. Member,
  16. Category,
  17. Channel,
  18. Post,
  19. Seat,
  20. Thread,
  21. } from "./types";
  22. import { types } from "@joystream/types";
  23. import { ApiPromise, WsProvider } from "@polkadot/api";
  24. import { Header } from "@polkadot/types/interfaces";
  25. import { VoteKind } from "@joystream/types/proposals";
  26. interface IProps {}
  27. const version = 0.3;
  28. const initialState = {
  29. blocks: [],
  30. now: 0,
  31. block: 0,
  32. loading: true,
  33. nominators: [],
  34. validators: [],
  35. channels: [],
  36. posts: [],
  37. councils: [],
  38. categories: [],
  39. threads: [],
  40. proposals: [],
  41. proposalCount: 0,
  42. domain,
  43. handles: {},
  44. members: [],
  45. proposalPosts,
  46. reports: {},
  47. termEndsAt: 0,
  48. stage: {},
  49. stakes: {},
  50. stashes: [],
  51. stars: {},
  52. lastReward: 0,
  53. };
  54. class App extends React.Component<IProps, IState> {
  55. async initializeSocket() {
  56. console.debug(`Connecting to ${wsLocation}`);
  57. const provider = new WsProvider(wsLocation);
  58. const api = await ApiPromise.create({ provider, types });
  59. await api.isReady;
  60. console.log(`Connected to ${wsLocation}`);
  61. let blocks: Block[] = [];
  62. let lastBlock: Block = { id: 0, timestamp: 0, duration: 6 };
  63. let era = 0;
  64. let termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
  65. this.save("termEndsAt", termEndsAt);
  66. let round: number = Number(
  67. (await api.query.councilElection.round()).toJSON()
  68. );
  69. this.save("round", round);
  70. let stage: any = await api.query.councilElection.stage();
  71. this.save("stage", stage);
  72. let councilElection = { termEndsAt, stage: stage.toJSON(), round };
  73. this.setState({ councilElection });
  74. let stageEndsAt: number = termEndsAt;
  75. let lastCategory = await get.currentCategoryId(api);
  76. this.fetchCategories(api, lastCategory);
  77. let lastChannel = await get.currentChannelId(api);
  78. this.fetchChannels(api, lastChannel);
  79. let lastPost = await get.currentPostId(api);
  80. this.fetchPosts(api, lastPost);
  81. let lastThread = await get.currentThreadId(api);
  82. this.fetchThreads(api, lastThread);
  83. let lastMember = await api.query.members.nextMemberId();
  84. this.fetchMembers(api, Number(lastMember));
  85. api.rpc.chain.subscribeNewHeads(
  86. async (header: Header): Promise<void> => {
  87. // current block
  88. const id = header.number.toNumber();
  89. if (blocks.find((b) => b.id === id)) return;
  90. const timestamp = (await api.query.timestamp.now()).toNumber();
  91. const duration = timestamp - lastBlock.timestamp;
  92. const block: Block = { id, timestamp, duration };
  93. blocks = blocks.concat(block);
  94. this.setState({ blocks, loading: false });
  95. this.save("block", id);
  96. this.save("now", timestamp);
  97. const proposalCount = await get.proposalCount(api);
  98. if (proposalCount > this.state.proposalCount) {
  99. this.fetchProposal(api, proposalCount);
  100. this.setState({ proposalCount });
  101. }
  102. const currentChannel = await get.currentChannelId(api);
  103. if (currentChannel > lastChannel)
  104. lastChannel = await this.fetchChannels(api, currentChannel);
  105. const currentCategory = await get.currentCategoryId(api);
  106. if (currentCategory > lastCategory)
  107. lastCategory = await this.fetchCategories(api, currentCategory);
  108. const currentPost = await get.currentPostId(api);
  109. if (currentPost > lastPost)
  110. lastPost = await this.fetchPosts(api, currentPost);
  111. const currentThread = await get.currentThreadId(api);
  112. if (currentThread > lastThread)
  113. lastThread = await this.fetchThreads(api, currentThread);
  114. const postCount = await api.query.proposalsDiscussion.postCount();
  115. this.setState({ proposalComments: Number(postCount) });
  116. lastBlock = block;
  117. // validators
  118. const currentEra = Number(await api.query.staking.currentEra());
  119. if (currentEra > era) {
  120. era = currentEra;
  121. this.fetchStakes(api, era, this.state.validators);
  122. this.save("era", era);
  123. this.fetchLastReward(api, era - 1);
  124. } else if (this.state.lastReward === 0)
  125. this.fetchLastReward(api, currentEra);
  126. this.fetchEraRewardPoints(api, Number(era));
  127. // check election stage
  128. if (id < termEndsAt || id < stageEndsAt) return;
  129. const json = stage.toJSON();
  130. const key = Object.keys(json)[0];
  131. stageEndsAt = json[key];
  132. //console.log(id, stageEndsAt, json, key);
  133. termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
  134. round = Number((await api.query.councilElection.round()).toJSON());
  135. stage = await api.query.councilElection.stage();
  136. councilElection = { termEndsAt, stage: stage.toJSON(), round };
  137. this.setState({ councilElection });
  138. }
  139. );
  140. this.fetchCouncils(api, round);
  141. this.fetchProposals(api);
  142. this.fetchValidators(api);
  143. this.fetchNominators(api);
  144. }
  145. async fetchLastReward(api: Api, era: number) {
  146. const lastReward = Number(await api.query.staking.erasValidatorReward(era));
  147. console.debug(`last reward`, era, lastReward);
  148. if (lastReward > 0) this.save("lastReward", lastReward);
  149. else this.fetchLastReward(api, era - 1);
  150. }
  151. async fetchTokenomics() {
  152. console.debug(`Updating tokenomics`);
  153. const { data } = await axios.get("https://status.joystream.org/status");
  154. if (!data) return;
  155. this.save("tokenomics", data);
  156. }
  157. async fetchChannels(api: Api, lastId: number) {
  158. for (let id = lastId; id > 0; id--) {
  159. if (this.state.channels.find((c) => c.id === id)) continue;
  160. console.debug(`Fetching channel ${id}`);
  161. const data = await api.query.contentWorkingGroup.channelById(id);
  162. const handle = String(data.handle);
  163. const title = String(data.title);
  164. const description = String(data.description);
  165. const avatar = String(data.avatar);
  166. const banner = String(data.banner);
  167. const content = String(data.content);
  168. const ownerId = Number(data.owner);
  169. const accountId = String(data.role_account);
  170. const publicationStatus =
  171. data.publication_status === "Public" ? true : false;
  172. const curation = String(data.curation_status);
  173. const createdAt = data.created;
  174. const principal = Number(data.principal_id);
  175. //this.fetchMemberByAccount(api, accountId);
  176. const channel: Channel = {
  177. id,
  178. handle,
  179. title,
  180. description,
  181. avatar,
  182. banner,
  183. content,
  184. ownerId,
  185. accountId,
  186. publicationStatus,
  187. curation,
  188. createdAt,
  189. principal,
  190. };
  191. //console.debug(data, channel);
  192. const channels = this.state.channels.concat(channel);
  193. this.save("channels", channels);
  194. }
  195. return lastId;
  196. }
  197. async fetchCategories(api: Api, lastId: number) {
  198. for (let id = lastId; id > 0; id--) {
  199. if (this.state.categories.find((c) => c.id === id)) continue;
  200. console.debug(`fetching category ${id}`);
  201. const data = await api.query.forum.categoryById(id);
  202. const threadId = Number(data.thread_id);
  203. const title = String(data.title);
  204. const description = String(data.description);
  205. const createdAt = Number(data.created_at.block);
  206. const deleted = data.deleted;
  207. const archived = data.archived;
  208. const subcategories = Number(data.num_direct_subcategories);
  209. const moderatedThreads = Number(data.num_direct_moderated_threads);
  210. const unmoderatedThreads = Number(data.num_direct_unmoderated_threads);
  211. const position = Number(data.position_in_parent_category);
  212. const moderatorId = String(data.moderator_id);
  213. const category: Category = {
  214. id,
  215. threadId,
  216. title,
  217. description,
  218. createdAt,
  219. deleted,
  220. archived,
  221. subcategories,
  222. moderatedThreads,
  223. unmoderatedThreads,
  224. position,
  225. moderatorId,
  226. };
  227. const categories = this.state.categories.concat(category);
  228. this.save("categories", categories);
  229. }
  230. return lastId;
  231. }
  232. async fetchPosts(api: Api, lastId: number) {
  233. for (let id = lastId; id > 0; id--) {
  234. if (this.state.posts.find((p) => p.id === id)) continue;
  235. console.debug(`fetching post ${id}`);
  236. const data = await api.query.forum.postById(id);
  237. const threadId = Number(data.thread_id);
  238. const text = data.current_text;
  239. //const moderation = data.moderation;
  240. //const history = data.text_change_history;
  241. //const createdAt = moment(data.created_at);
  242. const createdAt = data.created_at;
  243. const authorId = String(data.author_id);
  244. const post: Post = { id, threadId, text, authorId, createdAt };
  245. const posts = this.state.posts.concat(post);
  246. this.save("posts", posts);
  247. }
  248. return lastId;
  249. }
  250. async fetchThreads(api: Api, lastId: number) {
  251. for (let id = lastId; id > 0; id--) {
  252. if (this.state.threads.find((t) => t.id === id)) continue;
  253. console.debug(`fetching thread ${id}`);
  254. const data = await api.query.forum.threadById(id);
  255. const title = String(data.title);
  256. const categoryId = Number(data.category_id);
  257. const nrInCategory = Number(data.nr_in_category);
  258. const moderation = data.moderation;
  259. const createdAt = String(data.created_at.block);
  260. const authorId = String(data.author_id);
  261. const thread: Thread = {
  262. id,
  263. title,
  264. categoryId,
  265. nrInCategory,
  266. moderation,
  267. createdAt,
  268. authorId,
  269. };
  270. const threads = this.state.threads.concat(thread);
  271. this.save("threads", threads);
  272. }
  273. return lastId;
  274. }
  275. async fetchCouncils(api: Api, currentRound: number) {
  276. let { councils } = this.state;
  277. const cycle = 201600;
  278. for (let round = 0; round < currentRound; round++) {
  279. const block = 57601 + round * cycle;
  280. if (councils[round] || block > this.state.block) continue;
  281. console.debug(`Fetching council at block ${block}`);
  282. const blockHash = await api.rpc.chain.getBlockHash(block);
  283. if (!blockHash) continue;
  284. councils[round] = await api.query.council.activeCouncil.at(blockHash);
  285. this.save("councils", councils);
  286. }
  287. }
  288. // proposals
  289. async fetchProposals(api: Api) {
  290. const proposalCount = await get.proposalCount(api);
  291. for (let i = proposalCount; i > 0; i--) this.fetchProposal(api, i);
  292. }
  293. async fetchProposal(api: Api, id: number) {
  294. let { proposals } = this.state;
  295. const exists = proposals.find((p) => p && p.id === id);
  296. if (exists && exists.stage === "Finalized")
  297. if (exists.votesByAccount && exists.votesByAccount.length) return;
  298. else return this.fetchVotesPerProposal(api, exists);
  299. console.debug(`Fetching proposal ${id}`);
  300. const proposal = await get.proposalDetail(api, id);
  301. proposals[id] = proposal;
  302. this.save("proposals", proposals);
  303. this.fetchVotesPerProposal(api, proposal);
  304. }
  305. async fetchVotesPerProposal(api: Api, proposal: ProposalDetail) {
  306. const { votesByAccount } = proposal;
  307. if (votesByAccount && votesByAccount.length) return;
  308. console.debug(`Fetching proposal votes (${proposal.id})`);
  309. const { councils, proposals } = this.state;
  310. let members: Member[] = [];
  311. councils.map((seats) =>
  312. seats.forEach(async (seat: Seat) => {
  313. if (members.find((member) => member.account === seat.member)) return;
  314. const member = this.state.members.find(
  315. (m) => m.account === seat.member
  316. );
  317. member && members.push(member);
  318. })
  319. );
  320. const { id } = proposal;
  321. proposal.votesByAccount = await Promise.all(
  322. members.map(async (member) => {
  323. const vote = await this.fetchVoteByProposalByVoter(api, id, member.id);
  324. return { vote, handle: member.handle };
  325. })
  326. );
  327. proposals[id] = proposal;
  328. this.save("proposals", proposals);
  329. }
  330. async fetchVoteByProposalByVoter(
  331. api: Api,
  332. proposalId: number,
  333. voterId: number
  334. ): Promise<string> {
  335. console.debug(`Fetching vote by ${voterId} for proposal ${proposalId}`);
  336. const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
  337. proposalId,
  338. voterId
  339. );
  340. const hasVoted: number = (
  341. await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
  342. proposalId,
  343. voterId
  344. )
  345. ).toNumber();
  346. return hasVoted ? String(vote) : "";
  347. }
  348. // nominators, validators
  349. async fetchNominators(api: Api) {
  350. const nominatorEntries = await api.query.staking.nominators.entries();
  351. const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()));
  352. this.save("nominators", nominators);
  353. }
  354. async fetchValidators(api: Api) {
  355. // session.disabledValidators: Vec<u32>
  356. // TODO check online: imOnline.keys
  357. // imOnline.authoredBlocks: 2
  358. // TODO session.currentIndex: 17,081
  359. const stashes = await api.derive.staking.stashes();
  360. this.save(
  361. "stashes",
  362. stashes.map((s: any) => String(s))
  363. );
  364. const validatorEntries = await api.query.session.validators();
  365. const validators = await validatorEntries.map((v: any) => String(v));
  366. this.save("validators", validators);
  367. }
  368. async fetchStakes(api: Api, era: number, validators: string[]) {
  369. // TODO staking.bondedEras: Vec<(EraIndex,SessionIndex)>
  370. console.debug(`fetching stakes`);
  371. const { stashes } = this.state;
  372. if (!stashes) return;
  373. stashes.forEach(async (validator: string) => {
  374. try {
  375. const prefs = await api.query.staking.erasValidatorPrefs(
  376. era,
  377. validator
  378. );
  379. const commission = Number(prefs.commission) / 10000000;
  380. const data = await api.query.staking.erasStakers(era, validator);
  381. let { total, own, others } = data.toJSON();
  382. let { stakes = {} } = this.state;
  383. stakes[validator] = { total, own, others, commission };
  384. this.save("stakes", stakes);
  385. } catch (e) {
  386. console.warn(
  387. `Failed to fetch stakes for ${validator} in era ${era}`,
  388. e
  389. );
  390. }
  391. });
  392. }
  393. async fetchEraRewardPoints(api: Api, era: number) {
  394. const data = await api.query.staking.erasRewardPoints(era);
  395. this.setState({ rewardPoints: data.toJSON() });
  396. }
  397. // data objects
  398. fetchDataObjects() {
  399. // TODO dataDirectory.knownContentIds: Vec<ContentId>
  400. }
  401. // accounts
  402. async fetchMembers(api: Api, lastId: number) {
  403. for (let id = lastId; id > 0; id--) {
  404. this.fetchMember(api, id);
  405. }
  406. }
  407. async fetchMemberByAccount(api: Api, account: string): Promise<Member> {
  408. const exists = this.state.members.find(
  409. (m: Member) => String(m.account) === String(account)
  410. );
  411. if (exists) return exists;
  412. const id = await get.memberIdByAccount(api, account);
  413. if (!id)
  414. return { id: -1, handle: `unknown`, account, about: ``, registeredAt: 0 };
  415. return await this.fetchMember(api, Number(id));
  416. }
  417. async fetchMember(api: Api, id: number): Promise<Member> {
  418. const exists = this.state.members.find((m: Member) => m.id === id);
  419. if (exists) return exists;
  420. console.debug(`Fetching member ${id}`);
  421. const membership = await get.membership(api, id);
  422. const handle = String(membership.handle);
  423. const account = String(membership.root_account);
  424. const about = String(membership.about);
  425. const registeredAt = Number(membership.registered_at_block);
  426. const member: Member = { id, handle, account, registeredAt, about };
  427. const members = this.state.members.concat(member);
  428. this.save(`members`, members);
  429. this.updateHandles(members);
  430. return member;
  431. }
  432. updateHandles(members: Member[]) {
  433. if (!members.length) return;
  434. let handles: Handles = {};
  435. members.forEach((m) => (handles[String(m.account)] = m.handle));
  436. this.save(`handles`, handles);
  437. }
  438. // Reports
  439. async fetchReports() {
  440. const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`;
  441. const apiBase = `https://api.github.com/repos/joystream/community-repo/contents/council-reports`;
  442. const urls: { [key: string]: string } = {
  443. alexandria: `${apiBase}/alexandria-testnet`,
  444. archive: `${apiBase}/archived-reports`,
  445. template: `${domain}/templates/council_report_template_v1.md`,
  446. };
  447. ["alexandria", "archive"].map((folder) =>
  448. this.fetchGithubDir(urls[folder])
  449. );
  450. // template
  451. this.fetchGithubFile(urls.template);
  452. }
  453. async saveReport(name: string, content: Promise<string>) {
  454. const { reports } = this.state;
  455. reports[name] = await content;
  456. this.save("reports", reports);
  457. }
  458. async fetchGithubFile(url: string): Promise<string> {
  459. const { data } = await axios.get(url);
  460. return data;
  461. }
  462. async fetchGithubDir(url: string) {
  463. const { data } = await axios.get(url);
  464. data.forEach(
  465. async (o: {
  466. name: string;
  467. type: string;
  468. url: string;
  469. download_url: string;
  470. }) => {
  471. const match = o.name.match(/^(.+)\.md$/);
  472. const name = match ? match[1] : o.name;
  473. if (o.type === "file")
  474. this.saveReport(name, this.fetchGithubFile(o.download_url));
  475. else this.fetchGithubDir(o.url);
  476. }
  477. );
  478. }
  479. loadMembers() {
  480. const members = this.load("members");
  481. if (!members) return;
  482. this.updateHandles(members);
  483. this.setState({ members });
  484. }
  485. loadCouncils() {
  486. const councils = this.load("councils");
  487. if (!councils || !councils.length || typeof councils[0][0] === "number")
  488. return;
  489. this.setState({ councils });
  490. }
  491. loadProposals() {
  492. const proposals = this.load("proposals");
  493. if (proposals) this.setState({ proposals });
  494. }
  495. loadChannels() {
  496. const channels = this.load("channels");
  497. if (channels) this.setState({ channels });
  498. }
  499. loadCategories() {
  500. const categories = this.load("categories");
  501. if (categories) this.setState({ categories });
  502. }
  503. loadPosts() {
  504. const posts = this.load("posts");
  505. if (posts) this.setState({ posts });
  506. }
  507. loadThreads() {
  508. const threads = this.load("threads");
  509. if (threads) this.setState({ threads });
  510. }
  511. loadValidators() {
  512. const validators = this.load("validators");
  513. if (validators) this.setState({ validators });
  514. const stashes = this.load("stashes") || [];
  515. if (stashes) this.setState({ stashes });
  516. }
  517. loadNominators() {
  518. const nominators = this.load("nominators");
  519. if (nominators) this.setState({ nominators });
  520. }
  521. loadHandles() {
  522. const handles = this.load("handles");
  523. if (handles) this.setState({ handles });
  524. }
  525. loadReports() {
  526. const reports = this.load("reports");
  527. if (!reports) return this.fetchReports();
  528. this.setState({ reports });
  529. }
  530. loadTokenomics() {
  531. const tokenomics = this.load("tokenomics");
  532. if (tokenomics) this.setState({ tokenomics });
  533. }
  534. loadMint() {
  535. const mint = this.load("mint");
  536. if (mint) this.setState({ mint });
  537. }
  538. loadStakes() {
  539. const stakes = this.load("stakes");
  540. if (stakes) this.setState({ stakes });
  541. }
  542. clearData() {
  543. this.save("version", version);
  544. this.save("proposals", []);
  545. }
  546. async loadData() {
  547. const lastVersion = this.load("version");
  548. if (lastVersion !== version) return this.clearData();
  549. console.log(`Loading data`);
  550. const termEndsAt = this.load("termEndsAt");
  551. await this.loadMembers();
  552. await this.loadCouncils();
  553. await this.loadCategories();
  554. await this.loadChannels();
  555. await this.loadProposals();
  556. await this.loadPosts();
  557. await this.loadThreads();
  558. await this.loadValidators();
  559. await this.loadNominators();
  560. await this.loadHandles();
  561. await this.loadTokenomics();
  562. await this.loadReports();
  563. await this.loadStakes();
  564. const block = this.load("block");
  565. const now = this.load("now");
  566. const era = this.load("era") || `..`;
  567. const round = this.load("round");
  568. const stage = this.load("stage");
  569. const stars = this.load("stars") || {};
  570. const lastReward = this.load("lastReward") || 0;
  571. const loading = false;
  572. this.setState({
  573. block,
  574. era,
  575. now,
  576. round,
  577. stage,
  578. stars,
  579. termEndsAt,
  580. loading,
  581. lastReward,
  582. });
  583. console.debug(`Finished loading.`);
  584. }
  585. load(key: string) {
  586. try {
  587. const data = localStorage.getItem(key);
  588. if (data) return JSON.parse(data);
  589. } catch (e) {
  590. console.warn(`Failed to load ${key}`, e);
  591. }
  592. }
  593. save(key: string, data: any) {
  594. try {
  595. localStorage.setItem(key, JSON.stringify(data));
  596. } catch (e) {
  597. console.warn(`Failed to save ${key}`, e);
  598. } finally {
  599. //console.debug(`saving ${key}`, data);
  600. this.setState({ [key]: data });
  601. }
  602. }
  603. render() {
  604. if (this.state.loading) return <Loading />;
  605. return <Routes load={this.load} save={this.save} {...this.state} />;
  606. }
  607. componentDidMount() {
  608. this.loadData();
  609. this.initializeSocket();
  610. this.fetchTokenomics();
  611. setInterval(this.fetchTokenomics, 900000);
  612. }
  613. componentWillUnmount() {
  614. console.debug("unmounting...");
  615. }
  616. constructor(props: IProps) {
  617. super(props);
  618. this.state = initialState;
  619. this.fetchTokenomics = this.fetchTokenomics.bind(this);
  620. this.fetchProposal = this.fetchProposal.bind(this);
  621. this.load = this.load.bind(this);
  622. this.save = this.save.bind(this);
  623. }
  624. }
  625. export default App;