Browse Source

Merge branch 'olympia' into olympia-readme

Leszek Wiesner 3 years ago
parent
commit
7454c98be0
68 changed files with 4016 additions and 1156 deletions
  1. 2 0
      .github/workflows/integration-tests.yml
  2. 1 1
      .github/workflows/joystream-node-benchmarks.yml
  3. 1 1
      README.md
  4. 3 3
      devops/git-hooks/pre-push
  5. 4 4
      joystream-node.Dockerfile
  6. 3 3
      node/README.md
  7. 2 1
      package.json
  8. 5 2
      query-node/codegen/package.json
  9. 609 226
      query-node/codegen/yarn.lock
  10. 75 1
      query-node/manifest.yml
  11. 10 0
      query-node/mappings/.eslintrc.js
  12. 0 35
      query-node/mappings/genesis.ts
  13. 8 6
      query-node/mappings/package.json
  14. 23 0
      query-node/mappings/scripts/postHydraCLIInstall.ts
  15. 45 0
      query-node/mappings/scripts/postInstall.ts
  16. 19 0
      query-node/mappings/scripts/utils.ts
  17. 2 2
      query-node/mappings/src/common.ts
  18. 2 2
      query-node/mappings/src/content/channel.ts
  19. 2 2
      query-node/mappings/src/content/curatorGroup.ts
  20. 0 0
      query-node/mappings/src/content/index.ts
  21. 1 1
      query-node/mappings/src/content/utils.ts
  22. 2 2
      query-node/mappings/src/content/video.ts
  23. 950 0
      query-node/mappings/src/council.ts
  24. 2 2
      query-node/mappings/src/forum.ts
  25. 0 0
      query-node/mappings/src/genesis-data/index.ts
  26. 0 0
      query-node/mappings/src/genesis-data/members.json
  27. 0 0
      query-node/mappings/src/genesis-data/membershipSystem.json
  28. 0 0
      query-node/mappings/src/genesis-data/workers.json
  29. 0 0
      query-node/mappings/src/genesis-data/workingGroups.json
  30. 67 0
      query-node/mappings/src/genesis.ts
  31. 1 0
      query-node/mappings/src/index.ts
  32. 6 2
      query-node/mappings/src/membership.ts
  33. 2 2
      query-node/mappings/src/proposals.ts
  34. 2 2
      query-node/mappings/src/proposalsDiscussion.ts
  35. 2 2
      query-node/mappings/src/storage.ts
  36. 2 2
      query-node/mappings/src/workingGroups.ts
  37. 1 1
      query-node/mappings/tsconfig.json
  38. 2 2
      query-node/package.json
  39. 266 0
      query-node/schemas/council.graphql
  40. 563 0
      query-node/schemas/councilEvents.graphql
  41. 14 0
      query-node/schemas/membership.graphql
  42. 0 155
      query-node/scripts/initializeDefaultSchemas.ts
  43. 38 12
      runtime-modules/council/src/lib.rs
  44. 52 1
      runtime-modules/council/src/tests.rs
  45. 9 9
      runtime-modules/proposals/engine/src/tests/mod.rs
  46. 2 2
      scripts/cargo-build.sh
  47. 2 2
      scripts/cargo-tests-with-networking.sh
  48. 1 1
      scripts/raspberry-cross-build.sh
  49. 4 4
      scripts/run-dev-chain.sh
  50. 1 0
      scripts/runtime-code-shasum.sh
  51. 2 4
      setup.sh
  52. 9 1
      tests/integration-tests/src/Api.ts
  53. 33 0
      tests/integration-tests/src/QueryNodeApi.ts
  54. 17 1
      tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts
  55. 57 0
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts
  56. 82 0
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts
  57. 67 0
      tests/integration-tests/src/fixtures/council/common.ts
  58. 2 0
      tests/integration-tests/src/fixtures/council/index.ts
  59. 0 1
      tests/integration-tests/src/fixtures/membership/UpdateProfileHappyCaseFixture.ts
  60. 14 11
      tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts
  61. 0 1
      tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts
  62. 20 0
      tests/integration-tests/src/flows/council/failToElect.ts
  63. 72 11
      tests/integration-tests/src/graphql/generated/queries.ts
  64. 299 426
      tests/integration-tests/src/graphql/generated/schema.ts
  65. 35 0
      tests/integration-tests/src/graphql/queries/council.graphql
  66. 10 0
      tests/integration-tests/src/scenarios/council.ts
  67. 5 0
      tests/integration-tests/src/scenarios/full.ts
  68. 484 207
      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

+ 1 - 1
.github/workflows/joystream-node-benchmarks.yml

@@ -35,7 +35,7 @@ jobs:
       - name: Build
         run: |
           pushd node
