Browse Source

Merge pull request #2672 from ondratra/olympiaCouncilInputSchema

query node - council & referndum input schema + generic event mappings
Bedeho Mender 3 years ago
parent
commit
000e69a87b
47 changed files with 2689 additions and 518 deletions
  1. 2 0
      .github/workflows/integration-tests.yml
  2. 75 1
      query-node/manifest.yml
  3. 10 0
      query-node/mappings/.eslintrc.js
  4. 0 35
      query-node/mappings/genesis.ts
  5. 2 2
      query-node/mappings/package.json
  6. 0 0
      query-node/mappings/src/common.ts
  7. 1 1
      query-node/mappings/src/content/channel.ts
  8. 1 1
      query-node/mappings/src/content/curatorGroup.ts
  9. 0 0
      query-node/mappings/src/content/index.ts
  10. 0 0
      query-node/mappings/src/content/utils.ts
  11. 1 1
      query-node/mappings/src/content/video.ts
  12. 950 0
      query-node/mappings/src/council.ts
  13. 1 1
      query-node/mappings/src/forum.ts
  14. 0 0
      query-node/mappings/src/genesis-data/index.ts
  15. 0 0
      query-node/mappings/src/genesis-data/members.json
  16. 0 0
      query-node/mappings/src/genesis-data/membershipSystem.json
  17. 0 0
      query-node/mappings/src/genesis-data/workers.json
  18. 0 0
      query-node/mappings/src/genesis-data/workingGroups.json
  19. 67 0
      query-node/mappings/src/genesis.ts
  20. 1 0
      query-node/mappings/src/index.ts
  21. 5 1
      query-node/mappings/src/membership.ts
  22. 1 1
      query-node/mappings/src/proposals.ts
  23. 1 1
      query-node/mappings/src/proposalsDiscussion.ts
  24. 1 1
      query-node/mappings/src/storage.ts
  25. 1 1
      query-node/mappings/src/workingGroups.ts
  26. 1 1
      query-node/mappings/tsconfig.json
  27. 1 1
      query-node/package.json
  28. 266 0
      query-node/schemas/council.graphql
  29. 563 0
      query-node/schemas/councilEvents.graphql
  30. 14 0
      query-node/schemas/membership.graphql
  31. 9 1
      tests/integration-tests/src/Api.ts
  32. 33 0
      tests/integration-tests/src/QueryNodeApi.ts
  33. 17 1
      tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts
  34. 57 0
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts
  35. 82 0
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts
  36. 67 0
      tests/integration-tests/src/fixtures/council/common.ts
  37. 2 0
      tests/integration-tests/src/fixtures/council/index.ts
  38. 0 1
      tests/integration-tests/src/fixtures/membership/UpdateProfileHappyCaseFixture.ts
  39. 14 11
      tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts
  40. 0 1
      tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts
  41. 20 0
      tests/integration-tests/src/flows/council/failToElect.ts
  42. 72 11
      tests/integration-tests/src/graphql/generated/queries.ts
  43. 299 426
      tests/integration-tests/src/graphql/generated/schema.ts
  44. 35 0
      tests/integration-tests/src/graphql/queries/council.graphql
  45. 10 0
      tests/integration-tests/src/scenarios/council.ts
  46. 5 0
      tests/integration-tests/src/scenarios/full.ts
  47. 2 17
      yarn.lock

+ 2 - 0
.github/workflows/integration-tests.yml

@@ -20,6 +20,7 @@ jobs:
         yarn workspace @joystream/types build
         yarn workspace @joystream/metadata-protobuf build
         yarn workspace integration-tests checks --quiet
+        yarn workspace query-node-root lint
 
   network_build_osx:
     name: MacOS Checks
@@ -39,3 +40,4 @@ jobs:
         yarn workspace @joystream/types build
         yarn workspace @joystream/metadata-protobuf build
         yarn workspace integration-tests checks --quiet
+        yarn workspace query-node-root lint

+ 75 - 1
query-node/manifest.yml

@@ -108,6 +108,31 @@ typegen:
     - data_directory.ContentAccepted
     - data_directory.ContentRejected
     - data_directory.ContentUploadingStatusUpdated
+    # Council
+    - council.AnnouncingPeriodStarted
+    - council.NotEnoughCandidates
+    - council.VotingPeriodStarted
+    - council.NewCandidate
+    - council.NewCouncilElected
+    - council.NewCouncilNotElected
+    - council.CandidacyStakeRelease
+    - council.CandidacyWithdraw
+    - council.CandidacyNoteSet
+    - council.RewardPayment
+    - council.BudgetBalanceSet
+    - council.BudgetRefill
+    - council.BudgetRefillPlanned
+    - council.BudgetIncrementUpdated
+    - council.CouncilorRewardUpdated
+    - council.RequestFunded
+    # Referendum
+    - referendum.ReferendumStarted
+    - referendum.ReferendumStartedForcefully
+    - referendum.RevealingStageStarted
+    - referendum.ReferendumFinished
+    - referendum.VoteCast
+    - referendum.VoteRevealed
+    - referendum.StakeReleased
   calls:
     # Content directory
     - content.create_curator_group
@@ -149,7 +174,7 @@ typegen:
     typedefsLoc: '../types/augment/all/defs.json'
 mappings:
   # js module that exports the handler functions
-  mappingsModule: mappings/lib
+  mappingsModule: mappings/lib/src
   # additinal libraries the processor loads
   # typically it is a module with event and extrinsic types generated by hydra-typegen
   imports:
@@ -567,6 +592,55 @@ mappings:
     # not handled at the moment
     #- event: dataDirectory.ContentUploadingStatusUpdated
     #  handler: data_directory_ContentUploadingStatusUpdated
+
+    # Council
+    - event: council.AnnouncingPeriodStarted
+      handler: council_AnnouncingPeriodStarted
+    - event: council.NotEnoughCandidates
+      handler: council_NotEnoughCandidates
+    - event: council.VotingPeriodStarted
+      handler: council_VotingPeriodStarted
+    - event: council.NewCandidate
+      handler: council_NewCandidate
+    - event: council.NewCouncilElected
+      handler: council_NewCouncilElected
+    - event: council.NewCouncilNotElected
+      handler: council_NewCouncilNotElected
+    - event: council.CandidacyStakeRelease
+      handler: council_CandidacyStakeRelease
+    - event: council.CandidacyWithdraw
+      handler: council_CandidacyWithdraw
+    - event: council.CandidacyNoteSet
+      handler: council_CandidacyNoteSet
+    - event: council.RewardPayment
+      handler: council_RewardPayment
+    - event: council.BudgetBalanceSet
+      handler: council_BudgetBalanceSet
+    - event: council.BudgetRefill
+      handler: council_BudgetRefill
+    - event: council.BudgetRefillPlanned
+      handler: council_BudgetRefillPlanned
+    - event: council.BudgetIncrementUpdated
+      handler: council_BudgetIncrementUpdated
+    - event: council.CouncilorRewardUpdated
+      handler: council_CouncilorRewardUpdated
+    - event: council.RequestFunded
+      handler: council_RequestFunded
+    # Referendum
+    - event: referendum.ReferendumStarted
+      handler: referendum_ReferendumStarted
+    - event: referendum.ReferendumStartedForcefully
+      handler: referendum_ReferendumStartedForcefully
+    - event: referendum.RevealingStageStarted
+      handler: referendum_RevealingStageStarted
+    - event: referendum.ReferendumFinished
+      handler: referendum_ReferendumFinished
+    - event: referendum.VoteCast
+      handler: referendum_VoteCast
+    - event: referendum.VoteRevealed
+      handler: referendum_VoteRevealed
+    - event: referendum.StakeReleased
+      handler: referendum_StakeReleased
   extrinsicHandlers:
     # infer defaults here
     #- extrinsic: Balances.Transfer

+ 10 - 0
query-node/mappings/.eslintrc.js

@@ -0,0 +1,10 @@
+module.exports = {
+  env: {
+    node: true,
+  },
+  rules: {
+    '@typescript-eslint/naming-convention': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
+  },
+}

+ 0 - 35
query-node/mappings/genesis.ts

@@ -1,35 +0,0 @@
-import { StoreContext } from '@joystream/hydra-common'
-import BN from 'bn.js'
-import { MembershipSystemSnapshot, WorkingGroup } from 'query-node/dist/model'
-import { membershipSystem, workingGroups } from './genesis-data'
-
-export async function loadGenesisData({ store }: StoreContext): Promise<void> {
-  // Membership system
-  await store.save<MembershipSystemSnapshot>(
-    new MembershipSystemSnapshot({
-      createdAt: new Date(0),
-      updatedAt: new Date(0),
-      snapshotBlock: 0,
-      ...membershipSystem,
-      membershipPrice: new BN(membershipSystem.membershipPrice),
-      invitedInitialBalance: new BN(membershipSystem.invitedInitialBalance),
-    })
-  )
-
-  // Working groups
-  await Promise.all(
-    workingGroups.map(async (group) =>
-      store.save<WorkingGroup>(
-        new WorkingGroup({
-          createdAt: new Date(0),
-          updatedAt: new Date(0),
-          id: group.name,
-          name: group.name,
-          budget: new BN(group.budget),
-        })
-      )
-    )
-  )
-
-  // TODO: members, workers
-}

+ 2 - 2
query-node/mappings/package.json

@@ -2,11 +2,11 @@
   "name": "query-node-mappings",
   "version": "0.1.0",
   "description": "Mappings for hydra-processor",
