index.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. import { Op } from 'sequelize'
  2. import {
  3. Account,
  4. Balance,
  5. Block,
  6. Category,
  7. Channel,
  8. Council,
  9. Consul,
  10. ConsulStake,
  11. Era,
  12. Event,
  13. Member,
  14. Post,
  15. Proposal,
  16. ProposalVote,
  17. Thread,
  18. } from '../db/models'
  19. import * as get from './lib/getters'
  20. //import {fetchReports} from './lib/github'
  21. import axios from 'axios'
  22. import moment from 'moment'
  23. import chalk from 'chalk'
  24. import { VoteKind } from '@joystream/types/proposals'
  25. import { Seats } from '@joystream/types/council'
  26. import { AccountInfo } from '@polkadot/types/interfaces/system'
  27. import {
  28. Api,
  29. Handles,
  30. IState,
  31. MemberType,
  32. CategoryType,
  33. ChannelType,
  34. PostType,
  35. Seat,
  36. ThreadType,
  37. CouncilType,
  38. ProposalDetail,
  39. Status,
  40. } from '../types'
  41. import {
  42. AccountId,
  43. Moment,
  44. ActiveEraInfo,
  45. EventRecord,
  46. } from '@polkadot/types/interfaces'
  47. import Option from '@polkadot/types/codec/Option'
  48. import { Vec } from '@polkadot/types'
  49. // TODO fetch consts from db/chain
  50. const TERMDURATION = 144000
  51. const VOTINGDURATION = 57601
  52. const CYCLE = VOTINGDURATION + TERMDURATION
  53. const DELAY = 0 // ms
  54. let lastUpdate = 0
  55. let queuedAll = false
  56. let processing = false
  57. let queue: any[] = []
  58. let fetching = ''
  59. const getBlockHash = (api: Api, blockId: number) =>
  60. api.rpc.chain.getBlockHash(blockId)
  61. const getEraAtBlock = (api: Api, hash: string) =>
  62. api.query.staking.activeEra.at(hash)
  63. const getTimestamp = async (api: Api, hash?: string) =>
  64. moment
  65. .utc(
  66. hash
  67. ? await api.query.timestamp.now.at(hash)
  68. : await api.query.timestamp.now(),
  69. )
  70. .valueOf()
  71. const addBlock = async (
  72. api: Api,
  73. io: any,
  74. header: { number: number; author: string },
  75. status: Status = {
  76. era: 0,
  77. round: 0,
  78. members: 0,
  79. channels: 0,
  80. categories: 0,
  81. threads: 0,
  82. posts: 0,
  83. proposals: 0,
  84. proposalPosts: 0,
  85. },
  86. ): Promise<Status> => {
  87. const id = +header.number
  88. const last = await Block.findByPk(id - 1)
  89. const exists = await Block.findByPk(id)
  90. if (exists) {
  91. console.error(`TODO handle fork`, String(header.author))
  92. return status
  93. }
  94. const timestamp = await getTimestamp(api)
  95. const blocktime = last ? timestamp - last.timestamp : 6000
  96. const address = header.author?.toString()
  97. const account = await Account.findOrCreate({ where: { address } })
  98. const block = await Block.create({ id, timestamp, blocktime })
  99. block.setValidator(account.id)
  100. const currentEra = Number(await api.query.staking.currentEra())
  101. await Era.findOrCreate({ where: { id: currentEra } })
  102. await block.setEra(currentEra)
  103. io.emit('block', await Block.findByIdWithIncludes(block.id))
  104. // logging
  105. const member = await fetchMemberByAccount(api, address)
  106. const handle = member ? member.handle : address
  107. const f = fetching !== '' ? `, fetching ${fetching}` : ''
  108. const q = queue.length ? ` (${queue.length} queued${f})` : ''
  109. console.log(`[Joystream] block ${block.id} ${handle}${q}`)
  110. await processEvents(api, id)
  111. if (await isEraInfoAvailable(api, id)) {
  112. await updateEra(api, currentEra, id)
  113. }
  114. //updateBalances(api, id)
  115. return updateStatus(api, status, currentEra)
  116. }
  117. const addBlockRange = async (
  118. api: Api,
  119. startBlock: number,
  120. endBlock: number,
  121. ) => {
  122. const previousHash = await getBlockHash(api, startBlock - 1)
  123. let previousEra = (await api.query.staking.activeEra.at(
  124. previousHash,
  125. )) as Option<ActiveEraInfo>
  126. for (let i = startBlock; i < endBlock; i++) {
  127. console.log(`[Joystream] Processing block ${i}`)
  128. const hash = await getBlockHash(api, i)
  129. const blockEra = (await api.query.staking.activeEra.at(
  130. hash,
  131. )) as Option<ActiveEraInfo>
  132. const currentEra = blockEra.unwrap().index.toNumber();
  133. if (
  134. currentEra ===
  135. previousEra.unwrap().index.toNumber()
  136. ) {
  137. continue
  138. }
  139. if (await isEraInfoAvailable(api, i)) {
  140. await Era.findOrCreate({ where: { id: currentEra } })
  141. await updateEra(api, currentEra, i)
  142. previousEra = blockEra
  143. }
  144. }
  145. }
  146. const updateStatus = async (api: Api, old: Status, era: number) => {
  147. const status = {
  148. era,
  149. round: Number(await api.query.councilElection.round()),
  150. members: (await api.query.members.nextMemberId()) - 1,
  151. channels: await get.currentChannelId(api),
  152. categories: await get.currentCategoryId(api),
  153. threads: await get.currentThreadId(api),
  154. posts: await get.currentPostId(api),
  155. proposals: await get.proposalCount(api),
  156. proposalPosts: await api.query.proposalsDiscussion.postCount(),
  157. }
  158. if (!queuedAll) fetchAll(api, status)
  159. else {
  160. // TODO catch if more than one are added
  161. status.members > old.members && fetchMember(api, status.members)
  162. status.posts > old.posts && fetchPost(api, status.posts)
  163. status.proposals > old.proposals && fetchProposal(api, status.proposals)
  164. status.channels > old.channels && fetchChannel(api, status.channels)
  165. status.categories > old.categories && fetchCategory(api, status.categories)
  166. status.proposalPosts > old.proposalPosts &&
  167. fetchProposalPosts(api, status.proposalPosts)
  168. }
  169. return status
  170. }
  171. const fetchAll = async (api: Api, status: Status) => {
  172. // trying to avoid SequelizeUniqueConstraintError
  173. for (let id = status.members; id > 0; id--) {
  174. queue.push(() => fetchMember(api, id))
  175. }
  176. for (let id = status.round; id > 0; id--) {
  177. queue.push(() => fetchCouncil(api, id))
  178. }
  179. for (let id = status.proposals; id > 0; id--) {
  180. queue.push(() => fetchProposal(api, id))
  181. }
  182. // queue.push(() => fetchProposalPosts(api, status.proposalPosts))
  183. for (let id = status.channels; id > 0; id--) {
  184. queue.push(() => fetchChannel(api, id))
  185. }
  186. for (let id = status.categories; id > 0; id--) {
  187. queue.push(() => fetchCategory(api, id))
  188. }
  189. for (let id = status.threads; id > 0; id--) {
  190. queue.push(() => fetchThread(api, id))
  191. }
  192. for (let id = status.posts; id > 0; id--) {
  193. queue.push(() => fetchPost(api, id))
  194. }
  195. queuedAll = true
  196. processNext()
  197. }
  198. const processNext = async () => {
  199. if (processing) return
  200. processing = true
  201. const task = queue.shift()
  202. if (!task) return
  203. const result = await task()
  204. processing = false
  205. setTimeout(() => processNext(), DELAY)
  206. }
  207. const processEvents = async (api: Api, blockId: number) => {
  208. const blockHash = await getBlockHash(api, blockId)
  209. const blockEvents = await api.query.system.events.at(blockHash)
  210. blockEvents.forEach(({ event }: EventRecord) => {
  211. let { section, method, data } = event
  212. Event.create({ blockId, section, method, data: JSON.stringify(data) })
  213. })
  214. // TODO catch votes, posts, proposals?
  215. }
  216. const isEraInfoAvailable = async (api: Api, blockId: number) => {
  217. const hash = await getBlockHash(api, blockId)
  218. let totalValidators = (await api.query.staking.snapshotValidators.at(
  219. hash,
  220. )) as Option<Vec<AccountId>>
  221. return !totalValidators.isEmpty
  222. }
  223. const updateEra = async (api: Api, eraId: number, blockId: number) => {
  224. let dbEra = await Era.findByPk(eraId)
  225. if (dbEra.actives !== null) {
  226. console.log(`[Joystream] Era ${eraId} contains info, skipping update...`)
  227. return
  228. }
  229. const hash = await getBlockHash(api, blockId)
  230. let totalValidators = (await api.query.staking.snapshotValidators.at(
  231. hash,
  232. )) as Option<Vec<AccountId>>
  233. console.log(`[Joystream] Processing era ${eraId}`)
  234. const totalNrValidators = totalValidators.unwrap().length
  235. dbEra.maxSlots = Number(
  236. (await api.query.staking.validatorCount.at(hash)).toString(),
  237. )
  238. dbEra.actives = Math.min(dbEra.maxSlots, totalNrValidators)
  239. dbEra.waiting =
  240. totalNrValidators > dbEra.maxSlots ? totalNrValidators - dbEra.maxSlots : 0
  241. const chainTimestamp = (await api.query.timestamp.now.at(hash)) as Moment
  242. dbEra.timestamp = new Date(chainTimestamp.toNumber())
  243. dbEra.stake = await api.query.staking.erasTotalStake.at(hash, eraId)
  244. await dbEra.save();
  245. }
  246. const validatorStatus = async (api: Api, blockId: number) => {
  247. const hash = await getBlockHash(api, blockId)
  248. let totalValidators = await api.query.staking.snapshotValidators.at(hash)
  249. if (totalValidators.isEmpty) return
  250. let totalNrValidators = totalValidators.unwrap().length
  251. const maxSlots = Number(await api.query.staking.validatorCount.at(hash))
  252. const actives = Math.min(maxSlots, totalNrValidators)
  253. const waiting =
  254. totalNrValidators > maxSlots ? totalNrValidators - maxSlots : 0
  255. let timestamp = await api.query.timestamp.now.at(hash)
  256. const date = moment(timestamp.toNumber()).valueOf()
  257. return { blockId, actives, waiting, maxSlots, date }
  258. }
  259. const getAccountAtBlock = (
  260. api: Api,
  261. hash: string,
  262. account: string,
  263. ): Promise<AccountInfo> => api.query.system.account.at(hash, account)
  264. // TODO when to cal
  265. const fetchAccounts = async (api: Api, blockId: number) => {
  266. api.query.system.account.entries().then((account: any) => {
  267. const address = account[0].toHuman()[0]
  268. Account.create({ address })
  269. })
  270. }
  271. const updateBalances = async (api: Api, blockId: number) => {
  272. const blockHash = await getBlockHash(api, blockId)
  273. const currentEra: number = await api.query.staking.currentEra.at(blockHash)
  274. const era = await Era.findOrCreate({ where: { id: currentEra } })
  275. Account.findAll().then(async (account: any) => {
  276. const { id, address } = account
  277. if (!address) return
  278. console.log(`updating balance of`, id, address)
  279. const { data } = await getAccountAtBlock(api, blockHash, address)
  280. const { free, reserved, miscFrozen, feeFrozen } = data
  281. const balance = { available: free, reserved, frozen: miscFrozen }
  282. Balance.create(balance).then((balance: any) => {
  283. balance.setAccount(id)
  284. balance.setEra(era.id)
  285. console.log(`balance`, era.id, address, balance.available)
  286. })
  287. })
  288. }
  289. const fetchTokenomics = async () => {
  290. console.debug(`Updating tokenomics`)
  291. const { data } = await axios.get('https://status.joystream.org/status')
  292. if (!data) return
  293. // TODO save 'tokenomics', data
  294. }
  295. const fetchChannel = async (api: Api, id: number) => {
  296. if (id <= 0) return
  297. const exists = await Channel.findByPk(id)
  298. if (exists) return exists
  299. fetching = `channel ${id}`
  300. const data = await api.query.contentWorkingGroup.channelById(id)
  301. const { handle, title, description, avatar, banner, content, created } = data
  302. //const accountId = String(data.role_account)
  303. const channel = {
  304. id,
  305. handle: String(handle),
  306. title: String(title),
  307. description: String(description),
  308. avatar: String(avatar),
  309. banner: String(banner),
  310. content: String(content),
  311. publicationStatus: data.publication_status === 'Public' ? true : false,
  312. curation: String(data.curation_status),
  313. createdAt: +created,
  314. principal: Number(data.principal_id),
  315. }
  316. const chan = await Channel.create(channel)
  317. const owner = await fetchMember(api, data.owner)
  318. chan.setOwner(owner)
  319. return chan
  320. }
  321. const fetchCategory = async (api: Api, id: number) => {
  322. if (id <= 0) return
  323. const exists = await Category.findByPk(+id)
  324. if (exists) return exists
  325. fetching = `category ${id}`
  326. const data = await api.query.forum.categoryById(id)
  327. const { title, description, deleted, archived } = data
  328. const threadId = +data.thread_id // TODO needed?
  329. const moderator: string = String(data.moderator_id) // account
  330. const cat = {
  331. id,
  332. title,
  333. description,
  334. createdAt: +data.created_at.block,
  335. deleted,
  336. archived,
  337. subcategories: Number(data.num_direct_subcategories),
  338. moderatedThreads: Number(data.num_direct_moderated_threads),
  339. unmoderatedThreads: Number(data.num_direct_unmoderated_threads),
  340. //position:+data.position_in_parent_category // TODO sometimes NaN,
  341. }
  342. const category = await Category.create(cat)
  343. const mod = await fetchMemberByAccount(api, moderator)
  344. if (mod) category.setModerator(mod.id)
  345. return category
  346. }
  347. const fetchPost = async (api: Api, id: number) => {
  348. if (id <= 0) return
  349. const exists = await Post.findByPk(id)
  350. if (exists) return exists
  351. fetching = `post ${id}`
  352. const data = await api.query.forum.postById(id)
  353. const threadId = Number(data.thread_id)
  354. const text = data.current_text
  355. const moderation = data.moderation
  356. //const history = data.text_change_history;
  357. const createdAt = data.created_at.block
  358. const author: string = String(data.author_id)
  359. const post = await Post.create({ id, text, createdAt })
  360. const thread = await fetchThread(api, threadId)
  361. if (thread) post.setThread(thread.id)
  362. const member = await fetchMemberByAccount(api, author)
  363. if (member) {
  364. post.setAuthor(member.id)
  365. member.addPost(post.id)
  366. }
  367. if (moderation) {
  368. const mod = await fetchMemberByAccount(api, moderation)
  369. post.setModerator(mod)
  370. }
  371. return post
  372. }
  373. const fetchThread = async (api: Api, id: number) => {
  374. if (id <= 0) return
  375. const exists = await Thread.findByPk(id)
  376. if (exists) return exists
  377. fetching = `thread ${id}`
  378. const data = await api.query.forum.threadById(id)
  379. const { title, moderation, nr_in_category } = data
  380. const account = String(data.author_id)
  381. const t = {
  382. id,
  383. title,
  384. nrInCategory: +nr_in_category,
  385. createdAt: +data.created_at.block,
  386. }
  387. const thread = await Thread.create(t)
  388. const category = await fetchCategory(api, +data.category_id)
  389. if (category) thread.setCategory(category.id)
  390. const author = await fetchMemberByAccount(api, account)
  391. if (author) thread.setCreator(author.id)
  392. if (moderation) {
  393. /* TODO
  394. Error: Invalid value ModerationAction(3) [Map] {
  395. [1] 'moderated_at' => BlockAndTime(2) [Map] {
  396. [1] 'block' => <BN: 4f4ff>,
  397. [1] 'time' => <BN: 17526e65a40>,
  398. [1] registry: TypeRegistry {},
  399. [1] block: [Getter],
  400. [1] time: [Getter],
  401. [1] typeDefs: { block: [Function: U32], time: [Function: U64] }
  402. [1] },
  403. [1] 'moderator_id'
  404. [1] 'rationale' => [String (Text): 'Irrelevant as posted in another thread.'] {
  405. */
  406. //console.log(`thread mod`, moderation
  407. //const mod = await fetchMemberByAccount(api, moderation)
  408. //if (mod) thread.setModeration(mod.id)
  409. }
  410. return thread
  411. }
  412. const fetchCouncil = async (api: Api, round: number) => {
  413. if (round <= 0) return console.log(chalk.red(`[fetchCouncil] round:${round}`))
  414. const exists = await Council.findByPk(round)
  415. if (exists) return exists
  416. fetching = `council ${round}`
  417. const start = 57601 + (round - 1) * CYCLE
  418. const end = start + TERMDURATION
  419. let council = { round, start, end, startDate: 0, endDate: 0 }
  420. let seats: Seats
  421. try {
  422. const startHash = await getBlockHash(api, start)
  423. council.startDate = await getTimestamp(api, startHash)
  424. seats = await api.query.council.activeCouncil.at(startHash)
  425. } catch (e) {
  426. return console.log(`council term ${round} lies in the future ${start}`)
  427. }
  428. try {
  429. const endHash = await getBlockHash(api, end)
  430. council.endDate = await getTimestamp(api, endHash)
  431. } catch (e) {
  432. console.warn(`end of council term ${round} lies in the future ${end}`)
  433. }
  434. try {
  435. Council.create(council).then(({ round }: any) =>
  436. seats.map(({ member, stake, backers }) =>
  437. fetchMemberByAccount(api, member.toHuman()).then((m: any) =>
  438. Consul.create({
  439. stake: Number(stake),
  440. councilRound: round,
  441. memberId: m.id,
  442. }).then((consul: any) =>
  443. backers.map(async ({ member, stake }) =>
  444. fetchMemberByAccount(api, member.toHuman()).then(({ id }: any) =>
  445. ConsulStake.create({
  446. stake: Number(stake),
  447. consulId: consul.id,
  448. memberId: id,
  449. }),
  450. ),
  451. ),
  452. ),
  453. ),
  454. ),
  455. )
  456. } catch (e) {
  457. console.error(`Failed to save council ${round}`, e)
  458. }
  459. }
  460. const fetchProposal = async (api: Api, id: number) => {
  461. if (id <= 0) return
  462. const exists = await Proposal.findByPk(+id)
  463. if (exists) {
  464. fetchProposalVotes(api, exists)
  465. return exists
  466. }
  467. fetching = `proposal ${id}`
  468. const proposal = await get.proposalDetail(api, id)
  469. fetchProposalVotes(api, proposal)
  470. return Proposal.create(proposal)
  471. }
  472. const fetchProposalPosts = async (api: Api, max: number) => {
  473. console.log(`posts`, max)
  474. let postId = 1
  475. for (let threadId = 1; postId <= max; threadId++) {
  476. fetching = `proposal posts ${threadId} ${postId}`
  477. const post = await api.query.proposalsDiscussion.postThreadIdByPostId(
  478. threadId,
  479. postId,
  480. )
  481. if (post.text.length) {
  482. console.log(postId, threadId, post.text.toHuman())
  483. postId++
  484. }
  485. }
  486. }
  487. const findCouncilAtBlock = (api: Api, block: number) => {
  488. if (!block) {
  489. console.error(`[findCouncilAtBlock] empty block`)
  490. return
  491. }
  492. return Council.findOne({
  493. where: {
  494. start: { [Op.lte]: block },
  495. end: { [Op.gte]: block - VOTINGDURATION },
  496. },
  497. })
  498. }
  499. const fetchProposalVotes = async (api: Api, proposal: ProposalDetail) => {
  500. if (!proposal) return console.error(`[fetchProposalVotes] empty proposal`)
  501. fetching = `votes proposal ${proposal.id}`
  502. const { createdAt } = proposal
  503. if (!createdAt) return console.error(`empty start block`, proposal)
  504. try {
  505. const start = await findCouncilAtBlock(api, createdAt)
  506. if (start) start.addProposal(proposal.id)
  507. else return console.error(`no council found for proposal ${proposal.id}`)
  508. // some proposals make it into a second term
  509. const end = await findCouncilAtBlock(api, proposal.finalizedAt)
  510. const councils = [start.round, end && end.round]
  511. const consuls = await Consul.findAll({
  512. where: { councilRound: { [Op.or]: councils } },
  513. })
  514. consuls.map(({ id, memberId }: any) =>
  515. fetchProposalVoteByConsul(api, proposal.id, id, memberId),
  516. )
  517. } catch (e) {
  518. console.log(`failed to fetch votes of proposal ${proposal.id}`, e)
  519. }
  520. }
  521. const fetchProposalVoteByConsul = async (
  522. api: Api,
  523. proposalId: number,
  524. consulId: number,
  525. memberId: number,
  526. ): Promise<any> => {
  527. fetching = `vote by ${consulId} for proposal ${proposalId}`
  528. const exists = await ProposalVote.findOne({
  529. where: { proposalId, memberId, consulId },
  530. })
  531. if (exists) return exists
  532. const query = api.query.proposalsEngine
  533. const args = [proposalId, memberId]
  534. const hasVoted = await query.voteExistsByProposalByVoter.size(...args)
  535. if (!hasVoted.toNumber()) return
  536. const vote = (await query.voteExistsByProposalByVoter(...args)).toHuman()
  537. return ProposalVote.create({ vote: vote, proposalId, consulId, memberId })
  538. }
  539. // accounts
  540. const fetchMemberByAccount = async (
  541. api: Api,
  542. account: string,
  543. ): Promise<MemberType | undefined> => {
  544. if (!account) {
  545. console.error(`fetchMemberByAccount called without account`)
  546. return undefined
  547. }
  548. const exists = await Member.findOne({ where: { account } })
  549. if (exists) return exists
  550. const id: number = Number(await get.memberIdByAccount(api, account))
  551. return id ? fetchMember(api, id) : undefined
  552. }
  553. const fetchMember = async (
  554. api: Api,
  555. id: number,
  556. ): Promise<MemberType | undefined> => {
  557. if (id <= 0) return
  558. const exists = await Member.findByPk(+id)
  559. if (exists) return exists
  560. fetching = `member ${id}`
  561. const membership = await get.membership(api, id)
  562. const about = String(membership.about)
  563. const account = String(membership.root_account)
  564. const handle = String(membership.handle)
  565. const createdAt = +membership.registered_at_block
  566. return Member.create({ id, about, account, createdAt, handle })
  567. }
  568. module.exports = { addBlock, addBlockRange }