-          WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo build --release --features runtime-benchmarks
+          WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 build --release --features runtime-benchmarks
           popd
         if: env.GIT_DIFF
 

+ 1 - 1
README.md

@@ -89,7 +89,7 @@ You can also run your our own joystream-node:
 
 ```sh
 git checkout master
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 build --release
 ./target/release/joystream-node -- --pruning archive --chain testnets/joy-testnet-5.json
 ```
 

+ 3 - 3
devops/git-hooks/pre-push

@@ -1,13 +1,13 @@
 #!/bin/sh
 set -e
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 echo 'running clippy (rust linter)'
 # When custom build.rs triggers wasm-build-runner-impl to build we get error:
 # "Rust WASM toolchain not installed, please install it!"
 # So we skip building the WASM binary by setting BUILD_DUMMY_WASM_BINARY=1
-BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
+BUILD_DUMMY_WASM_BINARY=1 cargo +nightly-2021-02-20 clippy --release --all -- -D warnings
 
 echo 'running cargo unit tests'
-cargo test --release --all
+cargo +nightly-2021-02-20 test --release --all

+ 4 - 4
joystream-node.Dockerfile

@@ -1,7 +1,7 @@
 FROM liuchong/rustup:nightly AS rustup
-RUN rustup install nightly-2021-03-24
-RUN rustup default nightly-2021-03-24
-RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-03-24
+RUN rustup install nightly-2021-02-20
+RUN rustup default nightly-2021-02-20
+RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2021-02-20
 RUN apt-get update && \
   apt-get install -y curl git gcc xz-utils sudo pkg-config unzip clang llvm libc6-dev
 
@@ -12,7 +12,7 @@ COPY . /joystream
 
 # Build all cargo crates
 # Ensure our tests and linter pass before actual build
-ENV WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+ENV WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 ARG TEST_NODE
 RUN echo "TEST_NODE=$TEST_NODE"
 RUN test -n "$TEST_NODE" && sed -i 's/MILLISECS_PER_BLOCK: Moment = 6000/MILLISECS_PER_BLOCK: Moment = 1000/' ./runtime/src/constants.rs; exit 0

+ 3 - 3
node/README.md

@@ -26,7 +26,7 @@ cd joystream/
 Compile the node and runtime:
 
 ```bash
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 build --release
 ```
 
 This produces the binary in `./target/release/joystream-node`
@@ -57,7 +57,7 @@ Use the `--chain` argument, and specify the path to the genesis `chain.json` fil
 Running unit tests:
 
 ```bash
-cargo test --release --all
+cargo +nightly-2021-02-20 test --release --all
 ```
 
 Running full suite of checks, tests, formatting and linting:
@@ -79,7 +79,7 @@ If you are building a tagged release from `master` branch and want to install th
 This will install the executable `joystream-node` to your `~/.cargo/bin` folder, which you would normally have in your `$PATH` environment.
 
 ```bash
-WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 cargo install joystream-node --path node/ --locked
+WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 cargo +nightly-2021-02-20 install joystream-node --path node/ --locked
 ```
 
 Now you can run and connect to the testnet:

+ 2 - 1
package.json

@@ -39,7 +39,8 @@
     "typescript": "^4.3.5",
     "bn.js": "^5.1.2",
     "rxjs": "^7.2.0",
-    "typeorm": "^0.2.31",
+    "typeorm": "0.2.34",
+    "@joystream/warthog": "2.39.0",
     "pg": "^8.4.0",
     "chalk": "^4.0.0"
   },

+ 5 - 2
query-node/codegen/package.json

@@ -4,8 +4,11 @@
   "description": "Hydra codegen tools for Joystream Query Node",
   "author": "",
   "license": "ISC",
+  "scripts": {
+    "postinstall": "cd .. && yarn workspace query-node-mappings postHydraCLIInstall"
+  },
   "dependencies": {
-    "@dzlzv/hydra-cli": "3.1.0-alpha.0",
-    "@dzlzv/hydra-typegen": "3.1.0-alpha.0"
+    "@joystream/hydra-cli": "3.1.0-alpha.13",
+    "@joystream/hydra-typegen": "3.1.0-alpha.13"
   }
 }

File diff suppressed because it is too large
+ 609 - 226
query-node/codegen/yarn.lock


+ 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 '@dzlzv/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
-}

+ 8 - 6
query-node/mappings/package.json

@@ -2,18 +2,20 @@
   "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\"",