-  "main": "lib/mappings/index.js",
+  "main": "lib/src/index.js",
   "license": "MIT",
   "scripts": {
     "build": "rm -rf lib && tsc --build tsconfig.json && cp ./generated/types/typedefs.json ./lib/generated/types/typedefs.json",
-    "lint": "echo \"Skippinng\"",
+    "lint": "eslint ./src --ext .ts",
     "clean": "rm -rf lib",
     "postinstall": "yarn ts-node ./scripts/postInstall.ts",
     "postHydraCLIInstall": "yarn ts-node ./scripts/postHydraCLIInstall.ts"

+ 0 - 0
query-node/mappings/common.ts → query-node/mappings/src/common.ts


+ 1 - 1
query-node/mappings/content/channel.ts → query-node/mappings/src/content/channel.ts

@@ -5,7 +5,7 @@ import { EventContext, StoreContext } from '@joystream/hydra-common'
 import { In } from 'typeorm'
 import { AccountId } from '@polkadot/types/interfaces'
 import { Option } from '@polkadot/types/codec'
-import { Content } from '../generated/types'
+import { Content } from '../../generated/types'
 import { convertContentActorToChannelOwner, processChannelMetadata } from './utils'
 import { AssetNone, Channel, ChannelCategory, DataObject } from 'query-node/dist/model'
 import { deserializeMetadata, inconsistentState, logger } from '../common'

+ 1 - 1
query-node/mappings/content/curatorGroup.ts → query-node/mappings/src/content/curatorGroup.ts

@@ -4,7 +4,7 @@ eslint-disable @typescript-eslint/naming-convention
 import { EventContext, StoreContext } from '@joystream/hydra-common'
 import { FindConditions } from 'typeorm'
 import { CuratorGroup } from 'query-node/dist/model'
-import { Content } from '../generated/types'
+import { Content } from '../../generated/types'
 import { inconsistentState, logger } from '../common'
 
 export async function content_CuratorGroupCreated({ store, event }: EventContext & StoreContext): Promise<void> {

+ 0 - 0
query-node/mappings/content/index.ts → query-node/mappings/src/content/index.ts


+ 0 - 0
query-node/mappings/content/utils.ts → query-node/mappings/src/content/utils.ts


+ 1 - 1
query-node/mappings/content/video.ts → query-node/mappings/src/content/video.ts

@@ -3,7 +3,7 @@ eslint-disable @typescript-eslint/naming-convention
 */
 import { EventContext, StoreContext } from '@joystream/hydra-common'
 import { In } from 'typeorm'
-import { Content } from '../generated/types'
+import { Content } from '../../generated/types'
 import { deserializeMetadata, inconsistentState, logger } from '../common'
 import { processVideoMetadata } from './utils'
 import { AssetNone, Channel, Video, VideoCategory } from 'query-node/dist/model'

+ 950 - 0
query-node/mappings/src/council.ts

@@ -0,0 +1,950 @@
+import { EventContext, StoreContext, DatabaseManager } from '@joystream/hydra-common'
+import { CURRENT_NETWORK, deserializeMetadata, genericEventFields } from './common'
+import BN from 'bn.js'
+import { FindConditions, SelectQueryBuilder } from 'typeorm'
+
+import {
+  // Council events
+  AnnouncingPeriodStartedEvent,
+  NotEnoughCandidatesEvent,
+  VotingPeriodStartedEvent,
+  NewCandidateEvent,
+  NewCouncilElectedEvent,
+  NewCouncilNotElectedEvent,
+  CandidacyStakeReleaseEvent,
+  CandidacyWithdrawEvent,
+  CandidacyNoteSetEvent,
+  RewardPaymentEvent,
+  BudgetBalanceSetEvent,
+  BudgetRefillEvent,
+  BudgetRefillPlannedEvent,
+  BudgetIncrementUpdatedEvent,
+  CouncilorRewardUpdatedEvent,
+  RequestFundedEvent,
+
+  // Referendum events
+  ReferendumStartedEvent,
+  ReferendumStartedForcefullyEvent,
+  RevealingStageStartedEvent,
+  ReferendumFinishedEvent,
+  VoteCastEvent,
+  VoteRevealedEvent,
+  StakeReleasedEvent,
+
+  // Council & referendum structures
+  ReferendumStageVoting,
+  ReferendumStageRevealing,
+
+  // Council & referendum schema types
+  CouncilStageUpdate,
+  CouncilStageAnnouncing,
+  CouncilStageIdle,
+  CouncilStageElection,
+  CouncilStage,
+  ElectionProblem,
+  Candidate,
+  CouncilMember,
+  ElectionRound,
+  ElectedCouncil,
+  CastVote,
+  CandidacyNoteMetadata,
+
+  // Misc
+  Membership,
+} from 'query-node/dist/model'
+import { Council, Referendum } from '../generated/types'
+import { CouncilCandidacyNoteMetadata } from '@joystream/metadata-protobuf'
+import { isSet } from '@joystream/metadata-protobuf/utils'
+
+/// /////////////// Common - Gets //////////////////////////////////////////////
+
+/*
+  Retrieves the member record by its id.
+*/
+async function getMembership(store: DatabaseManager, memberId: string): Promise<Membership | undefined> {
+  const member = await store.get(Membership, { where: { id: memberId } })
+
+  if (!member) {
+    throw new Error(`Membership not found. memberId '${memberId}'`)
+  }
+
+  return member
+}
+
+/*
+  Retrieves the council candidate by its member id. Returns the last record for the member
+  if the election round isn't explicitly set.
+*/
+async function getCandidate(
+  store: DatabaseManager,
+  memberId: string,
+  electionRound?: ElectionRound,
+  relations: string[] = []
+): Promise<Candidate> {
+  const event = await store.get(NewCandidateEvent, {
+    join: { alias: 'event', innerJoin: { candidate: 'event.candidate' } },
+    where: (qb: SelectQueryBuilder<NewCandidateEvent>) => {
+      qb.where('candidate.memberId = :memberId', { memberId })
+      if (electionRound) {
+        qb.andWhere('candidate.electionRoundId = :electionRoundId', { electionRoundId: electionRound.id })
+      }
+    },
+    order: { inBlock: 'DESC', indexInBlock: 'DESC' },
+    relations: ['candidate'].concat(relations.map((r) => `candidate.${r}`)),
+  })
+
+  if (!event) {
+    throw new Error(`Candidate not found. memberId '${memberId}' electionRound '${electionRound?.id}'`)
+  }
+
+  return event.candidate
+}
+
+/*
+  Retrieves the member's last council member record.
+*/
+async function getCouncilMember(store: DatabaseManager, memberId: string): Promise<CouncilMember> {
+  const councilMember = await store.get(CouncilMember, {
+    where: { memberId: memberId },
+    order: { createdAt: 'DESC' },
+  })
+
+  if (!councilMember) {
+    throw new Error(`Council member not found. memberId '${memberId}'`)
+  }
+
+  return councilMember
+}
+
+/*
+  Returns the current election round record.
+*/
+async function getCurrentElectionRound(store: DatabaseManager, relations: string[] = []): Promise<ElectionRound> {
+  const electionRound = await store.get(ElectionRound, { order: { cycleId: 'DESC' }, relations: relations })
+
+  if (!electionRound) {
+    throw new Error(`No election round found`)
+  }
+
+  return electionRound
+}
+
+/*
+  Returns the last council stage update.
+*/
+async function getCurrentStageUpdate(store: DatabaseManager): Promise<CouncilStageUpdate> {
+  const stageUpdate = await store.get(CouncilStageUpdate, { order: { changedAt: 'DESC' } })
+
+  if (!stageUpdate) {
+    throw new Error('No stage update found.')
+  }
+
+  return stageUpdate
+}
+
+/*
+  Returns current elected council record.
+*/
+async function getCurrentElectedCouncil(store: DatabaseManager): Promise<ElectedCouncil> {
+  const electedCouncil = await store.get(ElectedCouncil, { order: { electedAtBlock: 'DESC' } })
+
+  // elected council's existence is guaranteed because one is inserted in `genesis.ts`
+  return electedCouncil as ElectedCouncil
+}
+
+/*
+  Returns the last vote cast in an election by the given account. Returns the last record for the account
+  if the election round isn't explicitly set.
+*/
+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, order: { createdAt: 'DESC' } })
+
+  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
+}
+
+/*
+  Custom typeguard for council stage - announcing candidacy.
+*/
+function isCouncilStageAnnouncing(councilStage: typeof CouncilStage): councilStage is CouncilStageAnnouncing {
+  return councilStage.isTypeOf === 'CouncilStageAnnouncing'
+}
+
+/// /////////////// Common /////////////////////////////////////////////////////
+
+/*
+  Creates new council stage update record.
+*/
+async function updateCouncilStage(
+  store: DatabaseManager,
+  councilStage: typeof CouncilStage,
+  blockNumber: number,
+  electionProblem?: ElectionProblem
+): Promise<void> {
+  const electedCouncil = await getCurrentElectedCouncil(store)
+
+  const councilStageUpdate = new CouncilStageUpdate({
+    stage: councilStage,
+    changedAt: new BN(blockNumber),
+    electionProblem,
+    electedCouncil,
+  })
+
+  await store.save<CouncilStageUpdate>(councilStageUpdate)
+}
+
+/*
+  Concludes current election round and starts the next one.
+*/
+async function startNextElectionRound(
+  store: DatabaseManager,
+  electedCouncil: ElectedCouncil,
+  blockNumber: number,
+  electionProblem?: ElectionProblem
+): Promise<ElectionRound> {
+  // finish last election round
+  const lastElectionRound = 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)
+
+  // update council stage
+
+  const stage = new CouncilStageAnnouncing()
+  stage.candidatesCount = new BN(0)
+  await updateCouncilStage(store, stage, blockNumber, electionProblem)
+
+  return electionRound
+}
+
+/*
+  Converts successful council candidate records to council member records.
+*/
+async function convertCandidatesToCouncilMembers(
+  store: DatabaseManager,
+  candidates: Candidate[],
+  blockNumber: number
+): Promise<CouncilMember[]> {
+  const councilMembers = await candidates.reduce(async (councilMembersPromise, candidate) => {
+    const councilMembers = await councilMembersPromise
+
+    const member = new Membership({ id: candidate.member.id.toString() })
+
+    const councilMember = new CouncilMember({
+      // id: candidate.id // TODO: are ids needed?
+      stakingAccountId: candidate.stakingAccountId,
+      rewardAccountId: candidate.rewardAccountId,
+      member,
+      stake: candidate.stake,
+
+      lastPaymentBlock: new BN(blockNumber),
+
+      unpaidReward: new BN(0),
+      accumulatedReward: new BN(0),
+    })
+
+    return [...councilMembers, councilMember]
+  }, Promise.resolve([] as CouncilMember[]))
+
+  return councilMembers
+}
+
+/// /////////////// Council events /////////////////////////////////////////////
+
+/*
+  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({
+    ...genericEventFields(event),
+  })
+
+  await store.save<AnnouncingPeriodStartedEvent>(announcingPeriodStartedEvent)
+
+  // specific event processing
+
+  // restart elections
+  const electedCouncil = await getCurrentElectedCouncil(store)
+  await startNextElectionRound(store, electedCouncil, event.blockNumber)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+  })
+
+  await store.save<NotEnoughCandidatesEvent>(notEnoughCandidatesEvent)
+
+  // specific event processing
+
+  // restart elections
+  const electedCouncil = await getCurrentElectedCouncil(store)
+  await startNextElectionRound(store, electedCouncil, event.blockNumber, ElectionProblem.NOT_ENOUGH_CANDIDATES)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    numOfCandidates,
+  })
+
+  await store.save<VotingPeriodStartedEvent>(votingPeriodStartedEvent)
+
+  // specific event processing
+
+  // add stage update record
+  const stage = new CouncilStageElection()
+  stage.candidatesCount = new BN(numOfCandidates.toString()) // toString() is needed to duplicate BN
+
+  await updateCouncilStage(store, stage, event.blockNumber)
+}
+
+/*
+  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 - init
+
+  const [memberId, stakingAccount, rewardAccount, balance] = new Council.NewCandidateEvent(event).params
+  const member = await getMembership(store, memberId.toString())
+
+  // specific event processing
+
+  // increase candidate count in stage update record
+  const lastStageUpdate = await getCurrentStageUpdate(store)
+  if (!isCouncilStageAnnouncing(lastStageUpdate.stage)) {
+    throw new Error(`Unexpected council stage "${lastStageUpdate.stage.isTypeOf}"`)
+  }
+
+  lastStageUpdate.stage.candidatesCount = new BN(lastStageUpdate.stage.candidatesCount).add(new BN(1))
+  await store.save<CouncilStageUpdate>(lastStageUpdate)
+
+  const electionRound = await getCurrentElectionRound(store)
+
+  // prepare note metadata record (empty until explicitily set via different extrinsic)
+  const noteMetadata = new CandidacyNoteMetadata({
+    bulletPoints: [],
+  })
+  await store.save<CandidacyNoteMetadata>(noteMetadata)
+
+  // 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),
+    noteMetadata,
+    votesRecieved: [],
+  })
+  await store.save<Candidate>(candidate)
+
+  // common event processing - save
+
+  const newCandidateEvent = new NewCandidateEvent({
+    ...genericEventFields(event),
+    candidate,
+    stakingAccount: stakingAccount.toString(),
+    rewardAccount: rewardAccount.toString(),
+    balance,
+  })
+
+  await store.save<NewCandidateEvent>(newCandidateEvent)
+}
+
+/*
+  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 - init
+
+  const [memberIds] = new Council.NewCouncilElectedEvent(event).params
+  const electedMemberIds = memberIds.map((item) => item.toString())
+
+  // specific event processing
+
+  // mark old council as resinged
+  const oldElectedCouncil = await getCurrentElectedCouncil(store)
+  oldElectedCouncil.isResigned = true
+  oldElectedCouncil.endedAtBlock = event.blockNumber
+  oldElectedCouncil.endedAtTime = new Date(event.blockTimestamp)
+  oldElectedCouncil.endedAtNetwork = CURRENT_NETWORK
+  await store.save<ElectedCouncil>(oldElectedCouncil)
+
+  // get election round and its candidates
+  const electionRound = await getCurrentElectionRound(store)
+
+  // TODO: uncomment when following query will be working (after some QN patches make it to Olympia)
+  // const electedCandidates = await store.getMany(Candidate, { where: { electionRoundId: electionRound.id, member: { id_in: electedMemberIds } } })
+  const electedCandidates = (
+    await store.getMany(Candidate, { where: { electionRoundId: electionRound.id }, relations: ['member'] })
+  ).filter((item: Candidate) => electedMemberIds.find((tmpId) => tmpId === item.member.id.toString()))
+
+  // create new council record
+  const electedCouncil = new ElectedCouncil({
+    councilMembers: await convertCandidatesToCouncilMembers(store, electedCandidates, event.blockNumber),
+    updates: [],
+    electedAtBlock: event.blockNumber,
+    electedAtTime: new Date(event.blockTimestamp),
+    electedAtNetwork: CURRENT_NETWORK,
+    councilElections: oldElectedCouncil?.nextCouncilElections || [],
+    nextCouncilElections: [],
+    isResigned: false,
+  })
+
+  await store.save<ElectedCouncil>(electedCouncil)
+
+  // save new council members
+  await Promise.all(
+    (electedCouncil.councilMembers || []).map(async (councilMember) => {
+      councilMember.electedInCouncil = electedCouncil
+
+      await store.save<CouncilMember>(councilMember)
+    })
+  )
+
+  // add council stage update
+  const stage = new CouncilStageIdle()
+  await updateCouncilStage(store, stage, event.blockNumber)
+
+  // unset `isCouncilMember` sign for old council's members
+  const oldElectedMembers = await store.getMany(Membership, { where: { isCouncilMember: true } })
+  await Promise.all(
+    oldElectedMembers.map(async (member) => {
+      member.isCouncilMember = false
+
+      await store.save<Membership>(member)
+    })
+  )
+
+  // set `isCouncilMember` sign for new council's members
+  await Promise.all(
+    (electedCouncil.councilMembers || []).map(async (councilMember) => {
+      const member = councilMember.member
+      member.isCouncilMember = true
+
+      await store.save<Membership>(member)
+    })
+  )
+
+  // common event processing - save
+
+  const newCouncilElectedEvent = new NewCouncilElectedEvent({
+    ...genericEventFields(event),
+    electedCouncil,
+  })
+
+  await store.save<NewCouncilElectedEvent>(newCouncilElectedEvent)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+  })
+
+  await store.save<NewCouncilNotElectedEvent>(newCouncilNotElectedEvent)
+
+  // specific event processing
+
+  // restart elections
+  const electedCouncil = await getCurrentElectedCouncil(store)
+  await startNextElectionRound(store, electedCouncil, event.blockNumber, ElectionProblem.NEW_COUNCIL_NOT_ELECTED)
+}
+
+/*
+  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 candidate = await getCandidate(store, memberId.toString()) // get last member's candidacy record
+
+  const candidacyStakeReleaseEvent = new CandidacyStakeReleaseEvent({
+    ...genericEventFields(event),
+    candidate,
+  })
+
+  await store.save<CandidacyStakeReleaseEvent>(candidacyStakeReleaseEvent)
+
+  // specific event processing
+
+  // update candidate info about stake lock
+  candidate.stakeLocked = false
+  await store.save<Candidate>(candidate)
+}
+
+/*
+  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 candidate = await getCandidate(store, memberId.toString())
+
+  const candidacyWithdrawEvent = new CandidacyWithdrawEvent({
+    ...genericEventFields(event),
+    candidate,
+  })
+
+  await store.save<CandidacyWithdrawEvent>(candidacyWithdrawEvent)
+
+  // specific event processing
+
+  // mark candidacy as withdrawn
+  candidate.candidacyWithdrawn = true
+  await store.save<Candidate>(candidate)
+}
+
+/*
+  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
+
+  // load candidate recored
+  const electionRound = await getCurrentElectionRound(store)
+  const candidate = await getCandidate(store, memberId.toString(), electionRound, ['noteMetadata'])
+
+  const areBulletPointsSet = (metadataBulletPoints: string[] | null | undefined) => !!metadataBulletPoints
+  const areBulletPointsBeingUnset = (metadataBulletPoints: string[]) => {
+    // assumes areBulletPointsSet() were checked before
+
+    return metadataBulletPoints.length && metadataBulletPoints[0] === ''
+  }
+
+  // unpack note's metadata and save it to db
+  const metadata = deserializeMetadata(CouncilCandidacyNoteMetadata, note)
+  const noteMetadata = candidate.noteMetadata
+  // `XXX || (null as any)` construct clears metadata if requested (see https://github.com/Joystream/hydra/issues/435)
+  noteMetadata.header = isSet(metadata?.header) ? metadata?.header || (null as any) : noteMetadata.header
+  noteMetadata.bulletPoints = areBulletPointsSet(metadata?.bulletPoints)
+    ? areBulletPointsBeingUnset(metadata?.bulletPoints as string[]) // check deletion request
+      ? [] // empty bullet points
+      : (metadata?.bulletPoints as string[]) // set new value
+    : noteMetadata.bulletPoints // keep previous value
+  noteMetadata.bannerImageUri = isSet(metadata?.bannerImageUri)
+    ? metadata?.bannerImageUri || (null as any)
+    : noteMetadata.bannerImageUri
+  noteMetadata.description = isSet(metadata?.description)
+    ? metadata?.description || (null as any)
+    : noteMetadata.description
+  await store.save<CandidacyNoteMetadata>(noteMetadata)
+
+  // save metadata set by this event
+  const noteMetadataSnapshot = new CandidacyNoteMetadata({
+    header: metadata?.header ?? undefined,
+    bulletPoints: areBulletPointsSet(metadata?.bulletPoints) ? (metadata?.bulletPoints as string[]) : [],
+    bannerImageUri: metadata?.bannerImageUri ?? undefined,
+    description: metadata?.description ?? undefined,
+  })
+
+  await store.save<CandidacyNoteMetadata>(noteMetadataSnapshot)
+
+  const candidacyNoteSetEvent = new CandidacyNoteSetEvent({
+    ...genericEventFields(event),
+    candidate,
+    noteMetadata: noteMetadataSnapshot,
+  })
+
+  await store.save<CandidacyNoteSetEvent>(candidacyNoteSetEvent)
+
+  // no specific event processing
+}
+
+/*
+  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 councilMember = await getCouncilMember(store, memberId.toString())
+
+  const rewardPaymentEvent = new RewardPaymentEvent({
+    ...genericEventFields(event),
+    councilMember,
+    rewardAccount: rewardAccount.toString(),
+    paidBalance,
+    missingBalance,
+  })
+
+  await store.save<RewardPaymentEvent>(rewardPaymentEvent)
+
+  // specific event processing
+
+  // update (un)paid reward info
+  councilMember.accumulatedReward = councilMember.accumulatedReward.add(paidBalance)
+  councilMember.unpaidReward = missingBalance
+  councilMember.lastPaymentBlock = new BN(event.blockNumber)
+  await store.save<CouncilMember>(councilMember)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    balance,
+  })
+
+  await store.save<BudgetBalanceSetEvent>(budgetBalanceSetEvent)
+
+  // no specific event processing
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    balance,
+  })
+
+  await store.save<BudgetRefillEvent>(budgetRefillEvent)
+
+  // no specific event processing
+}
+
+/*
+  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 [nextRefillInBlock] = new Council.BudgetRefillPlannedEvent(event).params
+
+  const budgetRefillPlannedEvent = new BudgetRefillPlannedEvent({
+    ...genericEventFields(event),
+    nextRefillInBlock: nextRefillInBlock.toNumber(),
+  })
+
+  await store.save<BudgetRefillPlannedEvent>(budgetRefillPlannedEvent)
+
+  // no specific event processing
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    amount,
+  })
+
+  await store.save<BudgetIncrementUpdatedEvent>(budgetIncrementUpdatedEvent)
+
+  // no specific event processing
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    rewardAmount,
+  })
+
+  await store.save<CouncilorRewardUpdatedEvent>(councilorRewardUpdatedEvent)
+
+  // no specific event processing
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    account: account.toString(),
+    amount,
+  })
+
+  await store.save<RequestFundedEvent>(requestFundedEvent)
+
+  // no specific event processing
+}
+
+/// /////////////// Referendum events //////////////////////////////////////////
+
+/*
+  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 [winningTargetCount] = new Referendum.ReferendumStartedEvent(event).params
+
+  const referendumStartedEvent = new ReferendumStartedEvent({
+    ...genericEventFields(event),
+    winningTargetCount,
+  })
+
+  await store.save<ReferendumStartedEvent>(referendumStartedEvent)
+
+  // specific event processing
+
+  await recordReferendumVotingStart(store, event.blockNumber, winningTargetCount.toNumber())
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    winningTargetCount,
+  })
+
+  await store.save<ReferendumStartedForcefullyEvent>(referendumStartedForcefullyEvent)
+
+  // specific event processing
+
+  await recordReferendumVotingStart(store, event.blockNumber, winningTargetCount.toNumber())
+}
+
+/*
+  Adds record about referendum voting start to the current election round.
+*/
+async function recordReferendumVotingStart(store: DatabaseManager, blockNumber: number, winningTargetCount: number) {
+  const electionRound = await getCurrentElectionRound(store)
+
+  // add referendum voting stage record to election round
+  const referendumStage = new ReferendumStageVoting()
+  referendumStage.startedAtBlock = new BN(blockNumber)
+  referendumStage.winningTargetCount = new BN(winningTargetCount)
+  referendumStage.electionRound = electionRound
+  await store.save<ReferendumStageVoting>(referendumStage)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+  })
+
+  await store.save<RevealingStageStartedEvent>(revealingStageStartedEvent)
+
+  // specific event processing
+
+  const electionRound = await getCurrentElectionRound(store, ['referendumStageVoting'])
+
+  // add referendum revealing stage record to election round
+  const referendumStage = new ReferendumStageRevealing()
+  referendumStage.startedAtBlock = new BN(event.blockNumber)
+  referendumStage.winningTargetCount = (electionRound.referendumStageVoting as ReferendumStageVoting).winningTargetCount
+  referendumStage.electionRound = electionRound
+  await store.save<ReferendumStageRevealing>(referendumStage)
+}
+
+/*
+  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 referendumFinishedEvent = new ReferendumFinishedEvent({
+    ...genericEventFields(event),
+  })
+
+  await store.save<ReferendumFinishedEvent>(referendumFinishedEvent)
+
+  // no specific event processing
+}
+
+/*
+  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 - init
+
+  const [account, hash, stake] = new Referendum.VoteCastEvent(event).params
+  const votePower = calculateVotePower(account.toString(), stake)
+
+  // specific event processing
+
+  const electionRound = await getCurrentElectionRound(store)
+
+  const castVote = new CastVote({
+    commitment: hash.toString(),
+    electionRound,
+    stake,
+    stakeLocked: true,
+    castBy: account.toString(),
+    votePower: votePower,
+  })
+  await store.save<CastVote>(castVote)
+
+  // common event processing - save
+
+  const voteCastEvent = new VoteCastEvent({
+    ...genericEventFields(event),
+    castVote,
+  })
+
+  await store.save<VoteCastEvent>(voteCastEvent)
+}
+
+/*
+  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 - init
+
+  const [account, memberId /*, salt */] = new Referendum.VoteRevealedEvent(event).params
+
+  // specific event processing
+
+  // read vote info
+  const electionRound = await getCurrentElectionRound(store)
+  const candidate = await getCandidate(store, memberId.toString(), electionRound, ['member'])
+  const castVote = await getAccountCastVote(store, account.toString(), electionRound)
+
+  // update cast vote's voteFor info
+  castVote.voteFor = candidate
+  await store.save<CastVote>(castVote)
+
+  // increase candidate's total vote power received accordingly
+  candidate.votePower = candidate.votePower.add(castVote.votePower)
+  candidate.lastVoteReceivedAtBlock = new BN(event.blockNumber)
+  candidate.lastVoteReceivedAtEventNumber = event.indexInBlock
+  await store.save<Candidate>(candidate)
+
+  // common event processing - save
+
+  const voteRevealedEvent = new VoteRevealedEvent({
+    ...genericEventFields(event),
+    castVote,
+  })
+
+  await store.save<VoteRevealedEvent>(voteRevealedEvent)
+}
+
+/*
+  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({
+    ...genericEventFields(event),
+    stakingAccount: stakingAccount.toString(),
+  })
+
+  await store.save<StakeReleasedEvent>(stakeReleasedEvent)
+
+  // specific event processing
+
+  const castVote = await getAccountCastVote(store, stakingAccount.toString())
+  castVote.stakeLocked = false
+
+  await store.save<CastVote>(castVote)
+}

+ 1 - 1
query-node/mappings/forum.ts → query-node/mappings/src/forum.ts

@@ -55,7 +55,7 @@ import {
   PostStatusRemoved,
   ForumThreadTag,
 } from 'query-node/dist/model'
-import { Forum } from './generated/types'
+import { Forum } from '../generated/types'
 import { PostReactionId, PrivilegedActor } from '@joystream/types/augment/all'
 import {
   ForumPostMetadata,

+ 0 - 0
query-node/mappings/genesis-data/index.ts → query-node/mappings/src/genesis-data/index.ts


+ 0 - 0
query-node/mappings/genesis-data/members.json → query-node/mappings/src/genesis-data/members.json


+ 0 - 0
query-node/mappings/genesis-data/membershipSystem.json → query-node/mappings/src/genesis-data/membershipSystem.json


+ 0 - 0
query-node/mappings/genesis-data/workers.json → query-node/mappings/src/genesis-data/workers.json


+ 0 - 0
query-node/mappings/genesis-data/workingGroups.json → query-node/mappings/src/genesis-data/workingGroups.json


+ 67 - 0
query-node/mappings/src/genesis.ts

@@ -0,0 +1,67 @@
+import { StoreContext, DatabaseManager } from '@joystream/hydra-common'
+import BN from 'bn.js'
+import { MembershipSystemSnapshot, WorkingGroup, ElectedCouncil, ElectionRound } from 'query-node/dist/model'
+import { membershipSystem, workingGroups } from './genesis-data'
+import { CURRENT_NETWORK } from './common'
+
+export async function loadGenesisData({ store }: StoreContext): Promise<void> {
+  await initMembershipSystem(store)
+
+  await initWorkingGroups(store)
+
+  await initFirstElectionRound(store)
+
+  // TODO: members, workers
+}
+
+async function initMembershipSystem(store: DatabaseManager) {
+  await store.save<MembershipSystemSnapshot>(
+    new MembershipSystemSnapshot({
+      createdAt: new Date(0),
+      updatedAt: new Date(0),
+      snapshotBlock: 0,
+      ...membershipSystem,
+      membershipPrice: new BN(membershipSystem.membershipPrice),
+      invitedInitialBalance: new BN(membershipSystem.invitedInitialBalance),
+    })
+  )
+}
+
+async function initWorkingGroups(store: DatabaseManager) {
+  await Promise.all(
+    workingGroups.map(async (group) =>
+      store.save<WorkingGroup>(
+        new WorkingGroup({
+          createdAt: new Date(0),
+          updatedAt: new Date(0),
+          id: group.name,
+          name: group.name,
+          budget: new BN(group.budget),
+        })
+      )
+    )
+  )
+}
+
+async function initFirstElectionRound(store: DatabaseManager) {
+  const electedCouncil = new ElectedCouncil({
+    councilMembers: [],
+    updates: [],
+    electedAtBlock: 0,
+    electedAtTime: new Date(0),
+    electedAtNetwork: CURRENT_NETWORK,
+    councilElections: [],
+    nextCouncilElections: [],
+    isResigned: false,
+  })
+  await store.save<ElectedCouncil>(electedCouncil)
+
+  const initialElectionRound = new ElectionRound({
+    cycleId: 0,
+    isFinished: false,
+    castVotes: [],
+    electedCouncil,
+    candidates: [],
+  })
+  await store.save<ElectionRound>(initialElectionRound)
+}

+ 1 - 0
query-node/mappings/index.ts → query-node/mappings/src/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'

+ 5 - 1
query-node/mappings/membership.ts → query-node/mappings/src/membership.ts

@@ -2,7 +2,7 @@
 eslint-disable @typescript-eslint/naming-convention
 */
 import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@joystream/hydra-common'
