Browse Source

query node - council & referendum mappings I

ondratra 3 years ago
parent
commit
578f4de13d

+ 539 - 43
query-node/mappings/council.ts

@@ -1,5 +1,7 @@
 import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
 import { bytesToString, genericEventFields } from './common'
+import BN from 'bn.js'
+import { FindConditions } from 'typeorm'
 
 import {
   // Council events
@@ -32,17 +34,224 @@ import {
   // Council & referendum structures
   ReferendumStageRevealingOptionResult,
 
+  // Council & referendum schema types
+  ElectionProblemNotEnoughCandidates,
+  CouncilStageUpdate,
+  CouncilStageAnnouncing,
+  CouncilStage,
+  ElectionProblem,
+  Candidate,
+  CouncilMember,
+  ElectionRound,
+  ElectedCouncil,
+  CouncilStageElection,
+  VariantNone,
+  CastVote,
+
   // Misc
   Membership,
 } from 'query-node/dist/model'
 import { Council, Referendum } from './generated/types'
 
+/////////////////// Common - Gets //////////////////////////////////////////////
+
+async function getMembership(store: DatabaseManager, memberId: string): Promise<Membership> {
+  // TODO: is this enough to load existing membership? this technic was adpoted from forum mappings (`forum.ts`)
+  const member = new Membership({ id: memberId })
+
+  return member
+}
+
+async function getCandidate(
+  store: DatabaseManager,
+  memberId: string,
+  electionRound?: ElectionRound
+): Promise<Candidate> {
+  const where = { memberId: memberId } as FindConditions<Candidate>
+  if (electionRound) {
+    where.electionRound = electionRound
+  }
+
+  const candidate = await store.get(Candidate, { where, order: { id: 'DESC' } })
+
+  if (!candidate) {
+    throw new Error(`Candidate not found. memberId '${memberId}' electionRound`)
+  }
+
+  return candidate
+}
+
+async function getCouncilMember(store: DatabaseManager, memberId: string): Promise<CouncilMember> {
+  const councilMember = await store.get(CouncilMember, {
+    where: { memberId: memberId },
+    order: { id: 'DESC' },
+  })
+
+  if (!councilMember) {
+    throw new Error(`Council member not found. memberId '${memberId}'`)
+  }
+
+  return councilMember
+}
+
+async function getCurrentElectionRound(store: DatabaseManager): Promise<ElectionRound> {
+  const electionRound = await store.get(ElectionRound, { order: { id: 'DESC' } })
+
+  if (!electionRound) {
+    throw new Error('No election cycle found.')
+  }
+
+  return electionRound
+}
+
+async function getCurrentStageUpdate(store: DatabaseManager): Promise<CouncilStageUpdate> {
+  const stageUpdate = await store.get(CouncilStageUpdate, { order: { id: 'DESC' } })
+
+  if (!stageUpdate) {
+    throw new Error('No stage update found.')
+  }
+
+  return stageUpdate
+}
+
+async function getCurrentElectedCouncil(
+  store: DatabaseManager,
+  canFail: boolean = false
+): Promise<ElectedCouncil | undefined> {
+  const electedCouncil = await store.get(ElectedCouncil, { order: { id: 'DESC' } })
+
+  if (!electedCouncil && !canFail) {
+    throw new Error('No council is elected.')
+  }
+
+  return electedCouncil
+}
+
+async function getAccountCastVote(
+  store: DatabaseManager,
+  account: string,
+  electionRound?: ElectionRound
+): Promise<CastVote> {
+  const where = { castBy: account } as FindConditions<Candidate>
+  if (electionRound) {
+    where.electionRound = electionRound
+  }
+
+  const castVote = await store.get(CastVote, { where })
+
+  if (!castVote) {
+    throw new Error(
+      `No vote cast by the given account in the curent election round. accountId '${account}', cycleId '${electionRound?.cycleId}'`
+    )
+  }
+
+  return castVote
+}
+
+/*
+  Vote power calculation should correspond to implementation of `referendum::Trait<ReferendumInstance>`
+  in `runtime/src/lib.rs`.
+*/
+function calculateVotePower(accountId: string, stake: BN): BN {
+  return stake
+}
+
+function isCouncilStageElection(councilStage: typeof CouncilStage): councilStage is CouncilStageElection {
+  return councilStage.isTypeOf == 'CouncilStageElection'
+}
+
+/////////////////// Common /////////////////////////////////////////////////////
+
+async function updateCouncilStage(
+  store: DatabaseManager,
+  councilStage: typeof CouncilStage,
+  blockNumber: number,
+  electionProblem?: typeof ElectionProblem
+): Promise<void> {
+  const councilStageUpdate = new CouncilStageUpdate({
+    stage: councilStage,
+    changedAt: new BN(blockNumber),
+    electionProblem,
+  })
+
+  await store.save<CouncilStageUpdate>(councilStageUpdate)
+
+  // update council record
+  const electedCouncil = await getCurrentElectedCouncil(store, true)
+  if (!electedCouncil) {
+    return
+  }
+
+  // electedCouncil.updates.push(councilStageUpdate) // uncomment after solving https://github.com/Joystream/hydra/issues/462
+  electedCouncil.updates = (electedCouncil.updates || []).concat([councilStageUpdate])
+  await store.save<ElectedCouncil>(electedCouncil)
+}
+
+async function startNextElectionRound(
+  store: DatabaseManager,
+  electedCouncil: ElectedCouncil,
+  previousElectionRound?: ElectionRound
+): Promise<ElectionRound> {
+  // finish last election round
+  const lastElectionRound = previousElectionRound || (await getCurrentElectionRound(store))
+  lastElectionRound.isFinished = true
+  lastElectionRound.nextElectedCouncil = electedCouncil
+
+  // save last election
+  await store.save<ElectionRound>(lastElectionRound)
+
+  // create election round record
+  const electionRound = new ElectionRound({
+    cycleId: lastElectionRound.cycleId + 1,
+    isFinished: false,
+    castVotes: [],
+    electedCouncil,
+    candidates: [],
+  })
+
+  // save new election
+  await store.save<ElectionRound>(electionRound)
+
+  return electionRound
+}
+
+async function convertCandidatesToCouncilMembers(
+  store: DatabaseManager,
+  candidates: Candidate[],
+  blockNumber: number
+): Promise<CouncilMember[]> {
+  const councilMembers = await candidates.reduce(async (councilMembersPromise, candidate) => {
+    const councilMembers = await councilMembersPromise
+
+    const councilMember = new CouncilMember({
+      // id: candidate.id // TODO: are ids needed?
+      stakingAccountId: candidate.stakingAccountId,
+      rewardAccountId: candidate.rewardAccountId,
+      member: candidate.member,
+      stake: candidate.stake,
+
+      lastPaymentBlock: new BN(blockNumber),
+
+      accumulatedReward: new BN(0),
+    })
+
+    await store.save<CouncilMember>(councilMember)
+
+    return [...councilMembers, councilMember]
+  }, Promise.resolve([] as CouncilMember[]))
+
+  return councilMembers
+}
+
 /////////////////// Council events /////////////////////////////////////////////
 
-export async function council_AnnouncingPeriodStartedEvent({
-  event,
-  store,
-}: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when a new round of elections begins (can be caused by multiple reasons) and candidates can announce
+  their candidacies.
+*/
+export async function council_AnnouncingPeriodStarted({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Council.AnnouncingPeriodStartedEvent(event).params
 
   const announcingPeriodStartedEvent = new AnnouncingPeriodStartedEvent({
@@ -50,8 +259,21 @@ export async function council_AnnouncingPeriodStartedEvent({
   })
 
   await store.save<AnnouncingPeriodStartedEvent>(announcingPeriodStartedEvent)
+
+  // specific event processing
+
+  const stage = new CouncilStageAnnouncing()
+  stage.candidatesCount = new BN(0)
+
+  await updateCouncilStage(store, stage, event.blockNumber)
 }
-export async function council_NotEnoughCandidatesEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a candidacy announcment period has ended, but not enough members announced.
+*/
+export async function council_NotEnoughCandidates({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Council.NotEnoughCandidatesEvent(event).params
 
   const notEnoughCandidatesEvent = new NotEnoughCandidatesEvent({
@@ -59,8 +281,21 @@ export async function council_NotEnoughCandidatesEvent({ event, store }: EventCo
   })
 
   await store.save<NotEnoughCandidatesEvent>(notEnoughCandidatesEvent)
+
+  // specific event processing
+
+  const stage = new CouncilStageAnnouncing()
+  stage.candidatesCount = new BN(0)
+
+  await updateCouncilStage(store, stage, event.blockNumber, new ElectionProblemNotEnoughCandidates())
 }
-export async function council_VotingPeriodStartedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a new round of elections begins (can be caused by multiple reasons).
+*/
+export async function council_VotingPeriodStarted({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [numOfCandidates] = new Council.VotingPeriodStartedEvent(event).params
 
   const votingPeriodStartedEvent = new VotingPeriodStartedEvent({
@@ -69,10 +304,24 @@ export async function council_VotingPeriodStartedEvent({ event, store }: EventCo
   })
 
   await store.save<VotingPeriodStartedEvent>(votingPeriodStartedEvent)
+
+  // specific event processing
+
+  // add stage update record
+  const stage = new CouncilStageElection()
+  stage.candidatesCount = new BN(0)
+
+  await updateCouncilStage(store, stage, event.blockNumber)
 }
-export async function council_NewCandidateEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a member announces candidacy to the council.
+*/
+export async function council_NewCandidate({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberId, stakingAccount, rewardAccount, balance] = new Council.NewCandidateEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const newCandidateEvent = new NewCandidateEvent({
     ...genericEventFields(event),
@@ -83,8 +332,42 @@ export async function council_NewCandidateEvent({ event, store }: EventContext &
   })
 
   await store.save<NewCandidateEvent>(newCandidateEvent)
+
+  // specific event processing
+
+  // increase candidate count in stage update record
+  const lastStageUpdate = await getCurrentStageUpdate(store)
+  if (!isCouncilStageElection(lastStageUpdate.stage)) {
+    throw new Error(`Unexpected council stage "${lastStageUpdate.stage.isTypeOf}"`)
+  }
+
+  lastStageUpdate.stage.candidatesCount = lastStageUpdate.stage.candidatesCount.add(new BN(1))
+  await store.save<CouncilStageUpdate>(lastStageUpdate)
+
+  const electionRound = await getCurrentElectionRound(store)
+
+  // save candidate record
+  const candidate = new Candidate({
+    stakingAccountId: stakingAccount.toString(),
+    rewardAccountId: rewardAccount.toString(),
+    member,
+
+    electionRound,
+    stake: balance,
+    stakeLocked: true,
+    candidacyWithdrawn: false,
+    votePower: new BN(0),
+    note: '', // note is empty before explicitely set
+  })
+  await store.save<Candidate>(candidate)
 }
-export async function council_NewCouncilElectedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the new council is elected. Sufficient members were elected and there is no other problem.
+*/
+export async function council_NewCouncilElected({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberIds] = new Council.NewCouncilElectedEvent(event).params
   const members = await store.getMany(Membership, { where: { id_in: memberIds.map((item) => item.toString()) } })
 
@@ -94,8 +377,45 @@ export async function council_NewCouncilElectedEvent({ event, store }: EventCont
   })
 
   await store.save<NewCouncilElectedEvent>(newCouncilElectedEvent)
+
+  // specific event processing
+
+  const oldElectedCouncil = await getCurrentElectedCouncil(store, true)
+  if (oldElectedCouncil) {
+    oldElectedCouncil.isResigned = true
+    await store.save<ElectedCouncil>(oldElectedCouncil)
+  }
+
+  // create new council record
+  const electionRound = await getCurrentElectionRound(store)
+  const electedCouncil = new ElectedCouncil({
+    councilMembers: await convertCandidatesToCouncilMembers(store, electionRound.candidates || [], event.blockNumber),
+    updates: [],
+    electedAtBlock: event.blockNumber,
+    endedAtBlock: event.blockNumber,
+    councilElections: oldElectedCouncil?.nextCouncilElections || [],
+    nextCouncilElections: [],
+    isResigned: false,
+  })
+
+  await store.save<ElectedCouncil>(electedCouncil)
+
+  // end the last election round and start new one
+  const nextElectionRound = await startNextElectionRound(store, electedCouncil, electionRound)
+
+  // update next council elections list
+  // electedCouncil.nextCouncilElections.push(nextElectionRound) // uncomment after solving https://github.com/Joystream/hydra/issues/462
+  electedCouncil.nextCouncilElections = (electedCouncil.nextCouncilElections || []).concat([nextElectionRound])
+  await store.save<ElectedCouncil>(electedCouncil)
 }
-export async function council_NewCouncilNotElectedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the new council couldn't be elected because not enough candidates received some votes.
+  This can be vaguely translated as the public not having enough interest in the candidates.
+*/
+export async function council_NewCouncilNotElected({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Council.NewCouncilNotElectedEvent(event).params
 
   const newCouncilNotElectedEvent = new NewCouncilNotElectedEvent({
@@ -103,10 +423,22 @@ export async function council_NewCouncilNotElectedEvent({ event, store }: EventC
   })
 
   await store.save<NewCouncilNotElectedEvent>(newCouncilNotElectedEvent)
+
+  // specific event processing
+
+  // restart elections
+  const electedCouncil = (await getCurrentElectedCouncil(store))!
+  await startNextElectionRound(store, electedCouncil)
 }
-export async function council_CandidacyStakeReleaseEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the member is releasing it's candidacy stake that is no longer needed.
+*/
+export async function council_CandidacyStakeRelease({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberId] = new Council.CandidacyStakeReleaseEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const candidacyStakeReleaseEvent = new CandidacyStakeReleaseEvent({
     ...genericEventFields(event),
@@ -114,10 +446,24 @@ export async function council_CandidacyStakeReleaseEvent({ event, store }: Event
   })
 
   await store.save<CandidacyStakeReleaseEvent>(candidacyStakeReleaseEvent)
+
+  // specific event processing
+
+  // update candidate info about stake lock
+  const electionRound = await getCurrentElectionRound(store)
+  const candidate = await getCandidate(store, memberId.toString()) // get last member's candidacy record
+  candidate.stakeLocked = false
+  await store.save<Candidate>(candidate)
 }
-export async function council_CandidacyWithdrawEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the member is revoking its candidacy during a candidacy announcement stage.
+*/
+export async function council_CandidacyWithdraw({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberId] = new Council.CandidacyWithdrawEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const candidacyWithdrawEvent = new CandidacyWithdrawEvent({
     ...genericEventFields(event),
@@ -125,10 +471,24 @@ export async function council_CandidacyWithdrawEvent({ event, store }: EventCont
   })
 
   await store.save<CandidacyWithdrawEvent>(candidacyWithdrawEvent)
+
+  // specific event processing
+
+  // mark candidacy as withdrawn
+  const electionRound = await getCurrentElectionRound(store)
+  const candidate = await getCandidate(store, memberId.toString(), electionRound)
+  candidate.candidacyWithdrawn = false
+  await store.save<Candidate>(candidate)
 }
-export async function council_CandidacyNoteSetEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the candidate changes its candidacy note.
+*/
+export async function council_CandidacyNoteSet({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberId, note] = new Council.CandidacyNoteSetEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const candidacyNoteSetEvent = new CandidacyNoteSetEvent({
     ...genericEventFields(event),
@@ -137,10 +497,24 @@ export async function council_CandidacyNoteSetEvent({ event, store }: EventConte
   })
 
   await store.save<CandidacyNoteSetEvent>(candidacyNoteSetEvent)
+
+  // specific event processing
+
+  // update candidacy note
+  const electionRound = await getCurrentElectionRound(store)
+  const candidate = await getCandidate(store, memberId.toString(), electionRound)
+  candidate.note = bytesToString(note)
+  await store.save<Candidate>(candidate)
 }
-export async function council_RewardPaymentEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the council member receives its reward.
+*/
+export async function council_RewardPayment({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [memberId, rewardAccount, paidBalance, missingBalance] = new Council.RewardPaymentEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const rewardPaymentEvent = new RewardPaymentEvent({
     ...genericEventFields(event),
@@ -151,8 +525,22 @@ export async function council_RewardPaymentEvent({ event, store }: EventContext
   })
 
   await store.save<RewardPaymentEvent>(rewardPaymentEvent)
+
+  // specific event processing
+
+  // update (un)paid reward info
+  const councilMember = await getCouncilMember(store, memberId.toString())
+  councilMember.accumulatedReward = councilMember.accumulatedReward.add(paidBalance)
+  councilMember.unpaidReward = missingBalance
+  await store.save<CouncilMember>(councilMember)
 }
-export async function council_BudgetBalanceSetEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a new budget balance is set.
+*/
+export async function council_BudgetBalanceSet({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [balance] = new Council.BudgetBalanceSetEvent(event).params
 
   const budgetBalanceSetEvent = new BudgetBalanceSetEvent({
@@ -161,8 +549,14 @@ export async function council_BudgetBalanceSetEvent({ event, store }: EventConte
   })
 
   await store.save<BudgetBalanceSetEvent>(budgetBalanceSetEvent)
+
+  // no specific event processing
 }
-export async function council_BudgetRefillEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a planned budget refill occurs.
+*/
+export async function council_BudgetRefill({ event, store }: EventContext & StoreContext): Promise<void> {
   const [balance] = new Council.BudgetRefillEvent(event).params
 
   const budgetRefillEvent = new BudgetRefillEvent({
@@ -171,8 +565,16 @@ export async function council_BudgetRefillEvent({ event, store }: EventContext &
   })
 
   await store.save<BudgetRefillEvent>(budgetRefillEvent)
+
+  // no specific event processing
 }
-export async function council_BudgetRefillPlannedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a new budget refill is planned.
+*/
+export async function council_BudgetRefillPlanned({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Council.BudgetRefillPlannedEvent(event).params
 
   const budgetRefillPlannedEvent = new BudgetRefillPlannedEvent({
@@ -180,11 +582,16 @@ export async function council_BudgetRefillPlannedEvent({ event, store }: EventCo
   })
 
   await store.save<BudgetRefillPlannedEvent>(budgetRefillPlannedEvent)
+
+  // no specific event processing
 }
-export async function council_BudgetIncrementUpdatedEvent({
-  event,
-  store,
-}: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when a regular budget increment amount is updated.
+*/
+export async function council_BudgetIncrementUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [amount] = new Council.BudgetIncrementUpdatedEvent(event).params
 
   const budgetIncrementUpdatedEvent = new BudgetIncrementUpdatedEvent({
@@ -193,11 +600,16 @@ export async function council_BudgetIncrementUpdatedEvent({
   })
 
   await store.save<BudgetIncrementUpdatedEvent>(budgetIncrementUpdatedEvent)
+
+  // no specific event processing
 }
-export async function council_CouncilorRewardUpdatedEvent({
-  event,
-  store,
-}: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when the reward amount for council members is updated.
+*/
+export async function council_CouncilorRewardUpdated({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [rewardAmount] = new Council.CouncilorRewardUpdatedEvent(event).params
 
   const councilorRewardUpdatedEvent = new CouncilorRewardUpdatedEvent({
@@ -206,8 +618,16 @@ export async function council_CouncilorRewardUpdatedEvent({
   })
 
   await store.save<CouncilorRewardUpdatedEvent>(councilorRewardUpdatedEvent)
+
+  // no specific event processing
 }
-export async function council_RequestFundedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+
+/*
+  The event is emitted when funds are transfered from the council budget to an account.
+*/
+export async function council_RequestFunded({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [account, amount] = new Council.RequestFundedEvent(event).params
 
   const requestFundedEvent = new RequestFundedEvent({
@@ -217,11 +637,18 @@ export async function council_RequestFundedEvent({ event, store }: EventContext
   })
 
   await store.save<RequestFundedEvent>(requestFundedEvent)
+
+  // no specific event processing
 }
 
 /////////////////// Referendum events //////////////////////////////////////////
 
-export async function referendum_ReferendumStartedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when the voting stage of elections starts.
+*/
+export async function referendum_ReferendumStarted({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Referendum.ReferendumStartedEvent(event).params
 
   const referendumStartedEvent = new ReferendumStartedEvent({
@@ -229,12 +656,19 @@ export async function referendum_ReferendumStartedEvent({ event, store }: EventC
   })
 
   await store.save<ReferendumStartedEvent>(referendumStartedEvent)
+
+  // no specific event processing
 }
 
-export async function referendum_ReferendumStartedForcefullyEvent({
+/*
+  The event is emitted when the voting stage of elections starts (in a fail-safe way).
+*/
+export async function referendum_ReferendumStartedForcefully({
   event,
   store,
 }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [winningTargetCount] = new Referendum.ReferendumStartedForcefullyEvent(event).params
 
   const referendumStartedForcefullyEvent = new ReferendumStartedForcefullyEvent({
@@ -243,12 +677,16 @@ export async function referendum_ReferendumStartedForcefullyEvent({
   })
 
   await store.save<ReferendumStartedForcefullyEvent>(referendumStartedForcefullyEvent)
+
+  // no specific event processing
 }
 
-export async function referendum_RevealingStageStartedEvent({
-  event,
-  store,
-}: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when the vote revealing stage of elections starts.
+*/
+export async function referendum_RevealingStageStarted({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [] = new Referendum.RevealingStageStartedEvent(event).params
 
   const revealingStageStartedEvent = new RevealingStageStartedEvent({
@@ -256,9 +694,16 @@ export async function referendum_RevealingStageStartedEvent({
   })
 
   await store.save<RevealingStageStartedEvent>(revealingStageStartedEvent)
+
+  // no specific event processing
 }
 
-export async function referendum_ReferendumFinishedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when referendum finished and all revealed votes were counted.
+*/
+export async function referendum_ReferendumFinished({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [optionResultsRaw] = new Referendum.ReferendumFinishedEvent(event).params
 
   const members = await store.getMany(Membership, {
@@ -277,24 +722,52 @@ export async function referendum_ReferendumFinishedEvent({ event, store }: Event
   })
 
   await store.save<ReferendumFinishedEvent>(referendumFinishedEvent)
+
+  // no specific event processing
 }
 
-export async function referendum_VoteCastEvent({ event, store }: EventContext & StoreContext): Promise<void> {
-  const [account, hash, votePower] = new Referendum.VoteCastEvent(event).params
+/*
+  The event is emitted when a vote is casted in the council election.
+*/
+export async function referendum_VoteCast({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
+  const [account, hash, stake] = new Referendum.VoteCastEvent(event).params
+  const votePower = calculateVotePower(account.toString(), stake)
+  const hashString = bytesToString(hash)
 
   const voteCastEvent = new VoteCastEvent({
     ...genericEventFields(event),
     account: account.toString(),
-    hash: bytesToString(hash),
+    hash: hashString,
     votePower,
   })
 
   await store.save<VoteCastEvent>(voteCastEvent)
+
+  // specific event processing
+
+  const electionRound = await getCurrentElectionRound(store)
+
+  const castVote = new CastVote({
+    commitment: hashString,
+    electionRound,
+    stake,
+    stakeLocked: true,
+    castBy: account.toString(),
+    votePower: votePower,
+  })
+  await store.save<CastVote>(castVote)
 }
 
-export async function referendum_VoteRevealedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when a previously casted vote is revealed.
+*/
+export async function referendum_VoteRevealed({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [account, memberId, salt] = new Referendum.VoteRevealedEvent(event).params
-  const member = new Membership({ id: memberId.toString() })
+  const member = await getMembership(store, memberId.toString())
 
   const voteRevealedEvent = new VoteRevealedEvent({
     ...genericEventFields(event),
@@ -304,9 +777,24 @@ export async function referendum_VoteRevealedEvent({ event, store }: EventContex
   })
 
   await store.save<VoteRevealedEvent>(voteRevealedEvent)
+
+  // specific event processing
+
+  // read vote info
+  const electionRound = await getCurrentElectionRound(store)
+  const castVote = await getAccountCastVote(store, account.toString(), electionRound)
+
+  const candidate = await getCandidate(store, memberId.toString(), electionRound)
+  candidate.votePower = candidate.votePower.add(castVote.votePower)
+  await store.save<Candidate>(candidate)
 }
 
-export async function referendum_StakeReleasedEvent({ event, store }: EventContext & StoreContext): Promise<void> {
+/*
+  The event is emitted when a vote's stake is released.
+*/
+export async function referendum_StakeReleased({ event, store }: EventContext & StoreContext): Promise<void> {
+  // common event processing
+
   const [stakingAccount] = new Referendum.StakeReleasedEvent(event).params
 
   const stakeReleasedEvent = new StakeReleasedEvent({
@@ -315,4 +803,12 @@ export async function referendum_StakeReleasedEvent({ event, store }: EventConte
   })
 
   await store.save<StakeReleasedEvent>(stakeReleasedEvent)
+
+  // specific event processing
+
+  const electionRound = await getCurrentElectionRound(store)
+  const castVote = await getAccountCastVote(store, stakingAccount.toString())
+  castVote.stakeLocked = false
+
+  await store.save<CastVote>(castVote)
 }

+ 1 - 0
query-node/mappings/index.ts

@@ -8,6 +8,7 @@ BN.prototype.toJSON = function () {
 export * from './content'
 export * from './membership'
 export * from './storage'
+export * from './council'
 export * from './workingGroups'
 export * from './proposals'
 export * from './proposalsDiscussion'

+ 79 - 36
query-node/schemas/council.graphql

@@ -17,6 +17,9 @@ type CouncilStageUpdate @entity {
 
   "Council elected in this stage update if any."
   electedCouncil: ElectedCouncil
+
+  "Election not completed due to insufficient candidates or winners."
+  electionProblem: ElectionProblem
 }
 
 type CouncilStageAnnouncing @variant {
@@ -38,6 +41,22 @@ type CouncilStageIdle @variant {
 
 union CouncilStage = CouncilStageAnnouncing | CouncilStageElection | CouncilStageIdle | VariantNone
 
+type ElectionProblemNotEnoughCandidates @variant {
+  # no properties
+
+  # TODO: remove me - variant needs to have at least 1 property now
+  dummy: Int
+}
+
+type ElectionProblemNewCouncilNotElected @variant {
+  # no properties
+
+  # TODO: remove me - variant needs to have at least 1 property now
+  dummy: Int
+}
+
+union ElectionProblem = ElectionProblemNotEnoughCandidates | ElectionProblemNewCouncilNotElected | VariantNone
+
 type Candidate @entity {
   "Account used for staking currency needed for the candidacy."
   stakingAccountId: String!
@@ -45,10 +64,25 @@ type Candidate @entity {
   "Account that will receive rewards if candidate's elected to the council."
   rewardAccountId: String!
 
+  "Candidate's membership."
+  member: Membership!
+
   "Election cycle"
-  cycleId: ElectionRound!
+  electionRound: ElectionRound!
+
+  "Stake locked for the candidacy."
   stake: BigInt!
+
+  "Reflects if the stake is still locked for candidacy or has been already released by the member."
+  stakeLocked: Boolean!
+
+  "Reflects if the candidacy was withdrawn before voting started."
+  candidacyWithdrawn: Boolean!
+
+  "Sum of power of all votes received."
   votePower: BigInt!
+
+  "Candidacy note."
   note: String!
 }
 
@@ -74,6 +108,9 @@ type CouncilMember @entity {
   "Reward amount that should have been paid but couldn't be paid off due to insufficient budget."
   unpaidReward: BigInt!
 
+  "Amount of reward collected by this council member so far."
+  accumulatedReward: BigInt!
+
   electedInCouncil: ElectedCouncil!
 }
 
@@ -94,7 +131,7 @@ type ReferendumStageVoting @variant {
   winningTargetCount: BigInt!
 
   "Index of current election"
-  cycleId: ElectionRound!
+  electionRound: ElectionRound!
 }
 
 type ReferendumStageRevealing @variant {
@@ -105,10 +142,10 @@ type ReferendumStageRevealing @variant {
   winningTargetCount: BigInt!
 
   "Intermediate winning options"
-  intermediateWinners: [ReferendumStageRevealingOptionResult!]
+  intermediateWinners: [ReferendumStageRevealingOptionResult!]!
 
   "Index of current election"
-  cycleId: ElectionRound!
+  electionRound: ElectionRound!
 }
 
 type ReferendumStageRevealingOptionResult @entity {
@@ -119,10 +156,10 @@ type ReferendumStageRevealingOptionResult @entity {
   votePower: BigInt!
 
   # TODO: reference variant (how?)
-  #referendumRevealingStages: [ReferendumStageRevealing!] @derivedFrom(field: "intermediateWinners")
+  #referendumRevealingStages: [ReferendumStageRevealing!]! @derivedFrom(field: "intermediateWinners")
 
   "Election round."
-  cycleId: ElectionRound!
+  electionRound: ElectionRound!
 
   "Event that concluded the referendum."
   referendumFinishedEvent: ReferendumFinishedEvent!
@@ -136,26 +173,32 @@ type CastVote @entity {
   commitment: String!
 
   "Election round."
-  cycleId: ElectionRound!
+  electionRound: ElectionRound!
 
   "Stake used to back up the vote."
   stake: BigInt!
 
+  "Reflects if the stake is still locked for candidacy or has been already released by the member."
+  stakeLocked: Boolean!
+
   "Account that cast the vote."
   castBy: String!
 
   "Member receiving the vote."
   voteFor: Membership
+
+  "Vote's power."
+  votePower: BigInt!
 }
 
 ################### Derived ####################################################
 
 type ElectedCouncil @entity {
   "Members that were elected to the council."
-  councilMembers: [CouncilMember!] @derivedFrom(field: "electedInCouncil")
+  councilMembers: [CouncilMember!]! @derivedFrom(field: "electedInCouncil")
 
   "Changes to council status that were made during it's reign."
-  updates: [CouncilStageUpdate!] @derivedFrom(field: "electedCouncil")
+  updates: [CouncilStageUpdate!]! @derivedFrom(field: "electedCouncil")
 
   "Block number at which the council was elected."
   electedAtBlock: Int!
@@ -164,13 +207,13 @@ type ElectedCouncil @entity {
   endedAtBlock: Int
 
   "Elections held before the council was rightfully elected."
-  councilElections: [ElectionRound!] @derivedFrom(field: "electedCouncil")
+  councilElections: [ElectionRound!]! @derivedFrom(field: "electedCouncil")
 
   "Elections held before the next council was or will be rightfully elected."
-  nextCouncilElections: [ElectionRound!] @derivedFrom(field: "nextElectedCouncil")
+  nextCouncilElections: [ElectionRound!]! @derivedFrom(field: "nextElectedCouncil")
 
-  # TODO: make this nullable to represent that council is already resigned https://github.com/Joystream/hydra/issues/434
-  stage: ReferendumStage!
+  "Sign if council is already resigned."
+  isResigned: Boolean!
 }
 
 type ElectionRound @entity {
@@ -181,7 +224,7 @@ type ElectionRound @entity {
   isFinished: Boolean!
 
   "Vote cast in the election round."
-  castVotes: [CastVote!] @derivedFrom(field: "cycleId")
+  castVotes: [CastVote!]! @derivedFrom(field: "electionRound")
 
   # TODO: reference variant (how?)
   #referendumStageVoting: ReferendumStage @derivedFrom(field: "cycleId")
@@ -194,27 +237,27 @@ type ElectionRound @entity {
   nextElectedCouncil: ElectedCouncil
 
   "Candidates in this election round."
-  candidates: [Candidate!] @derivedFrom(field: "cycleId")
+  candidates: [Candidate!]! @derivedFrom(field: "electionRound")
 }
 
-type Budget @entity {
-  "Block number at which the next rewards will be paid."
-  nextRewardPaymentsAt: BigInt!
-}
-
-type BudgetPayment @entity {
-  "Block number at which the payment was done."
-  paidAtBlock: Int!
-
-  "Member that was paid."
-  member: Membership!
-
-  "Account that received the payment"
-  account: String!
-
-  "Amount that was paid."
-  amount: BigInt!
-
-  "Amount that couldn't be paid due to insufficient council budget's balance."
-  unpaidAmount: BigInt!
-}
+#type Budget @entity {
+#  "Block number at which the next rewards will be paid."
+#  nextRewardPaymentsAt: BigInt!
+#}
+#
+#type BudgetPayment @entity {
+#  "Block number at which the payment was done."
+#  paidAtBlock: Int!
+#
+#  "Member that was paid."
+#  member: Membership!
+#
+#  "Account that received the payment"
+#  account: String!
+#
+#  "Amount that was paid."
+#  amount: BigInt!
+#
+#  "Amount that couldn't be paid due to insufficient council budget's balance."
+#  unpaidAmount: BigInt!
+#}

+ 23 - 23
query-node/schemas/councilEvents.graphql

@@ -1,6 +1,6 @@
 ################### Council ####################################################
 
-type AnnouncingPeriodStartedEvent @entity {
+type AnnouncingPeriodStartedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -21,7 +21,7 @@ type AnnouncingPeriodStartedEvent @entity {
   ### SPECIFIC DATA ###
 }
 
-type NotEnoughCandidatesEvent @entity {
+type NotEnoughCandidatesEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -42,7 +42,7 @@ type NotEnoughCandidatesEvent @entity {
   ### SPECIFIC DATA ###
 }
 
-type VotingPeriodStartedEvent @entity {
+type VotingPeriodStartedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -66,7 +66,7 @@ type VotingPeriodStartedEvent @entity {
   numOfCandidates: BigInt!
 }
 
-type NewCandidateEvent @entity {
+type NewCandidateEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -99,7 +99,7 @@ type NewCandidateEvent @entity {
   balance: BigInt!
 }
 
-type NewCouncilElectedEvent @entity {
+type NewCouncilElectedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -123,7 +123,7 @@ type NewCouncilElectedEvent @entity {
   electedMembers: [Membership!]
 }
 
-type NewCouncilNotElectedEvent @entity {
+type NewCouncilNotElectedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -144,7 +144,7 @@ type NewCouncilNotElectedEvent @entity {
   ### SPECIFIC DATA ###
 }
 
-type CandidacyStakeReleaseEvent @entity {
+type CandidacyStakeReleaseEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -168,7 +168,7 @@ type CandidacyStakeReleaseEvent @entity {
   member: Membership!
 }
 
-type CandidacyWithdrawEvent @entity {
+type CandidacyWithdrawEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -192,7 +192,7 @@ type CandidacyWithdrawEvent @entity {
   member: Membership!
 }
 
-type CandidacyNoteSetEvent @entity {
+type CandidacyNoteSetEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -219,7 +219,7 @@ type CandidacyNoteSetEvent @entity {
   note: String!
 }
 
-type RewardPaymentEvent @entity {
+type RewardPaymentEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -252,7 +252,7 @@ type RewardPaymentEvent @entity {
   missingBalance: BigInt!
 }
 
-type BudgetBalanceSetEvent @entity {
+type BudgetBalanceSetEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -276,7 +276,7 @@ type BudgetBalanceSetEvent @entity {
   balance: BigInt!
 }
 
-type BudgetRefillEvent @entity {
+type BudgetRefillEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -300,7 +300,7 @@ type BudgetRefillEvent @entity {
   balance: BigInt!
 }
 
-type BudgetRefillPlannedEvent @entity {
+type BudgetRefillPlannedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -321,7 +321,7 @@ type BudgetRefillPlannedEvent @entity {
   ### SPECIFIC DATA ###
 }
 
-type BudgetIncrementUpdatedEvent @entity {
+type BudgetIncrementUpdatedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -345,7 +345,7 @@ type BudgetIncrementUpdatedEvent @entity {
   amount: BigInt!
 }
 
-type CouncilorRewardUpdatedEvent @entity {
+type CouncilorRewardUpdatedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -369,7 +369,7 @@ type CouncilorRewardUpdatedEvent @entity {
   rewardAmount: BigInt!
 }
 
-type RequestFundedEvent @entity {
+type RequestFundedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -398,7 +398,7 @@ type RequestFundedEvent @entity {
 
 ################### Referendum #################################################
 
-type ReferendumStartedEvent @entity {
+type ReferendumStartedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -422,7 +422,7 @@ type ReferendumStartedEvent @entity {
   winningTargetCount: BigInt!
 }
 
-type ReferendumStartedForcefullyEvent @entity {
+type ReferendumStartedForcefullyEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -446,7 +446,7 @@ type ReferendumStartedForcefullyEvent @entity {
   winningTargetCount: BigInt!
 }
 
-type RevealingStageStartedEvent @entity {
+type RevealingStageStartedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -467,7 +467,7 @@ type RevealingStageStartedEvent @entity {
   ### SPECIFIC DATA ###
 }
 
-type ReferendumFinishedEvent @entity {
+type ReferendumFinishedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -491,7 +491,7 @@ type ReferendumFinishedEvent @entity {
   optionResults: [ReferendumStageRevealingOptionResult!] @derivedFrom(field: "referendumFinishedEvent")
 }
 
-type VoteCastEvent @entity {
+type VoteCastEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -521,7 +521,7 @@ type VoteCastEvent @entity {
   votePower: BigInt!
 }
 
-type VoteRevealedEvent @entity {
+type VoteRevealedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"
@@ -551,7 +551,7 @@ type VoteRevealedEvent @entity {
   salt: String!
 }
 
-type StakeReleasedEvent @entity {
+type StakeReleasedEvent implements Event @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"

+ 2 - 2
query-node/schemas/membership.graphql

@@ -86,8 +86,8 @@ type Membership @entity {
 
   # Council & Referendum relations
 
-  "Council reward payment made received by the member."
-  budgetPayments: [BudgetPayment!] @derivedFrom(field: "member")
+  #"Council reward payment made received by the member."
+  #budgetPayments: [BudgetPayment!] @derivedFrom(field: "member")
 
   "Elected councils' memberships of the member."
   councilMembers: [CouncilMember!] @derivedFrom(field: "member")