index.ts 17 KB

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