-import { Members } from './generated/types'
+import { Members } from '../generated/types'
 import { MemberId, BuyMembershipParameters, InviteMembershipParameters } from '@joystream/types/augment/all'
 import { MembershipMetadata } from '@joystream/metadata-protobuf'
 import { bytesToString, deserializeMetadata, genericEventFields } from './common'
@@ -104,6 +104,10 @@ async function createNewMemberFromParams(
         ? new Membership({ id: (params as InviteMembershipParameters).inviting_member_id.toString() })
         : undefined,
     isFoundingMember: false,
+    isCouncilMember: false,
+
+    councilCandidacies: [],
+    councilMembers: [],
   })
 
   await store.save<MemberMetadata>(member.metadata)

+ 1 - 1
query-node/mappings/proposals.ts → query-node/mappings/src/proposals.ts

@@ -61,7 +61,7 @@ import {
   ProposalDiscussionThreadModeOpen,
 } from 'query-node/dist/model'
 import { bytesToString, genericEventFields, getWorkingGroupModuleName, MemoryCache, perpareString } from './common'
-import { ProposalsEngine, ProposalsCodex } from './generated/types'
+import { ProposalsEngine, ProposalsCodex } from '../generated/types'
 import { createWorkingGroupOpeningMetadata } from './workingGroups'
 import { blake2AsHex } from '@polkadot/util-crypto'
 import { Bytes } from '@polkadot/types'