-    "clean": "rm -rf lib"
+    "lint": "eslint ./src --ext .ts",
+    "clean": "rm -rf lib",
+    "postinstall": "yarn ts-node ./scripts/postInstall.ts",
+    "postHydraCLIInstall": "yarn ts-node ./scripts/postHydraCLIInstall.ts"
   },
   "dependencies": {
-    "@dzlzv/hydra-common": "3.1.0-alpha.0",
-    "@dzlzv/hydra-db-utils": "3.1.0-alpha.0",
+    "@joystream/hydra-common": "3.1.0-alpha.13",
+    "@joystream/hydra-db-utils": "3.1.0-alpha.13",
     "@joystream/types": "^0.17.0",
-    "warthog": "https://github.com/metmirr/warthog/releases/download/v2.30.0/warthog-v2.30.0.tgz",
+    "@joystream/warthog": "2.39.0",
     "@joystream/metadata-protobuf": "^1.0.0",
     "iso-639-1": "^2.1.8"
   },

+ 23 - 0
query-node/mappings/scripts/postHydraCLIInstall.ts

@@ -0,0 +1,23 @@
+// A script to be executed post hydra-cli install, that may include patches for Hydra CLI
+import path from 'path'
+import { replaceInFile } from './utils'
+
+// FIXME: Temporary fix for missing JOIN and HAVING conditions in search queries (Hydra)
+const searchServiceTemplatePath = path.resolve(
+  __dirname,
+  '../../codegen/node_modules/@joystream/hydra-cli/lib/src/templates/textsearch/service.ts.mst'
+)
+
+replaceInFile({
+  filePath: searchServiceTemplatePath,
+  regex: /queries = queries\.concat\(generateSqlQuery\(repositories\[index\]\.metadata\.tableName, WHERE\)\);/,
+  newContent:
+    'queries = queries.concat(generateSqlQuery(repositories[index].metadata.tableName, qb.createJoinExpression(), WHERE, qb.createHavingExpression()));',
+})
+
+replaceInFile({
+  filePath: searchServiceTemplatePath,
+  regex: /const generateSqlQuery =[\s\S]+\+ where;/,
+  newContent: `const generateSqlQuery = (table: string, joins: string, where: string, having: string) =>
+  \`SELECT '\${table}_' || "\${table}"."id" AS unique_id FROM "\${table}" \` + joins + ' ' + where + ' ' + having;`,
+})

+ 45 - 0
query-node/mappings/scripts/postInstall.ts

@@ -0,0 +1,45 @@
+// A script to be executed post query-node install, that may include workarounds in Hydra node_modules
+import path from 'path'
+import { replaceInFile } from './utils'
+
+// FIXME: Temporarly remove broken sanitizeNullCharacter call
+const subscribersJsPath = path.resolve(
+  __dirname,
+  '../../../node_modules/@joystream/hydra-processor/lib/db/subscribers.js'
+)
+replaceInFile({
+  filePath: subscribersJsPath,
+  regex: /sanitizeNullCharacter\(entity, field\);/g,
+  newContent: '//sanitizeNullCharacter(entity, field)',
+})
+
+// FIXME: Temporarly replace broken relations resolution in @joystream/warthog
+const dataLoaderJsPath = path.resolve(
+  __dirname,
+  '../../../node_modules/@joystream/warthog/dist/middleware/DataLoaderMiddleware.js'
+)
+replaceInFile({
+  filePath: dataLoaderJsPath,
+  regex: /return context\.connection\.relationIdLoader[\s\S]+return group\.related;\s+\}\);\s+\}\)/,
+  newContent: `return Promise.all(
+    entities.map(entity => context.connection.relationLoader.load(relation, entity))
+  ).then(function (results) {
+    return results.map(function (related) {
+      return (relation.isManyToOne || relation.isOneToOne) ? related[0] : related
+    })
+  })`,
+})
+
+// FIXME: Temporary fix for "table name x specified more than once"
+const baseServiceJsPath = path.resolve(__dirname, '../../../node_modules/@joystream/warthog/dist/core/BaseService.js')
+replaceInFile({
+  filePath: baseServiceJsPath,
+  regex: /function common\(parameters, localIdColumn, foreignTableName, foreignColumnMap, foreignColumnName\) \{[^}]+\}/,
+  newContent: `function common(parameters, localIdColumn, foreignTableName, foreignColumnMap, foreignColumnName) {
+    const uuid = require('uuid/v4')
+    const foreignTableAlias = uuid().replace('-', '')
+    var foreingIdColumn = "\\"" + foreignTableAlias + "\\".\\"" + foreignColumnMap[foreignColumnName] + "\\"";
+    parameters.topLevelQb.leftJoin(foreignTableName, foreignTableAlias, localIdColumn + " = " + foreingIdColumn);
+    addWhereCondition(parameters, foreignTableAlias, foreignColumnMap);
+  }`,
+})

+ 19 - 0
query-node/mappings/scripts/utils.ts

