App.tsx 18 KB

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