+ 1 - 1
query-node/mappings/proposalsDiscussion.ts → query-node/mappings/src/proposalsDiscussion.ts

@@ -18,7 +18,7 @@ import {
   ProposalDiscussionPostStatusRemoved,
 } from 'query-node/dist/model'
 import { bytesToString, deserializeMetadata, genericEventFields, MemoryCache } from './common'
-import { ProposalsDiscussion } from './generated/types'
+import { ProposalsDiscussion } from '../generated/types'
 import { ProposalsDiscussionPostMetadata } from '@joystream/metadata-protobuf'
 import { In } from 'typeorm'
 

+ 1 - 1
query-node/mappings/storage.ts → query-node/mappings/src/storage.ts

@@ -11,7 +11,7 @@ import {
   logger,
   unexpectedData,
 } from './common'
-import { DataDirectory } from './generated/types'
+import { DataDirectory } from '../generated/types'
 import { ContentId, StorageObjectOwner } from '@joystream/types/augment'
 import { ContentId as Custom_ContentId } from '@joystream/types/storage'
 import { registry } from '@joystream/types'

+ 1 - 1
query-node/mappings/workingGroups.ts → query-node/mappings/src/workingGroups.ts

@@ -3,7 +3,7 @@ eslint-disable @typescript-eslint/naming-convention
 */
 import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@joystream/hydra-common'
 
