index.ts 16 KB

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