index.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. import {
  2. Block,
  3. Category,
  4. Channel,
  5. Council,
  6. Era,
  7. Event,
  8. Member,
  9. Post,
  10. Proposal,
  11. Thread,
  12. } from '../db/models'
  13. const models: { [key: string]: any } = {
  14. channel: Channel,
  15. proposal: Proposal,
  16. category: Category,
  17. thread: Thread,
  18. post: Post,
  19. block: Block,
  20. council: Council,
  21. member: Member,
  22. era: Era,
  23. }
  24. import * as get from './lib/getters'
  25. import axios from 'axios'
  26. import moment from 'moment'
  27. import { VoteKind } from '@joystream/types/proposals'
  28. import {
  29. Api,
  30. Handles,
  31. IState,
  32. MemberType,
  33. CategoryType,
  34. ChannelType,
  35. PostType,
  36. Seat,
  37. ThreadType,
  38. CouncilType,
  39. ProposalDetail,
  40. Status,
  41. } from '../types'
  42. // queuing
  43. let lastUpdate = 0
  44. const queue: any[] = []
  45. let inProgress = false
  46. const enqueue = (fn: any) => {
  47. queue.push(fn)
  48. processNext()
  49. }
  50. const processNext = async () => {
  51. if (inProgress) return
  52. inProgress = true
  53. const task = queue.pop()
  54. if (task) await task()
  55. inProgress = false
  56. //processNext()
  57. //return queue.length
  58. }
  59. const save = async (model: any, data: any) => {
  60. const Model = models[model]
  61. try {
  62. const exists = await Model.findByPk(data.id)
  63. if (exists) return exists.update(data)
  64. } catch (e) {}
  65. //console.debug(`saving ${data.id}`, `queued tasks: ${queue.length}`)
  66. try {
  67. return Model.create(data)
  68. } catch (e) {
  69. console.warn(`Failed to save ${Model}`, e.message)
  70. }
  71. }
  72. const addBlock = async (
  73. api: Api,
  74. io: any,
  75. header: { number: number; author: string },
  76. status: Status
  77. ): Promise<Status> => {
  78. const id = +header.number
  79. const last = await Block.findByPk(id - 1)
  80. const timestamp = moment.utc(await api.query.timestamp.now()).valueOf()
  81. const blocktime = last ? timestamp - last.timestamp : 6000
  82. const block = await save('block', { id, timestamp, blocktime })
  83. io.emit('block', block)
  84. const author = header.author?.toString()
  85. const member = await fetchMemberByAccount(api, author)
  86. if (member && member.id) block.setAuthor(member)
  87. const currentEra = Number(await api.query.staking.currentEra())
  88. const era = await save('era', { id: currentEra })
  89. era.addBlock(block)
  90. const handle = member ? member.handle : author
  91. const queued = `(queued: ${queue.length})`
  92. console.log(`[Joystream] block ${block.id} ${handle} ${queued}`)
  93. processEvents(api, id)
  94. return updateEra(api, io, status, currentEra)
  95. }
  96. const getBlockHash = (api: Api, blockId: number) =>
  97. api.rpc.chain.getBlockHash(blockId)
  98. interface BlockEvent {
  99. section: string
  100. method: string
  101. data: string
  102. }
  103. const processEvents = async (api: Api, blockId: number) => {
  104. const blockHash = await getBlockHash(api, blockId)
  105. const blockEvents = await api.query.system.events.at(blockHash)
  106. blockEvents.forEach(({ section, method, data }: BlockEvent) => {
  107. console.log(`event`, section, method, data)
  108. Event.create({ blockId, section, method, data: JSON.stringify(data) })
  109. })
  110. }
  111. const updateEra = async (api: Api, io: any, status: any, era: number) => {
  112. const now: number = moment().valueOf()
  113. if (lastUpdate + 60000 > now) return status
  114. //console.log(`updating status`, lastUpdate)
  115. lastUpdate = now
  116. // session.disabledValidators: Vec<u32>
  117. // check online: imOnline.keys
  118. // imOnline.authoredBlocks: 2
  119. // session.currentIndex: 17,081
  120. const lastReward = Number(await api.query.staking.erasValidatorReward(era))
  121. console.debug(`last reward`, era, lastReward)
  122. if (lastReward > 0) {
  123. } // TODO save lastReward
  124. const nominatorEntries = await api.query.staking.nominators.entries()
  125. const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()))
  126. const rewardPoints = await api.query.staking.erasRewardPoints(era)
  127. const validatorEntries = await api.query.session.validators()
  128. const validators = validatorEntries.map((v: any) => String(v))
  129. // TODO staking.bondedEras: Vec<(EraIndex,SessionIndex)>
  130. const stashes = await api.derive.staking.stashes()
  131. console.debug(`fetching stakes`)
  132. if (!stashes) return
  133. stashes.forEach(async (validator: string) => {
  134. try {
  135. const prefs = await api.query.staking.erasValidatorPrefs(era, validator)
  136. const commission = Number(prefs.commission) / 10000000
  137. const data = await api.query.staking.erasStakers(era, validator)
  138. let { total, own, others } = data.toJSON()
  139. } catch (e) {
  140. console.warn(`Failed to fetch stakes for ${validator} in era ${era}`, e)
  141. }
  142. })
  143. return {
  144. members: (await api.query.members.nextMemberId()) - 1,
  145. categories: await get.currentCategoryId(api),
  146. threads: await get.currentThreadId(api),
  147. proposals: await get.proposalCount(api),
  148. channels: await get.currentChannelId(api),
  149. posts: await get.currentPostId(api),
  150. proposalPosts: await api.query.proposalsDiscussion.postCount(),
  151. queued: queue.length,
  152. era,
  153. }
  154. }
  155. const validatorStatus = async (api: Api, blockId: number) => {
  156. const hash = await getBlockHash(api, blockId)
  157. let totalValidators = await api.query.staking.snapshotValidators.at(hash)
  158. if (totalValidators.isEmpty) return
  159. let totalNrValidators = totalValidators.unwrap().length
  160. const maxSlots = Number(await api.query.staking.validatorCount.at(hash))
  161. const actives = Math.min(maxSlots, totalNrValidators)
  162. const waiting =
  163. totalNrValidators > maxSlots ? totalNrValidators - maxSlots : 0
  164. let timestamp = await api.query.timestamp.now.at(hash)
  165. const date = moment(timestamp.toNumber()).valueOf()
  166. return { blockId, actives, waiting, maxSlots, date }
  167. }
  168. const fetchTokenomics = async () => {
  169. console.debug(`Updating tokenomics`)
  170. const { data } = await axios.get('https://status.joystream.org/status')
  171. if (!data) return
  172. // TODO save 'tokenomics', data
  173. }
  174. const fetchChannel = async (api: Api, id: number) => {
  175. const exists = await Channel.findByPk(id)
  176. if (exists) return exists
  177. console.debug(`Fetching channel ${id}`)
  178. const data = await api.query.contentWorkingGroup.channelById(id)
  179. const handle = String(data.handle)
  180. const title = String(data.title)
  181. const description = String(data.description)
  182. const avatar = String(data.avatar)
  183. const banner = String(data.banner)
  184. const content = String(data.content)
  185. const ownerId = Number(data.owner)
  186. const accountId = String(data.role_account)
  187. const publicationStatus = data.publication_status === 'Public' ? true : false
  188. const curation = String(data.curation_status)
  189. const createdAt = +data.created
  190. const principal = Number(data.principal_id)
  191. const channel = {
  192. id,
  193. handle,
  194. title,
  195. description,
  196. avatar,
  197. banner,
  198. content,
  199. publicationStatus,
  200. curation,
  201. createdAt,
  202. principal,
  203. }
  204. const chan = await save('channel', channel)
  205. const owner = await fetchMember(api, ownerId)
  206. chan.setOwner(owner)
  207. if (id > 1) fetchChannel(api, id - 1)
  208. return chan
  209. }
  210. const fetchCategory = async (api: Api, id: number) => {
  211. const exists = await Category.findByPk(id)
  212. if (exists) return exists
  213. console.debug(`fetching category ${id}`)
  214. const data = await api.query.forum.categoryById(id)
  215. const threadId = +data.thread_id
  216. const title = String(data.title)
  217. const description = String(data.description)
  218. const createdAt = +data.created_at.block
  219. const deleted = data.deleted
  220. const archived = data.archived
  221. const subcategories = Number(data.num_direct_subcategories)
  222. const moderatedThreads = Number(data.num_direct_moderated_threads)
  223. const unmoderatedThreads = Number(data.num_direct_unmoderated_threads)
  224. const position = +data.position_in_parent_category // TODO sometimes NaN
  225. const moderator: string = String(data.moderator_id) // account
  226. const cat = {
  227. id,
  228. title,
  229. description,
  230. createdAt,
  231. deleted,
  232. archived,
  233. subcategories,
  234. moderatedThreads,
  235. unmoderatedThreads,
  236. //position,
  237. }
  238. const category = await save('category', cat)
  239. const mod = await fetchMemberByAccount(api, moderator)
  240. if (mod) category.setModerator(mod)
  241. if (id > 1) fetchCategory(api, id - 1)
  242. return category
  243. }
  244. const fetchPost = async (api: Api, id: number) => {
  245. const exists = await Post.findByPk(id)
  246. if (exists) return exists
  247. console.debug(`fetching post ${id}`)
  248. const data = await api.query.forum.postById(id)
  249. const threadId = Number(data.thread_id)
  250. const text = data.current_text
  251. const moderation = data.moderation
  252. //const history = data.text_change_history;
  253. const createdAt = data.created_at.block
  254. const author: string = String(data.author_id)
  255. const post = await save('post', { id, text, createdAt })
  256. const thread = await fetchThread(api, threadId)
  257. if (thread) post.setThread(thread)
  258. const member = await fetchMemberByAccount(api, author)
  259. if (member) post.setAuthor(member)
  260. const mod = await fetchMemberByAccount(api, moderation)
  261. if (id > 1) fetchPost(api, id - 1)
  262. return post
  263. }
  264. const fetchThread = async (api: Api, id: number) => {
  265. const exists = await Thread.findByPk(id)
  266. if (exists) return exists
  267. console.debug(`fetching thread ${id}`)
  268. const data = await api.query.forum.threadById(id)
  269. const title = String(data.title)
  270. const categoryId = Number(data.category_id)
  271. const nrInCategory = Number(data.nr_in_category)
  272. const moderation = data.moderation
  273. const createdAt = +data.created_at.block
  274. const account = String(data.author_id)
  275. const thread = await save('thread', { id, title, nrInCategory, createdAt })
  276. const category = await fetchCategory(api, categoryId)
  277. if (category) thread.setCategory(category)
  278. const author = await fetchMemberByAccount(api, account)
  279. if (author) thread.setAuthor(author)
  280. if (moderation) {
  281. /* TODO
  282. Error: Invalid value ModerationAction(3) [Map] {
  283. [1] 'moderated_at' => BlockAndTime(2) [Map] {
  284. [1] 'block' => <BN: 4f4ff>,
  285. [1] 'time' => <BN: 17526e65a40>,
  286. [1] registry: TypeRegistry {},
  287. [1] block: [Getter],
  288. [1] time: [Getter],
  289. [1] typeDefs: { block: [Function: U32], time: [Function: U64] }
  290. [1] },
  291. [1] 'moderator_id'
  292. [1] 'rationale' => [String (Text): 'Irrelevant as posted in another thread.'] {
  293. */
  294. //const mod = await fetchMemberByAccount(api, moderation)
  295. //if (mod) thread.setModeration(mod)
  296. }
  297. if (id > 1) fetchThread(api, id - 1)
  298. return thread
  299. }
  300. const fetchCouncils = async (api: Api, lastBlock: number) => {
  301. const round = await api.query.councilElection.round()
  302. let councils: CouncilType[] = await Council.findAll()
  303. const cycle = 201600
  304. for (let round = 0; round < round; round++) {
  305. const block = 57601 + round * cycle
  306. if (councils.find((c) => c.round === round) || block > lastBlock) continue
  307. //enqueue(() => fetchCouncil(api, block))
  308. }
  309. }
  310. const fetchCouncil = async (api: Api, block: number) => {
  311. console.debug(`Fetching council at block ${block}`)
  312. const blockHash = await api.rpc.chain.getBlockHash(block)
  313. if (!blockHash)
  314. return console.error(`Error: empty blockHash fetchCouncil ${block}`)
  315. const council = await api.query.council.activeCouncil.at(blockHash)
  316. return save('council', council)
  317. }
  318. const fetchProposal = async (api: Api, id: number) => {
  319. const exists = await Proposal.findByPk(id)
  320. if (exists) return exists
  321. //if (exists && exists.stage === 'Finalized')
  322. //if (exists.votesByAccount && exists.votesByAccount.length) return
  323. //else return //TODO fetchVotesPerProposal(api, exists)
  324. console.debug(`Fetching proposal ${id}`)
  325. const proposal = await get.proposalDetail(api, id)
  326. if (id > 1) fetchProposal(api, id - 1)
  327. return save('proposal', proposal)
  328. //TODO fetchVotesPerProposal(api, proposal)
  329. }
  330. const fetchVotesPerProposal = async (api: Api, proposal: ProposalDetail) => {
  331. if (proposal.votesByAccount && proposal.votesByAccount.length) return
  332. const proposals = await Proposal.findAll()
  333. const councils = await Council.findAll()
  334. console.debug(`Fetching proposal votes (${proposal.id})`)
  335. let members: MemberType[] = []
  336. councils.map((seats: Seat[]) =>
  337. seats.forEach(async (seat: Seat) => {
  338. if (members.find((member) => member.account === seat.member)) return
  339. const member = await Member.findOne({ where: { account: seat.member } })
  340. member && members.push(member)
  341. })
  342. )
  343. const { id } = proposal
  344. const votesByAccount = await Promise.all(
  345. members.map(async (member) => {
  346. const vote = await fetchVoteByProposalByVoter(api, id, member.id)
  347. return { vote, handle: member.handle }
  348. })
  349. )
  350. Proposal.findByPk(id).then((p: any) => p.update({ votesByAccount }))
  351. }
  352. const fetchVoteByProposalByVoter = async (
  353. api: Api,
  354. proposalId: number,
  355. voterId: number
  356. ): Promise<string> => {
  357. console.debug(`Fetching vote by ${voterId} for proposal ${proposalId}`)
  358. const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
  359. proposalId,
  360. voterId
  361. )
  362. const hasVoted: number = (
  363. await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
  364. proposalId,
  365. voterId
  366. )
  367. ).toNumber()
  368. return hasVoted ? String(vote) : ''
  369. }
  370. // accounts
  371. const fetchMemberByAccount = async (
  372. api: Api,
  373. account: string
  374. ): Promise<MemberType | undefined> => {
  375. const exists = await Member.findOne({ where: { account } })
  376. if (exists) return exists
  377. const id: number = Number(await get.memberIdByAccount(api, account))
  378. return id ? fetchMember(api, id) : undefined
  379. }
  380. const fetchMember = async (api: Api, id: number): Promise<MemberType> => {
  381. try {
  382. const exists = await Member.findByPk(id)
  383. if (exists) return exists
  384. } catch (e) {
  385. console.debug(`Fetching member ${id}`)
  386. }
  387. const membership = await get.membership(api, id)
  388. const handle = String(membership.handle)
  389. const account = String(membership.root_account)
  390. const about = String(membership.about)
  391. const createdAt = +membership.registered_at_block
  392. if (id > 1) fetchMember(api, id - 1)
  393. return save('member', { id, handle, createdAt, about })
  394. }
  395. const fetchReports = () => {
  396. const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`
  397. const apiBase = `https://api.github.com/repos/joystream/community-repo/contents/council-reports`
  398. const urls: { [key: string]: string } = {
  399. alexandria: `${apiBase}/alexandria-testnet`,
  400. archive: `${apiBase}/archived-reports`,
  401. template: `${domain}/templates/council_report_template_v1.md`,
  402. }
  403. ;['alexandria', 'archive'].map((folder) => fetchGithubDir(urls[folder]))
  404. fetchGithubFile(urls.template)
  405. }
  406. const fetchGithubFile = async (url: string): Promise<string> => {
  407. const { data } = await axios.get(url)
  408. return data
  409. }
  410. const fetchGithubDir = async (url: string) => {
  411. const { data } = await axios.get(url)
  412. data.forEach(
  413. async (o: {
  414. name: string
  415. type: string
  416. url: string
  417. download_url: string
  418. }) => {
  419. const match = o.name.match(/^(.+)\.md$/)
  420. const name = match ? match[1] : o.name
  421. if (o.type === 'file') {
  422. const file = await fetchGithubFile(o.download_url)
  423. // TODO save file
  424. } else fetchGithubDir(o.url)
  425. }
  426. )
  427. }
  428. module.exports = { addBlock }