@@ -0,0 +1,19 @@
+import fs from 'fs'
+import { blake2AsHex } from '@polkadot/util-crypto'
+
+type ReplaceLinesInFileParams = {
+  filePath: string
+  regex: RegExp
+  newContent: string
+}
+
+export function replaceInFile({ filePath, regex, newContent }: ReplaceLinesInFileParams): void {
+  const paramsHash = blake2AsHex(filePath + '|' + regex.source + '|' + newContent)
+  const startMark = `/* BEGIN REPLACED CONTENT ${paramsHash} */`
+  const endMark = `/* END REPLACED CONTENT ${paramsHash} */`
+  const fileContent = fs.readFileSync(filePath).toString()
+  if (fileContent.includes(startMark)) {
+    return
+  }
+  fs.writeFileSync(filePath, fileContent.replace(regex, `${startMark}\n${newContent}\n${endMark}`))
+}

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

@@ -5,11 +5,11 @@ import {
   ExtrinsicArg,
   EventContext,
   StoreContext,
-} from '@dzlzv/hydra-common'
+} from '@joystream/hydra-common'
 import { Bytes } from '@polkadot/types'
 import { WorkingGroup, WorkerId, ThreadId, ContentParameters } from '@joystream/types/augment/all'
 import { Worker, Event, Network, DataObject, LiaisonJudgement, DataObjectOwner } from 'query-node/dist/model'
-import { BaseModel } from 'warthog'
+import { BaseModel } from '@joystream/warthog'
 import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
 import { registry } from '@joystream/types'
 import { metaToObject } from '@joystream/metadata-protobuf/utils'

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

@@ -1,11 +1,11 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext } from '@dzlzv/hydra-common'
+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'

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

@@ -1,10 +1,10 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext } from '@dzlzv/hydra-common'
+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


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

@@ -6,7 +6,7 @@
 //         every time query node codegen is run (that will overwrite said manual changes)
 //       - verify in integration tests that the records are trully created/updated/removed as expected
 
-import { DatabaseManager, EventContext, StoreContext } from '@dzlzv/hydra-common'
+import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
 import ISO6391 from 'iso-639-1'
 import { FindConditions } from 'typeorm'
 import {

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

@@ -1,9 +1,9 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext } from '@dzlzv/hydra-common'
+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)
+}

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

@@ -1,7 +1,7 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
+import { EventContext, StoreContext, DatabaseManager } from '@joystream/hydra-common'
 import {
   bytesToString,
   deserializeMetadata,
@@ -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'

+ 6 - 2
query-node/mappings/membership.ts → query-node/mappings/src/membership.ts

@@ -1,8 +1,8 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
-import { Members } from './generated/types'
+import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@joystream/hydra-common'
+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)

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

