index.ts 20 KB

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