|
@@ -1,3 +1,4 @@
|
|
|
+import { Op } from 'sequelize'
|
|
|
import {
|
|
|
Account,
|
|
|
Balance,
|
|
@@ -5,13 +6,17 @@ import {
|
|
|
Category,
|
|
|
Channel,
|
|
|
Council,
|
|
|
+ Consul,
|
|
|
+ ConsulStake,
|
|
|
Era,
|
|
|
Event,
|
|
|
Member,
|
|
|
Post,
|
|
|
Proposal,
|
|
|
+ ProposalVote,
|
|
|
Thread,
|
|
|
} from '../db/models'
|
|
|
+
|
|
|
import * as get from './lib/getters'
|
|
|
//import {fetchReports} from './lib/github'
|
|
|
import axios from 'axios'
|
|
@@ -19,7 +24,7 @@ import moment from 'moment'
|
|
|
import chalk from 'chalk'
|
|
|
|
|
|
import { VoteKind } from '@joystream/types/proposals'
|
|
|
-import { EventRecord } from '@polkadot/types/interfaces'
|
|
|
+import { Seats } from '@joystream/types/council'
|
|
|
import { AccountInfo } from '@polkadot/types/interfaces/system'
|
|
|
import {
|
|
|
Api,
|
|
@@ -36,10 +41,20 @@ import {
|
|
|
Status,
|
|
|
} from '../types'
|
|
|
|
|
|
-import { AccountId, Moment, ActiveEraInfo } from '@polkadot/types/interfaces'
|
|
|
+import {
|
|
|
+ AccountId,
|
|
|
+ Moment,
|
|
|
+ ActiveEraInfo,
|
|
|
+ EventRecord,
|
|
|
+} from '@polkadot/types/interfaces'
|
|
|
import Option from '@polkadot/types/codec/Option'
|
|
|
import { Vec } from '@polkadot/types'
|
|
|
|
|
|
+// TODO fetch consts from db/chain
|
|
|
+const TERMDURATION = 144000
|
|
|
+const VOTINGDURATION = 57601
|
|
|
+const CYCLE = VOTINGDURATION + TERMDURATION
|
|
|
+
|
|
|
const DELAY = 0 // ms
|
|
|
let lastUpdate = 0
|
|
|
let queuedAll = false
|
|
@@ -53,12 +68,22 @@ const getBlockHash = (api: Api, blockId: number) =>
|
|
|
const getEraAtBlock = (api: Api, hash: string) =>
|
|
|
api.query.staking.activeEra.at(hash)
|
|
|
|
|
|
+const getTimestamp = async (api: Api, hash?: string) =>
|
|
|
+ moment
|
|
|
+ .utc(
|
|
|
+ hash
|
|
|
+ ? await api.query.timestamp.now.at(hash)
|
|
|
+ : await api.query.timestamp.now()
|
|
|
+ )
|
|
|
+ .valueOf()
|
|
|
+
|
|
|
const addBlock = async (
|
|
|
api: Api,
|
|
|
io: any,
|
|
|
header: { number: number; author: string },
|
|
|
status: Status = {
|
|
|
era: 0,
|
|
|
+ round: 0,
|
|
|
members: 0,
|
|
|
channels: 0,
|
|
|
categories: 0,
|
|
@@ -75,28 +100,28 @@ const addBlock = async (
|
|
|
console.error(`TODO handle fork`, String(header.author))
|
|
|
return status
|
|
|
}
|
|
|
- const timestamp = moment.utc(await api.query.timestamp.now()).valueOf()
|
|
|
+ const timestamp = await getTimestamp(api)
|
|
|
const blocktime = last ? timestamp - last.timestamp : 6000
|
|
|
+ const address = header.author?.toString()
|
|
|
+ const account = await Account.findOrCreate({ where: { address } })
|
|
|
const block = await Block.create({ id, timestamp, blocktime })
|
|
|
- io.emit('block', block)
|
|
|
-
|
|
|
- const author = header.author?.toString()
|
|
|
- const member = await fetchMemberByAccount(api, author)
|
|
|
- if (member && member.id) block.setAuthor(member.id)
|
|
|
- updateBalances(api, id)
|
|
|
+ block.setValidator(account.id)
|
|
|
|
|
|
const currentEra = Number(await api.query.staking.currentEra())
|
|
|
- Era.findOrCreate({ where: { id: currentEra } }).then(() =>
|
|
|
- block.setEra(currentEra)
|
|
|
- )
|
|
|
+ const era = await Era.findOrCreate({ where: { id: currentEra } })
|
|
|
+ await block.setEra(currentEra)
|
|
|
+ io.emit('block', await Block.findByIdWithIncludes(block.id))
|
|
|
|
|
|
- const handle = member ? member.handle : author
|
|
|
+ // logging
|
|
|
+ const member = await fetchMemberByAccount(api, address)
|
|
|
+ const handle = member ? member.handle : address
|
|
|
const f = fetching !== '' ? `, fetching ${fetching}` : ''
|
|
|
const q = queue.length ? ` (${queue.length} queued${f})` : ''
|
|
|
console.log(`[Joystream] block ${block.id} ${handle}${q}`)
|
|
|
|
|
|
processEvents(api, id)
|
|
|
//updateEra(api, io, status, currentEra)
|
|
|
+ //updateBalances(api, id)
|
|
|
return updateStatus(api, status, currentEra)
|
|
|
}
|
|
|
|
|
@@ -105,13 +130,13 @@ const addBlockRange = async (
|
|
|
startBlock: number,
|
|
|
endBlock: number
|
|
|
) => {
|
|
|
- const previousHash = await api.rpc.chain.getBlockHash(startBlock - 1)
|
|
|
+ const previousHash = await getBlockHash(api, startBlock - 1)
|
|
|
let previousEra = (await api.query.staking.activeEra.at(
|
|
|
previousHash
|
|
|
)) as Option<ActiveEraInfo>
|
|
|
|
|
|
for (let i = startBlock; i < endBlock; i++) {
|
|
|
- const hash = await api.rpc.chain.getBlockHash(i)
|
|
|
+ const hash = await getBlockHash(api, i)
|
|
|
const blockEra = (await api.query.staking.activeEra.at(
|
|
|
hash
|
|
|
)) as Option<ActiveEraInfo>
|
|
@@ -155,6 +180,7 @@ const addBlockRange = async (
|
|
|
const updateStatus = async (api: Api, old: Status, era: number) => {
|
|
|
const status = {
|
|
|
era,
|
|
|
+ round: Number(await api.query.councilElection.round()),
|
|
|
members: (await api.query.members.nextMemberId()) - 1,
|
|
|
channels: await get.currentChannelId(api),
|
|
|
|
|
@@ -173,6 +199,8 @@ const updateStatus = async (api: Api, old: Status, era: number) => {
|
|
|
status.proposals > old.proposals && fetchProposal(api, status.proposals)
|
|
|
status.channels > old.channels && fetchChannel(api, status.channels)
|
|
|
status.categories > old.categories && fetchCategory(api, status.categories)
|
|
|
+ status.proposalPosts > old.proposalPosts &&
|
|
|
+ fetchProposalPosts(api, status.proposalPosts)
|
|
|
}
|
|
|
return status
|
|
|
}
|
|
@@ -183,18 +211,28 @@ const fetchAll = async (api: Api, status: Status) => {
|
|
|
for (let id = status.members; id > 0; id--) {
|
|
|
queue.push(() => fetchMember(api, id))
|
|
|
}
|
|
|
- for (let id = status.posts; id > 0; id--) {
|
|
|
- queue.push(() => fetchPost(api, id))
|
|
|
+ for (let id = status.round; id > 0; id--) {
|
|
|
+ queue.push(() => fetchCouncil(api, id))
|
|
|
}
|
|
|
+
|
|
|
for (let id = status.proposals; id > 0; id--) {
|
|
|
queue.push(() => fetchProposal(api, id))
|
|
|
}
|
|
|
+ queue.push(() => fetchProposalPosts(api, status.proposalPosts))
|
|
|
+
|
|
|
for (let id = status.channels; id > 0; id--) {
|
|
|
queue.push(() => fetchChannel(api, id))
|
|
|
}
|
|
|
for (let id = status.categories; id > 0; id--) {
|
|
|
queue.push(() => fetchCategory(api, id))
|
|
|
}
|
|
|
+ for (let id = status.threads; id > 0; id--) {
|
|
|
+ queue.push(() => fetchThread(api, id))
|
|
|
+ }
|
|
|
+ for (let id = status.posts; id > 0; id--) {
|
|
|
+ queue.push(() => fetchPost(api, id))
|
|
|
+ }
|
|
|
+
|
|
|
queuedAll = true
|
|
|
processNext()
|
|
|
}
|
|
@@ -202,7 +240,7 @@ const fetchAll = async (api: Api, status: Status) => {
|
|
|
const processNext = async () => {
|
|
|
if (processing) return
|
|
|
processing = true
|
|
|
- const task = queue.pop()
|
|
|
+ const task = queue.shift()
|
|
|
if (!task) return
|
|
|
const result = await task()
|
|
|
processing = false
|
|
@@ -392,8 +430,14 @@ const fetchPost = async (api: Api, id: number) => {
|
|
|
const thread = await fetchThread(api, threadId)
|
|
|
if (thread) post.setThread(thread.id)
|
|
|
const member = await fetchMemberByAccount(api, author)
|
|
|
- if (member) post.setAuthor(member.id)
|
|
|
- const mod = await fetchMemberByAccount(api, moderation)
|
|
|
+ if (member) {
|
|
|
+ post.setAuthor(member.id)
|
|
|
+ member.addPost(post.id)
|
|
|
+ }
|
|
|
+ if (moderation) {
|
|
|
+ const mod = await fetchMemberByAccount(api, moderation)
|
|
|
+ post.setModerator(mod)
|
|
|
+ }
|
|
|
return post
|
|
|
}
|
|
|
|
|
@@ -416,7 +460,7 @@ const fetchThread = async (api: Api, id: number) => {
|
|
|
const category = await fetchCategory(api, +data.category_id)
|
|
|
if (category) thread.setCategory(category.id)
|
|
|
const author = await fetchMemberByAccount(api, account)
|
|
|
- if (author) thread.setAuthor(author.id)
|
|
|
+ if (author) thread.setCreator(author.id)
|
|
|
if (moderation) {
|
|
|
/* TODO
|
|
|
Error: Invalid value ModerationAction(3) [Map] {
|
|
@@ -431,92 +475,147 @@ const fetchThread = async (api: Api, id: number) => {
|
|
|
[1] 'moderator_id'
|
|
|
[1] 'rationale' => [String (Text): 'Irrelevant as posted in another thread.'] {
|
|
|
*/
|
|
|
+ //console.log(`thread mod`, moderation
|
|
|
//const mod = await fetchMemberByAccount(api, moderation)
|
|
|
//if (mod) thread.setModeration(mod.id)
|
|
|
}
|
|
|
return thread
|
|
|
}
|
|
|
|
|
|
-const fetchCouncils = async (api: Api, lastBlock: number) => {
|
|
|
- const round = await api.query.councilElection.round()
|
|
|
- let councils: CouncilType[] = await Council.findAll()
|
|
|
- const cycle = 201600
|
|
|
+const fetchCouncil = async (api: Api, round: number) => {
|
|
|
+ if (round <= 0) return console.log(chalk.red(`[fetchCouncil] round:${round}`))
|
|
|
+
|
|
|
+ const exists = await Council.findByPk(round)
|
|
|
+ if (exists) return exists
|
|
|
+
|
|
|
+ fetching = `council ${round}`
|
|
|
+ const start = 57601 + (round - 1) * CYCLE
|
|
|
+ const end = start + TERMDURATION
|
|
|
+ let council = { round, start, end, startDate: 0, endDate: 0 }
|
|
|
+ let seats: Seats
|
|
|
+ try {
|
|
|
+ const startHash = await getBlockHash(api, start)
|
|
|
+ council.startDate = await getTimestamp(api, startHash)
|
|
|
+ seats = await api.query.council.activeCouncil.at(startHash)
|
|
|
+ } catch (e) {
|
|
|
+ return console.log(`council term ${round} lies in the future ${start}`)
|
|
|
+ }
|
|
|
|
|
|
- for (let round = 0; round < round; round++) {
|
|
|
- const block = 57601 + round * cycle
|
|
|
- if (councils.find((c) => c.round === round) || block > lastBlock) continue
|
|
|
- fetchCouncil(api, block)
|
|
|
+ try {
|
|
|
+ const endHash = await getBlockHash(api, end)
|
|
|
+ council.endDate = await getTimestamp(api, endHash)
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`end of council term ${round} lies in the future ${end}`)
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-const fetchCouncil = async (api: Api, block: number) => {
|
|
|
- fetching = `council at block ${block}`
|
|
|
- const blockHash = await api.rpc.chain.getBlockHash(block)
|
|
|
- if (!blockHash)
|
|
|
- return console.error(`Error: empty blockHash fetchCouncil ${block}`)
|
|
|
- const council = await api.query.council.activeCouncil.at(blockHash)
|
|
|
- return Council.create(council)
|
|
|
+ try {
|
|
|
+ Council.create(council).then(({ round }: any) =>
|
|
|
+ seats.map(({ member, stake, backers }) =>
|
|
|
+ fetchMemberByAccount(api, member.toHuman()).then((m: any) =>
|
|
|
+ Consul.create({
|
|
|
+ stake: Number(stake),
|
|
|
+ councilRound: round,
|
|
|
+ memberId: m.id,
|
|
|
+ }).then((consul: any) =>
|
|
|
+ backers.map(async ({ member, stake }) =>
|
|
|
+ fetchMemberByAccount(api, member.toHuman()).then(({ id }: any) =>
|
|
|
+ ConsulStake.create({
|
|
|
+ stake: Number(stake),
|
|
|
+ consulId: consul.id,
|
|
|
+ memberId: id,
|
|
|
+ })
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Failed to save council ${round}`, e)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const fetchProposal = async (api: Api, id: number) => {
|
|
|
if (id <= 0) return
|
|
|
const exists = await Proposal.findByPk(+id)
|
|
|
- if (exists) return exists
|
|
|
-
|
|
|
- //if (exists && exists.stage === 'Finalized')
|
|
|
- //if (exists.votesByAccount && exists.votesByAccount.length) return
|
|
|
- //else return //TODO fetchVotesPerProposal(api, exists)
|
|
|
+ if (exists) {
|
|
|
+ fetchProposalVotes(api, exists)
|
|
|
+ return exists
|
|
|
+ }
|
|
|
|
|
|
fetching = `proposal ${id}`
|
|
|
const proposal = await get.proposalDetail(api, id)
|
|
|
+ fetchProposalVotes(api, proposal)
|
|
|
return Proposal.create(proposal)
|
|
|
- //TODO fetchVotesPerProposal(api, proposal)
|
|
|
}
|
|
|
|
|
|
-const fetchVotesPerProposal = async (api: Api, proposal: ProposalDetail) => {
|
|
|
- if (proposal.votesByAccount && proposal.votesByAccount.length) return
|
|
|
-
|
|
|
- const proposals = await Proposal.findAll()
|
|
|
- const councils = await Council.findAll()
|
|
|
+const fetchProposalPosts = async (api: Api, max: number) => {
|
|
|
+ console.log(`posts`, max)
|
|
|
+ let postId = 1
|
|
|
+ for (let threadId = 1; postId <= max; threadId++) {
|
|
|
+ fetching = `proposal posts ${threadId} ${postId}`
|
|
|
+ const post = await api.query.proposalsDiscussion.postThreadIdByPostId(
|
|
|
+ threadId,
|
|
|
+ postId
|
|
|
+ )
|
|
|
+ if (post.text.length) {
|
|
|
+ console.log(postId, threadId, post.text.toHuman())
|
|
|
+ postId++
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- fetching = `proposal votes (${proposal.id})`
|
|
|
- let members: MemberType[] = []
|
|
|
- councils.map((seats: Seat[]) =>
|
|
|
- seats.forEach(async (seat: Seat) => {
|
|
|
- if (members.find((member) => member.account === seat.member)) return
|
|
|
- const member = await Member.findOne({ where: { account: seat.member } })
|
|
|
- member && members.push(member)
|
|
|
- })
|
|
|
- )
|
|
|
+const findCouncilAtBlock = (api: Api, block: number) =>
|
|
|
+ Council.findOne({
|
|
|
+ where: {
|
|
|
+ start: { [Op.lte]: block },
|
|
|
+ end: { [Op.gte]: block - VOTINGDURATION },
|
|
|
+ },
|
|
|
+ })
|
|
|
|
|
|
- const { id } = proposal
|
|
|
- const votesByAccount = await Promise.all(
|
|
|
- members.map(async (member) => {
|
|
|
- const vote = await fetchVoteByProposalByVoter(api, id, member.id)
|
|
|
- return { vote, handle: member.handle }
|
|
|
+const fetchProposalVotes = async (api: Api, proposal: ProposalDetail) => {
|
|
|
+ if (!proposal) return console.error(`[fetchProposalVotes] empty proposal`)
|
|
|
+ fetching = `votes proposal ${proposal.id}`
|
|
|
+ const { createdAt } = proposal
|
|
|
+ if (!createdAt) return console.error(`empty start block`, proposal)
|
|
|
+ try {
|
|
|
+ const start = await findCouncilAtBlock(api, createdAt)
|
|
|
+ if (start) start.addProposal(proposal.id)
|
|
|
+ else return console.error(`no council found for proposal ${proposal.id}`)
|
|
|
+ // some proposals make it into a second term
|
|
|
+ const end = await findCouncilAtBlock(api, proposal.finalizedAt)
|
|
|
+ const councils = [start.round, end && end.round]
|
|
|
+ const consuls = await Consul.findAll({
|
|
|
+ where: { councilRound: { [Op.or]: councils } },
|
|
|
})
|
|
|
- )
|
|
|
- Proposal.findByPk(id).then((p: any) => p.update({ votesByAccount }))
|
|
|
+ consuls.map(({ id, memberId }: any) =>
|
|
|
+ fetchProposalVoteByConsul(api, proposal.id, id, memberId)
|
|
|
+ )
|
|
|
+ } catch (e) {
|
|
|
+ console.log(`failed to fetch votes of proposal ${proposal.id}`, e)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const fetchVoteByProposalByVoter = async (
|
|
|
+const fetchProposalVoteByConsul = async (
|
|
|
api: Api,
|
|
|
proposalId: number,
|
|
|
- voterId: number
|
|
|
-): Promise<string> => {
|
|
|
- fetching = `vote by ${voterId} for proposal ${proposalId}`
|
|
|
- const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
|
|
|
- proposalId,
|
|
|
- voterId
|
|
|
- )
|
|
|
- const hasVoted: number = (
|
|
|
- await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
|
|
|
- proposalId,
|
|
|
- voterId
|
|
|
- )
|
|
|
- ).toNumber()
|
|
|
+ consulId: number,
|
|
|
+ memberId: number
|
|
|
+): Promise<any> => {
|
|
|
+ fetching = `vote by ${consulId} for proposal ${proposalId}`
|
|
|
+ const exists = await ProposalVote.findOne({
|
|
|
+ where: { proposalId, memberId, consulId },
|
|
|
+ })
|
|
|
+ if (exists) return exists
|
|
|
+
|
|
|
+ const query = api.query.proposalsEngine
|
|
|
+ const args = [proposalId, memberId]
|
|
|
+
|
|
|
+ const hasVoted = await query.voteExistsByProposalByVoter.size(...args)
|
|
|
+ if (!hasVoted.toNumber()) return
|
|
|
|
|
|
- return hasVoted ? String(vote) : ''
|
|
|
+ const vote = (await query.voteExistsByProposalByVoter(...args)).toHuman()
|
|
|
+ return ProposalVote.create({ vote: vote, proposalId, consulId, memberId })
|
|
|
}
|
|
|
|
|
|
// accounts
|
|
@@ -524,9 +623,12 @@ const fetchMemberByAccount = async (
|
|
|
api: Api,
|
|
|
account: string
|
|
|
): Promise<MemberType | undefined> => {
|
|
|
+ if (!account) {
|
|
|
+ console.error(`fetchMemberByAccount called without account`)
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
const exists = await Member.findOne({ where: { account } })
|
|
|
if (exists) return exists
|
|
|
-
|
|
|
const id: number = Number(await get.memberIdByAccount(api, account))
|
|
|
return id ? fetchMember(api, id) : undefined
|
|
|
}
|
|
@@ -541,11 +643,11 @@ const fetchMember = async (
|
|
|
|
|
|
fetching = `member ${id}`
|
|
|
const membership = await get.membership(api, id)
|
|
|
- const handle = String(membership.handle)
|
|
|
- const account = String(membership.root_account)
|
|
|
const about = String(membership.about)
|
|
|
+ const account = String(membership.root_account)
|
|
|
+ const handle = String(membership.handle)
|
|
|
const createdAt = +membership.registered_at_block
|
|
|
- return Member.create({ id, handle, createdAt, about })
|
|
|
+ return Member.create({ id, about, account, createdAt, handle })
|
|
|
}
|
|
|
|
|
|
module.exports = { addBlock, addBlockRange }
|