@@ -1,7 +1,7 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { SubstrateEvent, DatabaseManager, EventContext, StoreContext } from '@dzlzv/hydra-common'
+import { SubstrateEvent, DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
 import { ProposalDetails as RuntimeProposalDetails } from '@joystream/types/augment/all'
 import BN from 'bn.js'
 import {
@@ -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'

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

@@ -1,7 +1,7 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
+import { EventContext, StoreContext, DatabaseManager } from '@joystream/hydra-common'
 import {
   Membership,
   ProposalDiscussionPostStatusActive,
@@ -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'
 

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

@@ -1,7 +1,7 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager } from '@dzlzv/hydra-common'
+import { EventContext, StoreContext, DatabaseManager } from '@joystream/hydra-common'
 import { FindConditions, In, Raw } from 'typeorm'
 import {
   createDataObject,
@@ -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'

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

@@ -1,9 +1,9 @@
 /*
 eslint-disable @typescript-eslint/naming-convention
 */
-import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
+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/**/*"]
 }

+ 2 - 2
query-node/package.json

@@ -7,7 +7,7 @@
     "start": "./start.sh",
     "kill": "./kill.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",
@@ -41,7 +41,7 @@
     "tslib": "^2.0.0",
     "@types/bn.js": "^4.11.6",
     "bn.js": "^5.1.2",
-    "@dzlzv/hydra-processor": "3.1.0-alpha.0",
+    "@joystream/hydra-processor": "3.1.0-alpha.13",
     "envsub": "4.0.7"
   },
   "volta": {

+ 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 {

+ 0 - 155
query-node/scripts/initializeDefaultSchemas.ts

@@ -1,155 +0,0 @@
-/* eslint-disable import/first */
-import 'reflect-metadata'
-
-import { loadConfig } from '../generated/graphql-server/src/config'
-loadConfig()
-
-import BN from 'bn.js'
-import { nanoid } from 'nanoid'
-import { SnakeNamingStrategy } from '@dzlzv/hydra-db-utils'
-import { createConnection, ConnectionOptions, getConnection, EntityManager } from 'typeorm'
-
-import { Video } from '../generated/graphql-server/src/modules/video/video.model'
-import { Channel } from '../generated/graphql-server/src/modules/channel/channel.model'
-import { Block, Network } from '../generated/graphql-server/src/modules/block/block.model'
-import { Category } from '../generated/graphql-server/src/modules/category/category.model'
-import { VideoMedia } from '../generated/graphql-server/src/modules/video-media/video-media.model'
-import { LicenseEntity } from '../generated/graphql-server/src/modules/license-entity/license-entity.model'
-import { JoystreamMediaLocation, KnownLicense } from '../generated/graphql-server/src/modules/variants/variants.model'
-import { KnownLicenseEntity } from '../generated/graphql-server/src/modules/known-license-entity/known-license-entity.model'
-import { VideoMediaEncoding } from '../generated/graphql-server/src/modules/video-media-encoding/video-media-encoding.model'
-import { MediaLocationEntity } from '../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
-import { HttpMediaLocationEntity } from '../generated/graphql-server/src/modules/http-media-location-entity/http-media-location-entity.model'
-import { JoystreamMediaLocationEntity } from '../generated/graphql-server/src/modules/joystream-media-location-entity/joystream-media-location-entity.model'
-
-function getConnectionOptions() {
-  const connectionConfig: ConnectionOptions = {
-    type: 'postgres',
-    host: process.env.WARTHOG_DB_HOST,
-    port: parseInt(process.env.WARTHOG_DB_PORT!),
-    username: process.env.WARTHOG_DB_USERNAME,
-    password: process.env.WARTHOG_DB_PASSWORD,
-    database: process.env.WARTHOG_DB_DATABASE,
-    entities: [process.env.WARTHOG_DB_ENTITIES!],
-    namingStrategy: new SnakeNamingStrategy(),
-    logging: true,
-  }
-  return connectionConfig
-}
-
-export async function main(): Promise<void> {
-  console.log(`Initializing...`)
-  await createConnection(getConnectionOptions())
-  await getConnection().transaction(async (db: EntityManager) => {
-    const id = '0'
-    const createdAt = new Date()
-    const createdById = '0'
-    const version = 0
-
-    // ///////// Block /////////////////
-    const happenedIn = new Block({
-      createdAt,
-      createdById,
-      version,
-      block: 0,
-      timestamp: new BN(Date.now()),
-      network: Network.BABYLON,
-    })
-    await db.save<Block>(happenedIn)
-    // ///////// Block /////////////////
-
-    const commonProperties = { id, happenedIn, createdAt, createdById, version }
-
-    // ///////// HttpMediaLocationEntity /////////////////
-    const httpMediaLocation = new HttpMediaLocationEntity({
-      ...commonProperties,
-      url: '5FyzfM2YtZa75hHYCAo5evNS8bH8P4Kw8EyXqKkC5upVSDBQ',
-    })
-    await db.save<HttpMediaLocationEntity>(httpMediaLocation)
-    // ///////// HttpMediaLocationEntity /////////////////
-
-    // ///////// JoystreamMediaLocationEntity /////////////////
-    const joyMediaLocation = new JoystreamMediaLocationEntity({
-      ...commonProperties,
-      dataObjectId: '5FyzfM2YtZa75hHYCAo5evNS8bH8P4Kw8EyXqKkC5upVSDBQ',
-    })
-    await db.save<JoystreamMediaLocationEntity>(joyMediaLocation)
-    // ///////// JoystreamMediaLocationEntity /////////////////
-
-    // ///////// KnownLicenseEntity /////////////////
-    const knownLicense = new KnownLicenseEntity({ ...commonProperties, code: 'NA' })
-    await db.save<KnownLicenseEntity>(knownLicense)
-    // ///////// KnownLicenseEntity /////////////////
-
-    // ///////// License /////////////////
-    const k = new KnownLicense()
-    k.code = knownLicense.code
-    const license = new LicenseEntity({ ...commonProperties, type: k })
-    await db.save<LicenseEntity>(license)
-    // ///////// License /////////////////
-
-    // ///////// MediaLocationEntity /////////////////
-    const mediaLocEntity = new MediaLocationEntity({ ...commonProperties, joystreamMediaLocation: joyMediaLocation })
-    await db.save<MediaLocationEntity>(mediaLocEntity)
-    // ///////// MediaLocationEntity /////////////////
-
-    // ///////// Channel /////////////////
-    const channel = new Channel({
-      ...commonProperties,
-      handle: `Channel(0) - ${nanoid()}`,
-      description: `Channel 0`,
-      isPublic: false,
-      isCurated: false,
-    })
-    await db.save<Channel>(channel)
-    // ///////// Channel /////////////////
-
-    // ///////// Category /////////////////
-    const category = new Category({ ...commonProperties, name: `Other` })
-    await db.save<Category>(category)
-    // ///////// Category /////////////////
-
-    // ///////// VideoMediaEncoding /////////////////
-    const videoMediaEncod = new VideoMediaEncoding({ ...commonProperties, name: 'NA' })
-    await db.save<VideoMediaEncoding>(videoMediaEncod)
-    // ///////// VideoMediaEncoding /////////////////
-
-    // ///////// VideoMedia /////////////////
-    const location = new JoystreamMediaLocation()
-    location.dataObjectId = joyMediaLocation.dataObjectId
-    const videoMedia = new VideoMedia({
-      ...commonProperties,
-      location,
-      locationEntity: mediaLocEntity,
-      encoding: videoMediaEncod,
-      pixelHeight: 0,
-      pixelWidth: 0,
-    })
-    await db.save<VideoMedia>(videoMedia)
-    // ///////// VideoMedia /////////////////
-
-    // ///////// Video /////////////////
-    const v = new Video({ ...commonProperties })
-    v.category = category
-    v.channel = channel
-    v.media = videoMedia
-    v.license = license
-    v.title = `Video(0)`
-    v.description = `Video(0)`
-    v.duration = 0
-    v.thumbnailUrl = 'https://eu-central-1.linodeobjects.com/joystream/1.png'
-    v.isPublic = false
-    v.isCurated = false
-    v.isExplicit = false
-    v.isFeatured = false
-    await db.save<Video>(v)
-    // ///////// Video /////////////////
-  })
-}
-
-main()
-  .then(() => {
-    console.log(`Done.`)
-    process.exit()
-  })
-  .catch(console.log)

+ 38 - 12
runtime-modules/council/src/lib.rs

@@ -631,7 +631,7 @@ decl_module! {
         /// # </weight>
         #[weight = CouncilWeightInfo::<T>::withdraw_candidacy()]
         pub fn withdraw_candidacy(origin, membership_id: T::MemberId) -> Result<(), Error<T>> {
-            let staking_account_id =
+            let (stage_data, candidate) =
                 EnsureChecks::<T>::can_withdraw_candidacy(origin, &membership_id)?;
 
             //
@@ -639,7 +639,7 @@ decl_module! {
             //
 
             // update state
-            Mutations::<T>::release_candidacy_stake(&membership_id, &staking_account_id);
+            Mutations::<T>::withdraw_candidacy(&stage_data, &membership_id, &candidate);
 
             // emit event
             Self::deposit_event(RawEvent::CandidacyWithdraw(membership_id));
@@ -814,7 +814,7 @@ decl_module! {
             let funding_total: Balance<T> =
                 funding_requests.iter().fold(
                     Zero::zero(),
-                    |accumulated, funding_request| accumulated + funding_request.amount,
+                    |accumulated, funding_request| accumulated.saturating_add(funding_request.amount),
                 );
 
             let current_budget = Self::budget();
@@ -846,7 +846,7 @@ decl_module! {
             // == MUTATION SAFE ==
             //
 
-            Mutations::<T>::set_budget(current_budget - funding_total);
+            Mutations::<T>::set_budget(current_budget.saturating_sub(funding_total));
 
             for funding_request in funding_requests {
                 let amount = funding_request.amount;
@@ -902,10 +902,10 @@ impl<T: Trait> Module<T> {
 
     // Finish voting and start ravealing.
     fn end_announcement_period(stage_data: CouncilStageAnnouncing) {
-        let candidate_count = T::CouncilSize::get() + T::MinNumberOfExtraCandidates::get();
+        let min_candidate_count = T::CouncilSize::get() + T::MinNumberOfExtraCandidates::get();
 
         // reset announcing period when not enough candidates registered
-        if stage_data.candidates_count < candidate_count {
+        if stage_data.candidates_count < min_candidate_count {
             Mutations::<T>::start_announcing_period();
 
             // emit event
@@ -1051,7 +1051,7 @@ impl<T: Trait> Module<T> {
                 ));
 
                 // return new balance
-                balance - available_balance
+                balance.saturating_sub(available_balance)
             },
         );
 
@@ -1322,7 +1322,7 @@ impl<T: Trait> Mutations<T> {
             candidates_count: stage_data.candidates_count + 1,
         };
 
-        // store new candidacy list
+        // store new stage
         Stage::<T>::mutate(|value| {
             *value = CouncilStageUpdate {
                 stage: CouncilStage::Announcing(new_stage_data),
@@ -1336,6 +1336,30 @@ impl<T: Trait> Mutations<T> {
         T::CandidacyLock::lock(&candidate.staking_account_id, *stake);
     }
 
+    fn withdraw_candidacy(
+        stage_data: &CouncilStageAnnouncing,
+        membership_id: &T::MemberId,
+        candidate: &CandidateOf<T>,
+    ) {
+        // release candidacy stake
+        Self::release_candidacy_stake(&membership_id, &candidate.staking_account_id);
+
+        // prepare new stage
+        let new_stage_data = CouncilStageAnnouncing {
+            candidates_count: stage_data.candidates_count.saturating_sub(1),
+        };
+
+        // store new stage
+        Stage::<T>::mutate(|value| {
+            *value = CouncilStageUpdate {
+                stage: CouncilStage::Announcing(new_stage_data),
+
+                // keep changed_at (and other values) - stage phase haven't changed
+                ..*value
+            }
+        });
+    }
+
     // Release user's stake that was used for candidacy.
     fn release_candidacy_stake(membership_id: &T::MemberId, account_id: &T::AccountId) {
         // release stake amount
@@ -1525,7 +1549,7 @@ impl<T: Trait> EnsureChecks<T> {
     fn can_withdraw_candidacy(
         origin: T::Origin,
         membership_id: &T::MemberId,
-    ) -> Result<T::AccountId, Error<T>> {
+    ) -> Result<(CouncilStageAnnouncing, CandidateOf<T>), Error<T>> {
         // ensure user's membership
         Self::ensure_user_membership(origin, membership_id)?;
 
@@ -1537,17 +1561,19 @@ impl<T: Trait> EnsureChecks<T> {
         let candidate = Candidates::<T>::get(membership_id);
 
         // ensure candidacy announcing period is running now
-        match Stage::<T>::get().stage {
-            CouncilStage::Announcing(_) => {
+        let stage_data = match Stage::<T>::get().stage {
+            CouncilStage::Announcing(stage_data) => {
                 // ensure candidacy was announced in current election cycle
                 if candidate.cycle_id != AnnouncementPeriodNr::get() {
                     return Err(Error::NotCandidatingNow);
                 }
+
+                stage_data
             }
             _ => return Err(Error::CantWithdrawCandidacyNow),
         };
 
-        Ok(candidate.staking_account_id)
+        Ok((stage_data, candidate))
     }
 
     // Ensures there is no problem in setting new note for the candidacy.

+ 52 - 1
runtime-modules/council/src/tests.rs

@@ -244,7 +244,8 @@ fn council_candidacy_release_candidate_stake() {
     });
 }
 
-// Test that only valid members can candidate.
+// Test that the announcement period is reset in case that not enough candidates
+// to fill the council has announced their candidacy.
 #[test]
 fn council_announcement_reset_on_insufficient_candidates() {
     let config = default_genesis_config();
@@ -288,6 +289,56 @@ fn council_announcement_reset_on_insufficient_candidates() {
     });
 }
 
+// Test that the announcement period is reset in case that not enough candidates
+// to fill the council has announced and not withdrawn their candidacy.
+#[test]
+fn council_announcement_reset_on_insufficient_candidates_after_candidacy_withdrawal() {
+    let config = default_genesis_config();
+
+    build_test_externalities(config).execute_with(|| {
+        let council_settings = CouncilSettings::<Runtime>::extract_settings();
+
+        // generate candidates
+        let candidates: Vec<CandidateInfo<Runtime>> = (0..council_settings.min_candidate_count)
+            .map(|i| {
+                MockUtils::generate_candidate(u64::from(i), council_settings.min_candidate_stake)
+            })
+            .collect();
+
+        let params = CouncilCycleParams {
+            council_settings: council_settings.clone(),
+            cycle_start_block_number: 0,
+            expected_initial_council_members: vec![],
+            expected_final_council_members: vec![], // not needed in this scenario
+            candidates_announcing: candidates.clone(),
+            expected_candidates: vec![], // not needed in this scenario
+            voters: vec![],              // not needed in this scenario
+
+            // escape before voting
+            interrupt_point: Some(CouncilCycleInterrupt::AfterCandidatesAnnounce),
+        };
+
+        Mocks::simulate_council_cycle(params.clone());
+
+        Mocks::withdraw_candidacy(
+            candidates[0].origin.clone(),
+            candidates[0].account_id.clone(),
+            Ok(()),
+        );
+
+        // forward to election-voting period
+        MockUtils::increase_block_number(council_settings.announcing_stage_duration + 1);
+
+        // check announcements were reset
+        Mocks::check_announcing_period(
+            params.cycle_start_block_number + council_settings.announcing_stage_duration,
+            CouncilStageAnnouncing {
+                candidates_count: 0,
+            },
+        );
+    });
+}
+
 // Test that announcement phase is reset when not enough candidates to fill council recieved votes
 #[test]
 fn council_announcement_reset_on_not_enough_winners() {

+ 9 - 9
runtime-modules/proposals/engine/src/tests/mod.rs

@@ -890,9 +890,10 @@ fn veto_proposal_event_emitted() {
         let veto_proposal = VetoProposalFixture::new(proposal_id);
         veto_proposal.veto_and_assert(Ok(()));
 
-        EventFixture::assert_events(vec![
-            RawEvent::ProposalDecisionMade(proposal_id, ProposalDecision::Vetoed),
-        ]);
+        EventFixture::assert_events(vec![RawEvent::ProposalDecisionMade(
+            proposal_id,
+            ProposalDecision::Vetoed,
+        )]);
     });
 }
 
@@ -930,9 +931,7 @@ fn vote_proposal_event_emitted() {
         let mut vote_generator = VoteGenerator::new(proposal_id);
         vote_generator.vote_and_assert_ok(VoteKind::Approve);
 
-        EventFixture::assert_events(vec![
-            RawEvent::Voted(1, 1, VoteKind::Approve, Vec::new()),
-        ]);
+        EventFixture::assert_events(vec![RawEvent::Voted(1, 1, VoteKind::Approve, Vec::new())]);
     });
 }
 
@@ -957,9 +956,10 @@ fn create_proposal_and_expire_it() {
         run_to_block_and_finalize(expected_expriration_block);
 
         assert!(!<crate::Proposals<Test>>::contains_key(proposal_id));
-        EventFixture::assert_events(vec![
-            RawEvent::ProposalDecisionMade(proposal_id, ProposalDecision::Expired),
-        ]);
+        EventFixture::assert_events(vec![RawEvent::ProposalDecisionMade(
+            proposal_id,
+            ProposalDecision::Expired,
+        )]);
     });
 }
 

+ 2 - 2
scripts/cargo-build.sh

@@ -1,5 +1,5 @@
 #!/usr/bin/env bash
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
-cargo build --release
+cargo +nightly-2021-02-20 build --release

+ 2 - 2
scripts/cargo-tests-with-networking.sh

@@ -1,7 +1,7 @@
 #!/bin/sh
 set -e
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 echo 'running all cargo tests'
-cargo test --release --all -- --ignored
+cargo +nightly-2021-02-20 test --release --all -- --ignored

+ 1 - 1
scripts/raspberry-cross-build.sh

@@ -9,7 +9,7 @@
 export WORKSPACE_ROOT=`cargo metadata --offline --no-deps --format-version 1 | jq .workspace_root -r`
 
 docker run \
-    -e WASM_BUILD_TOOLCHAIN=nightly-2021-03-24 \
+    -e WASM_BUILD_TOOLCHAIN=nightly-2021-02-20 \
     --volume ${WORKSPACE_ROOT}/:/home/cross/project \
     --volume ${HOME}/.cargo/registry:/home/cross/.cargo/registry \
     joystream/rust-raspberry \

+ 4 - 4
scripts/run-dev-chain.sh

@@ -1,13 +1,13 @@
 #!/usr/bin/env bash
 
-export WASM_BUILD_TOOLCHAIN=nightly-2021-03-24
+export WASM_BUILD_TOOLCHAIN=nightly-2021-02-20
 
 # Build release binary
-cargo build --release
+cargo +nightly-2021-02-20 build --release
 
 # Purge existing local chain
-yes | cargo run --release -- purge-chain --dev
+yes | cargo +nightly-2021-02-20 run --release -- purge-chain --dev
 
 # Run local development chain -
 # No need to specify `-p joystream-node` it is the default bin crate in the cargo workspace
-cargo run --release -- --dev --log runtime
+cargo +nightly-2021-02-20 run --release -- --dev --log runtime

+ 1 - 0
scripts/runtime-code-shasum.sh

@@ -23,6 +23,7 @@ ${TAR} -c --sort=name --owner=root:0 --group=root:0 --mode 644 --mtime='UTC 2020
     runtime-modules \
     utils/chain-spec-builder \
     joystream-node.Dockerfile \
+    node \
     $(test -n "$TEST_NODE" && echo "$TEST_PROPOSALS_PARAMETERS_PATH") \
     | if [[ -n "$TEST_NODE" ]]; then sed '$a'"$TEST_NODE_BLOCKTIME"; else tee; fi \
     | shasum \

+ 2 - 4
setup.sh

@@ -25,10 +25,8 @@ curl https://getsubstrate.io -sSf | bash -s -- --fast
 
 source ~/.cargo/env
 
-rustup install nightly-2021-03-24
-rustup target add wasm32-unknown-unknown --toolchain nightly-2021-03-24
-
-rustup default nightly-2021-03-24
+rustup install nightly-2021-02-20
+rustup target add wasm32-unknown-unknown --toolchain nightly-2021-02-20
 
 rustup component add rustfmt clippy
 

+ 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)
 })

File diff suppressed because it is too large
+ 484 - 207
yarn.lock


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