-import { StorageWorkingGroup as WorkingGroups } from './generated/types'
+import { StorageWorkingGroup as WorkingGroups } from '../generated/types'
 import {
   ApplicationMetadata,
   IAddUpcomingOpening,

+ 1 - 1
query-node/mappings/tsconfig.json

@@ -21,5 +21,5 @@
       // "query-node/*": [ "../generated/graphql-server/src/*" ]
     }
   },
-  "include": ["./**/*"]
+  "include": ["./src/**/*"]
 }

+ 1 - 1
query-node/package.json

@@ -6,7 +6,7 @@
     "build": "./build.sh",
     "start": "./start.sh",
     "rebuild": "yarn db:drop && yarn clean:query-node && yarn codegen:query-node && yarn db:prepare && yarn db:migrate",
-    "lint": "echo \"Skippinng\"",
+    "lint": "yarn workspace query-node-mappings lint",
     "clean": "rm -rf ./generated",
     "clean:query-node": "rm -rf ./generated/graphql-server",
     "processor:start": "DEBUG=${DEBUG} hydra-processor run -e ../.env",

+ 266 - 0
query-node/schemas/council.graphql

@@ -0,0 +1,266 @@
+# TODO:
+# - do we need some fulltext search for council/election?
+
+# workaround for https://github.com/Joystream/hydra/issues/434
+type VariantNone @variant {
+  _phantom: Int
+}
+
+################### Council ####################################################
+
+type CouncilStageUpdate @entity {
+  "The new stage council got into."
+  stage: CouncilStage!
+
+  "Block number at which change happened."
+  changedAt: BigInt!
+
+  "Council term during which the update happened (if any)."
+  electedCouncil: ElectedCouncil
+
+  "Election not completed due to insufficient candidates or winners."
+  electionProblem: ElectionProblem
+}
+
+type CouncilStageAnnouncing @variant {
+  "Number of candidates aspiring to be elected as council members."
+  candidatesCount: BigInt!
+}
+
+type CouncilStageElection @variant {
+  "Number of candidates aspiring to be elected as council members."
+  candidatesCount: BigInt!
+}
+
+type CouncilStageIdle @variant {
+  # no properties
+
+  # TODO: remove me - variant needs to have at least 1 property now
+  dummy: Int
+}
+
+union CouncilStage = CouncilStageAnnouncing | CouncilStageElection | CouncilStageIdle | VariantNone
+
+enum ElectionProblem {
+  NOT_ENOUGH_CANDIDATES
+  NEW_COUNCIL_NOT_ELECTED
+}
+
+type Candidate @entity {
+  "Account used for staking currency needed for the candidacy."
+  stakingAccountId: String!
+
+  "Account that will receive rewards if candidate's elected to the council."
+  rewardAccountId: String!
+
+  "Candidate's membership."
+  member: Membership!
+
+  "Election cycle"
+  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!
+
+  "Block in which the last vote was received."
+  lastVoteReceivedAtBlock: BigInt
+
+  "Event number in block in which the last vote was received."
+  lastVoteReceivedAtEventNumber: Int
+
+  "The metadata contained in note."
+  noteMetadata: CandidacyNoteMetadata!
+
+  "Votes recieved in referendums by this member."
+  votesRecieved: [CastVote!]! @derivedFrom(field: "voteFor")
+}
+
+type CouncilMember @entity {
+  "Runtime council member id"
+  id: ID!
+
+  "Account used for staking currency for council membership."
+  stakingAccountId: String!
+
+  "Account that will receive used for reward currency for council membership."
+  rewardAccountId: String!
+
+  "Council member's membership."
+  member: Membership!
+
+  "Stake used for the council membership."
+  stake: BigInt!
+
+  "Block number in which council member recieved the last reward payment."
+  lastPaymentBlock: BigInt!
+
+  "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!
+}
+
+type CandidacyNoteMetadata @entity {
+  "Candidacy header text."
+  header: String
+
+  "Candidate program in form of bullet points. Takes array with one empty string [''] as deletion request."
+  bulletPoints: [String!]
+
+  "Image uri of candidate's banner."
+  bannerImageUri: String
+
+  "Candidacy description (Markdown-formatted)."
+  description: String
+}
+
+################### Referendum #################################################
+
+# NOTE: Due to the bug https://github.com/Joystream/hydra/issues/467 `ReferendumStage*` variants were transformed to entities.
+#       It shouldn't have any negative impact on current usage, but it might need remodeling in the future depending on usage.
+
+type ReferendumStageVoting @entity {
+  "Block in which referendum started."
+  startedAtBlock: BigInt!
+
+  "Target number of winners."
+  winningTargetCount: BigInt!
+
+  "Election round"
+  electionRound: ElectionRound!
+}
+
+type ReferendumStageRevealing @entity {
+  "Block in which referendum started"
+  startedAtBlock: BigInt!
+
+  "Target number of winners"
+  winningTargetCount: BigInt!
+
+  "Election round."
+  electionRound: ElectionRound!
+}
+
+type CastVote @entity {
+  "Hashed vote that was casted before being revealed. Hex format."
+  commitment: String!
+
+  "Election round."
+  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: Candidate
+
+  "Vote's power."
+  votePower: BigInt!
+}
+
+################### Derived ####################################################
+
+type ElectedCouncil @entity {
+  "Members that were elected to the council."
+  councilMembers: [CouncilMember!]! @derivedFrom(field: "electedInCouncil")
+
+  "Changes to council status that were made during it's reign."
+  updates: [CouncilStageUpdate!]! @derivedFrom(field: "electedCouncil")
+
+  "Block number at which the council was elected."
+  electedAtBlock: Int!
+
+  "Block number at which the council reign ended and a new council was elected."
+  endedAtBlock: Int
+
+  "Time at which the council was elected."
+  electedAtTime: DateTime!
+
+  "Time at which the council reign ended and a new council was elected."
+  endedAtTime: DateTime
+
+  "Network running at the time of election."
+  electedAtNetwork: Network!
+
+  "Network running at the time of resignation."
+  endedAtNetwork: Network
+
+  # it might seems that derived field is wrongly set to `nextElectedCouncil`, but that's how it should be
+  "Elections held before the council was rightfully elected."
+  councilElections: [ElectionRound!]! @derivedFrom(field: "nextElectedCouncil")
+
+  # it might seems that derived field is wrongly set to `electedCouncil`, but that's how it should be
+  "Elections held before the next council was or will be rightfully elected."
+  nextCouncilElections: [ElectionRound!]! @derivedFrom(field: "electedCouncil")
+
+  "Sign if council is already resigned."
+  isResigned: Boolean!
+}
+
+type ElectionRound @entity {
+  "Election cycle ID."
+  cycleId: Int!
+
+  "Sign if election has already finished."
+  isFinished: Boolean!
+
+  "Vote cast in the election round."
+  castVotes: [CastVote!]! @derivedFrom(field: "electionRound")
+
+  "Referendum voting stage that happened during this election round."
+  referendumStageVoting: ReferendumStageVoting @derivedFrom(field: "electionRound")
+
+  "Referendum revealing stage that happened during this election round."
+  referendumStageRevealing: ReferendumStageRevealing @derivedFrom(field: "electionRound")
+
+  "Council that is ruling during the election."
+  electedCouncil: ElectedCouncil!
+
+  "Council that was elected in this election round."
+  nextElectedCouncil: ElectedCouncil
+
+  "Candidates in this election round."
+  candidates: [Candidate!]! @derivedFrom(field: "electionRound")
+}
+
+# Not yet sure if this will be needed by apps using query node.
+#
+#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!
+#}

+ 563 - 0
query-node/schemas/councilEvents.graphql

@@ -0,0 +1,563 @@
+################### Council ####################################################
+
+type AnnouncingPeriodStartedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+}
+
+type NotEnoughCandidatesEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+}
+
+type VotingPeriodStartedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Number of candidates in the election."
+  numOfCandidates: BigInt!
+}
+
+type NewCandidateEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Related candidate."
+  candidate: Candidate!
+
+  "Candidate's account used to stake currency."
+  stakingAccount: String!
+
+  "Candidate's account that will be recieving rewards if candidate's elected."
+  rewardAccount: String!
+
+  "Amount of currency to be staked for the candidacy."
+  balance: BigInt!
+}
+
+type NewCouncilElectedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Newly elected council."
+  electedCouncil: ElectedCouncil!
+}
+
+type NewCouncilNotElectedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+}
+
+type CandidacyStakeReleaseEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Related candidate."
+  candidate: Candidate!
+}
+
+type CandidacyWithdrawEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Related candidate."
+  candidate: Candidate!
+}
+
+type CandidacyNoteSetEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Related candidate."
+  candidate: Candidate!
+
+  "The metadata contained in note."
+  noteMetadata: CandidacyNoteMetadata!
+}
+
+type RewardPaymentEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Related council member."
+  councilMember: CouncilMember!
+
+  "Candidate's account that will be recieving rewards if candidate's elected."
+  rewardAccount: String!
+
+  "Amount paid to the council member"
+  paidBalance: BigInt!
+
+  "Amount that couldn't be paid and will be paid the next time."
+  missingBalance: BigInt!
+}
+
+type BudgetBalanceSetEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Budget balance that has been set."
+  balance: BigInt!
+}
+
+type BudgetRefillEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Balance that has been refilled."
+  balance: BigInt!
+}
+
+type BudgetRefillPlannedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  nextRefillInBlock: Int!
+}
+
+type BudgetIncrementUpdatedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Amount that is added to the budget each time it's refilled."
+  amount: BigInt!
+}
+
+type CouncilorRewardUpdatedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "New reward amount paid each reward period."
+  rewardAmount: BigInt!
+}
+
+type RequestFundedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Target account."
+  account: String!
+
+  "Funding amount."
+  amount: BigInt!
+}
+
+################### Referendum #################################################
+
+type ReferendumStartedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Amount of winning referendum options."
+  winningTargetCount: BigInt!
+}
+
+type ReferendumStartedForcefullyEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Amount of winning referendum options."
+  winningTargetCount: BigInt!
+}
+
+type RevealingStageStartedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+}
+
+type ReferendumFinishedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+}
+
+type VoteCastEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Vote cast."
+  castVote: CastVote!
+}
+
+type VoteRevealedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Vote cast."
+  castVote: CastVote!
+}
+
+type StakeReleasedEvent implements Event @entity {
+  ### GENERIC DATA ###
+
+  "(network}-{blockNumber}-{indexInBlock}"
+  id: ID!
+
+  "Hash of the extrinsic which caused the event to be emitted"
+  inExtrinsic: String
+
+  "Blocknumber of the block in which the event was emitted."
+  inBlock: Int!
+
+  "Network the block was produced in"
+  network: Network!
+
+  "Index of event in block from which it was emitted."
+  indexInBlock: Int!
+
+  ### SPECIFIC DATA ###
+
+  "Account used to stake the value."
+  stakingAccount: String!
+}

