Browse Source

backend: use typescript

Joystream Stats 4 years ago
parent
commit
f1af9d52d1

+ 7 - 1
package.json

@@ -30,7 +30,7 @@
     "socket.io": "^2.2.0"
   },
   "scripts": {
-    "start": "concurrently \"PORT=3050 HOST=127.0.0.1 react-scripts start\" \"nodemon -i src server \"",
+    "start": "concurrently \"PORT=3050 HOST=127.0.0.1 react-scripts start\" \"nodemon -i src server/index.ts\"",
     "build": "react-scripts build",
     "prod": "node server",
     "test": "react-scripts test --env=jsdom",
@@ -51,5 +51,11 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.11",
+    "@types/node": "^14.14.31",
+    "ts-node": "^9.1.1",
+    "typescript": "^4.1.5"
   }
 }

+ 24 - 15
server/index.js → server/index.ts

@@ -1,9 +1,25 @@
-const express = require("express");
-const path = require("path");
+//import express from "express"
+
+const PORT = process.env.PORT || 3500;
+//const URL = ["http://localhost:3050"];
+
+const express = require("express")
 const app = express();
+
+  const server = app.listen(PORT, () => {
+    console.log(chalk.blue(`[Express] Listening on port ${PORT}`));
+  });
+
+const socketio = require("socket.io");
+const io = socketio(server);
+require("./socket")(io);
+
+
+const path = require("path");
+
 //const cors = require("cors");
 const morgan = require("morgan");
-const socketio = require("socket.io");
+
 const pg = require("pg");
 delete pg.native;
 const db = require("./db");
@@ -16,9 +32,6 @@ const chalk = require("chalk");
 //const sessionStore = new SequelizeStore({ db })
 //const bot = require('../ircbot')
 
-const PORT = process.env.PORT || 3500;
-//const URL = ["http://localhost:3050"];
-
 app.use(morgan("dev"));
 //app.use(cors({ credentials: true, origin: URL }))
 // passport.use(
@@ -78,16 +91,16 @@ app.use(
   "/static",
   express.static(path.resolve(__dirname, "..", "build", "static"))
 );