+ 14 - 0
query-node/schemas/membership.graphql

@@ -71,6 +71,9 @@ type Membership @entity {
   "Whether member is founding member."
   isFoundingMember: Boolean!
 
+  "Whether member is elected in the current council."
+  isCouncilMember: Boolean!
+
   "Member's working group roles (current and past)"
   roles: [Worker!] @derivedFrom(field: "membership")
 
@@ -80,6 +83,17 @@ type Membership @entity {
 
   "Content channels the member owns"
   channels: [Channel!] @derivedFrom(field: "ownerMember")
+
+  # Council & Referendum relations
+
+  #"Council reward payment made received by the member."
+  #budgetPayments: [BudgetPayment!] @derivedFrom(field: "member")
+
+  "Candidacies announced by this member."
+  councilCandidacies: [Candidate!] @derivedFrom(field: "member")
+
+  "Elected councils' memberships of the member."
+  councilMembers: [CouncilMember!] @derivedFrom(field: "member")
 }
 
 type MembershipSystemSnapshot @entity {

+ 9 - 1
tests/integration-tests/src/Api.ts

@@ -535,6 +535,7 @@ export class Api {
 
   public async untilCouncilStage(
     targetStage: 'Announcing' | 'Voting' | 'Revealing' | 'Idle',
+    announcementPeriodNr: number | null = null,
     blocksReserve = 3,
     intervalMs = BLOCKTIME
   ): Promise<void> {
@@ -562,9 +563,16 @@ export class Api {
 
         const currentStageEndsIn = currentStageStartedAt.add(durationByStage[currentStage]).sub(currentBlock)
 
+        const currentAnnouncementPeriodNr =
+          announcementPeriodNr === null ? null : (await this.api.query.council.announcementPeriodNr()).toNumber()
+
         debug(`Current stage: ${currentStage}, blocks left: ${currentStageEndsIn.toNumber()}`)
 
-        return currentStage === targetStage && currentStageEndsIn.gten(blocksReserve)
+        return (
+          currentStage === targetStage &&
+          currentStageEndsIn.gten(blocksReserve) &&
+          announcementPeriodNr === currentAnnouncementPeriodNr
+        )
       },
       intervalMs
     )

+ 33 - 0
tests/integration-tests/src/QueryNodeApi.ts

@@ -4,6 +4,14 @@ import { extendDebug, Debugger } from './Debugger'
 import { ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { EventDetails, WorkingGroupModuleName } from './types'
 import {
+  ElectedCouncilFieldsFragment,
+  GetCurrentCouncilMembers,
+  GetCurrentCouncilMembersQuery,
+  GetCurrentCouncilMembersQueryVariables,
+  CandidateFieldsFragment,
+  GetReferendumIntermediateWinners,
+  GetReferendumIntermediateWinnersQuery,
+  GetReferendumIntermediateWinnersQueryVariables,
   GetMemberByIdQuery,
   GetMemberByIdQueryVariables,
   GetMemberById,
@@ -434,6 +442,31 @@ export class QueryNodeApi {
     >(GetMemberInvitedEventsByEventIds, { eventIds }, 'memberInvitedEvents')
   }
 
+  public async getCurrentCouncilMembers(): Promise<ElectedCouncilFieldsFragment | null> {
+    return this.firstEntityQuery<GetCurrentCouncilMembersQuery, GetCurrentCouncilMembersQueryVariables>(
+      GetCurrentCouncilMembers,
+      {},
+      'electedCouncils'
+    )
+  }
+
+  public async getReferendumIntermediateWinners(
+    electionRoundCycleId: number,
+    councilSize: number
+  ): Promise<CandidateFieldsFragment[]> {
+    return this.multipleEntitiesQuery<
+      GetReferendumIntermediateWinnersQuery,
+      GetReferendumIntermediateWinnersQueryVariables
+    >(
+      GetReferendumIntermediateWinners,
+      {
+        electionRoundCycleId,
+        councilSize,
+      },
+      'candidates'
+    )
+  }
+
   // TODO: Use event id
   public async getInvitesTransferredEvent(
     sourceMemberId: MemberId

+ 17 - 1
tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts

@@ -1,5 +1,6 @@
 import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
 import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
+import { assertCouncilMembersRuntimeQnMatch } from './common'
 import { blake2AsHex } from '@polkadot/util-crypto'
 import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
 import { assert } from 'chai'
@@ -76,12 +77,27 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
     await api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
     await api.sendExtrinsicsAndGetResults(revealingTxs, votersStakingAccounts)
 
+    const candidatesToWinIds = candidatesMemberIds.slice(0, councilSize.toNumber()).map((id) => id.toString())
+
+    // check intermediate election winners are properly set
+    await query.tryQueryWithTimeout(
+      () => query.getReferendumIntermediateWinners(cycleId.toNumber(), councilSize.toNumber()),
+      (qnReferendumIntermediateWinners) => {
+        assert.sameMembers(
+          qnReferendumIntermediateWinners.map((item) => item.member.id.toString()),
+          candidatesToWinIds
+        )
+      }
+    )
+
     await this.api.untilCouncilStage('Idle')
 
     const councilMembers = await api.query.council.councilMembers()
     assert.sameMembers(
       councilMembers.map((m) => m.membership_id.toString()),
-      candidatesMemberIds.slice(0, councilSize.toNumber()).map((id) => id.toString())
+      candidatesToWinIds
     )
+
+    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
   }
 }

+ 57 - 0
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts

@@ -0,0 +1,57 @@
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { assertCouncilMembersRuntimeQnMatch, prepareFailToElectResources } from './common'
+import { assert } from 'chai'
+
+export class NotEnoughCandidatesFixture extends BaseQueryNodeFixture {
+  /*
+      Execute scenario when not enough candidates announce their candidacy and candidacy announcement stage
+      has to be repeated.
+  */
+  public async execute(): Promise<void> {
+    const {
+      candidatesMemberIds,
+      candidatesStakingAccounts,
+      candidatesMemberAccounts,
+      councilCandidateStake,
+      councilMemberIds,
+    } = await prepareFailToElectResources(this.api, this.query)
+
+    const lessCandidatesNumber = 1
+    const candidatingMemberIds = candidatesMemberIds.slice(0, candidatesMemberIds.length - lessCandidatesNumber)
+
+    // announcing stage
+    await this.api.untilCouncilStage('Announcing')
+
+    // ensure no voting is in progress
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
+
+    // announce candidacies
+    const applyForCouncilTxs = candidatingMemberIds.map((memberId, i) =>
+      this.api.tx.council.announceCandidacy(
+        memberId,
+        candidatesStakingAccounts[i],
+        candidatesMemberAccounts[i],
+        councilCandidateStake
+      )
+    )
+    await this.api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
+    await this.api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
+
+    // wait for next announcement stage that should be right after the previous one
+    await this.api.untilCouncilStage('Announcing', announcementPeriodNrInit.toNumber() + 1)
+    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
+
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
+
+    // ensure council members haven't changed
+    const councilMembersEnding = await this.api.query.council.councilMembers()
+    assert.sameMembers(
+      councilMemberIds.map((item) => item.toString()),
+      councilMembersEnding.map((item) => item.membership_id.toString())
+    )
+
+    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
+  }
+}

+ 82 - 0
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts

@@ -0,0 +1,82 @@
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { assertCouncilMembersRuntimeQnMatch, prepareFailToElectResources } from './common'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import { assert } from 'chai'
+import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
+
+export class NotEnoughCandidatesWithVotesFixture extends BaseQueryNodeFixture {
+  public async execute(): Promise<void> {
+    const {
+      candidatesMemberIds,
+      candidatesStakingAccounts,
+      candidatesMemberAccounts,
+      councilCandidateStake,
+      councilMemberIds,
+    } = await prepareFailToElectResources(this.api, this.query)
+
+    const lessVotersNumber = 1
+    const numberOfCandidates = candidatesMemberIds.length
+    const numberOfVoters = numberOfCandidates - 1
+
+    // create voters
+    const voteStake = this.api.consts.referendum.minimumStake
+    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
+    await this.api.treasuryTransferBalanceToAccounts(
+      votersStakingAccounts,
+      voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
+    )
+
+    // announcing stage
+    await this.api.untilCouncilStage('Announcing')
+
+    // ensure no voting is in progress
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    const announcementPeriodNrInit = await this.api.query.council.announcementPeriodNr()
+
+    // announce candidacies
+    const applyForCouncilTxs = candidatesMemberIds.map((memberId, i) =>
+      this.api.tx.council.announceCandidacy(
+        memberId,
+        candidatesStakingAccounts[i],
+        candidatesMemberAccounts[i],
+        councilCandidateStake
+      )
+    )
+    await this.api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
+    await this.api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
+
+    // voting stage
+    await this.api.untilCouncilStage('Voting')
+
+    // vote
+    const cycleId = (await this.api.query.referendum.stage()).asType('Voting').current_cycle_id
+    const votingTxs = votersStakingAccounts.map((account, i) => {
+      const accountId = this.api.createType('AccountId', account)
+      const optionId = candidatesMemberIds[i % numberOfCandidates]
+      const salt = this.api.createType('Bytes', `salt${i}`)
+
+      const payload = Buffer.concat([accountId.toU8a(), optionId.toU8a(), salt.toU8a(), cycleId.toU8a()])
+      const commitment = blake2AsHex(payload)
+      return this.api.tx.referendum.vote(commitment, voteStake)
+    })
+    await this.api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
+    await this.api.sendExtrinsicsAndGetResults(votingTxs, votersStakingAccounts)
+
+    // Announcing stage
+    await this.api.untilCouncilStage('Announcing')
+    const announcementPeriodNrEnding = await this.api.query.council.announcementPeriodNr()
+
+    // ensure new announcement stage started
+    assert((await this.api.query.referendum.stage()).isOfType('Inactive'))
+    assert.equal(announcementPeriodNrEnding.toNumber(), announcementPeriodNrInit.toNumber() + 1)
+
+    // ensure council members haven't changed
+    const councilMembersEnding = await this.api.query.council.councilMembers()
+    assert.sameMembers(
+      councilMemberIds.map((item) => item.toString()),
+      councilMembersEnding.map((item) => item.membership_id.toString())
+    )
+
+    await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
+  }
+}

+ 67 - 0
tests/integration-tests/src/fixtures/council/common.ts

@@ -0,0 +1,67 @@
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
+import { FixtureRunner } from '../../Fixture'
+import { MemberId } from '@joystream/types/common'
+import { Balance } from '@polkadot/types/interfaces'
+
+interface IFailToElectResources {
+  candidatesMemberIds: MemberId[]
+  candidatesStakingAccounts: string[]
+  candidatesMemberAccounts: string[]
+  councilCandidateStake: Balance
+  councilMemberIds: MemberId[]
+}
+
+export async function assertCouncilMembersRuntimeQnMatch(api: Api, query: QueryNodeApi) {
+  const runtimeCouncilMembers = await api.query.council.councilMembers()
+
+  await query.tryQueryWithTimeout(
+    () => query.getCurrentCouncilMembers(),
+    (qnElectedCouncil) => {
+      assert.sameMembers(
+        (qnElectedCouncil?.councilMembers || []).map((item: any) => item.member.id.toString()),
+        runtimeCouncilMembers.map((item: any) => item.membership_id.toString())
+      )
+    }
+  )
+}
+
+export async function prepareFailToElectResources(api: Api, query: QueryNodeApi): Promise<IFailToElectResources> {
+  const { councilSize, minNumberOfExtraCandidates } = api.consts.council
+  const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
+
+  // prepare memberships
+  const candidatesMemberAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, candidatesMemberAccounts)
+  await new FixtureRunner(buyMembershipsFixture).run()
+  const candidatesMemberIds = buyMembershipsFixture.getCreatedMembers()
+
+  // prepare staking accounts
+  const councilCandidateStake = api.consts.council.minCandidateStake
+
+  const candidatesStakingAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
+    api,
+    query,
+    candidatesStakingAccounts.map((account, i) => ({
+      asMember: candidatesMemberIds[i],
+      account,
+      stakeAmount: councilCandidateStake,
+    }))
+  )
+  await new FixtureRunner(addStakingAccountsFixture).run()
+
+  // retrieve currently elected council's members
+  const councilMembers = await api.query.council.councilMembers()
+  const councilMemberIds = councilMembers.map((item) => item.membership_id)
+
+  return {
+    candidatesMemberIds,
+    candidatesStakingAccounts,
+    candidatesMemberAccounts,
+    councilCandidateStake,
+    councilMemberIds,
+  }
+}

+ 2 - 0
tests/integration-tests/src/fixtures/council/index.ts

@@ -1 +1,3 @@
 export { ElectCouncilFixture } from './ElectCouncilFixture'
+export { NotEnoughCandidatesFixture } from './NotEnoughCandidatesFixture'
+export { NotEnoughCandidatesWithVotesFixture } from './NotEnoughCandidatesWithVotesFixture'

+ 0 - 1
tests/integration-tests/src/fixtures/membership/UpdateProfileHappyCaseFixture.ts

@@ -35,7 +35,6 @@ export class UpdateProfileHappyCaseFixture extends BaseQueryNodeFixture {
     this.memberContext = memberContext
     this.oldValues = oldValues
     this.newValues = newValues
-    console.log({ oldValues, newValues })
   }
 
   private assertProfileUpdateSuccesful(qMember: MembershipFieldsFragment | null) {

+ 14 - 11
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -125,8 +125,8 @@ export class CreateProposalsFixture extends StandardizedFixture {
         Utils.assert(qProposal.details.__typename === 'CreateWorkingGroupLeadOpeningProposalDetails')
         const details = proposalDetails.asType('CreateWorkingGroupLeadOpening')
         assert.equal(qProposal.details.group?.id, getWorkingGroupModuleName(details.working_group))
-        assert.equal(qProposal.details.rewardPerBlock, details.reward_per_block.toString())
-        assert.equal(qProposal.details.stakeAmount, details.stake_policy.stake_amount.toString())
+        assert.equal(qProposal.details.rewardPerBlock.toString(), details.reward_per_block.toString())
+        assert.equal(qProposal.details.stakeAmount.toString(), details.stake_policy.stake_amount.toString())
         assert.equal(qProposal.details.unstakingPeriod, details.stake_policy.leaving_unstaking_period.toNumber())
         Utils.assert(qProposal.details.metadata)
         assertQueriedOpeningMetadataIsValid(
@@ -140,7 +140,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
         const details = proposalDetails.asType('DecreaseWorkingGroupLeadStake')
         const [workerId, amount, group] = details
         const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
-        assert.equal(qProposal.details.amount, amount.toString())
+        assert.equal(qProposal.details.amount.toString(), amount.toString())
         assert.equal(qProposal.details.lead?.id, expectedId)
         break
       }
@@ -200,19 +200,19 @@ export class CreateProposalsFixture extends StandardizedFixture {
       case 'SetCouncilBudgetIncrement': {
         Utils.assert(qProposal.details.__typename === 'SetCouncilBudgetIncrementProposalDetails')
         const details = proposalDetails.asType('SetCouncilBudgetIncrement')
-        assert.equal(qProposal.details.newAmount, details.toString())
+        assert.equal(qProposal.details.newAmount.toString(), details.toString())
         break
       }
       case 'SetCouncilorReward': {
         Utils.assert(qProposal.details.__typename === 'SetCouncilorRewardProposalDetails')
         const details = proposalDetails.asType('SetCouncilorReward')
-        assert.equal(qProposal.details.newRewardPerBlock, details.toString())
+        assert.equal(qProposal.details.newRewardPerBlock.toString(), details.toString())
         break
       }
       case 'SetInitialInvitationBalance': {
         Utils.assert(qProposal.details.__typename === 'SetInitialInvitationBalanceProposalDetails')
         const details = proposalDetails.asType('SetInitialInvitationBalance')
-        assert.equal(qProposal.details.newInitialInvitationBalance, details.toString())
+        assert.equal(qProposal.details.newInitialInvitationBalance.toString(), details.toString())
         break
       }
       case 'SetInitialInvitationCount': {
@@ -236,7 +236,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
       case 'SetMembershipPrice': {
         Utils.assert(qProposal.details.__typename === 'SetMembershipPriceProposalDetails')
         const details = proposalDetails.asType('SetMembershipPrice')
-        assert.equal(qProposal.details.newPrice, details.toString())
+        assert.equal(qProposal.details.newPrice.toString(), details.toString())
         break
       }
       case 'SetReferralCut': {
@@ -250,7 +250,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
         const details = proposalDetails.asType('SetWorkingGroupLeadReward')
         const [workerId, reward, group] = details
         const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
-        assert.equal(qProposal.details.newRewardPerBlock, reward.toString())
+        assert.equal(qProposal.details.newRewardPerBlock.toString(), reward.toString())
         assert.equal(qProposal.details.lead?.id, expectedId)
         break
       }
@@ -266,7 +266,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
         const [workerId, amount, group] = details
         const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
         assert.equal(qProposal.details.lead?.id, expectedId)
-        assert.equal(qProposal.details.amount, amount.toString())
+        assert.equal(qProposal.details.amount.toString(), amount.toString())
         break
       }
       case 'TerminateWorkingGroupLead': {
@@ -274,7 +274,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
         const details = proposalDetails.asType('TerminateWorkingGroupLead')
         const expectedId = `${getWorkingGroupModuleName(details.working_group)}-${details.worker_id.toString()}`
         assert.equal(qProposal.details.lead?.id, expectedId)
-        assert.equal(qProposal.details.slashingAmount, details.slashing_amount.toString())
+        assert.equal(qProposal.details.slashingAmount!.toString(), details.slashing_amount.toString())
         break
       }
       case 'UnlockBlogPost': {
@@ -287,7 +287,10 @@ export class CreateProposalsFixture extends StandardizedFixture {
         Utils.assert(qProposal.details.__typename === 'UpdateWorkingGroupBudgetProposalDetails')
         const details = proposalDetails.asType('UpdateWorkingGroupBudget')
         const [balance, group, balanceKind] = details
-        assert.equal(qProposal.details.amount, (balanceKind.isOfType('Negative') ? '-' : '') + balance.toString())
+        assert.equal(
+          qProposal.details.amount.toString(),
+          (balanceKind.isOfType('Negative') ? '-' : '') + balance.toString()
+        )
         assert.equal(qProposal.details.group?.id, getWorkingGroupModuleName(group))
         break
       }

+ 0 - 1
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -180,7 +180,6 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
         const isAccepted = acceptedApplicationsIds.some((id) => id.toNumber() === qApplication.runtimeId)
         if (isAccepted) {
           Utils.assert(qApplication.status.__typename === 'ApplicationStatusAccepted', 'Invalid application status')
-          console.log('qApplication.status', qApplication.status)
           // FIXME: Missing due to Hydra bug now
           // Utils.assert(qApplication.status.openingFilledEvent, 'Query node: Missing openingFilledEvent relation')
           // assert.equal(qApplication.status.openingFilledEvent.id, qEvent.id)

+ 20 - 0
tests/integration-tests/src/flows/council/failToElect.ts

@@ -0,0 +1,20 @@
+import { FlowProps } from '../../Flow'
+import { extendDebug } from '../../Debugger'
+import { FixtureRunner } from '../../Fixture'
+import { NotEnoughCandidatesFixture, NotEnoughCandidatesWithVotesFixture } from '../../fixtures/council'
+
+// Currently only used by Olympia flow
+
+export default async function failToElectCouncil({ api, query }: FlowProps): Promise<void> {
+  const debug = extendDebug('flow:fail-to-elect-council')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const notEnoughCandidatesFixture = new NotEnoughCandidatesFixture(api, query)
+  await new FixtureRunner(notEnoughCandidatesFixture).run()
+
+  const notEnoughCandidatesWithVotesFixture = new NotEnoughCandidatesWithVotesFixture(api, query)
+  await new FixtureRunner(notEnoughCandidatesWithVotesFixture).run()
+
+  debug('Done')
+}

+ 72 - 11
tests/integration-tests/src/graphql/generated/queries.ts

@@ -1,6 +1,23 @@
 import * as Types from './schema'
 
 import gql from 'graphql-tag'
+export type CouncilMemberFieldsFragment = { id: string; member: { id: string } }
+
+export type ElectedCouncilFieldsFragment = { councilMembers: Array<CouncilMemberFieldsFragment> }
+
+export type CandidateFieldsFragment = { id: string; member: { id: string } }
+
+export type GetCurrentCouncilMembersQueryVariables = Types.Exact<{ [key: string]: never }>
+
+export type GetCurrentCouncilMembersQuery = { electedCouncils: Array<ElectedCouncilFieldsFragment> }
+
+export type GetReferendumIntermediateWinnersQueryVariables = Types.Exact<{
+  electionRoundCycleId: Types.Scalars['Int']
+  councilSize: Types.Scalars['Int']
+}>
+
+export type GetReferendumIntermediateWinnersQuery = { candidates: Array<CandidateFieldsFragment> }
+
 export type ForumCategoryFieldsFragment = {
   id: string
   createdAt: any
@@ -838,9 +855,9 @@ type ProposalDetailsFields_SetMaxValidatorCountProposalDetails_Fragment = {
 
 type ProposalDetailsFields_CreateWorkingGroupLeadOpeningProposalDetails_Fragment = {
   __typename: 'CreateWorkingGroupLeadOpeningProposalDetails'
-  stakeAmount: any
+  stakeAmount: number
   unstakingPeriod: number
-  rewardPerBlock: any
+  rewardPerBlock: number
   metadata?: Types.Maybe<OpeningMetadataFieldsFragment>
   group?: Types.Maybe<{ id: string }>
 }
@@ -853,31 +870,31 @@ type ProposalDetailsFields_FillWorkingGroupLeadOpeningProposalDetails_Fragment =
 
 type ProposalDetailsFields_UpdateWorkingGroupBudgetProposalDetails_Fragment = {
   __typename: 'UpdateWorkingGroupBudgetProposalDetails'
-  amount: any
+  amount: number
   group?: Types.Maybe<{ id: string }>
 }
 
 type ProposalDetailsFields_DecreaseWorkingGroupLeadStakeProposalDetails_Fragment = {
   __typename: 'DecreaseWorkingGroupLeadStakeProposalDetails'
-  amount: any
+  amount: number
   lead?: Types.Maybe<{ id: string }>
 }
 
 type ProposalDetailsFields_SlashWorkingGroupLeadProposalDetails_Fragment = {
   __typename: 'SlashWorkingGroupLeadProposalDetails'
-  amount: any
+  amount: number
   lead?: Types.Maybe<{ id: string }>
 }
 
 type ProposalDetailsFields_SetWorkingGroupLeadRewardProposalDetails_Fragment = {
   __typename: 'SetWorkingGroupLeadRewardProposalDetails'
-  newRewardPerBlock: any
+  newRewardPerBlock: number
   lead?: Types.Maybe<{ id: string }>
 }
 
 type ProposalDetailsFields_TerminateWorkingGroupLeadProposalDetails_Fragment = {
   __typename: 'TerminateWorkingGroupLeadProposalDetails'
-  slashingAmount?: Types.Maybe<any>
+  slashingAmount?: Types.Maybe<number>
   lead?: Types.Maybe<{ id: string }>
 }
 
@@ -893,22 +910,22 @@ type ProposalDetailsFields_CancelWorkingGroupLeadOpeningProposalDetails_Fragment
 
 type ProposalDetailsFields_SetMembershipPriceProposalDetails_Fragment = {
   __typename: 'SetMembershipPriceProposalDetails'
-  newPrice: any
+  newPrice: number
 }
 
 type ProposalDetailsFields_SetCouncilBudgetIncrementProposalDetails_Fragment = {
   __typename: 'SetCouncilBudgetIncrementProposalDetails'
-  newAmount: any
+  newAmount: number
 }
 
 type ProposalDetailsFields_SetCouncilorRewardProposalDetails_Fragment = {
   __typename: 'SetCouncilorRewardProposalDetails'
-  newRewardPerBlock: any
+  newRewardPerBlock: number
 }
 
 type ProposalDetailsFields_SetInitialInvitationBalanceProposalDetails_Fragment = {
   __typename: 'SetInitialInvitationBalanceProposalDetails'
-  newInitialInvitationBalance: any
+  newInitialInvitationBalance: number
 }
 
 type ProposalDetailsFields_SetInitialInvitationCountProposalDetails_Fragment = {
@@ -1883,6 +1900,30 @@ export type GetBudgetSpendingEventsByEventIdsQueryVariables = Types.Exact<{
 
 export type GetBudgetSpendingEventsByEventIdsQuery = { budgetSpendingEvents: Array<BudgetSpendingEventFieldsFragment> }
 
+export const CouncilMemberFields = gql`
+  fragment CouncilMemberFields on CouncilMember {
+    id
+    member {
+      id
+    }
+  }
+`
+export const ElectedCouncilFields = gql`
+  fragment ElectedCouncilFields on ElectedCouncil {
+    councilMembers {
+      ...CouncilMemberFields
+    }
+  }
+  ${CouncilMemberFields}
+`
+export const CandidateFields = gql`
+  fragment CandidateFields on Candidate {
+    id
+    member {
+      id
+    }
+  }
+`
 export const ForumCategoryFields = gql`
   fragment ForumCategoryFields on ForumCategory {
     id
@@ -3616,6 +3657,26 @@ export const BudgetSpendingEventFields = gql`
     rationale
   }
 `
+export const GetCurrentCouncilMembers = gql`
+  query getCurrentCouncilMembers {
+    electedCouncils(where: { endedAtBlock_eq: null }) {
+      ...ElectedCouncilFields
+    }
+  }
+  ${ElectedCouncilFields}
+`
+export const GetReferendumIntermediateWinners = gql`
+  query getReferendumIntermediateWinners($electionRoundCycleId: Int!, $councilSize: Int!) {
+    candidates(
+      where: { electionRound: { cycleId_eq: $electionRoundCycleId }, votePower_gt: 0 }
+      orderBy: [votePower_DESC, lastVoteReceivedAtBlock_ASC, lastVoteReceivedAtEventNumber_ASC]
+      limit: $councilSize
+    ) {
+      ...CandidateFields
+    }
+  }
+  ${CandidateFields}
+`
 export const GetCategoriesByIds = gql`
   query getCategoriesByIds($ids: [ID!]) {
     forumCategories(where: { id_in: $ids }) {

File diff suppressed because it is too large
+ 299 - 426
tests/integration-tests/src/graphql/generated/schema.ts


+ 35 - 0
tests/integration-tests/src/graphql/queries/council.graphql

@@ -0,0 +1,35 @@
+fragment CouncilMemberFields on CouncilMember {
+  id
+  member {
+    id
+  }
+}
+
+fragment ElectedCouncilFields on ElectedCouncil {
+  councilMembers {
+    ...CouncilMemberFields
+  }
+}
+
+fragment CandidateFields on Candidate {
+  id
+  member {
+    id
+  }
+}
+
+query getCurrentCouncilMembers {
+  electedCouncils(where: { endedAtBlock_eq: null }) {
+    ...ElectedCouncilFields
+  }
+}
+
+query getReferendumIntermediateWinners($electionRoundCycleId: Int!, $councilSize: Int!) {
+  candidates(
+    where: { electionRound: { cycleId_eq: $electionRoundCycleId }, votePower_gt: 0 }
+    orderBy: [votePower_DESC, lastVoteReceivedAtBlock_ASC, lastVoteReceivedAtEventNumber_ASC]
+    limit: $councilSize
+  ) {
+    ...CandidateFields
+  }
+}

+ 10 - 0
tests/integration-tests/src/scenarios/council.ts

@@ -0,0 +1,10 @@
+import electCouncil from '../flows/council/elect'
+import failToElectCouncil from '../flows/council/failToElect'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  const councilJob = job('electing council', electCouncil)
+  const secondCouncilJob = job('electing second council', electCouncil).requires(councilJob)
+
+  job('council election failures', failToElectCouncil).requires(secondCouncilJob)
+})

+ 5 - 0
tests/integration-tests/src/scenarios/full.ts

@@ -21,6 +21,7 @@ import proposals from '../flows/proposals'
 import cancellingProposals from '../flows/proposals/cancellingProposal'
 import vetoProposal from '../flows/proposals/vetoProposal'
 import electCouncil from '../flows/council/elect'
+import failToElect from '../flows/council/failToElect'
 import runtimeUpgradeProposal from '../flows/proposals/runtimeUpgradeProposal'
 import exactExecutionBlock from '../flows/proposals/exactExecutionBlock'
 import expireProposal from '../flows/proposals/expireProposal'
@@ -75,4 +76,8 @@ scenario(async ({ job, env }) => {
   job('forum polls', polls).requires(sudoHireLead)
   job('forum posts', posts).requires(sudoHireLead)
   job('forum moderation', moderation).requires(sudoHireLead)
+
+  // Council
+  const secondCouncilJob = job('electing second council', electCouncil).requires(membershipSystemJob)
+  job('council election failures', failToElect).requires(secondCouncilJob)
 })

+ 2 - 17
yarn.lock

@@ -418,7 +418,7 @@
   dependencies:
     "@babel/types" "^7.16.0"
 
-"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0":
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.15.4", "@babel/helper-module-imports@^7.16.0":
   version "7.16.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3"
   integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==
@@ -432,13 +432,6 @@
   dependencies:
     "@babel/types" "^7.14.5"
 
-"@babel/helper-module-imports@^7.15.4":
-  version "7.15.4"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f"
-  integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==
-  dependencies:
-    "@babel/types" "^7.15.4"
-
 "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.16.0":
   version "7.16.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5"
@@ -1086,14 +1079,6 @@
     "@babel/helper-validator-identifier" "^7.14.9"
     to-fast-properties "^2.0.0"
 
-"@babel/types@^7.15.4":
-  version "7.15.4"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.4.tgz#74eeb86dbd6748d2741396557b9860e57fce0a0d"
-  integrity sha512-0f1HJFuGmmbrKTCZtbm3cU+b/AqdEYk5toj5iQur58xkVMlS0JWaKxTBSmCXd47uiN7vbcozAupm6Mvs80GNhw==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.14.9"
-    to-fast-properties "^2.0.0"
-
 "@cnakazawa/watch@^1.0.3":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@@ -1796,7 +1781,7 @@
     yaml "^1.10.0"
     yaml-validator "^3.0.0"
 
-"@joystream/warthog@2.39.0", "@joystream/warthog@2.40.0":
+"@joystream/warthog@2.39.0", "@joystream/warthog@^2.40.0":
   version "2.39.0"
   resolved "https://registry.yarnpkg.com/@joystream/warthog/-/warthog-2.39.0.tgz#3587b94953aed929bff809a7ba763d495e03170c"
   integrity sha512-gwZ8oBqcN7Xez8BfBDeDIyMhZ7VcL2paMuj0n3qOplyH+sxsBwgBemDzV6RThmAGi3GOhVVQJqOMq3w6siWqzA==

Some files were not shown because too many files changed in this diff