-app.get("/manifest.json", (req, res) => {
+app.get("/manifest.json", (req:any, res:any) => {
   res.sendFile(path.resolve(__dirname, "..", "build", "manifest.json"));
 });
-app.get("/favicon.png", (req, res) => {
+app.get("/favicon.png", (req:any, res:any) => {
   res.sendFile(path.resolve(__dirname, "..", "build", "favicon.png"));
 });
 app.use("*", express.static(path.resolve(__dirname, "..", "build")));
 
 // error handling endware
-app.use((err, req, res, next) => {
+app.use((err :any, req:any, res:any, next:any) => {
   console.error(err);
   console.error(err.stack);
   res.status(err.status || 500).send(err.message || "Internal server error.");
@@ -95,12 +108,6 @@ app.use((err, req, res, next) => {
 });
 
 const startListening = () => {
-  const server = app.listen(PORT, () => {
-    console.log(chalk.blue(`[Express] Listening on port ${PORT}`));
-  });
-
-  const io = socketio(server);
-  require("./socket")(io);
 };
 
 const startApp = async () => {
@@ -109,3 +116,5 @@ const startApp = async () => {
 };
 
 startApp();
+
+module.exports = {}

+ 104 - 0
server/joystream/index.ts

@@ -0,0 +1,104 @@
+
+const addBlock = async (api, io, header, status) => {
+          const id = Number(header.number)
+        const exists = await Block.findByPk(id)
+        if (exists) return
+        const timestamp = (await api.query.timestamp.now()).toNumber()
+                                     const last = await Block.findByPk(id - 1)
+                                               
+						 const blocktime = last ? timestamp - last.timestamp : 6000
+                                               const author = 	header.author?.toString()
+ 			                                         
+        const block = await Block.create({          id,
+          timestamp,
+          blocktime,
+          author
+        })
+        console.log(
+          '[Joystream] block',
+          block.id,
+          block.blocktime,
+          block.author
+        )
+        io.emit('block', block)
+
+						 update() 
+}
+
+const processEvents = (blockHash) => {
+          const blockEvents = api.query.system.events.at(
+            blockHash
+          ) as Vec<EventRecord>
+          let transfers = blockEvents.filter((event) => {
+            return event.section == 'balances' && event.method == 'Transfer'
+          })
+          let validatorRewards = blockEvents.filter((event) => {
+            return event.section == 'staking' && event.method == 'Reward'
+          })
+}
+
+
+const update = () => {
+const id = header.number.toNumber();
+        if (blocks.find((b) => b.id === id)) return;
+        const timestamp = (await api.query.timestamp.now()).toNumber();
+        const duration = timestamp - lastBlock.timestamp;
+        const block: Block = { id, timestamp, duration };
+        blocks = blocks.concat(block);
+        this.setState({ blocks, loading: false });
+        this.save("block", id);
+        this.save("now", timestamp);
+
+        const proposalCount = await get.proposalCount(api);
+        if (proposalCount > this.state.proposalCount) {
+          this.fetchProposal(api, proposalCount);
+          this.setState({ proposalCount });
+        }
+
+        const currentChannel = await get.currentChannelId(api);
+        if (currentChannel > lastChannel)
+          lastChannel = await this.fetchChannels(api, currentChannel);
+
+        const currentCategory = await get.currentCategoryId(api);
+        if (currentCategory > lastCategory)
+          lastCategory = await this.fetchCategories(api, currentCategory);
+
+        const currentPost = await get.currentPostId(api);
+        if (currentPost > lastPost)
+          lastPost = await this.fetchPosts(api, currentPost);
+
+        const currentThread = await get.currentThreadId(api);
+        if (currentThread > lastThread)
+          lastThread = await this.fetchThreads(api, currentThread);
+
+        const postCount = await api.query.proposalsDiscussion.postCount();
+        this.setState({ proposalComments: Number(postCount) });
+
+        lastBlock = block;
+
+        // validators
+        const currentEra = Number(await api.query.staking.currentEra());
+        if (currentEra > era) {
+          era = currentEra;
+          this.fetchStakes(api, era, this.state.validators);
+          this.save("era", era);
+          this.fetchLastReward(api, era - 1);
+        } else if (this.state.lastReward === 0)
+          this.fetchLastReward(api, currentEra);
+
+        this.fetchEraRewardPoints(api, Number(era));
+
+        // check election stage
+        if (id < termEndsAt || id < stageEndsAt) return;
+        const json = stage.toJSON();
+        const key = Object.keys(json)[0];
+        stageEndsAt = json[key];
+        //console.log(id, stageEndsAt, json, key);
+
+        termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
+        round = Number((await api.query.councilElection.round()).toJSON());
+        stage = await api.query.councilElection.stage();
+        councilElection = { termEndsAt, stage: stage.toJSON(), round };
+        this.setState({ councilElection });
+
+}

+ 270 - 0
server/joystream/lib/announcements.ts

@@ -0,0 +1,270 @@
+import { Api, Council, ProposalDetail, Proposals, Summary } from "../types";
+import { BlockNumber } from "@polkadot/types/interfaces";
+import { Channel } from "@joystream/types/augment";
+import { Category, Thread, Post } from "@joystream/types/forum";
+import { formatTime } from "./util";
+import {
+  categoryById,
+  memberHandle,
+  memberHandleByAccount,
+  proposalDetail,
+} from "./getters";
+import { domain } from "../config";
+import moment from "moment";
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+// query API repeatedly to ensure a result
+const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
+  let result = await cb();
+  for (let i: number = 0; i < 10; i++) {
+    if (result[test] !== "") return result;
+    result = await cb();
+    await sleep(5000);
+  }
+};
+
+// announce latest channels
+export const channels = async (
+  api: Api,
+  channels: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const [last, current] = channels;
+  const messages: string[] = [];
+
+  for (let id: number = +last + 1; id <= current; id++) {
+    const channel: Channel = await query("title", () =>
+      api.query.contentWorkingGroup.channelById(id)
+    );
+    const member: any = { id: channel.owner, handle: "", url: "" };
+    member.handle = await memberHandle(api, member.id.toJSON());
+    member.url = `${domain}/#/members/${member.handle}`;
+    messages.push(
+      `<b>Channel <a href="${domain}/#//media/channels/${id}">${channel.title}</a> by <a href="${member.url}">${member.handle} (${member.id})</a></b>`
+    );
+  }
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
+
+// announce council change
+export const council = async (
+  api: Api,
+  council: Council,
+  currentBlock: number,
+  sendMessage: (msg: string) => void
+): Promise<Council> => {
+  const round: number = await api.query.councilElection.round();
+  const stage: any = await api.query.councilElection.stage();
+  let stageString = Object.keys(JSON.parse(JSON.stringify(stage)))[0];
+  let msg = "";
+
+  if (!stage || stage.toJSON() === null) {
+    stageString = "elected";
+    const councilEnd: BlockNumber = await api.query.council.termEndsAt();
+    const termDuration: BlockNumber = await api.query.councilElection.newTermDuration();
+    const block = councilEnd.toNumber() - termDuration.toNumber();
+    const remainingBlocks: number = councilEnd.toNumber() - currentBlock;
+    const endDate = moment()
+      // .add(remainingBlocks * 6, "s")
+      .format("DD/MM/YYYY");
+    msg = `<a href="${domain}/#/council/members">Council ${round}</a> elected at block ${block} until block ${councilEnd}. Next election: ${endDate} (${remainingBlocks} blocks)`;
+  } else {
+    if (stageString === "Announcing") {
+      msg = `Announcing election for round ${round} started.<a href="${domain}/#/council/applicants">Apply now!</a>`;
+    } else if (stageString === "Voting") {
+      msg = `Voting stage for council election started. <a href="${domain}/#/council/applicants">Vote now!</a>`;
+    } else if (stageString === "Revealing") {
+      msg = `Revealing stage for council election started. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`;
+    } else console.log(`[council] unrecognized stage: ${stageString}`);
+  }
+
+  if (round !== council.round && stageString !== council.last) sendMessage(msg);
+  return { round, last: stageString };
+};
+
+// forum
+// announce latest categories
+export const categories = async (
+  api: Api,
+  category: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const messages: string[] = [];
+
+  for (let id: number = +category[0] + 1; id <= category[1]; id++) {
+    const cat: Category = await query("title", () => categoryById(api, id));
+    const msg = `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`;
+    messages.push(msg);
+  }
+
+  sendMessage(messages.join("\r\n\r\n"));
+  return category[1];
+};
+
+// announce latest posts
+export const posts = async (
+  api: Api,
+  posts: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const [last, current] = posts;
+  const messages: string[] = [];
+
+  for (let id: number = +last + 1; id <= current; id++) {
+    const post: Post = await query("current_text", () =>
+      api.query.forum.postById(id)
+    );
+    const replyId: number = post.nr_in_thread.toNumber();
+    const message: string = post.current_text;
+    const excerpt: string = message.substring(0, 100);
+    const threadId: number = post.thread_id.toNumber();
+    const thread: Thread = await query("title", () =>
+      api.query.forum.threadById(threadId)
+    );
+    const threadTitle: string = thread.title;
+    const category: Category = await query("title", () =>
+      categoryById(api, thread.category_id.toNumber())
+    );
+    const handle = await memberHandleByAccount(api, post.author_id.toJSON());
+    const msg = `<b><a href="${domain}/#/members/${handle}">${handle}</a> posted <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">${threadTitle}</a> in <a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>:</b>\n\r<i>${excerpt}</i> <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">more</a>`;
+    messages.push(msg);
+  }
+
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
+
+// announce latest threads
+export const threads = async (
+  api: Api,
+  threads: number[],
+  sendMessage: (msg: string) => void
+): Promise<number> => {
+  const [last, current] = threads;
+  const messages: string[] = [];
+
+  for (let id: number = +last + 1; id <= current; id++) {
+    const thread: Thread = await query("title", () =>
+      api.query.forum.threadById(id)
+    );
+    const { title, author_id } = thread;
+    const handle: string = await memberHandleByAccount(api, author_id.toJSON());
+    const category: Category = await query("title", () =>
+      categoryById(api, thread.category_id.toNumber())
+    );
+    const msg = `Thread ${id}: <a href="${domain}/#/forum/threads/${id}">"${title}"</a> by <a href="${domain}/#/members/${handle}">${handle}</a> in category "<a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>" `;
+    messages.push(msg);
+  }
+
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
+
+// announce latest proposals
+export const proposals = async (
+  api: Api,
+  prop: Proposals,
+  sendMessage: (msg: string) => void
+): Promise<Proposals> => {
+  let { current, last, active, executing } = prop;
+
+  for (let id: number = +last + 1; id <= current; id++) {
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { createdAt, message, parameters } = proposal;
+    const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+    const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
+    sendMessage(msg);
+    active.push(id);
+  }
+
+  for (const id of active) {
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { finalizedAt, message, parameters, result, stage } = proposal;
+    if (stage === "Finalized") {
+      let label: string = result;
+      if (result === "Approved") {
+        const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
+        label = executed ? "Executed" : "Finalized";
+        if (!executed) executing.push(id);
+      }
+      const msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
+      sendMessage(msg);
+      active = active.filter((a) => a !== id);
+    }
+  }
+
+  for (const id of executing) {
+    const proposal = await proposalDetail(api, id);
+    const { exec, finalizedAt, message, parameters } = proposal;
+    const execStatus = exec ? Object.keys(exec)[0] : "";
+    const label = execStatus === "Executed" ? "has been" : "failed to be";
+    const block = +finalizedAt + parameters.gracePeriod.toNumber();
+    const msg = `Proposal ${id} <b>${label} executed</b> at block ${block}.\r\n${message}`;
+    sendMessage(msg);
+    executing = executing.filter((e) => e !== id);
+  }
+
+  return { current, last: current, active, executing };
+};
+
+// heartbeat
+
+const getAverage = (array: number[]) =>
+  array.reduce((a: number, b: number) => a + b, 0) / array.length;
+
+export const heartbeat = async (
+  api: Api,
+  summary: Summary,
+  timePassed: string,
+  accountId: string,
+  sendMessage: (msg: string) => void
+): Promise<void> => {
+  const { blocks, nominators, validators } = summary;
+  const avgDuration =
+    blocks.reduce((a, b) => a + b.duration, 0) / blocks.length;
+  const era: any = await api.query.staking.currentEra();
+  const totalStake: any = await api.query.staking.erasTotalStake(parseInt(era));
+  const stakers = await api.query.staking.erasStakers(parseInt(era), accountId);
+  const stakerCount = stakers.others.length;
+  const avgStake = parseInt(totalStake.toString()) / stakerCount;
+
+  console.log(`
+  Blocks produced during ${timePassed}h in era ${era}: ${blocks.length}
+  Average blocktime: ${Math.floor(avgDuration) / 1000} s
+  Average stake: ${avgStake / 1000000} M JOY (${stakerCount} stakers)
+  Average number of nominators: ${getAverage(nominators)}
+  Average number of validators: ${getAverage(validators)}`);
+};
+
+export const formatProposalMessage = (data: string[]): string => {
+  const [id, title, type, stage, result, handle] = data;
+  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>: <a href="${domain}/#/members/${handle}">${handle}</a>\r\n<b>Title</b>: <a href="${domain}/#/proposals/${id}">${title}</a>\r\n<b>Stage</b>: ${stage}\r\n<b>Result</b>: ${result}`;
+};
+
+// providers
+
+export const provider = (
+  id: number,
+  address: string,
+  status: string,
+  sendMessage: (msg: string) => void
+): void => {
+  const msg = `[${formatTime()}] Storage Provider ${id} (${address}) is ${status}`;
+  sendMessage(msg);
+};
+
+export const newOpening = (id: number, sendMessage: (msg: string) => void) => {
+  const msg = `New opening: <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`;
+  sendMessage(msg);
+};
+
+export const closeOpening = (
+  id: number,
+  handle: string,
+  sendMessage: (msg: string) => void
+): void => {
+  const msg = `<a href="${domain}/#/members/${handle}">${handle}</a> was choosen as <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`;
+  sendMessage(msg);
+};

+ 180 - 0
server/joystream/lib/getters.ts

@@ -0,0 +1,180 @@
+import { formatProposalMessage } from "./announcements";
+import fetch from "node-fetch";
+
+//types
+
+import { Api, ProposalArray, ProposalDetail } from "../types";
+import {
+  ChannelId,
+  PostId,
+  ProposalDetailsOf,
+  ThreadId,
+} from "@joystream/types/augment";
+import { Category, CategoryId } from "@joystream/types/forum";
+import { MemberId, Membership } from "@joystream/types/members";
+import { Proposal } from "@joystream/types/proposals";
+import { AccountId } from "@polkadot/types/interfaces";
+
+// channel
+
+export const currentChannelId = async (api: Api): Promise<number> => {
+  const id: ChannelId = await api.query.contentWorkingGroup.nextChannelId();
+  return id.toNumber() - 1;
+};
+
+// members
+
+export const membership = async (
+  api: Api,
+  id: MemberId | number
+): Promise<Membership> => {
+  return await api.query.members.membershipById(id);
+};
+
+export const memberHandle = async (api: Api, id: MemberId): Promise<string> => {
+  const member: Membership = await membership(api, id);
+  return member.handle.toJSON();
+};
+
+export const memberIdByAccount = async (
+  api: Api,
+  account: AccountId | string
+): Promise<MemberId | number> => {
+  const ids = await api.query.members.memberIdsByRootAccountId(account);
+  return ids.length ? ids[0] : 0;
+};
+
+export const memberHandleByAccount = async (
+  api: Api,
+  account: AccountId | string
+): Promise<string> => {
+  const id: MemberId = await api.query.members.memberIdsByRootAccountId(
+    account
+  );
+  const handle: string = await memberHandle(api, id);
+  return handle === "joystream_storage_member" ? "joystream" : handle;
+};
+
+// forum
+
+export const categoryById = async (api: Api, id: number): Promise<Category> => {
+  const category: Category = await api.query.forum.categoryById(id);
+  return category;
+};
+
+export const currentPostId = async (api: Api): Promise<number> => {
+  const postId: PostId = await api.query.forum.nextPostId();
+  return postId.toNumber() - 1;
+};
+
+export const currentThreadId = async (api: Api): Promise<number> => {
+  const threadId: ThreadId = await api.query.forum.nextThreadId();
+  return threadId.toNumber() - 1;
+};
+
+export const currentCategoryId = async (api: Api): Promise<number> => {
+  const categoryId: CategoryId = await api.query.forum.nextCategoryId();
+  return categoryId.toNumber() - 1;
+};
+
+// proposals
+
+export const proposalCount = async (api: Api): Promise<number> => {
+  const proposalCount: any = await api.query.proposalsEngine.proposalCount();
+  return proposalCount.toJSON() || 0;
+};
+
+export const activeProposalCount = async (api: Api): Promise<number> => {
+  const proposalCount: number = await api.query.proposalsEngine.activeProposalCount();
+  return proposalCount || 0;
+};
+
+export const pendingProposals = async (api: Api): Promise<ProposalArray> => {
+  const pending: ProposalArray = await api.query.proposalsEngine.pendingExecutionProposalIds(
+    await activeProposalCount(api)
+  );
+  //const pending: ProposalArray = pendingProposals.toJSON();
+  if (pending.length) console.debug("pending proposals", pending);
+  return pending;
+};
+
+export const activeProposals = async (api: Api): Promise<ProposalArray> => {
+  const active: ProposalArray = await api.query.proposalsEngine.activeProposalIds(
+    await activeProposalCount(api)
+  );
+  //const active: ProposalArray = result.toJSON();
+  if (active.length) console.debug("active proposals", active);
+  return active;
+};
+
+const getProposalType = async (api: Api, id: number): Promise<string> => {
+  const details: ProposalDetailsOf = await api.query.proposalsCodex.proposalDetailsByProposalId(
+    id
+  );
+  const [type]: string[] = Object.getOwnPropertyNames(details.toJSON());
+  return type;
+};
+
+export const proposalDetail = async (
+  api: Api,
+  id: number
+): Promise<ProposalDetail> => {
+  const proposal: Proposal = await api.query.proposalsEngine.proposals(id);
+  const status: { [key: string]: any } = proposal.status;
+  const stage: string = status.isActive ? "Active" : "Finalized";
+  const { finalizedAt, proposalStatus } = status[`as${stage}`];
+  const result: string = proposalStatus
+    ? (proposalStatus.isApproved && "Approved") ||
+      (proposalStatus.isCanceled && "Canceled") ||
+      (proposalStatus.isExpired && "Expired") ||
+      (proposalStatus.isRejected && "Rejected") ||
+      (proposalStatus.isSlashed && "Slashed") ||
+      (proposalStatus.isVetoed && "Vetoed")
+    : "Pending";
+  const exec = proposalStatus ? proposalStatus["Approved"] : null;
+
+  const { description, parameters, proposerId, votingResults } = proposal;
+  const author: string = await memberHandle(api, proposerId);
+  const title: string = proposal.title.toString();
+  const type: string = await getProposalType(api, id);
+  const args: string[] = [String(id), title, type, stage, result, author];
+  const message: string = formatProposalMessage(args);
+  const createdAt: number = proposal.createdAt.toNumber();
+
+  return {
+    id,
+    title,
+    createdAt,
+    finalizedAt,
+    parameters,
+    message,
+    stage,
+    result,
+    exec,
+    description,
+    votes: votingResults,
+    type,
+    author,
+    authorId: Number(proposerId)
+  };
+};
+
+// storage providers
+export const providerStatus = async (domain: string): Promise<boolean> => {
+  try {
+    const res = await fetch(`https://${domain}:5001/api/v0/version`);
+    return res.status >= 400 ? false : true;
+  } catch (e) {
+    return false;
+  }
+};
+
+export const nextOpeningId = async (api: Api): Promise<number> => {
+  const id = await api.query.storageWorkingGroup.nextOpeningId();
+  return id.toJSON();
+};
+
+export const nextWorkerId = async (api: Api): Promise<number> => {
+  const id = await api.query.storageWorkingGroup.nextWorkerId();
+  return id.toJSON();
+};

+ 57 - 0
server/joystream/lib/util.ts

@@ -0,0 +1,57 @@
+import { Options, Proposals } from "../types";
+import moment from "moment";
+
+export const parseArgs = (args: string[]): Options => {
+  const inArgs = (term: string): boolean => {
+    return args.find(a => a.search(term) > -1) ? true : false;
+  };
+
+  const options: Options = {
+    verbose: inArgs("--verbose") ? 2 : inArgs("--quiet") ? 0 : 1,
+    channel: inArgs("--channel"),
+    council: inArgs("--council"),
+    forum: inArgs("--forum"),
+    proposals: inArgs("--proposals")
+  };
+
+  if (options.verbose > 1) console.debug("args", args, "\noptions", options);
+  return options;
+};
+
+export const printStatus = (
+  opts: Options,
+  data: {
+    block: number;
+    cats: number[];
+    chain: string;
+    posts: number[];
+    proposals: Proposals;
+    threads: number[];
+  }
+): void => {
+  if (opts.verbose < 1) return;
+
+  const { block, chain, proposals, cats, posts, threads } = data;
+  const date = formatTime();
+  let message = `[${date}] Chain:${chain} Block:${block} `;
+
+  if (opts.forum)
+    message += `Post:${posts[1]} Cat:${cats[1]} Thread:${threads[1]} `;
+
+  if (opts.proposals)
+    message += `Proposals:${proposals.current} (Active:${proposals.active.length} Pending:${proposals.executing.length}) `;
+
+  console.log(message);
+};
+
+// time
+export const formatTime = (time?: any): string =>
+  moment(time).format("H:mm:ss");
+
+export const passedTime = (start: number, now: number): string =>
+  formatTime(moment.utc(moment(now).diff(moment(start))));
+
+export const exit = (log: (s: string) => void) => {
+  log("\nNo connection, exiting.\n");
+  process.exit();
+};

+ 0 - 42
server/socket.js

@@ -1,42 +0,0 @@
-const { Block } = require('./db/models')
-const chalk = require('chalk')
-
-const { types } = require('@joystream/types')
-const { ApiPromise, WsProvider } = require('@polkadot/api')
-const wsLocation = 'wss://rome-rpc-endpoint.joystream.org:9944'
-
-module.exports = (io) => {
-  const initializeSocket = async () => {
-    console.debug(`[Joystream] Connecting to ${wsLocation}`)
-    const provider = new WsProvider(wsLocation)
-    const api = await ApiPromise.create({ provider, types })
-    await api.isReady
-    console.debug(`[Joystream] Connected.`)
-
-    api.derive.chain.subscribeNewHeads(async (header) => {
-      const id = Number(header.number)
-      const exists = await Block.findByPk(id)
-      if (exists) return
-      const timestamp = (await api.query.timestamp.now()).toNumber()
-      const last = await Block.findByPk(id - 1)
-      const blocktime = last ? timestamp - last.timestamp : 6000
-      const block = await Block.create({
-        id,
-        timestamp,
-        blocktime,
-        author: header.author?.toString(),
-      })
-      console.log('[Joystream] block', block.id, block.blocktime, block.author)
-      io.emit('block', block)
-    })
-  }
-
-  io.on('connection', (socket) => {
-    socket.emit('welcome', 'Websockets are awesome!')
-    console.log(chalk.green(`[socket.io] Connection: ${socket.id}`))
-    socket.on('disconnect', async () => {
-      console.log(chalk.red(`[socket.io] Disconnect: ${socket.id}`))
-    })
-  })
-  initializeSocket()
-}

+ 54 - 0
server/socket.ts

@@ -0,0 +1,54 @@
+const { Block } = require('./db/models')
+//const chalk = require('chalk')
+
+const { types } = require('@joystream/types')
+const { ApiPromise, WsProvider } = require('@polkadot/api')
+const wsLocation = [
+  'wss://localhost:9933',
+  'wss://rome-rpc-endpoint.joystream.org:9944',
+]
+
+// TODO migrate lib: const { addBlock } = require('./joystream')
+
+module.exports = (io) => {
+  const initializeSocket = async () => {
+    console.debug(`[Joystream] Connecting to ${wsLocation}`)
+    try {
+      const provider = new WsProvider(wsLocation)
+      const api = await ApiPromise.create({ provider, types })
+      await api.isReady
+      console.debug(`[Joystream] Connected.`)
+      let status = {}
+      api.derive.chain.subscribeNewHeads(
+        async (h) => {}
+        //(status = (await addBlock(api, h, io, status)) || status)
+      )
+    } catch (e) {
+      return console.log(`[Joystream] Connection failed`)
+    }
+  }
+
+  io.on('connection', async (socket) => {
+    console.log(chalk.green(`[socket.io] Connection: ${socket.id}`))
+
+    socket.on('get blocks', async (limit) => {
+      const blocks = await Block.findAll({ limit, order: [['id', 'DESC']] })
+      socket.emit('blocks', blocks)
+    })
+
+    socket.on('disconnect', async () => {
+      console.log(chalk.red(`[socket.io] Disconnect: ${socket.id}`))
+    })
+  })
+
+  const connectUpstream = () => {
+    try {
+      initializeSocket()
+    } catch (e) {
+      console.log(`[Joystream] upstream connection failed`, e)
+      //setTimeout(connectUpstream, 1000)
+    }
+  }
+  //connectUpstream()
+  initializeSocket()
+}

+ 10 - 0
server/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "target": "es6",
+    "module": "commonjs",
+    "rootDir": "./",
+    "outDir": "./build",
+    "esModuleInterop": true,
+    "strict": false
+  }
+}