Selaa lähdekoodia

Proposals initial mappings, tests & types fixes

Leszek Wiesner 3 vuotta sitten
vanhempi
commit
3bef28370b
50 muutettua tiedostoa jossa 6020 lisäystä ja 803 poistoa
  1. 6 1
      .github/workflows/run-integration-tests.yml
  2. 0 47
      joystream-node.Dockerfile
  3. 40 0
      query-node/manifest.yml
  4. 26 0
      query-node/mappings/common.ts
  5. 2 0
      query-node/mappings/index.ts
  6. 424 0
      query-node/mappings/proposals.ts
  7. 24 0
      query-node/mappings/proposalsDiscussion.ts
  8. 4 5
      query-node/mappings/workingGroups.ts
  9. 8 18
      query-node/schemas/proposals.graphql
  10. 1 0
      query-node/schemas/proposalsEvents.graphql
  11. 2 1
      scripts/runtime-code-shasum.sh
  12. 104 0
      tests/integration-tests/proposal-parameters.json
  13. 93 35
      tests/integration-tests/src/Api.ts
  14. 12 8
      tests/integration-tests/src/Fixture.ts
  15. 29 14
      tests/integration-tests/src/QueryNodeApi.ts
  16. 15 0
      tests/integration-tests/src/consts.ts
  17. 89 0
      tests/integration-tests/src/fixtures/council/InitializeCouncilFixture.ts
  18. 1 0
      tests/integration-tests/src/fixtures/council/index.ts
  19. 72 74
      tests/integration-tests/src/fixtures/membership/AddStakingAccountsHappyCaseFixture.ts
  20. 351 0
      tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts
  21. 1 0
      tests/integration-tests/src/fixtures/proposals/index.ts
  22. 0 40
      tests/integration-tests/src/fixtures/workingGroups/BaseCreateOpeningFixture.ts
  23. 4 3
      tests/integration-tests/src/fixtures/workingGroups/CreateOpeningsFixture.ts
  24. 4 3
      tests/integration-tests/src/fixtures/workingGroups/CreateUpcomingOpeningsFixture.ts
  25. 9 13
      tests/integration-tests/src/fixtures/workingGroups/HireWorkersFixture.ts
  26. 2 3
      tests/integration-tests/src/fixtures/workingGroups/UpdateGroupStatusFixture.ts
  27. 38 1
      tests/integration-tests/src/fixtures/workingGroups/utils.ts
  28. 1 2
      tests/integration-tests/src/flows/membership/managingStakingAccounts.ts
  29. 106 0
      tests/integration-tests/src/flows/proposals/index.ts
  30. 5 4
      tests/integration-tests/src/flows/working-groups/leadOpening.ts
  31. 9 13
      tests/integration-tests/src/flows/working-groups/openingsAndApplications.ts
  32. 777 32
      tests/integration-tests/src/graphql/generated/queries.ts
  33. 2931 81
      tests/integration-tests/src/graphql/generated/schema.ts
  34. 4 4
      tests/integration-tests/src/graphql/queries/membershipEvents.graphql
  35. 215 0
      tests/integration-tests/src/graphql/queries/proposals.graphql
  36. 124 0
      tests/integration-tests/src/graphql/queries/proposalsEvents.graphql
  37. 1 0
      tests/integration-tests/src/scenarios/full.ts
  38. 6 0
      tests/integration-tests/src/scenarios/proposals.ts
  39. 7 2
      tests/integration-tests/src/sender.ts
  40. 21 0
      tests/integration-tests/src/types.ts
  41. 25 0
      tests/integration-tests/src/utils.ts
  42. 29 29
      types/augment-codec/augment-api-query.ts
  43. 157 157
      types/augment-codec/augment-api-tx.ts
  44. 5 4
      types/augment/all/defs.json
  45. 5 5
      types/augment/all/types.ts
  46. 29 29
      types/augment/augment-api-query.ts
  47. 157 157
      types/augment/augment-api-tx.ts
  48. 2 2
      types/src/council/index.ts
  49. 10 6
      types/src/index.ts
  50. 33 10
      types/src/proposals.ts

+ 6 - 1
.github/workflows/run-integration-tests.yml

@@ -29,6 +29,8 @@ jobs:
 
       - id: compute_shasum
         name: Compute runtime code shasum
+        env:
+          PROPOSALS_PARAMETERS_PATH: ./tests/integration-tests/proposal-parameters.json
         run: |
           export RUNTIME_CODE_SHASUM=`scripts/runtime-code-shasum.sh`
           echo "::set-output name=shasum::${RUNTIME_CODE_SHASUM}"
@@ -66,7 +68,10 @@ jobs:
       - name: Build new joystream/node image
         run: |
           if ! [ -f joystream-node-docker-image.tar.gz ]; then
-            docker build . --file joystream-node.Dockerfile --tag joystream/node
+            docker build .\
+              --file joystream-node.Dockerfile\
+              --tag joystream/node\
+              --build-arg PROPOSALS_PARAMETERS="$(cat ./tests/integration-tests/proposal-parameters.json)"
             docker save --output joystream-node-docker-image.tar joystream/node
             gzip joystream-node-docker-image.tar
             cp joystream-node-docker-image.tar.gz ~/docker-images/

+ 0 - 47
joystream-node.Dockerfile

@@ -1,47 +0,0 @@
-FROM liuchong/rustup:1.47.0 AS rustup
-RUN rustup component add rustfmt clippy
-RUN rustup install nightly-2020-10-06 --force
-RUN rustup target add wasm32-unknown-unknown --toolchain nightly-2020-10-06
-RUN apt-get update && \
-  apt-get install -y curl git gcc xz-utils sudo pkg-config unzip clang libc6-dev-i386
-
-FROM rustup AS builder
-LABEL description="Compiles all workspace artifacts"
-WORKDIR /joystream
-COPY . /joystream
-
-# Build all cargo crates
-# Ensure our tests and linter pass before actual build
-ENV WASM_BUILD_TOOLCHAIN=nightly-2020-10-06
-RUN BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings && \
-    cargo test --release --all && \
-    cargo build --release
-
-FROM debian:stretch
-LABEL description="Joystream node"
-WORKDIR /joystream
-COPY --from=builder /joystream/target/release/joystream-node /joystream/node
-COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm /joystream/runtime.compact.wasm
-COPY --from=builder /joystream/target/release/chain-spec-builder /joystream/chain-spec-builder
-
-# confirm it works
-RUN /joystream/node --version
-
-# https://manpages.debian.org/stretch/coreutils/b2sum.1.en.html
-# RUN apt-get install coreutils
-# print the blake2 256 hash of the wasm blob
-RUN b2sum -l 256 /joystream/runtime.compact.wasm
-# print the blake2 512 hash of the wasm blob
-RUN b2sum -l 512 /joystream/runtime.compact.wasm
-
-EXPOSE 30333 9933 9944
-
-# Use these volumes to persits chain state and keystore, eg.:
-# --base-path /data
-# optionally separate keystore (otherwise it will be stored in the base path)
-# --keystore-path /keystore
-# if base-path isn't specified, chain state is stored inside container in ~/.local/share/joystream-node/
-# which is not ideal
-VOLUME ["/data", "/keystore"]
-
-ENTRYPOINT ["/joystream/node"]

+ 40 - 0
query-node/manifest.yml

@@ -49,6 +49,20 @@ typegen:
     - storageWorkingGroup.BudgetSpending
     - storageWorkingGroup.RewardPaid
     - storageWorkingGroup.NewMissedRewardLevelReached
+    # Proposals
+    - proposalsCodex.ProposalCreated
+    - proposalsEngine.ProposalCreated
+    - proposalsEngine.ProposalStatusUpdated
+    - proposalsEngine.ProposalDecisionMade
+    - proposalsEngine.ProposalExecuted
+    - proposalsEngine.Voted
+    - proposalsEngine.ProposalCancelled
+    # Proposals discussion
+    - proposalsDiscussion.ThreadCreated
+    - proposalsDiscussion.PostCreated
+    - proposalsDiscussion.PostUpdated
+    - proposalsDiscussion.ThreadModeChanged
+    - proposalsDiscussion.PostDeleted
   calls:
     - members.updateProfile
     - members.updateAccounts
@@ -276,6 +290,32 @@ mappings:
       handler: workingGroups_NewMissedRewardLevelReached(DatabaseManager, SubstrateEvent)
     - event: contentDirectoryWorkingGroup.WorkerStartedLeaving
       handler: workingGroups_WorkerStartedLeaving(DatabaseManager, SubstrateEvent)
+    # Proposals
+    - event: proposalsCodex.ProposalCreated
+      handler: proposalsCodex_ProposalCreated(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.ProposalCreated
+      handler: proposalsEngine_ProposalCreated(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.ProposalStatusUpdated
+      handler: proposalsEngine_ProposalStatusUpdated(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.ProposalDecisionMade
+      handler: proposalsEngine_ProposalDecisionMade(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.ProposalExecuted
+      handler: proposalsEngine_ProposalExecuted(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.Voted
+      handler: proposalsEngine_Voted(DatabaseManager, SubstrateEvent)
+    - event: proposalsEngine.ProposalCancelled
+      handler: proposalsEngine_ProposalCancelled(DatabaseManager, SubstrateEvent)
+    # Proposals discussion
+    - event: proposalsDiscussion.ThreadCreated
+      handler: proposalsDiscussion_ThreadCreated(DatabaseManager, SubstrateEvent)
+    - event: proposalsDiscussion.PostCreated
+      handler: proposalsDiscussion_PostCreated(DatabaseManager, SubstrateEvent)
+    - event: proposalsDiscussion.PostUpdated
+      handler: proposalsDiscussion_PostUpdated(DatabaseManager, SubstrateEvent)
+    - event: proposalsDiscussion.ThreadModeChanged
+      handler: proposalsDiscussion_ThreadModeChanged(DatabaseManager, SubstrateEvent)
+    - event: proposalsDiscussion.PostDeleted
+      handler: proposalsDiscussion_PostDeleted(DatabaseManager, SubstrateEvent)
   extrinsicHandlers:
     # infer defaults here
     #- extrinsic: Balances.Transfer

+ 26 - 0
query-node/mappings/common.ts

@@ -2,6 +2,7 @@ import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { Network } from 'query-node/dist/src/modules/enums/enums'
 import { Event } from 'query-node/dist/src/modules/event/event.model'
 import { Bytes } from '@polkadot/types'
+import { WorkingGroup } from '@joystream/types/augment/all'
 
 export const CURRENT_NETWORK = Network.OLYMPIA
 
@@ -44,6 +45,11 @@ export function bytesToString(b: Bytes): string {
   return Buffer.from(b.toU8a(true)).toString()
 }
 
+export function perpareString(s: string): string {
+  // eslint-disable-next-line no-control-regex
+  return s.replace(/\u0000/g, '')
+}
+
 export function hasValuesForProperties<
   T extends Record<string, unknown>,
   P extends keyof T & string,
@@ -56,3 +62,23 @@ export function hasValuesForProperties<
   })
   return true
 }
+
+export type WorkingGroupModuleName =
+  | 'storageWorkingGroup'
+  | 'contentDirectoryWorkingGroup'
+  | 'forumWorkingGroup'
+  | 'membershipWorkingGroup'
+
+export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
+  if (group.isContent) {
+    return 'contentDirectoryWorkingGroup'
+  } else if (group.isMembership) {
+    return 'membershipWorkingGroup'
+  } else if (group.isForum) {
+    return 'forumWorkingGroup'
+  } else if (group.isStorage) {
+    return 'storageWorkingGroup'
+  }
+
+  throw new Error(`Unsupported working group: ${group.type}`)
+}

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

@@ -1,2 +1,4 @@
 export * from './membership'
 export * from './workingGroups'
+export * from './proposals'
+export * from './proposalsDiscussion'

+ 424 - 0
query-node/mappings/proposals.ts

@@ -0,0 +1,424 @@
+/*
+eslint-disable @typescript-eslint/naming-convention
+*/
+import { SubstrateEvent, DatabaseManager } from '@dzlzv/hydra-common'
+import { ProposalDetails as RuntimeProposalDetails, ProposalId } from '@joystream/types/augment/all'
+import BN from 'bn.js'
+import {
+  Proposal,
+  SignalProposalDetails,
+  RuntimeUpgradeProposalDetails,
+  FundingRequestProposalDetails,
+  SetMaxValidatorCountProposalDetails,
+  CreateWorkingGroupLeadOpeningProposalDetails,
+  FillWorkingGroupLeadOpeningProposalDetails,
+  UpdateWorkingGroupBudgetProposalDetails,
+  DecreaseWorkingGroupLeadStakeProposalDetails,
+  SlashWorkingGroupLeadProposalDetails,
+  SetWorkingGroupLeadRewardProposalDetails,
+  TerminateWorkingGroupLeadProposalDetails,
+  AmendConstitutionProposalDetails,
+  CancelWorkingGroupLeadOpeningProposalDetails,
+  SetMembershipPriceProposalDetails,
+  SetCouncilBudgetIncrementProposalDetails,
+  SetCouncilorRewardProposalDetails,
+  SetInitialInvitationBalanceProposalDetails,
+  SetInitialInvitationCountProposalDetails,
+  SetMembershipLeadInvitationQuotaProposalDetails,
+  SetReferralCutProposalDetails,
+  CreateBlogPostProposalDetails,
+  EditBlogPostProposalDetails,
+  LockBlogPostProposalDetails,
+  UnlockBlogPostProposalDetails,
+  VetoProposalDetails,
+  ProposalDetails,
+  FundingRequestDestinationsList,
+  FundingRequestDestination,
+  Membership,
+  ProposalStatusDeciding,
+  ProposalIntermediateStatus,
+  ProposalStatusDormant,
+  ProposalStatusGracing,
+  ProposalStatusUpdatedEvent,
+  ProposalDecisionStatus,
+  ProposalStatusCancelled,
+  ProposalStatusExpired,
+  ProposalStatusRejected,
+  ProposalStatusSlashed,
+  ProposalStatusVetoed,
+  ProposalDecisionMadeEvent,
+  ProposalStatusCanceledByRuntime,
+} from 'query-node/dist/model'
+import { genericEventFields, getWorkingGroupModuleName, perpareString } from './common'
+import { ProposalsEngine, ProposalsCodex } from './generated/types'
+import { createWorkingGroupOpeningMetadata } from './workingGroups'
+
+// FIXME: https://github.com/Joystream/joystream/issues/2457
+type ProposalsMappingsMemoryCache = {
+  lastCreatedProposalId: ProposalId | null
+}
+const proposalsMappingsMemoryCache: ProposalsMappingsMemoryCache = {
+  lastCreatedProposalId: null,
+}
+
+async function getProposal(db: DatabaseManager, id: string) {
+  const proposal = await db.get(Proposal, { where: { id } })
+  if (!proposal) {
+    throw new Error(`Proposal not found by id: ${id}`)
+  }
+
+  return proposal
+}
+
+async function parseProposalDetails(
+  event_: SubstrateEvent,
+  db: DatabaseManager,
+  proposalDetails: RuntimeProposalDetails
+): Promise<typeof ProposalDetails> {
+  const eventTime = new Date(event_.blockTimestamp)
+
+  // SignalProposalDetails:
+  if (proposalDetails.isSignal) {
+    const details = new SignalProposalDetails()
+    const specificDetails = proposalDetails.asSignal
+    details.text = perpareString(specificDetails.toString())
+    return details
+  }
+  // RuntimeUpgradeProposalDetails:
+  else if (proposalDetails.isRuntimeUpgrade) {
+    const details = new RuntimeUpgradeProposalDetails()
+    const specificDetails = proposalDetails.asRuntimeUpgrade
+    details.wasmBytecode = Buffer.from(specificDetails.toU8a(true))
+    return details
+  }
+  // FundingRequestProposalDetails:
+  else if (proposalDetails.isFundingRequest) {
+    const destinationsList = new FundingRequestDestinationsList()
+    const specificDetails = proposalDetails.asFundingRequest
+    await db.save<FundingRequestDestinationsList>(destinationsList)
+    await Promise.all(
+      specificDetails.map(({ account, amount }) =>
+        db.save(
+          new FundingRequestDestination({
+            createdAt: eventTime,
+            updatedAt: eventTime,
+            account: account.toString(),
+            amount: new BN(amount.toString()),
+            list: destinationsList,
+          })
+        )
+      )
+    )
+    const details = new FundingRequestProposalDetails()
+    details.destinationsListId = destinationsList.id
+    return details
+  }
+  // SetMaxValidatorCountProposalDetails:
+  else if (proposalDetails.isSetMaxValidatorCount) {
+    const details = new SetMaxValidatorCountProposalDetails()
+    const specificDetails = proposalDetails.asSetMaxValidatorCount
+    details.newMaxValidatorCount = specificDetails.toNumber()
+    return details
+  }
+  // CreateWorkingGroupLeadOpeningProposalDetails:
+  else if (proposalDetails.isCreateWorkingGroupLeadOpening) {
+    const details = new CreateWorkingGroupLeadOpeningProposalDetails()
+    const specificDetails = proposalDetails.asCreateWorkingGroupLeadOpening
+    const metadata = await createWorkingGroupOpeningMetadata(db, eventTime, specificDetails.description)
+    details.groupId = getWorkingGroupModuleName(specificDetails.working_group)
+    details.metadataId = metadata.id
+    details.rewardPerBlock = new BN(specificDetails.reward_per_block.unwrapOr(0).toString())
+    details.stakeAmount = new BN(specificDetails.stake_policy.stake_amount.toString())
+    details.unstakingPeriod = specificDetails.stake_policy.leaving_unstaking_period.toNumber()
+    return details
+  }
+  // FillWorkingGroupLeadOpeningProposalDetails:
+  else if (proposalDetails.isFillWorkingGroupLeadOpening) {
+    const details = new FillWorkingGroupLeadOpeningProposalDetails()
+    const specificDetails = proposalDetails.asFillWorkingGroupLeadOpening
+    const groupModuleName = getWorkingGroupModuleName(specificDetails.working_group)
+    details.openingId = `${groupModuleName}-${specificDetails.opening_id.toString()}`
+    details.applicationId = `${groupModuleName}-${specificDetails.successful_application_id.toString()}`
+    return details
+  }
+  // UpdateWorkingGroupBudgetProposalDetails:
+  else if (proposalDetails.isUpdateWorkingGroupBudget) {
+    const details = new UpdateWorkingGroupBudgetProposalDetails()
+    const specificDetails = proposalDetails.asUpdateWorkingGroupBudget
+    const [amount, workingGroup, balanceKind] = specificDetails
+    details.groupId = getWorkingGroupModuleName(workingGroup)
+    details.amount = amount.muln(balanceKind.isNegative ? -1 : 1)
+    return details
+  }
+  // DecreaseWorkingGroupLeadStakeProposalDetails:
+  else if (proposalDetails.isDecreaseWorkingGroupLeadStake) {
+    const details = new DecreaseWorkingGroupLeadStakeProposalDetails()
+    const specificDetails = proposalDetails.asDecreaseWorkingGroupLeadStake
+    const [workerId, amount, workingGroup] = specificDetails
+    details.amount = new BN(amount.toString())
+    details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
+    return details
+  }
+  // SlashWorkingGroupLeadProposalDetails:
+  else if (proposalDetails.isSlashWorkingGroupLead) {
+    const details = new SlashWorkingGroupLeadProposalDetails()
+    const specificDetails = proposalDetails.asSlashWorkingGroupLead
+    const [workerId, amount, workingGroup] = specificDetails
+    details.amount = new BN(amount.toString())
+    details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
+    return details
+  }
+  // SetWorkingGroupLeadRewardProposalDetails:
+  else if (proposalDetails.isSetWorkingGroupLeadReward) {
+    const details = new SetWorkingGroupLeadRewardProposalDetails()
+    const specificDetails = proposalDetails.asSetWorkingGroupLeadReward
+    const [workerId, reward, workingGroup] = specificDetails
+    details.newRewardPerBlock = new BN(reward.unwrapOr(0).toString())
+    details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
+    return details
+  }
+  // TerminateWorkingGroupLeadProposalDetails:
+  else if (proposalDetails.isTerminateWorkingGroupLead) {
+    const details = new TerminateWorkingGroupLeadProposalDetails()
+    const specificDetails = proposalDetails.asTerminateWorkingGroupLead
+    details.leadId = `${getWorkingGroupModuleName(
+      specificDetails.working_group
+    )}-${specificDetails.worker_id.toString()}`
+    details.slashingAmount = specificDetails.slashing_amount.isSome
+      ? new BN(specificDetails.slashing_amount.unwrap().toString())
+      : undefined
+    return details
+  }
+  // AmendConstitutionProposalDetails:
+  else if (proposalDetails.isAmendConstitution) {
+    const details = new AmendConstitutionProposalDetails()
+    const specificDetails = proposalDetails.asAmendConstitution
+    details.text = perpareString(specificDetails.toString())
+    return details
+  }
+  // CancelWorkingGroupLeadOpeningProposalDetails:
+  else if (proposalDetails.isCancelWorkingGroupLeadOpening) {
+    const details = new CancelWorkingGroupLeadOpeningProposalDetails()
+    const [openingId, workingGroup] = proposalDetails.asCancelWorkingGroupLeadOpening
+    details.openingId = `${getWorkingGroupModuleName(workingGroup)}-${openingId.toString()}`
+    return details
+  }
+  // SetCouncilBudgetIncrementProposalDetails:
+  else if (proposalDetails.isSetCouncilBudgetIncrement) {
+    const details = new SetCouncilBudgetIncrementProposalDetails()
+    const specificDetails = proposalDetails.asSetCouncilBudgetIncrement
+    details.newAmount = new BN(specificDetails.toString())
+    return details
+  }
+  // SetMembershipPriceProposalDetails:
+  else if (proposalDetails.isSetMembershipPrice) {
+    const details = new SetMembershipPriceProposalDetails()
+    const specificDetails = proposalDetails.asSetMembershipPrice
+    details.newPrice = new BN(specificDetails.toString())
+    return details
+  }
+  // SetCouncilorRewardProposalDetails:
+  else if (proposalDetails.isSetCouncilorReward) {
+    const details = new SetCouncilorRewardProposalDetails()
+    const specificDetails = proposalDetails.asSetCouncilorReward
+    details.newRewardPerBlock = new BN(specificDetails.toString())
+    return details
+  }
+  // SetInitialInvitationBalanceProposalDetails:
+  else if (proposalDetails.isSetInitialInvitationBalance) {
+    const details = new SetInitialInvitationBalanceProposalDetails()
+    const specificDetails = proposalDetails.asSetInitialInvitationBalance
+    details.newInitialInvitationBalance = new BN(specificDetails.toString())
+    return details
+  }
+  // SetInitialInvitationCountProposalDetails:
+  else if (proposalDetails.isSetInitialInvitationCount) {
+    const details = new SetInitialInvitationCountProposalDetails()
+    const specificDetails = proposalDetails.asSetInitialInvitationCount
+    details.newInitialInvitationsCount = specificDetails.toNumber()
+    return details
+  }
+  // SetMembershipLeadInvitationQuotaProposalDetails:
+  else if (proposalDetails.isSetMembershipLeadInvitationQuota) {
+    const details = new SetMembershipLeadInvitationQuotaProposalDetails()
+    const specificDetails = proposalDetails.asSetMembershipLeadInvitationQuota
+    details.newLeadInvitationQuota = specificDetails.toNumber()
+    return details
+  }
+  // SetReferralCutProposalDetails:
+  else if (proposalDetails.isSetReferralCut) {
+    const details = new SetReferralCutProposalDetails()
+    const specificDetails = proposalDetails.asSetReferralCut
+    details.newReferralCut = specificDetails.toNumber()
+    return details
+  }
+  // CreateBlogPostProposalDetails:
+  else if (proposalDetails.isCreateBlogPost) {
+    const details = new CreateBlogPostProposalDetails()
+    const specificDetails = proposalDetails.asCreateBlogPost
+    // TODO:
+  }
+  // EditBlogPostProposalDetails:
+  else if (proposalDetails.isEditBlogPost) {
+    const details = new EditBlogPostProposalDetails()
+    const specificDetails = proposalDetails.asEditBlogPost
+    // TODO:
+  }
+  // LockBlogPostProposalDetails:
+  else if (proposalDetails.isLockBlogPost) {
+    const details = new LockBlogPostProposalDetails()
+    const specificDetails = proposalDetails.asLockBlogPost
+    // TODO:
+  }
+  // UnlockBlogPostProposalDetails:
+  else if (proposalDetails.isUnlockBlogPost) {
+    const details = new UnlockBlogPostProposalDetails()
+    const specificDetails = proposalDetails.asUnlockBlogPost
+    // TODO:
+  }
+  // VetoProposalDetails:
+  else if (proposalDetails.isVetoProposal) {
+    const details = new VetoProposalDetails()
+    const specificDetails = proposalDetails.asVetoProposal
+    details.proposalId = specificDetails.toString()
+    return details
+  }
+
+  throw new Error(`Unspported proposal details type: ${proposalDetails.type}`)
+}
+
+export async function proposalsEngine_ProposalCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const [, proposalId] = new ProposalsEngine.ProposalCreatedEvent(event_).params
+
+  // Cache the id
+  proposalsMappingsMemoryCache.lastCreatedProposalId = proposalId
+}
+
+export async function proposalsCodex_ProposalCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const [generalProposalParameters, runtimeProposalDetails] = new ProposalsCodex.ProposalCreatedEvent(event_).params
+  const eventTime = new Date(event_.blockTimestamp)
+  const proposalDetails = await parseProposalDetails(event_, db, runtimeProposalDetails)
+
+  if (!proposalsMappingsMemoryCache.lastCreatedProposalId) {
+    throw new Error('Unexpected state: proposalsMappingsMemoryCache.lastCreatedProposalId is empty')
+  }
+
+  const proposal = new Proposal({
+    id: proposalsMappingsMemoryCache.lastCreatedProposalId.toString(),
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    details: proposalDetails,
+    councilApprovals: 0,
+    creator: new Membership({ id: generalProposalParameters.member_id.toString() }),
+    title: perpareString(generalProposalParameters.title.toString()),
+    description: perpareString(generalProposalParameters.description.toString()),
+    exactExecutionBlock: generalProposalParameters.exact_execution_block.unwrapOr(undefined)?.toNumber(),
+    stakingAccount: generalProposalParameters.staking_account_id.toString(),
+    status: new ProposalStatusDeciding(),
+    statusSetAtBlock: event_.blockNumber,
+    statusSetAtTime: eventTime,
+  })
+  await db.save<Proposal>(proposal)
+}
+
+export async function proposalsEngine_ProposalStatusUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  const [proposalId, status] = new ProposalsEngine.ProposalStatusUpdatedEvent(event_).params
+  const proposal = await getProposal(db, proposalId.toString())
+  const eventTime = new Date(event_.blockTimestamp)
+
+  let newStatus: typeof ProposalIntermediateStatus
+  if (status.isActive) {
+    newStatus = new ProposalStatusDeciding()
+  } else if (status.isPendingConstitutionality) {
+    newStatus = new ProposalStatusDormant()
+  } else if (status.isPendingExecution) {
+    newStatus = new ProposalStatusGracing()
+  } else {
+    throw new Error(`Unexpected proposal status: ${status.type}`)
+  }
+
+  const proposalStatusUpdatedEvent = new ProposalStatusUpdatedEvent({
+    ...genericEventFields(event_),
+    newStatus,
+    proposal,
+  })
+  await db.save<ProposalStatusUpdatedEvent>(proposalStatusUpdatedEvent)
+
+  newStatus.proposalStatusUpdatedEventId = proposalStatusUpdatedEvent.id
+  proposal.updatedAt = eventTime
+  proposal.status = newStatus
+
+  await db.save<Proposal>(proposal)
+}
+
+export async function proposalsEngine_ProposalDecisionMade(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  const [proposalId, decision] = new ProposalsEngine.ProposalDecisionMadeEvent(event_).params
+  const proposal = await getProposal(db, proposalId.toString())
+  const eventTime = new Date(event_.blockTimestamp)
+
+  let decisionStatus: typeof ProposalDecisionStatus
+  if (decision.isApproved) {
+    if (decision.asApproved.isPendingConstitutionality) {
+      decisionStatus = new ProposalStatusDormant()
+    } else {
+      decisionStatus = new ProposalStatusGracing()
+    }
+  } else if (decision.isCanceled) {
+    decisionStatus = new ProposalStatusCancelled()
+  } else if (decision.isCanceledByRuntime) {
+    decisionStatus = new ProposalStatusCanceledByRuntime()
+  } else if (decision.isExpired) {
+    decisionStatus = new ProposalStatusExpired()
+  } else if (decision.isRejected) {
+    decisionStatus = new ProposalStatusRejected()
+  } else if (decision.isSlashed) {
+    decisionStatus = new ProposalStatusSlashed()
+  } else if (decision.isVetoed) {
+    decisionStatus = new ProposalStatusVetoed()
+  } else {
+    throw new Error(`Unexpected proposal decision: ${decision.type}`)
+  }
+
+  const proposalDecisionMadeEvent = new ProposalDecisionMadeEvent({
+    ...genericEventFields(event_),
+    decisionStatus,
+    proposal,
+  })
+  await db.save<ProposalDecisionMadeEvent>(proposalDecisionMadeEvent)
+
+  // We don't handle Cancelled, Dormant and Gracing statuses here, since they emit separate events
+  if (
+    [
+      'ProposalStatusCanceledByRuntime',
+      'ProposalStatusExpired',
+      'ProposalStatusRejected',
+      'ProposalStatusSlashed',
+      'ProposalStatusVetoed',
+    ].includes(decisionStatus.isTypeOf)
+  ) {
+    ;(decisionStatus as
+      | ProposalStatusCanceledByRuntime
+      | ProposalStatusExpired
+      | ProposalStatusRejected
+      | ProposalStatusSlashed
+      | ProposalStatusVetoed).proposalDecisionMadeEventId = proposalDecisionMadeEvent.id
+    proposal.status = decisionStatus
+    proposal.updatedAt = eventTime
+    await db.save<Proposal>(proposal)
+  }
+}
+
+export async function proposalsEngine_ProposalExecuted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}
+
+export async function proposalsEngine_Voted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}
+
+export async function proposalsEngine_ProposalCancelled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}

+ 24 - 0
query-node/mappings/proposalsDiscussion.ts

@@ -0,0 +1,24 @@
+/*
+eslint-disable @typescript-eslint/naming-convention
+*/
+import { SubstrateEvent, DatabaseManager } from '@dzlzv/hydra-common'
+
+export async function proposalsDiscussion_ThreadCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}
+export async function proposalsDiscussion_PostCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}
+export async function proposalsDiscussion_PostUpdated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}
+export async function proposalsDiscussion_ThreadModeChanged(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  // TODO
+}
+
+export async function proposalsDiscussion_PostDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  // TODO
+}

+ 4 - 5
query-node/mappings/workingGroups.ts

@@ -147,9 +147,9 @@ function parseQuestionInputType(
   return InputTypeToApplicationFormQuestionType[validType]
 }
 
-async function createOpeningMeta(
+export async function createWorkingGroupOpeningMetadata(
   db: DatabaseManager,
-  event_: SubstrateEvent,
+  eventTime: Date,
   originalMeta: Bytes | IOpeningMetadata
 ): Promise<WorkingGroupOpeningMetadata> {
   let originallyValid: boolean
@@ -162,7 +162,6 @@ async function createOpeningMeta(
     metadata = originalMeta
     originallyValid = true
   }
-  const eventTime = new Date(event_.blockTimestamp)
 
   const {
     applicationFormQuestions,
@@ -241,7 +240,7 @@ async function handleAddUpcomingOpeningAction(
   const upcomingOpeningMeta = action.metadata || {}
   const group = await getWorkingGroup(db, event_)
   const eventTime = new Date(event_.blockTimestamp)
-  const openingMeta = await createOpeningMeta(db, event_, upcomingOpeningMeta.metadata || {})
+  const openingMeta = await createWorkingGroupOpeningMetadata(db, eventTime, upcomingOpeningMeta.metadata || {})
   const { rewardPerBlock, expectedStart, minApplicationStake } = upcomingOpeningMeta
   const upcomingOpening = new UpcomingWorkingGroupOpening({
     createdAt: eventTime,
@@ -399,7 +398,7 @@ export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: Su
     type: openingType.isLeader ? WorkingGroupOpeningType.LEADER : WorkingGroupOpeningType.REGULAR,
   })
 
-  const metadata = await createOpeningMeta(db, event_, metadataBytes)
+  const metadata = await createWorkingGroupOpeningMetadata(db, eventTime, metadataBytes)
   opening.metadata = metadata
 
   await db.save<WorkingGroupOpening>(opening)

+ 8 - 18
query-node/schemas/proposals.graphql

@@ -71,15 +71,10 @@ type ProposalStatusCanceledByRuntime @variant {
 union ProposalIntermediateStatus = ProposalStatusDeciding | ProposalStatusGracing | ProposalStatusDormant
 
 "Proposal status after the voting stage has finished for the current council."
-union ProposalDecisionStatus =
-    #
-  # Approved:
-  #
+union ProposalDecisionStatus = # Approved:
   ProposalStatusDormant
-  | ProposalStatusGracing #
-  | # Not approved:
-  #
-  ProposalStatusVetoed
+  | ProposalStatusGracing # Not approved:
+  | ProposalStatusVetoed
   | ProposalStatusSlashed
   | ProposalStatusRejected
   | ProposalStatusExpired
@@ -90,16 +85,11 @@ union ProposalDecisionStatus =
 union ProposalExecutionStatus = ProposalStatusExecuted | ProposalStatusExecutionFailed
 
 "All valid proposal statuses"
-union ProposalStatus =
-    #
-  # Intermediate statuses
-  #
+union ProposalStatus = # Intermediate statuses:
   ProposalStatusDeciding
   | ProposalStatusGracing
-  | ProposalStatusDormant #
-  | # Final statuses:
-  #
-  ProposalStatusVetoed
+  | ProposalStatusDormant # Final statuses:
+  | ProposalStatusVetoed
   | ProposalStatusExecuted
   | ProposalStatusExecutionFailed
   | ProposalStatusSlashed
@@ -157,7 +147,7 @@ type Proposal @entity {
   statusSetAtTime: DateTime!
 }
 
-type SingalProposalDetails @variant {
+type SignalProposalDetails @variant {
   "Signal proposal content"
   text: String!
 }
@@ -347,7 +337,7 @@ type VetoProposalDetails @variant {
 }
 
 union ProposalDetails =
-    SingalProposalDetails
+    SignalProposalDetails
   | RuntimeUpgradeProposalDetails
   | FundingRequestProposalDetails
   | SetMaxValidatorCountProposalDetails

+ 1 - 0
query-node/schemas/proposalsEvents.graphql

@@ -18,6 +18,7 @@ type ProposalCreatedEvent @entity {
 
   ### SPECIFIC DATA ###
 
+  # FIXME: https://github.com/Joystream/joystream/issues/2457
   "The created proposal"
   proposal: Proposal!
 }

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

@@ -19,4 +19,5 @@ ${TAR} -c --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' \
     runtime \
     runtime-modules \
     utils/chain-spec-builder \
-    joystream-node.Dockerfile | shasum | cut -d " " -f 1
+    joystream-node.Dockerfile \
+    $(echo "$PROPOSALS_PARAMETERS_PATH") | shasum | cut -d " " -f 1

+ 104 - 0
tests/integration-tests/proposal-parameters.json

@@ -0,0 +1,104 @@
+{
+  "set_max_validator_count_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "runtime_upgrade_proposal": {
+    "voting_period": 10,
+    "grace_period": 10,
+    "constitutionality": 2
+  },
+  "signal_proposal": {
+    "voting_period": 10,
+    "grace_period": 0
+  },
+  "funding_request_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "create_working_group_lead_opening_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "fill_working_group_lead_opening_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "update_working_group_budget_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "decrease_working_group_lead_stake_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "slash_working_group_lead_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "set_working_group_lead_reward_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "terminate_working_group_lead_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "amend_constitution_proposal": {
+      "voting_period": 10,
+      "grace_period": 10,
+      "constitutionality": 2
+  },
+  "cancel_working_group_lead_opening_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  },
+  "set_membership_price_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "set_council_budget_increment_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "set_councilor_reward_proposal": {
+      "voting_period": 10,
+      "grace_period": 20
+  },
+  "set_initial_invitation_balance_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "set_membership_lead_invitation_quota_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "set_referral_cut_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "set_invitation_count_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "create_blog_post_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "edit_blog_post_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "lock_blog_post_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "unlock_blog_post_proposal": {
+      "voting_period": 10,
+      "grace_period": 5
+  },
+  "veto_proposal_proposal": {
+      "voting_period": 10,
+      "grace_period": 0
+  }
+}

+ 93 - 35
tests/integration-tests/src/Api.ts

@@ -24,6 +24,8 @@ import {
   WorkingGroupModuleName,
   AppliedOnOpeningEventDetails,
   OpeningFilledEventDetails,
+  ProposalsEngineEventName,
+  ProposalCreatedEventDetails,
 } from './types'
 import {
   ApplicationId,
@@ -35,13 +37,7 @@ import {
 } from '@joystream/types/working-group'
 import { DeriveAllSections } from '@polkadot/api/util/decorate'
 import { ExactDerive } from '@polkadot/api-derive'
-
-export enum WorkingGroups {
-  StorageWorkingGroup = 'storageWorkingGroup',
-  ContentDirectoryWorkingGroup = 'contentDirectoryWorkingGroup',
-  MembershipWorkingGroup = 'membershipWorkingGroup',
-  ForumWorkingGroup = 'forumWorkingGroup',
-}
+import { ProposalId } from '@joystream/types/proposals'
 
 export class ApiFactory {
   private readonly api: ApiPromise
@@ -125,6 +121,10 @@ export class Api {
     return this.api.derive
   }
 
+  public get createType(): ApiPromise['createType'] {
+    return this.api.createType.bind(this.api)
+  }
+
   public async signAndSend(
     tx: SubmittableExtrinsic<'promise'>,
     sender: AccountId | string
@@ -133,20 +133,20 @@ export class Api {
   }
 
   public async sendExtrinsicsAndGetResults(
-    txs: SubmittableExtrinsic<'promise'>[],
-    sender: AccountId | string | AccountId[] | string[],
-    preserveOrder = false
+    // Extrinsics can be separated into batches in order to makes sure they are processed in specified order
+    txs: SubmittableExtrinsic<'promise'>[] | SubmittableExtrinsic<'promise'>[][],
+    sender: AccountId | string | AccountId[] | string[]
   ): Promise<ISubmittableResult[]> {
     let results: ISubmittableResult[] = []
-    if (preserveOrder) {
-      for (const i in txs) {
-        const tx = txs[i]
-        const result = await this.sender.signAndSend(tx, Array.isArray(sender) ? sender[i] : sender)
-        results.push(result)
-      }
-    } else {
-      results = await Promise.all(
-        txs.map((tx, i) => this.sender.signAndSend(tx, Array.isArray(sender) ? sender[i] : sender))
+    const batches = (Array.isArray(txs[0]) ? txs : [txs]) as SubmittableExtrinsic<'promise'>[][]
+    for (const i in batches) {
+      const batch = batches[i]
+      results = results.concat(
+        await Promise.all(
+          batch.map((tx, j) =>
+            this.sender.signAndSend(tx, Array.isArray(sender) ? sender[parseInt(i) * batch.length + j] : sender)
+          )
+        )
       )
     }
     return results
@@ -181,22 +181,6 @@ export class Api {
     return nKeyPairs
   }
 
-  // Well known WorkingGroup enum defined in runtime
-  public getWorkingGroupString(workingGroup: WorkingGroups): string {
-    switch (workingGroup) {
-      case WorkingGroups.StorageWorkingGroup:
-        return 'Storage'
-      case WorkingGroups.ContentDirectoryWorkingGroup:
-        return 'Content'
-      case WorkingGroups.ForumWorkingGroup:
-        return 'Forum'
-      case WorkingGroups.MembershipWorkingGroup:
-        return 'Membership'
-      default:
-        throw new Error(`Invalid working group string representation: ${workingGroup}`)
-    }
-  }
-
   public getBlockDuration(): BN {
     return this.api.createType('Moment', this.api.consts.babe.expectedBlockTime)
   }
@@ -439,4 +423,78 @@ export class Api {
   public async getLeaderStakingKey(group: WorkingGroupModuleName): Promise<string> {
     return (await this.getLeader(group)).staking_account_id.toString()
   }
+
+  public async retrieveProposalsEngineEventDetails(
+    result: ISubmittableResult,
+    eventName: ProposalsEngineEventName
+  ): Promise<EventDetails> {
+    const details = await this.retrieveEventDetails(result, 'proposalsEngine', eventName)
+    if (!details) {
+      throw new Error(`${eventName} event details not found in result: ${JSON.stringify(result.toHuman())}`)
+    }
+    return details
+  }
+
+  public async retrieveProposalCreatedEventDetails(result: ISubmittableResult): Promise<ProposalCreatedEventDetails> {
+    const details = await this.retrieveProposalsEngineEventDetails(result, 'ProposalCreated')
+    return {
+      ...details,
+      proposalId: details.event.data[1] as ProposalId,
+    }
+  }
+
+  public async getMemberSigners(inputs: { asMember: MemberId }[]): Promise<string[]> {
+    return await Promise.all(
+      inputs.map(async ({ asMember }) => {
+        const membership = await this.query.members.membershipById(asMember)
+        return membership.controller_account.toString()
+      })
+    )
+  }
+
+  public async untilProposalsCanBeCreated(numberOfProposals = 0, intervalMs = 6000, timeoutMs = 180000): Promise<void> {
+    await Utils.until(
+      `${numberOfProposals} proposals can be created`,
+      async ({ debug }) => {
+        const { maxActiveProposalLimit } = this.consts.proposalsEngine
+        const activeProposalsN = await this.query.proposalsEngine.activeProposalCount()
+        debug(`Currently active proposals: ${activeProposalsN.toNumber()}/${maxActiveProposalLimit.toNumber()}`)
+        return activeProposalsN.lt(maxActiveProposalLimit)
+      },
+      intervalMs,
+      timeoutMs
+    )
+  }
+
+  public async untilCouncilStage(
+    targetStage: 'Announcing' | 'Voting' | 'Revealing' | 'Idle',
+    blocksReserve = 3
+  ): Promise<void> {
+    await Utils.until(`council stage ${targetStage} (+${blocksReserve} blocks reserve)`, async ({ debug }) => {
+      const currentCouncilStage = await this.query.council.stage()
+      const currentElectionStage = await this.query.referendum.stage()
+      const currentStage = currentCouncilStage.stage.isOfType('Election')
+        ? (currentElectionStage.type as 'Voting' | 'Revealing')
+        : (currentCouncilStage.stage.type as 'Announcing' | 'Idle')
+      const currentStageStartedAt = currentCouncilStage.stage.isOfType('Election')
+        ? currentElectionStage.asType(currentElectionStage.type as 'Voting' | 'Revealing').started
+        : currentCouncilStage.changed_at
+
+      const currentBlock = await this.getBestBlock()
+      const { announcingPeriodDuration, idlePeriodDuration } = this.consts.council
+      const { voteStageDuration, revealStageDuration } = this.consts.referendum
+      const durationByStage = {
+        'Announcing': announcingPeriodDuration,
+        'Voting': voteStageDuration,
+        'Revealing': revealStageDuration,
+        'Idle': idlePeriodDuration,
+      } as const
+
+      const currentStageEndsIn = currentStageStartedAt.add(durationByStage[currentStage]).sub(currentBlock)
+
+      debug(`Current stage: ${currentStage}, blocks left: ${currentStageEndsIn.toNumber()}`)
+
+      return currentStage === targetStage && currentStageEndsIn.gten(blocksReserve)
+    })
+  }
 }

+ 12 - 8
tests/integration-tests/src/Fixture.ts

@@ -121,10 +121,9 @@ export abstract class StandardizedFixture extends BaseQueryNodeFixture {
   protected extrinsics: SubmittableExtrinsic<'promise'>[] = []
   protected results: ISubmittableResult[] = []
   protected events: EventDetails[] = []
-  protected areExtrinsicsOrderSensitive = false
 
   protected abstract getSignerAccountOrAccounts(): Promise<string | string[]>
-  protected abstract getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]>
+  protected abstract getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[] | SubmittableExtrinsic<'promise'>[][]>
   protected abstract getEventFromResult(result: ISubmittableResult): Promise<EventDetails>
   protected abstract assertQueryNodeEventIsValid(qEvent: AnyQueryNodeEvent, i: number): void
 
@@ -137,15 +136,20 @@ export abstract class StandardizedFixture extends BaseQueryNodeFixture {
     })
   }
 
+  private flattenExtrinsics(
+    extrinsics: SubmittableExtrinsic<'promise'>[] | SubmittableExtrinsic<'promise'>[][]
+  ): SubmittableExtrinsic<'promise'>[] {
+    return Array.isArray(extrinsics[0])
+      ? (extrinsics as SubmittableExtrinsic<'promise'>[][]).reduce((res, batch) => res.concat(batch), [])
+      : (extrinsics as SubmittableExtrinsic<'promise'>[])
+  }
+
   public async execute(): Promise<void> {
     const accountOrAccounts = await this.getSignerAccountOrAccounts()
-    this.extrinsics = await this.getExtrinsics()
+    const extrinsics = await this.getExtrinsics()
+    this.extrinsics = this.flattenExtrinsics(extrinsics)
     await this.api.prepareAccountsForFeeExpenses(accountOrAccounts, this.extrinsics)
-    this.results = await this.api.sendExtrinsicsAndGetResults(
-      this.extrinsics,
-      accountOrAccounts,
-      this.areExtrinsicsOrderSensitive
-    )
+    this.results = await this.api.sendExtrinsicsAndGetResults(extrinsics, accountOrAccounts)
     this.events = await Promise.all(this.results.map((r) => this.getEventFromResult(r)))
   }
 }

+ 29 - 14
tests/integration-tests/src/QueryNodeApi.ts

@@ -16,12 +16,6 @@ import {
   GetInvitesTransferredEventsBySourceMemberIdQuery,
   GetInvitesTransferredEventsBySourceMemberIdQueryVariables,
   GetInvitesTransferredEventsBySourceMemberId,
-  GetStakingAccountAddedEventsByMemberIdQuery,
-  GetStakingAccountAddedEventsByMemberIdQueryVariables,
-  GetStakingAccountAddedEventsByMemberId,
-  GetStakingAccountConfirmedEventsByMemberIdQuery,
-  GetStakingAccountConfirmedEventsByMemberIdQueryVariables,
-  GetStakingAccountConfirmedEventsByMemberId,
   GetStakingAccountRemovedEventsByMemberIdQuery,
   GetStakingAccountRemovedEventsByMemberIdQueryVariables,
   GetStakingAccountRemovedEventsByMemberId,
@@ -175,9 +169,20 @@ import {
   GetMemberInvitedEventsByEventIdsQuery,
   GetMemberInvitedEventsByEventIdsQueryVariables,
   GetMemberInvitedEventsByEventIds,
+  ProposalFieldsFragment,
+  GetProposalsByIdsQuery,
+  GetProposalsByIdsQueryVariables,
+  GetProposalsByIds,
+  GetStakingAccountConfirmedEventsByEventIdsQuery,
+  GetStakingAccountConfirmedEventsByEventIdsQueryVariables,
+  GetStakingAccountConfirmedEventsByEventIds,
+  GetStakingAccountAddedEventsByEventIdsQuery,
+  GetStakingAccountAddedEventsByEventIdsQueryVariables,
+  GetStakingAccountAddedEventsByEventIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
+import { ProposalId } from '@joystream/types/proposals'
 export class QueryNodeApi {
   private readonly queryNodeProvider: ApolloClient<NormalizedCacheObject>
   private readonly debug: Debugger.Debugger
@@ -335,20 +340,22 @@ export class QueryNodeApi {
     )
   }
 
-  public async getStakingAccountAddedEvents(memberId: MemberId): Promise<StakingAccountAddedEventFieldsFragment[]> {
+  public async getStakingAccountAddedEvents(events: EventDetails[]): Promise<StakingAccountAddedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
     return this.multipleEntitiesQuery<
-      GetStakingAccountAddedEventsByMemberIdQuery,
-      GetStakingAccountAddedEventsByMemberIdQueryVariables
-    >(GetStakingAccountAddedEventsByMemberId, { memberId: memberId.toString() }, 'stakingAccountAddedEvents')
+      GetStakingAccountAddedEventsByEventIdsQuery,
+      GetStakingAccountAddedEventsByEventIdsQueryVariables
+    >(GetStakingAccountAddedEventsByEventIds, { ids: eventIds }, 'stakingAccountAddedEvents')
   }
 
   public async getStakingAccountConfirmedEvents(
-    memberId: MemberId
+    events: EventDetails[]
   ): Promise<StakingAccountConfirmedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
     return this.multipleEntitiesQuery<
-      GetStakingAccountConfirmedEventsByMemberIdQuery,
-      GetStakingAccountConfirmedEventsByMemberIdQueryVariables
-    >(GetStakingAccountConfirmedEventsByMemberId, { memberId: memberId.toString() }, 'stakingAccountConfirmedEvents')
+      GetStakingAccountConfirmedEventsByEventIdsQuery,
+      GetStakingAccountConfirmedEventsByEventIdsQueryVariables
+    >(GetStakingAccountConfirmedEventsByEventIds, { ids: eventIds }, 'stakingAccountConfirmedEvents')
   }
 
   public async getStakingAccountRemovedEvents(memberId: MemberId): Promise<StakingAccountRemovedEventFieldsFragment[]> {
@@ -676,4 +683,12 @@ export class QueryNodeApi {
       'leaderUnsetEvents'
     )
   }
+
+  public async getProposalsByIds(ids: ProposalId[]): Promise<ProposalFieldsFragment[]> {
+    return this.multipleEntitiesQuery<GetProposalsByIdsQuery, GetProposalsByIdsQueryVariables>(
+      GetProposalsByIds,
+      { ids: ids.map((id) => id.toString()) },
+      'proposals'
+    )
+  }
 }

+ 15 - 0
tests/integration-tests/src/consts.ts

@@ -1,3 +1,4 @@
+import { WorkingGroup } from '@joystream/types/common'
 import BN from 'bn.js'
 import { WorkingGroupModuleName } from './types'
 
@@ -19,3 +20,17 @@ export const workingGroups: WorkingGroupModuleName[] = [
   'forumWorkingGroup',
   'membershipWorkingGroup',
 ]
+
+export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
+  if (group.isOfType('Content')) {
+    return 'contentDirectoryWorkingGroup'
+  } else if (group.isOfType('Membership')) {
+    return 'membershipWorkingGroup'
+  } else if (group.isOfType('Forum')) {
+    return 'forumWorkingGroup'
+  } else if (group.isOfType('Storage')) {
+    return 'storageWorkingGroup'
+  }
+
+  throw new Error(`Unsupported working group: ${group}`)
+}

+ 89 - 0
tests/integration-tests/src/fixtures/council/InitializeCouncilFixture.ts

@@ -0,0 +1,89 @@
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
+import { assert } from 'chai'
+
+export class InitializeCouncilFixture extends BaseQueryNodeFixture {
+  public async execute(): Promise<void> {
+    // Assert no council exists
+    if ((await this.api.query.council.councilMembers()).length) {
+      return this.error(new Error('Council election fixture expects no council seats to be filled'))
+    }
+
+    const { api, query } = this
+    const { councilSize, minNumberOfExtraCandidates } = api.consts.council
+    const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
+    const numberOfVoters = numberOfCandidates
+
+    // Prepare memberships
+    const candidatesMemberAccounts = (await this.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 voteStake = api.consts.referendum.minimumStake
+
+    const candidatesStakingAccounts = (await this.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()
+
+    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
+    await api.treasuryTransferBalanceToAccounts(votersStakingAccounts, voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE))
+
+    // Announcing stage
+    await this.api.untilCouncilStage('Announcing')
+
+    const applyForCouncilTxs = candidatesMemberIds.map((memberId, i) =>
+      api.tx.council.announceCandidacy(
+        memberId,
+        candidatesStakingAccounts[i],
+        candidatesMemberAccounts[i],
+        councilCandidateStake
+      )
+    )
+    await api.prepareAccountsForFeeExpenses(candidatesMemberAccounts, applyForCouncilTxs)
+    await api.sendExtrinsicsAndGetResults(applyForCouncilTxs, candidatesMemberAccounts)
+
+    // Voting stage
+    await this.api.untilCouncilStage('Voting')
+
+    const cycleId = (await this.api.query.referendum.stage()).asType('Voting').current_cycle_id
+    const votingTxs = votersStakingAccounts.map((account, i) => {
+      const accountId = api.createType('AccountId', account)
+      const optionId = candidatesMemberIds[i % numberOfCandidates]
+      const salt = api.createType('Bytes', `salt${i}`)
+
+      const payload = Buffer.concat([accountId.toU8a(), optionId.toU8a(), salt.toU8a(), cycleId.toU8a()])
+      const commitment = blake2AsHex(payload)
+      return api.tx.referendum.vote(commitment, voteStake)
+    })
+    await api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
+    await api.sendExtrinsicsAndGetResults(votingTxs, votersStakingAccounts)
+
+    // Revealing stage
+    await this.api.untilCouncilStage('Revealing')
+
+    const revealingTxs = votersStakingAccounts.map((account, i) => {
+      const optionId = candidatesMemberIds[i % numberOfCandidates]
+      return api.tx.referendum.revealVote(`salt${i}`, optionId)
+    })
+    await api.prepareAccountsForFeeExpenses(votersStakingAccounts, votingTxs)
+    await api.sendExtrinsicsAndGetResults(revealingTxs, votersStakingAccounts)
+
+    await this.api.untilCouncilStage('Idle')
+
+    const councilMembers = await api.query.council.councilMembers()
+    assert(councilMembers.length, 'Council initialization failed!')
+  }
+}

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

@@ -0,0 +1 @@
+export { InitializeCouncilFixture } from './InitializeCouncilFixture'

+ 72 - 74
tests/integration-tests/src/fixtures/membership/AddStakingAccountsHappyCaseFixture.ts

@@ -2,102 +2,100 @@ import { Api } from '../../Api'
 import { assert } from 'chai'
 import { QueryNodeApi } from '../../QueryNodeApi'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { BaseQueryNodeFixture } from '../../Fixture'
-import { MemberContext, EventDetails } from '../../types'
+import { StandardizedFixture } from '../../Fixture'
+import { EventDetails } from '../../types'
 import {
+  MembershipFieldsFragment,
   StakingAccountAddedEventFieldsFragment,
   StakingAccountConfirmedEventFieldsFragment,
 } from '../../graphql/generated/queries'
 import { MINIMUM_STAKING_ACCOUNT_BALANCE } from '../../consts'
+import { MemberId } from '@joystream/types/common'
+import BN from 'bn.js'
+import _ from 'lodash'
+import { Utils } from '../../utils'
+import { SubmittableResult } from '@polkadot/api'
 
-export class AddStakingAccountsHappyCaseFixture extends BaseQueryNodeFixture {
-  private memberContext: MemberContext
-  private accounts: string[]
+type AddStakingAccountInput = {
+  asMember: MemberId
+  account: string
+  stakeAmount?: BN
+}
 
-  private addExtrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private confirmExtrinsics: SubmittableExtrinsic<'promise'>[] = []
-  private addEvents: EventDetails[] = []
-  private confirmEvents: EventDetails[] = []
+export class AddStakingAccountsHappyCaseFixture extends StandardizedFixture {
+  protected inputs: AddStakingAccountInput[]
 
-  public constructor(api: Api, query: QueryNodeApi, memberContext: MemberContext, accounts: string[]) {
+  public constructor(api: Api, query: QueryNodeApi, inputs: AddStakingAccountInput[]) {
     super(api, query)
-    this.memberContext = memberContext
-    this.accounts = accounts
+    this.inputs = inputs
   }
 
-  private assertQueryNodeAddAccountEventIsValid(
-    eventDetails: EventDetails,
-    account: string,
-    txHash: string,
-    qEvents: StakingAccountAddedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    assert.equal(qEvent.inExtrinsic, txHash)
-    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
-    assert.equal(qEvent.account, account)
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    const addCandidateSigners = this.inputs.map((i) => i.account)
+    return addCandidateSigners.concat(await this.api.getMemberSigners(this.inputs))
   }
 
-  private assertQueryNodeConfirmAccountEventIsValid(
-    eventDetails: EventDetails,
-    account: string,
-    txHash: string,
-    qEvents: StakingAccountConfirmedEventFieldsFragment[]
-  ) {
-    const qEvent = this.findMatchingQueryNodeEvent(eventDetails, qEvents)
-    assert.equal(qEvent.inExtrinsic, txHash)
-    assert.equal(qEvent.member.id, this.memberContext.memberId.toString())
-    assert.equal(qEvent.account, account)
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[][]> {
+    const addExtrinsics = this.inputs.map(({ asMember }) => this.api.tx.members.addStakingAccountCandidate(asMember))
+    const confirmExtrinsics = this.inputs.map(({ asMember, account }) =>
+      this.api.tx.members.confirmStakingAccount(asMember, account)
+    )
+    return [addExtrinsics, confirmExtrinsics]
   }
 
-  async execute(): Promise<void> {
-    const { memberContext, accounts } = this
-    this.addExtrinsics = accounts.map(() => this.api.tx.members.addStakingAccountCandidate(memberContext.memberId))
-    this.confirmExtrinsics = accounts.map((a) => this.api.tx.members.confirmStakingAccount(memberContext.memberId, a))
-    const addStakingCandidateFee = await this.api.estimateTxFee(this.addExtrinsics[0], accounts[0])
-    const confirmStakingAccountFee = await this.api.estimateTxFee(this.confirmExtrinsics[0], memberContext.account)
+  protected async getEventFromResult(result: SubmittableResult): Promise<EventDetails> {
+    let event
+    // It's either of those, but since we can't be sure which one it is in this case, we use a try-catch workaround
+    try {
+      event = await this.api.retrieveMembershipEventDetails(result, 'StakingAccountAdded')
+    } catch (e) {
+      event = await this.api.retrieveMembershipEventDetails(result, 'StakingAccountConfirmed')
+    }
 
-    await this.api.treasuryTransferBalance(memberContext.account, confirmStakingAccountFee.muln(accounts.length))
-    const stakingAccountRequiredBalance = addStakingCandidateFee.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
-    await Promise.all(accounts.map((a) => this.api.treasuryTransferBalance(a, stakingAccountRequiredBalance)))
-    // Add staking account candidates
-    const addResults = await Promise.all(accounts.map((a, i) => this.api.signAndSend(this.addExtrinsics[i], a)))
-    this.addEvents = await Promise.all(
-      addResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountAdded'))
-    )
-    // Confirm staking accounts
-    const confirmResults = await Promise.all(
-      this.confirmExtrinsics.map((tx) => this.api.signAndSend(tx, memberContext.account))
-    )
-    this.confirmEvents = await Promise.all(
-      confirmResults.map((r) => this.api.retrieveMembershipEventDetails(r, 'StakingAccountConfirmed'))
+    return event
+  }
+
+  protected assertQueryNodeEventIsValid(
+    qEvent: StakingAccountAddedEventFieldsFragment | StakingAccountConfirmedEventFieldsFragment,
+    i: number
+  ): void {
+    assert.equal(qEvent.member.id, this.inputs[i % this.inputs.length].asMember.toString())
+    assert.equal(qEvent.account, this.inputs[i % this.inputs.length].account.toString())
+  }
+
+  protected assertQueriedMembersAreValid(qMembers: MembershipFieldsFragment[]): void {
+    const inputsByMember = _.groupBy(this.inputs, (v) => v.asMember.toString())
+
+    for (const [memberId, inputs] of Object.entries(inputsByMember)) {
+      const stakingAccounts = inputs.map((i) => i.account)
+      const qMember = qMembers.find((m) => m.id === memberId)
+      Utils.assert(qMember, 'Query node: Member not found!')
+      assert.includeMembers(qMember.boundAccounts, stakingAccounts)
+    }
+  }
+
+  async execute(): Promise<void> {
+    await Promise.all(
+      this.inputs.map(({ account, stakeAmount }) =>
+        this.api.treasuryTransferBalance(account, (stakeAmount || new BN(0)).addn(MINIMUM_STAKING_ACCOUNT_BALANCE))
+      )
     )
+    await super.execute()
   }
 
   async runQueryNodeChecks(): Promise<void> {
     await super.runQueryNodeChecks()
-    const { memberContext, accounts, addEvents, confirmEvents, addExtrinsics, confirmExtrinsics } = this
-    await this.query.tryQueryWithTimeout(
-      () => this.query.getMemberById(memberContext.memberId),
-      (qMember) => {
-        if (!qMember) {
-          throw new Error('Query node: Member not found')
-        }
-        assert.isNotEmpty(qMember.boundAccounts)
-        assert.includeMembers(qMember.boundAccounts, accounts)
-      }
+    const addedEvents = this.events.slice(0, this.inputs.length)
+    const confirmedEvents = this.events.slice(this.inputs.length)
+    // Query the events
+    const qConfirmedEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getStakingAccountConfirmedEvents(confirmedEvents),
+      (qEvents) => assert.equal(qEvents.length, confirmedEvents.length)
     )
+    const qAddedEvents = await this.query.getStakingAccountAddedEvents(addedEvents)
+    this.assertQueryNodeEventsAreValid(qAddedEvents.concat(qConfirmedEvents))
 
-    // Check events
-    const qAddedEvents = await this.query.getStakingAccountAddedEvents(memberContext.memberId)
-    const qConfirmedEvents = await this.query.getStakingAccountConfirmedEvents(memberContext.memberId)
-    accounts.forEach(async (account, i) => {
-      this.assertQueryNodeAddAccountEventIsValid(addEvents[i], account, addExtrinsics[i].hash.toString(), qAddedEvents)
-      this.assertQueryNodeConfirmAccountEventIsValid(
-        confirmEvents[i],
-        account,
-        confirmExtrinsics[i].hash.toString(),
-        qConfirmedEvents
-      )
-    })
+    const qMembers = await this.query.getMembersByIds(this.inputs.map(({ asMember }) => asMember))
+    await this.assertQueriedMembersAreValid(qMembers)
   }
 }

+ 351 - 0
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -0,0 +1,351 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { ProposalCreatedEventDetails, ProposalDetailsJsonByType, ProposalType } from '../../types'
+import { AugmentedConsts, SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { ProposalCreatedEventFieldsFragment, ProposalFieldsFragment } from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { ProposalId, ProposalParameters } from '@joystream/types/proposals'
+import { MemberId } from '@joystream/types/common'
+import { FixtureRunner, StandardizedFixture } from '../../Fixture'
+import { AddStakingAccountsHappyCaseFixture } from '../membership'
+import { getWorkingGroupModuleName } from '../../consts'
+import { assertQueriedOpeningMetadataIsValid } from '../workingGroups/utils'
+import { OpeningMetadata } from '@joystream/metadata-protobuf'
+
+export type ProposalCreationParams<T extends ProposalType = ProposalType> = {
+  asMember: MemberId
+  title: string
+  description: string
+  exactExecutionBlock?: number
+  type: T
+  details: ProposalDetailsJsonByType<T>
+}
+
+// Dummy const type validation function (see: https://stackoverflow.com/questions/57069802/as-const-is-ignored-when-there-is-a-type-definition)
+const validateType = <T>(obj: T) => obj
+
+const proposalTypeToProposalParamsKey = {
+  'AmendConstitution': 'amendConstitutionProposalParameters',
+  'CancelWorkingGroupLeadOpening': 'cancelWorkingGroupLeadOpeningProposalParameters',
+  'CreateBlogPost': 'createBlogPostProposalParameters',
+  'CreateWorkingGroupLeadOpening': 'createWorkingGroupLeadOpeningProposalParameters',
+  'DecreaseWorkingGroupLeadStake': 'decreaseWorkingGroupLeadStakeProposalParameters',
+  'EditBlogPost': 'editBlogPostProoposalParamters',
+  'FillWorkingGroupLeadOpening': 'fillWorkingGroupOpeningProposalParameters',
+  'FundingRequest': 'fundingRequestProposalParameters',
+  'LockBlogPost': 'lockBlogPostProposalParameters',
+  'RuntimeUpgrade': 'runtimeUpgradeProposalParameters',
+  'SetCouncilBudgetIncrement': 'setCouncilBudgetIncrementProposalParameters',
+  'SetCouncilorReward': 'setCouncilorRewardProposalParameters',
+  'SetInitialInvitationBalance': 'setInitialInvitationBalanceProposalParameters',
+  'SetInitialInvitationCount': 'setInvitationCountProposalParameters',
+  'SetMaxValidatorCount': 'setMaxValidatorCountProposalParameters',
+  'SetMembershipLeadInvitationQuota': 'setMembershipLeadInvitationQuotaProposalParameters',
+  'SetMembershipPrice': 'setMembershipPriceProposalParameters',
+  'SetReferralCut': 'setReferralCutProposalParameters',
+  'SetWorkingGroupLeadReward': 'setWorkingGroupLeadRewardProposalParameters',
+  'Signal': 'signalProposalParameters',
+  'SlashWorkingGroupLead': 'slashWorkingGroupLeadProposalParameters',
+  'TerminateWorkingGroupLead': 'terminateWorkingGroupLeadProposalParameters',
+  'UnlockBlogPost': 'unlockBlogPostProposalParameters',
+  'UpdateWorkingGroupBudget': 'updateWorkingGroupBudgetProposalParameters',
+  'VetoProposal': 'vetoProposalProposalParameters',
+} as const
+
+type ProposalTypeToProposalParamsKeyMap = { [K in ProposalType]: keyof AugmentedConsts<'promise'>['proposalsCodex'] }
+validateType<ProposalTypeToProposalParamsKeyMap>(proposalTypeToProposalParamsKey)
+
+export class CreateProposalsFixture extends StandardizedFixture {
+  protected events: ProposalCreatedEventDetails[] = []
+
+  protected proposalsParams: ProposalCreationParams[]
+  protected stakingAccounts: string[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, proposalsParams: ProposalCreationParams[]) {
+    super(api, query)
+    this.proposalsParams = proposalsParams
+  }
+
+  public getCreatedProposalsIds(): ProposalId[] {
+    if (!this.events.length) {
+      throw new Error('Trying to get created opening ids before they were created!')
+    }
+    return this.events.map((e) => e.proposalId)
+  }
+
+  protected proposalParams(i: number): ProposalParameters {
+    const proposalType = this.proposalsParams[i].type
+    const paramsKey = proposalTypeToProposalParamsKey[proposalType]
+    return this.api.consts.proposalsCodex[paramsKey]
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    return this.api.getMemberSigners(this.proposalsParams)
+  }
+
+  protected async initStakingAccounts(): Promise<void> {
+    const { api, query } = this
+    const stakingAccounts = (await this.api.createKeyPairs(this.proposalsParams.length)).map((kp) => kp.address)
+    const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
+      api,
+      query,
+      this.proposalsParams.map(({ asMember }, i) => ({
+        asMember,
+        account: stakingAccounts[i],
+        stakeAmount: this.proposalParams(i).requiredStake.unwrapOr(undefined),
+      }))
+    )
+    await new FixtureRunner(addStakingAccountsFixture).run()
+
+    this.stakingAccounts = stakingAccounts
+  }
+
+  public async execute(): Promise<void> {
+    await this.initStakingAccounts()
+    await super.execute()
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.proposalsParams.map(({ asMember, description, title, exactExecutionBlock, details, type }, i) => {
+      const proposalDetails = { [type]: details } as { [K in ProposalType]: ProposalDetailsJsonByType<K> }
+      return this.api.tx.proposalsCodex.createProposal(
+        {
+          member_id: asMember,
+          description: description,
+          title: title,
+          exact_execution_block: exactExecutionBlock,
+          staking_account_id: this.stakingAccounts[i],
+        },
+        proposalDetails
+      )
+    })
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<ProposalCreatedEventDetails> {
+    return this.api.retrieveProposalCreatedEventDetails(result)
+  }
+
+  protected assertProposalDetailsAreValid(
+    params: ProposalCreationParams<ProposalType>,
+    qProposal: ProposalFieldsFragment
+  ): void {
+    const proposalDetails = this.api.createType('ProposalDetails', params.details)
+    switch (params.type) {
+      case 'AmendConstitution': {
+        Utils.assert(qProposal.details.__typename === 'AmendConstitutionProposalDetails')
+        const details = proposalDetails.asType('AmendConstitution')
+        assert.equal(qProposal.details.text, details.toString())
+        break
+      }
+      case 'CancelWorkingGroupLeadOpening': {
+        Utils.assert(qProposal.details.__typename === 'CancelWorkingGroupLeadOpeningProposalDetails')
+        const details = proposalDetails.asType('CancelWorkingGroupLeadOpening')
+        const [openingId, workingGroup] = details
+        const expectedId = `${getWorkingGroupModuleName(workingGroup)}-${openingId.toString()}`
+        assert.equal(qProposal.details.opening?.id, expectedId)
+        break
+      }
+      case 'CreateBlogPost': {
+        Utils.assert(qProposal.details.__typename === 'CreateBlogPostProposalDetails')
+        const details = proposalDetails.asType('CreateBlogPost')
+        // TODO
+        break
+      }
+      case 'CreateWorkingGroupLeadOpening': {
+        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.unstakingPeriod, details.stake_policy.leaving_unstaking_period.toNumber())
+        Utils.assert(qProposal.details.metadata)
+        assertQueriedOpeningMetadataIsValid(
+          qProposal.details.metadata,
+          Utils.metadataFromBytes(OpeningMetadata, details.description)
+        )
+        break
+      }
+      case 'DecreaseWorkingGroupLeadStake': {
+        Utils.assert(qProposal.details.__typename === 'DecreaseWorkingGroupLeadStakeProposalDetails')
+        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.lead?.id, expectedId)
+        break
+      }
+      case 'EditBlogPost': {
+        Utils.assert(qProposal.details.__typename === 'EditBlogPostProposalDetails')
+        const details = proposalDetails.asType('EditBlogPost')
+        // TODO
+        break
+      }
+      case 'FillWorkingGroupLeadOpening': {
+        Utils.assert(qProposal.details.__typename === 'FillWorkingGroupLeadOpeningProposalDetails')
+        const details = proposalDetails.asType('FillWorkingGroupLeadOpening')
+        const expectedOpeningId = `${getWorkingGroupModuleName(details.working_group)}-${details.opening_id.toString()}`
+        const expectedApplicationId = `${getWorkingGroupModuleName(
+          details.working_group
+        )}-${details.successful_application_id.toString()}`
+        assert.equal(qProposal.details.opening?.id, expectedOpeningId)
+        assert.equal(qProposal.details.application?.id, expectedApplicationId)
+        break
+      }
+      case 'FundingRequest': {
+        Utils.assert(qProposal.details.__typename === 'FundingRequestProposalDetails')
+        const details = proposalDetails.asType('FundingRequest')
+        assert.sameMembers(
+          qProposal.details.destinationsList?.destinations.map(({ amount, account }) => ({ amount, account })) || [],
+          details.map((d) => ({ amount: d.amount.toString(), account: d.account.toString() }))
+        )
+        break
+      }
+      case 'LockBlogPost': {
+        Utils.assert(qProposal.details.__typename === 'LockBlogPostProposalDetails')
+        const details = proposalDetails.asType('LockBlogPost')
+        // TODO
+        break
+      }
+      case 'RuntimeUpgrade': {
+        Utils.assert(qProposal.details.__typename === 'RuntimeUpgradeProposalDetails')
+        const details = proposalDetails.asType('RuntimeUpgrade')
+        // TODO
+        break
+      }
+      case 'SetCouncilBudgetIncrement': {
+        Utils.assert(qProposal.details.__typename === 'SetCouncilBudgetIncrementProposalDetails')
+        const details = proposalDetails.asType('SetCouncilBudgetIncrement')
+        assert.equal(qProposal.details.newAmount, details.toString())
+        break
+      }
+      case 'SetCouncilorReward': {
+        Utils.assert(qProposal.details.__typename === 'SetCouncilorRewardProposalDetails')
+        const details = proposalDetails.asType('SetCouncilorReward')
+        assert.equal(qProposal.details.newRewardPerBlock, details.toString())
+        break
+      }
+      case 'SetInitialInvitationBalance': {
+        Utils.assert(qProposal.details.__typename === 'SetInitialInvitationBalanceProposalDetails')
+        const details = proposalDetails.asType('SetInitialInvitationBalance')
+        assert.equal(qProposal.details.newInitialInvitationBalance, details.toString())
+        break
+      }
+      case 'SetInitialInvitationCount': {
+        Utils.assert(qProposal.details.__typename === 'SetInitialInvitationCountProposalDetails')
+        const details = proposalDetails.asType('SetInitialInvitationCount')
+        assert.equal(qProposal.details.newInitialInvitationsCount, details.toNumber())
+        break
+      }
+      case 'SetMaxValidatorCount': {
+        Utils.assert(qProposal.details.__typename === 'SetMaxValidatorCountProposalDetails')
+        const details = proposalDetails.asType('SetMaxValidatorCount')
+        assert.equal(qProposal.details.newMaxValidatorCount, details.toNumber())
+        break
+      }
+      case 'SetMembershipLeadInvitationQuota': {
+        Utils.assert(qProposal.details.__typename === 'SetMembershipLeadInvitationQuotaProposalDetails')
+        const details = proposalDetails.asType('SetMembershipLeadInvitationQuota')
+        assert.equal(qProposal.details.newLeadInvitationQuota, details.toNumber())
+        break
+      }
+      case 'SetMembershipPrice': {
+        Utils.assert(qProposal.details.__typename === 'SetMembershipPriceProposalDetails')
+        const details = proposalDetails.asType('SetMembershipPrice')
+        assert.equal(qProposal.details.newPrice, details.toString())
+        break
+      }
+      case 'SetReferralCut': {
+        Utils.assert(qProposal.details.__typename === 'SetReferralCutProposalDetails')
+        const details = proposalDetails.asType('SetReferralCut')
+        assert.equal(qProposal.details.newReferralCut, details.toNumber())
+        break
+      }
+      case 'SetWorkingGroupLeadReward': {
+        Utils.assert(qProposal.details.__typename === 'SetWorkingGroupLeadRewardProposalDetails')
+        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.lead?.id, expectedId)
+        break
+      }
+      case 'Signal': {
+        Utils.assert(qProposal.details.__typename === 'SignalProposalDetails')
+        const details = proposalDetails.asType('Signal')
+        assert.equal(qProposal.details.text, details.toString())
+        break
+      }
+      case 'SlashWorkingGroupLead': {
+        Utils.assert(qProposal.details.__typename === 'SlashWorkingGroupLeadProposalDetails')
+        const details = proposalDetails.asType('SlashWorkingGroupLead')
+        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())
+        break
+      }
+      case 'TerminateWorkingGroupLead': {
+        Utils.assert(qProposal.details.__typename === 'TerminateWorkingGroupLeadProposalDetails')
+        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())
+        break
+      }
+      case 'UnlockBlogPost': {
+        Utils.assert(qProposal.details.__typename === 'UnlockBlogPostProposalDetails')
+        const details = proposalDetails.asType('UnlockBlogPost')
+        // TODO
+        break
+      }
+      case 'UpdateWorkingGroupBudget': {
+        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.group?.id, getWorkingGroupModuleName(group))
+        break
+      }
+      case 'VetoProposal': {
+        Utils.assert(qProposal.details.__typename === 'VetoProposalDetails')
+        const details = proposalDetails.asType('VetoProposal')
+        assert.equal(qProposal.details.proposal?.id, details.toString())
+        break
+      }
+    }
+  }
+
+  protected assertQueriedProposalsAreValid(qProposals: ProposalFieldsFragment[]): void {
+    this.events.map((e, i) => {
+      const proposalParams = this.proposalsParams[i]
+      const qProposal = qProposals.find((p) => p.id === e.proposalId.toString())
+      Utils.assert(qProposal, 'Query node: Proposal not found')
+      assert.equal(qProposal.councilApprovals, 0)
+      assert.equal(qProposal.creator.id, proposalParams.asMember.toString())
+      assert.equal(qProposal.description, proposalParams.description)
+      assert.equal(qProposal.title, proposalParams.title)
+      assert.equal(qProposal.stakingAccount, this.stakingAccounts[i].toString())
+      assert.equal(qProposal.exactExecutionBlock, proposalParams.exactExecutionBlock)
+      assert.equal(qProposal.status.__typename, 'ProposalStatusDeciding')
+      assert.equal(qProposal.statusSetAtBlock, e.blockNumber)
+      assert.equal(new Date(qProposal.statusSetAtTime).getTime(), e.blockTimestamp)
+    })
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: ProposalCreatedEventFieldsFragment, i: number): void {
+    // TODO
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // TODO: Events
+
+    // Query the proposals
+    const qProposals = await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalsByIds(this.events.map((e) => e.proposalId)),
+      (result) => this.assertQueriedProposalsAreValid(result)
+    )
+  }
+}

+ 1 - 0
tests/integration-tests/src/fixtures/proposals/index.ts

@@ -0,0 +1 @@
+export { CreateProposalsFixture, ProposalCreationParams } from './CreateProposalsFixture'

+ 0 - 40
tests/integration-tests/src/fixtures/workingGroups/BaseCreateOpeningFixture.ts

@@ -1,40 +0,0 @@
-import { IOpeningMetadata } from '@joystream/metadata-protobuf'
-import { assert } from 'chai'
-import { OpeningMetadataFieldsFragment } from '../../graphql/generated/queries'
-import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
-import { queryNodeQuestionTypeToMetadataQuestionType } from './utils'
-export abstract class BaseCreateOpeningFixture extends BaseWorkingGroupFixture {
-  protected assertQueriedOpeningMetadataIsValid(
-    qOpeningMeta: OpeningMetadataFieldsFragment,
-    expectedMetadata?: IOpeningMetadata | null
-  ): void {
-    const {
-      shortDescription,
-      description,
-      expectedEndingTimestamp,
-      hiringLimit,
-      applicationDetails,
-      applicationFormQuestions,
-    } = expectedMetadata || {}
-    assert.equal(qOpeningMeta.shortDescription, shortDescription || null)
-    assert.equal(qOpeningMeta.description, description || null)
-    assert.equal(
-      qOpeningMeta.expectedEnding ? new Date(qOpeningMeta.expectedEnding).getTime() : qOpeningMeta.expectedEnding,
-      expectedEndingTimestamp || null
-    )
-    assert.equal(qOpeningMeta.hiringLimit, hiringLimit || null)
-    assert.equal(qOpeningMeta.applicationDetails, applicationDetails || null)
-    assert.deepEqual(
-      qOpeningMeta.applicationFormQuestions
-        .sort((a, b) => a.index - b.index)
-        .map(({ question, type }) => ({
-          question,
-          type: queryNodeQuestionTypeToMetadataQuestionType(type),
-        })),
-      (applicationFormQuestions || []).map(({ question, type }) => ({
-        question: question || null,
-        type: type || 0,
-      }))
-    )
-  }
-}

+ 4 - 3
tests/integration-tests/src/fixtures/workingGroups/CreateOpeningsFixture.ts

@@ -1,5 +1,4 @@
 import { Api } from '../../Api'
-import { BaseCreateOpeningFixture } from './BaseCreateOpeningFixture'
 import { QueryNodeApi } from '../../QueryNodeApi'
 import { OpeningAddedEventDetails, WorkingGroupModuleName } from '../../types'
 import { OpeningId } from '@joystream/types/working-group'
@@ -15,6 +14,8 @@ import BN from 'bn.js'
 import { IOpeningMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
 import { createType } from '@joystream/types'
 import { Bytes } from '@polkadot/types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { assertQueriedOpeningMetadataIsValid } from './utils'
 
 export type OpeningParams = {
   stake: BN
@@ -41,7 +42,7 @@ export const DEFAULT_OPENING_PARAMS: Omit<OpeningParams, 'metadata'> & { metadat
   },
 }
 
-export class CreateOpeningsFixture extends BaseCreateOpeningFixture {
+export class CreateOpeningsFixture extends BaseWorkingGroupFixture {
   protected asSudo: boolean
   protected events: OpeningAddedEventDetails[] = []
 
@@ -127,7 +128,7 @@ export class CreateOpeningsFixture extends BaseCreateOpeningFixture {
       assert.equal(qOpening.stakeAmount, openingParams.stake.toString())
       assert.equal(qOpening.unstakingPeriod, openingParams.unstakingPeriod)
       // Metadata
-      this.assertQueriedOpeningMetadataIsValid(qOpening.metadata, this.getOpeningMetadata(openingParams))
+      assertQueriedOpeningMetadataIsValid(qOpening.metadata, this.getOpeningMetadata(openingParams))
     })
   }
 

+ 4 - 3
tests/integration-tests/src/fixtures/workingGroups/CreateUpcomingOpeningsFixture.ts

@@ -1,5 +1,4 @@
 import { Api } from '../../Api'
-import { BaseCreateOpeningFixture } from './BaseCreateOpeningFixture'
 import { QueryNodeApi } from '../../QueryNodeApi'
 import { EventDetails, WorkingGroupModuleName } from '../../types'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
@@ -17,6 +16,8 @@ import { Bytes } from '@polkadot/types'
 import moment from 'moment'
 import { DEFAULT_OPENING_PARAMS } from './CreateOpeningsFixture'
 import { createType } from '@joystream/types'
+import { assertQueriedOpeningMetadataIsValid } from './utils'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
 
 export const DEFAULT_UPCOMING_OPENING_META: IUpcomingOpeningMetadata = {
   minApplicationStake: Long.fromString(DEFAULT_OPENING_PARAMS.stake.toString()),
@@ -30,7 +31,7 @@ export type UpcomingOpeningParams = {
   expectMetadataFailure?: boolean
 }
 
-export class CreateUpcomingOpeningsFixture extends BaseCreateOpeningFixture {
+export class CreateUpcomingOpeningsFixture extends BaseWorkingGroupFixture {
   protected upcomingOpeningsParams: UpcomingOpeningParams[]
   protected createdUpcomingOpeningIds: string[] = []
 
@@ -117,7 +118,7 @@ export class CreateUpcomingOpeningsFixture extends BaseCreateOpeningFixture {
         )
         Utils.assert(qEvent.result.__typename === 'UpcomingOpeningAdded')
         assert.equal(qEvent.result.upcomingOpeningId, qUpcomingOpening.id)
-        this.assertQueriedOpeningMetadataIsValid(qUpcomingOpening.metadata, expectedMeta.metadata)
+        assertQueriedOpeningMetadataIsValid(qUpcomingOpening.metadata, expectedMeta.metadata)
       } else {
         assert.isUndefined(qUpcomingOpening)
       }

+ 9 - 13
tests/integration-tests/src/fixtures/workingGroups/HireWorkersFixture.ts

@@ -49,20 +49,16 @@ export class HireWorkersFixture extends BaseQueryNodeFixture {
     await new FixtureRunner(buyMembershipFixture).run()
     const memberIds = buyMembershipFixture.getCreatedMembers()
 
-    const applicantContexts = roleAccounts.map((account, i) => ({
-      account,
-      memberId: memberIds[i],
-    }))
-
-    await Promise.all(
-      applicantContexts.map((applicantContext, i) => {
-        const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(this.api, this.query, applicantContext, [
-          stakingAccounts[i],
-        ])
-        return new FixtureRunner(addStakingAccFixture).run()
-      })
+    const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(
+      this.api,
+      this.query,
+      memberIds.map((memberId, i) => ({
+        asMember: memberId,
+        account: stakingAccounts[i],
+        stakeAmount: openingStake,
+      }))
     )
-    await Promise.all(stakingAccounts.map((a) => this.api.treasuryTransferBalance(a, openingStake)))
+    await new FixtureRunner(addStakingAccFixture).run()
 
     const applicants: ApplicantDetails[] = memberIds.map((memberId, i) => ({
       memberId,

+ 2 - 3
tests/integration-tests/src/fixtures/workingGroups/UpdateGroupStatusFixture.ts

@@ -17,7 +17,6 @@ import { Bytes } from '@polkadot/types'
 
 export class UpdateGroupStatusFixture extends BaseWorkingGroupFixture {
   protected updates: IWorkingGroupMetadata[]
-  protected areExtrinsicsOrderSensitive = true
 
   public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, updates: IWorkingGroupMetadata[]) {
     super(api, query, group)
@@ -28,9 +27,9 @@ export class UpdateGroupStatusFixture extends BaseWorkingGroupFixture {
     return this.api.getLeadRoleKey(this.group)
   }
 
-  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[][]> {
     return this.updates.map((update) => {
-      return this.api.tx[this.group].setStatusText(this.getActionMetadataBytes(update))
+      return [this.api.tx[this.group].setStatusText(this.getActionMetadataBytes(update))]
     })
   }
 

+ 38 - 1
tests/integration-tests/src/fixtures/workingGroups/utils.ts

@@ -1,4 +1,7 @@
-import { OpeningMetadata } from '@joystream/metadata-protobuf'
+import { IOpeningMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
+import { assert } from 'chai'
+import { OpeningMetadataFieldsFragment } from '../../graphql/generated/queries'
+
 import { ApplicationFormQuestionType } from '../../graphql/generated/schema'
 
 export const queryNodeQuestionTypeToMetadataQuestionType = (
@@ -10,3 +13,37 @@ export const queryNodeQuestionTypeToMetadataQuestionType = (
 
   return OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA
 }
+
+export const assertQueriedOpeningMetadataIsValid = (
+  qOpeningMeta: OpeningMetadataFieldsFragment,
+  expectedMetadata?: IOpeningMetadata | null
+): void => {
+  const {
+    shortDescription,
+    description,
+    expectedEndingTimestamp,
+    hiringLimit,
+    applicationDetails,
+    applicationFormQuestions,
+  } = expectedMetadata || {}
+  assert.equal(qOpeningMeta.shortDescription, shortDescription || null)
+  assert.equal(qOpeningMeta.description, description || null)
+  assert.equal(
+    qOpeningMeta.expectedEnding ? new Date(qOpeningMeta.expectedEnding).getTime() : qOpeningMeta.expectedEnding,
+    expectedEndingTimestamp || null
+  )
+  assert.equal(qOpeningMeta.hiringLimit, hiringLimit || null)
+  assert.equal(qOpeningMeta.applicationDetails, applicationDetails || null)
+  assert.deepEqual(
+    qOpeningMeta.applicationFormQuestions
+      .sort((a, b) => a.index - b.index)
+      .map(({ question, type }) => ({
+        question,
+        type: queryNodeQuestionTypeToMetadataQuestionType(type),
+      })),
+    (applicationFormQuestions || []).map(({ question, type }) => ({
+      question: question || null,
+      type: type || 0,
+    }))
+  )
+}

+ 1 - 2
tests/integration-tests/src/flows/membership/managingStakingAccounts.ts

@@ -26,8 +26,7 @@ export default async function managingStakingAccounts({ api, query, env }: FlowP
   const addStakingAccountsHappyCaseFixture = new AddStakingAccountsHappyCaseFixture(
     api,
     query,
-    { account, memberId },
-    stakingAccounts
+    stakingAccounts.map((account) => ({ asMember: memberId, account }))
   )
   await new FixtureRunner(addStakingAccountsHappyCaseFixture).runWithQueryNodeChecks()
 

+ 106 - 0
tests/integration-tests/src/flows/proposals/index.ts

@@ -0,0 +1,106 @@
+import { FlowProps } from '../../Flow'
+import { CreateProposalsFixture } from '../../fixtures/proposals'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { ProposalDetailsJsonByType, ProposalType } from '../../types'
+import { Utils } from '../../utils'
+import { DEFAULT_OPENING_PARAMS } from '../../fixtures/workingGroups'
+import { OpeningMetadata } from '@joystream/metadata-protobuf'
+import _ from 'lodash'
+import { InitializeCouncilFixture } from '../../fixtures/council'
+
+// const testProposalDetails = {
+//   // TODO:
+//   // RuntimeUpgrade: [],
+//   // CreateBlogPost: [],
+//   // // Requires a proposal:
+//   // VetoProposal: [],
+//   // // Requires an opening:
+//   // CancelWorkingGroupLeadOpening: [],
+//   // FillWorkingGroupLeadOpening: [],
+//   // // Requires a lead:
+//   // DecreaseWorkingGroupLeadStake: [],
+//   // SetWorkingGroupLeadReward: [],
+//   // SlashWorkingGroupLead: [],
+//   // TerminateWorkingGroupLead: [],
+//   // // Requires a blog post
+//   // EditBlogPost: [],
+//   // LockBlogPost: [],
+//   // UnlockBlogPost: [],
+// }
+
+export default async function creatingProposals({ api, query }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:creating-proposals')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  debug('Initializing council...')
+  const initializeCouncilFixture = new InitializeCouncilFixture(api, query)
+  await new FixtureRunner(initializeCouncilFixture).run()
+  debug('Council initialized')
+
+  const accountsToFund = (await api.createKeyPairs(5)).map((key) => key.address)
+  const proposalsDetails: { [K in ProposalType]?: ProposalDetailsJsonByType<K> } = {
+    AmendConstitution: 'New constitution',
+    FundingRequest: accountsToFund.map((a, i) => ({ account: a, amount: (i + 1) * 1000 })),
+    Signal: 'Text',
+    CreateWorkingGroupLeadOpening: {
+      description: Utils.metadataToBytes(OpeningMetadata, DEFAULT_OPENING_PARAMS.metadata),
+      reward_per_block: 100,
+      stake_policy: {
+        leaving_unstaking_period: 10,
+        stake_amount: 10,
+      },
+      working_group: 'Content',
+    },
+    SetCouncilBudgetIncrement: 1_000_000,
+    SetCouncilorReward: 100,
+    SetInitialInvitationBalance: 10,
+    SetInitialInvitationCount: 5,
+    SetMaxValidatorCount: 100,
+    SetMembershipLeadInvitationQuota: 50,
+    SetMembershipPrice: 500,
+    SetReferralCut: 25,
+    UpdateWorkingGroupBudget: [1_000_000, 'Content', 'Negative'],
+  }
+
+  const proposalsN = Object.keys(proposalsDetails).length
+
+  const memberKeys = (await api.createKeyPairs(proposalsN)).map((key) => key.address)
+  const membersFixture = new BuyMembershipHappyCaseFixture(api, query, memberKeys)
+  await new FixtureRunner(membersFixture).run()
+  const memberIds = membersFixture.getCreatedMembers()
+
+  const { maxActiveProposalLimit } = api.consts.proposalsEngine
+  const proposalsPerBatch = maxActiveProposalLimit.toNumber()
+  let i = 0
+  let batch: [ProposalType, ProposalDetailsJsonByType][]
+  while (
+    (batch = (Object.entries(proposalsDetails) as [ProposalType, ProposalDetailsJsonByType][]).slice(
+      i * proposalsPerBatch,
+      (i + 1) * proposalsPerBatch
+    )).length
+  ) {
+    await api.untilProposalsCanBeCreated(proposalsPerBatch)
+    await Promise.all(
+      batch.map(async ([proposalType, proposalDetails], j) => {
+        debug(`Creating ${proposalType} proposal...`)
+        const createProposalFixture = new CreateProposalsFixture(api, query, [
+          {
+            asMember: memberIds[i * proposalsPerBatch + j],
+            title: `${_.startCase(proposalType)}`,
+            description: `Test ${proposalType} proposal`,
+            type: proposalType as ProposalType,
+            details: proposalDetails,
+          },
+        ])
+        await new FixtureRunner(createProposalFixture).runWithQueryNodeChecks()
+      })
+    )
+    ++i
+  }
+
+  debug('Done')
+}

+ 5 - 4
tests/integration-tests/src/flows/working-groups/leadOpening.ts

@@ -30,10 +30,11 @@ export default async function leadOpening({ api, query, env }: FlowProps): Promi
       await new FixtureRunner(buyMembershipFixture).run()
       const [memberId] = buyMembershipFixture.getCreatedMembers()
 
-      const applicantContext = { account: roleAccount, memberId }
-
-      const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(api, query, applicantContext, [
-        stakingAccount,
+      const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(api, query, [
+        {
+          asMember: memberId,
+          account: stakingAccount,
+        },
       ])
       await new FixtureRunner(addStakingAccFixture).run()
       await api.treasuryTransferBalance(stakingAccount, openingStake)

+ 9 - 13
tests/integration-tests/src/flows/working-groups/openingsAndApplications.ts

@@ -93,20 +93,16 @@ export default async function openingsAndApplications({ api, query, env }: FlowP
       await new FixtureRunner(buyMembershipFixture).run()
       const memberIds = buyMembershipFixture.getCreatedMembers()
 
-      const applicantContexts = roleAccounts.map((account, i) => ({
-        account,
-        memberId: memberIds[i],
-      }))
-
-      await Promise.all(
-        applicantContexts.map((applicantContext, i) => {
-          const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(api, query, applicantContext, [
-            stakingAccounts[i],
-          ])
-          return new FixtureRunner(addStakingAccFixture).run()
-        })
+      const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(
+        api,
+        query,
+        memberIds.map((memberId, i) => ({
+          asMember: memberId,
+          account: stakingAccounts[i],
+          stakeAmount: openingStake,
+        }))
       )
-      await Promise.all(stakingAccounts.map((a) => api.treasuryTransferBalance(a, openingStake)))
+      await new FixtureRunner(addStakingAccFixture).run()
 
       const applicants: ApplicantDetails[] = memberIds.map((memberId, i) => ({
         memberId,

+ 777 - 32
tests/integration-tests/src/graphql/generated/queries.ts

@@ -172,11 +172,11 @@ export type StakingAccountAddedEventFieldsFragment = {
   member: { id: string }
 }
 
-export type GetStakingAccountAddedEventsByMemberIdQueryVariables = Types.Exact<{
-  memberId: Types.Scalars['ID']
+export type GetStakingAccountAddedEventsByEventIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
 }>
 
-export type GetStakingAccountAddedEventsByMemberIdQuery = {
+export type GetStakingAccountAddedEventsByEventIdsQuery = {
   stakingAccountAddedEvents: Array<StakingAccountAddedEventFieldsFragment>
 }
 
@@ -191,11 +191,11 @@ export type StakingAccountConfirmedEventFieldsFragment = {
   member: { id: string }
 }
 
-export type GetStakingAccountConfirmedEventsByMemberIdQueryVariables = Types.Exact<{
-  memberId: Types.Scalars['ID']
+export type GetStakingAccountConfirmedEventsByEventIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
 }>
 
-export type GetStakingAccountConfirmedEventsByMemberIdQuery = {
+export type GetStakingAccountConfirmedEventsByEventIdsQuery = {
   stakingAccountConfirmedEvents: Array<StakingAccountConfirmedEventFieldsFragment>
 }
 
@@ -290,6 +290,404 @@ export type GetInitialInvitationCountUpdatedEventsByEventIdQuery = {
   initialInvitationCountUpdatedEvents: Array<InitialInvitationCountUpdatedEventFieldsFragment>
 }
 
+type ProposalStatusFields_ProposalStatusDeciding_Fragment = {
+  __typename: 'ProposalStatusDeciding'
+  proposalStatusUpdatedEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusGracing_Fragment = {
+  __typename: 'ProposalStatusGracing'
+  proposalStatusUpdatedEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusDormant_Fragment = {
+  __typename: 'ProposalStatusDormant'
+  proposalStatusUpdatedEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusVetoed_Fragment = {
+  __typename: 'ProposalStatusVetoed'
+  proposalDecisionMadeEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusExecuted_Fragment = {
+  __typename: 'ProposalStatusExecuted'
+  proposalExecutedEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusExecutionFailed_Fragment = {
+  __typename: 'ProposalStatusExecutionFailed'
+  errorMessage: string
+  proposalExecutedEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusSlashed_Fragment = {
+  __typename: 'ProposalStatusSlashed'
+  proposalDecisionMadeEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusRejected_Fragment = {
+  __typename: 'ProposalStatusRejected'
+  proposalDecisionMadeEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusExpired_Fragment = {
+  __typename: 'ProposalStatusExpired'
+  proposalDecisionMadeEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusCancelled_Fragment = {
+  __typename: 'ProposalStatusCancelled'
+  canelledInEvent?: Types.Maybe<{ id: string }>
+}
+
+type ProposalStatusFields_ProposalStatusCanceledByRuntime_Fragment = {
+  __typename: 'ProposalStatusCanceledByRuntime'
+  proposalDecisionMadeEvent?: Types.Maybe<{ id: string }>
+}
+
+export type ProposalStatusFieldsFragment =
+  | ProposalStatusFields_ProposalStatusDeciding_Fragment
+  | ProposalStatusFields_ProposalStatusGracing_Fragment
+  | ProposalStatusFields_ProposalStatusDormant_Fragment
+  | ProposalStatusFields_ProposalStatusVetoed_Fragment
+  | ProposalStatusFields_ProposalStatusExecuted_Fragment
+  | ProposalStatusFields_ProposalStatusExecutionFailed_Fragment
+  | ProposalStatusFields_ProposalStatusSlashed_Fragment
+  | ProposalStatusFields_ProposalStatusRejected_Fragment
+  | ProposalStatusFields_ProposalStatusExpired_Fragment
+  | ProposalStatusFields_ProposalStatusCancelled_Fragment
+  | ProposalStatusFields_ProposalStatusCanceledByRuntime_Fragment
+
+type ProposalDetailsFields_SignalProposalDetails_Fragment = { __typename: 'SignalProposalDetails'; text: string }
+
+type ProposalDetailsFields_RuntimeUpgradeProposalDetails_Fragment = {
+  __typename: 'RuntimeUpgradeProposalDetails'
+  wasmBytecode: any
+}
+
+type ProposalDetailsFields_FundingRequestProposalDetails_Fragment = {
+  __typename: 'FundingRequestProposalDetails'
+  destinationsList?: Types.Maybe<{ destinations: Array<{ amount: any; account: string }> }>
+}
+
+type ProposalDetailsFields_SetMaxValidatorCountProposalDetails_Fragment = {
+  __typename: 'SetMaxValidatorCountProposalDetails'
+  newMaxValidatorCount: number
+}
+
+type ProposalDetailsFields_CreateWorkingGroupLeadOpeningProposalDetails_Fragment = {
+  __typename: 'CreateWorkingGroupLeadOpeningProposalDetails'
+  stakeAmount: any
+  unstakingPeriod: number
+  rewardPerBlock: any
+  metadata?: Types.Maybe<OpeningMetadataFieldsFragment>
+  group?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_FillWorkingGroupLeadOpeningProposalDetails_Fragment = {
+  __typename: 'FillWorkingGroupLeadOpeningProposalDetails'
+  opening?: Types.Maybe<{ id: string }>
+  application?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_UpdateWorkingGroupBudgetProposalDetails_Fragment = {
+  __typename: 'UpdateWorkingGroupBudgetProposalDetails'
+  amount: any
+  group?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_DecreaseWorkingGroupLeadStakeProposalDetails_Fragment = {
+  __typename: 'DecreaseWorkingGroupLeadStakeProposalDetails'
+  amount: any
+  lead?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_SlashWorkingGroupLeadProposalDetails_Fragment = {
+  __typename: 'SlashWorkingGroupLeadProposalDetails'
+  amount: any
+  lead?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_SetWorkingGroupLeadRewardProposalDetails_Fragment = {
+  __typename: 'SetWorkingGroupLeadRewardProposalDetails'
+  newRewardPerBlock: any
+  lead?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_TerminateWorkingGroupLeadProposalDetails_Fragment = {
+  __typename: 'TerminateWorkingGroupLeadProposalDetails'
+  slashingAmount?: Types.Maybe<any>
+  lead?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_AmendConstitutionProposalDetails_Fragment = {
+  __typename: 'AmendConstitutionProposalDetails'
+  text: string
+}
+
+type ProposalDetailsFields_CancelWorkingGroupLeadOpeningProposalDetails_Fragment = {
+  __typename: 'CancelWorkingGroupLeadOpeningProposalDetails'
+  opening?: Types.Maybe<{ id: string }>
+}
+
+type ProposalDetailsFields_SetMembershipPriceProposalDetails_Fragment = {
+  __typename: 'SetMembershipPriceProposalDetails'
+  newPrice: any
+}
+
+type ProposalDetailsFields_SetCouncilBudgetIncrementProposalDetails_Fragment = {
+  __typename: 'SetCouncilBudgetIncrementProposalDetails'
+  newAmount: any
+}
+
+type ProposalDetailsFields_SetCouncilorRewardProposalDetails_Fragment = {
+  __typename: 'SetCouncilorRewardProposalDetails'
+  newRewardPerBlock: any
+}
+
+type ProposalDetailsFields_SetInitialInvitationBalanceProposalDetails_Fragment = {
+  __typename: 'SetInitialInvitationBalanceProposalDetails'
+  newInitialInvitationBalance: any
+}
+
+type ProposalDetailsFields_SetInitialInvitationCountProposalDetails_Fragment = {
+  __typename: 'SetInitialInvitationCountProposalDetails'
+  newInitialInvitationsCount: number
+}
+
+type ProposalDetailsFields_SetMembershipLeadInvitationQuotaProposalDetails_Fragment = {
+  __typename: 'SetMembershipLeadInvitationQuotaProposalDetails'
+  newLeadInvitationQuota: number
+}
+
+type ProposalDetailsFields_SetReferralCutProposalDetails_Fragment = {
+  __typename: 'SetReferralCutProposalDetails'
+  newReferralCut: number
+}
+
+type ProposalDetailsFields_CreateBlogPostProposalDetails_Fragment = { __typename: 'CreateBlogPostProposalDetails' }
+
+type ProposalDetailsFields_EditBlogPostProposalDetails_Fragment = { __typename: 'EditBlogPostProposalDetails' }
+
+type ProposalDetailsFields_LockBlogPostProposalDetails_Fragment = { __typename: 'LockBlogPostProposalDetails' }
+
+type ProposalDetailsFields_UnlockBlogPostProposalDetails_Fragment = { __typename: 'UnlockBlogPostProposalDetails' }
+
+type ProposalDetailsFields_VetoProposalDetails_Fragment = {
+  __typename: 'VetoProposalDetails'
+  proposal?: Types.Maybe<{ id: string }>
+}
+
+export type ProposalDetailsFieldsFragment =
+  | ProposalDetailsFields_SignalProposalDetails_Fragment
+  | ProposalDetailsFields_RuntimeUpgradeProposalDetails_Fragment
+  | ProposalDetailsFields_FundingRequestProposalDetails_Fragment
+  | ProposalDetailsFields_SetMaxValidatorCountProposalDetails_Fragment
+  | ProposalDetailsFields_CreateWorkingGroupLeadOpeningProposalDetails_Fragment
+  | ProposalDetailsFields_FillWorkingGroupLeadOpeningProposalDetails_Fragment
+  | ProposalDetailsFields_UpdateWorkingGroupBudgetProposalDetails_Fragment
+  | ProposalDetailsFields_DecreaseWorkingGroupLeadStakeProposalDetails_Fragment
+  | ProposalDetailsFields_SlashWorkingGroupLeadProposalDetails_Fragment
+  | ProposalDetailsFields_SetWorkingGroupLeadRewardProposalDetails_Fragment
+  | ProposalDetailsFields_TerminateWorkingGroupLeadProposalDetails_Fragment
+  | ProposalDetailsFields_AmendConstitutionProposalDetails_Fragment
+  | ProposalDetailsFields_CancelWorkingGroupLeadOpeningProposalDetails_Fragment
+  | ProposalDetailsFields_SetMembershipPriceProposalDetails_Fragment
+  | ProposalDetailsFields_SetCouncilBudgetIncrementProposalDetails_Fragment
+  | ProposalDetailsFields_SetCouncilorRewardProposalDetails_Fragment
+  | ProposalDetailsFields_SetInitialInvitationBalanceProposalDetails_Fragment
+  | ProposalDetailsFields_SetInitialInvitationCountProposalDetails_Fragment
+  | ProposalDetailsFields_SetMembershipLeadInvitationQuotaProposalDetails_Fragment
+  | ProposalDetailsFields_SetReferralCutProposalDetails_Fragment
+  | ProposalDetailsFields_CreateBlogPostProposalDetails_Fragment
+  | ProposalDetailsFields_EditBlogPostProposalDetails_Fragment
+  | ProposalDetailsFields_LockBlogPostProposalDetails_Fragment
+  | ProposalDetailsFields_UnlockBlogPostProposalDetails_Fragment
+  | ProposalDetailsFields_VetoProposalDetails_Fragment
+
+export type ProposalFieldsFragment = {
+  id: string
+  title: string
+  description: string
+  stakingAccount?: Types.Maybe<string>
+  exactExecutionBlock?: Types.Maybe<number>
+  councilApprovals: number
+  statusSetAtBlock: number
+  statusSetAtTime: any
+  details:
+    | ProposalDetailsFields_SignalProposalDetails_Fragment
+    | ProposalDetailsFields_RuntimeUpgradeProposalDetails_Fragment
+    | ProposalDetailsFields_FundingRequestProposalDetails_Fragment
+    | ProposalDetailsFields_SetMaxValidatorCountProposalDetails_Fragment
+    | ProposalDetailsFields_CreateWorkingGroupLeadOpeningProposalDetails_Fragment
+    | ProposalDetailsFields_FillWorkingGroupLeadOpeningProposalDetails_Fragment
+    | ProposalDetailsFields_UpdateWorkingGroupBudgetProposalDetails_Fragment
+    | ProposalDetailsFields_DecreaseWorkingGroupLeadStakeProposalDetails_Fragment
+    | ProposalDetailsFields_SlashWorkingGroupLeadProposalDetails_Fragment
+    | ProposalDetailsFields_SetWorkingGroupLeadRewardProposalDetails_Fragment
+    | ProposalDetailsFields_TerminateWorkingGroupLeadProposalDetails_Fragment
+    | ProposalDetailsFields_AmendConstitutionProposalDetails_Fragment
+    | ProposalDetailsFields_CancelWorkingGroupLeadOpeningProposalDetails_Fragment
+    | ProposalDetailsFields_SetMembershipPriceProposalDetails_Fragment
+    | ProposalDetailsFields_SetCouncilBudgetIncrementProposalDetails_Fragment
+    | ProposalDetailsFields_SetCouncilorRewardProposalDetails_Fragment
+    | ProposalDetailsFields_SetInitialInvitationBalanceProposalDetails_Fragment
+    | ProposalDetailsFields_SetInitialInvitationCountProposalDetails_Fragment
+    | ProposalDetailsFields_SetMembershipLeadInvitationQuotaProposalDetails_Fragment
+    | ProposalDetailsFields_SetReferralCutProposalDetails_Fragment
+    | ProposalDetailsFields_CreateBlogPostProposalDetails_Fragment
+    | ProposalDetailsFields_EditBlogPostProposalDetails_Fragment
+    | ProposalDetailsFields_LockBlogPostProposalDetails_Fragment
+    | ProposalDetailsFields_UnlockBlogPostProposalDetails_Fragment
+    | ProposalDetailsFields_VetoProposalDetails_Fragment
+  creator: { id: string }
+  proposalStatusUpdates: Array<{ id: string }>
+  votes: Array<{ id: string }>
+  status:
+    | ProposalStatusFields_ProposalStatusDeciding_Fragment
+    | ProposalStatusFields_ProposalStatusGracing_Fragment
+    | ProposalStatusFields_ProposalStatusDormant_Fragment
+    | ProposalStatusFields_ProposalStatusVetoed_Fragment
+    | ProposalStatusFields_ProposalStatusExecuted_Fragment
+    | ProposalStatusFields_ProposalStatusExecutionFailed_Fragment
+    | ProposalStatusFields_ProposalStatusSlashed_Fragment
+    | ProposalStatusFields_ProposalStatusRejected_Fragment
+    | ProposalStatusFields_ProposalStatusExpired_Fragment
+    | ProposalStatusFields_ProposalStatusCancelled_Fragment
+    | ProposalStatusFields_ProposalStatusCanceledByRuntime_Fragment
+}
+
+export type GetProposalsByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalsByIdsQuery = { proposals: Array<ProposalFieldsFragment> }
+
+export type ProposalCreatedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  proposal: { id: string }
+}
+
+export type GetProposalCreatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalCreatedEventsByEventIdsQuery = {
+  proposalCreatedEvents: Array<ProposalCreatedEventFieldsFragment>
+}
+
+export type ProposalStatusUpdatedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  proposal: { id: string }
+  newStatus:
+    | { __typename: 'ProposalStatusDeciding' }
+    | { __typename: 'ProposalStatusGracing' }
+    | { __typename: 'ProposalStatusDormant' }
+}
+
+export type GetProposalStatusUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalStatusUpdatedEventsByEventIdsQuery = {
+  proposalStatusUpdatedEvents: Array<ProposalStatusUpdatedEventFieldsFragment>
+}
+
+export type ProposalDecisionMadeEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  proposal: { id: string }
+  decisionStatus:
+    | { __typename: 'ProposalStatusDormant' }
+    | { __typename: 'ProposalStatusGracing' }
+    | { __typename: 'ProposalStatusVetoed' }
+    | { __typename: 'ProposalStatusSlashed' }
+    | { __typename: 'ProposalStatusRejected' }
+    | { __typename: 'ProposalStatusExpired' }
+    | { __typename: 'ProposalStatusCancelled' }
+    | { __typename: 'ProposalStatusCanceledByRuntime' }
+}
+
+export type GetProposalDecisionMadeEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalDecisionMadeEventsByEventIdsQuery = {
+  proposalDecisionMadeEvents: Array<ProposalDecisionMadeEventFieldsFragment>
+}
+
+export type ProposalExecutedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  proposal: { id: string }
+  executionStatus: { errorMessage: string }
+}
+
+export type GetProposalExecutedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalExecutedEventsByEventIdsQuery = {
+  proposalExecutedEvents: Array<ProposalExecutedEventFieldsFragment>
+}
+
+export type ProposalVotedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  voteKind: Types.ProposalVoteKind
+  rationale: string
+  votingRound: number
+  voter: { id: string }
+  proposal: { id: string }
+}
+
+export type GetProposalVotedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalVotedEventsByEventIdsQuery = { proposalVotedEvents: Array<ProposalVotedEventFieldsFragment> }
+
+export type ProposalCancelledEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  proposal: { id: string }
+}
+
+export type GetProposalCancelledEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetProposalCancelledEventsByEventIdsQuery = {
+  proposalCancelledEvents: Array<ProposalCancelledEventFieldsFragment>
+}
+
 export type ApplicationBasicFieldsFragment = {
   id: string
   runtimeId: number
@@ -1074,6 +1472,317 @@ export const InitialInvitationCountUpdatedEventFields = gql`
     newInitialInvitationCount
   }
 `
+export const ApplicationFormQuestionFields = gql`
+  fragment ApplicationFormQuestionFields on ApplicationFormQuestion {
+    question
+    type
+    index
+  }
+`
+export const OpeningMetadataFields = gql`
+  fragment OpeningMetadataFields on WorkingGroupOpeningMetadata {
+    shortDescription
+    description
+    hiringLimit
+    expectedEnding
+    applicationDetails
+    applicationFormQuestions {
+      ...ApplicationFormQuestionFields
+    }
+  }
+  ${ApplicationFormQuestionFields}
+`
+export const ProposalDetailsFields = gql`
+  fragment ProposalDetailsFields on ProposalDetails {
+    __typename
+    ... on SignalProposalDetails {
+      text
+    }
+    ... on RuntimeUpgradeProposalDetails {
+      wasmBytecode
+    }
+    ... on FundingRequestProposalDetails {
+      destinationsList {
+        destinations {
+          amount
+          account
+        }
+      }
+    }
+    ... on SetMaxValidatorCountProposalDetails {
+      newMaxValidatorCount
+    }
+    ... on CreateWorkingGroupLeadOpeningProposalDetails {
+      metadata {
+        ...OpeningMetadataFields
+      }
+      stakeAmount
+      unstakingPeriod
+      rewardPerBlock
+      group {
+        id
+      }
+    }
+    ... on FillWorkingGroupLeadOpeningProposalDetails {
+      opening {
+        id
+      }
+      application {
+        id
+      }
+    }
+    ... on UpdateWorkingGroupBudgetProposalDetails {
+      amount
+      group {
+        id
+      }
+    }
+    ... on DecreaseWorkingGroupLeadStakeProposalDetails {
+      lead {
+        id
+      }
+      amount
+    }
+    ... on SlashWorkingGroupLeadProposalDetails {
+      lead {
+        id
+      }
+      amount
+    }
+    ... on SetWorkingGroupLeadRewardProposalDetails {
+      lead {
+        id
+      }
+      newRewardPerBlock
+    }
+    ... on TerminateWorkingGroupLeadProposalDetails {
+      lead {
+        id
+      }
+      slashingAmount
+    }
+    ... on AmendConstitutionProposalDetails {
+      text
+    }
+    ... on CancelWorkingGroupLeadOpeningProposalDetails {
+      opening {
+        id
+      }
+    }
+    ... on SetMembershipPriceProposalDetails {
+      newPrice
+    }
+    ... on SetCouncilBudgetIncrementProposalDetails {
+      newAmount
+    }
+    ... on SetCouncilorRewardProposalDetails {
+      newRewardPerBlock
+    }
+    ... on SetInitialInvitationBalanceProposalDetails {
+      newInitialInvitationBalance
+    }
+    ... on SetInitialInvitationCountProposalDetails {
+      newInitialInvitationsCount
+    }
+    ... on SetMembershipLeadInvitationQuotaProposalDetails {
+      newLeadInvitationQuota
+    }
+    ... on SetReferralCutProposalDetails {
+      newReferralCut
+    }
+    ... on VetoProposalDetails {
+      proposal {
+        id
+      }
+    }
+  }
+  ${OpeningMetadataFields}
+`
+export const ProposalStatusFields = gql`
+  fragment ProposalStatusFields on ProposalStatus {
+    __typename
+    ... on ProposalStatusDeciding {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusGracing {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusDormant {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusVetoed {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+    ... on ProposalStatusExecuted {
+      proposalExecutedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusExecutionFailed {
+      proposalExecutedEvent {
+        id
+      }
+      errorMessage
+    }
+    ... on ProposalStatusSlashed {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+    ... on ProposalStatusRejected {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+    ... on ProposalStatusExpired {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+    ... on ProposalStatusCancelled {
+      canelledInEvent {
+        id
+      }
+    }
+    ... on ProposalStatusCanceledByRuntime {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+  }
+`
+export const ProposalFields = gql`
+  fragment ProposalFields on Proposal {
+    id
+    title
+    description
+    details {
+      ...ProposalDetailsFields
+    }
+    stakingAccount
+    creator {
+      id
+    }
+    exactExecutionBlock
+    councilApprovals
+    proposalStatusUpdates {
+      id
+    }
+    votes {
+      id
+    }
+    status {
+      ...ProposalStatusFields
+    }
+    statusSetAtBlock
+    statusSetAtTime
+  }
+  ${ProposalDetailsFields}
+  ${ProposalStatusFields}
+`
+export const ProposalCreatedEventFields = gql`
+  fragment ProposalCreatedEventFields on ProposalCreatedEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    proposal {
+      id
+    }
+  }
+`
+export const ProposalStatusUpdatedEventFields = gql`
+  fragment ProposalStatusUpdatedEventFields on ProposalStatusUpdatedEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    proposal {
+      id
+    }
+    newStatus {
+      __typename
+    }
+  }
+`
+export const ProposalDecisionMadeEventFields = gql`
+  fragment ProposalDecisionMadeEventFields on ProposalDecisionMadeEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    proposal {
+      id
+    }
+    decisionStatus {
+      __typename
+    }
+  }
+`
+export const ProposalExecutedEventFields = gql`
+  fragment ProposalExecutedEventFields on ProposalExecutedEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    proposal {
+      id
+    }
+    executionStatus {
+      ... on ProposalStatusExecutionFailed {
+        errorMessage
+      }
+    }
+  }
+`
+export const ProposalVotedEventFields = gql`
+  fragment ProposalVotedEventFields on ProposalVotedEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    voter {
+      id
+    }
+    voteKind
+    proposal {
+      id
+    }
+    rationale
+    votingRound
+  }
+`
+export const ProposalCancelledEventFields = gql`
+  fragment ProposalCancelledEventFields on ProposalCancelledEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    proposal {
+      id
+    }
+  }
+`
 export const ApplicationBasicFields = gql`
   fragment ApplicationBasicFields on WorkingGroupApplication {
     id
@@ -1118,26 +1827,6 @@ export const OpeningStatusFields = gql`
     }
   }
 `
-export const ApplicationFormQuestionFields = gql`
-  fragment ApplicationFormQuestionFields on ApplicationFormQuestion {
-    question
-    type
-    index
-  }
-`
-export const OpeningMetadataFields = gql`
-  fragment OpeningMetadataFields on WorkingGroupOpeningMetadata {
-    shortDescription
-    description
-    hiringLimit
-    expectedEnding
-    applicationDetails
-    applicationFormQuestions {
-      ...ApplicationFormQuestionFields
-    }
-  }
-  ${ApplicationFormQuestionFields}
-`
 export const OpeningFields = gql`
   fragment OpeningFields on WorkingGroupOpening {
     id
@@ -1713,17 +2402,17 @@ export const GetInvitesTransferredEventsBySourceMemberId = gql`
   }
   ${InvitesTransferredEventFields}
 `
-export const GetStakingAccountAddedEventsByMemberId = gql`
-  query getStakingAccountAddedEventsByMemberId($memberId: ID!) {
-    stakingAccountAddedEvents(where: { member: { id_eq: $memberId } }) {
+export const GetStakingAccountAddedEventsByEventIds = gql`
+  query getStakingAccountAddedEventsByEventIds($ids: [ID!]) {
+    stakingAccountAddedEvents(where: { id_in: $ids }) {
       ...StakingAccountAddedEventFields
     }
   }
   ${StakingAccountAddedEventFields}
 `
-export const GetStakingAccountConfirmedEventsByMemberId = gql`
-  query getStakingAccountConfirmedEventsByMemberId($memberId: ID!) {
-    stakingAccountConfirmedEvents(where: { member: { id_eq: $memberId } }) {
+export const GetStakingAccountConfirmedEventsByEventIds = gql`
+  query getStakingAccountConfirmedEventsByEventIds($ids: [ID!]) {
+    stakingAccountConfirmedEvents(where: { id_in: $ids }) {
       ...StakingAccountConfirmedEventFields
     }
   }
@@ -1769,6 +2458,62 @@ export const GetInitialInvitationCountUpdatedEventsByEventId = gql`
   }
   ${InitialInvitationCountUpdatedEventFields}
 `
+export const GetProposalsByIds = gql`
+  query getProposalsByIds($ids: [ID!]) {
+    proposals(where: { id_in: $ids }) {
+      ...ProposalFields
+    }
+  }
+  ${ProposalFields}
+`
+export const GetProposalCreatedEventsByEventIds = gql`
+  query getProposalCreatedEventsByEventIds($eventIds: [ID!]) {
+    proposalCreatedEvents(where: { id_in: $eventIds }) {
+      ...ProposalCreatedEventFields
+    }
+  }
+  ${ProposalCreatedEventFields}
+`
+export const GetProposalStatusUpdatedEventsByEventIds = gql`
+  query getProposalStatusUpdatedEventsByEventIds($eventIds: [ID!]) {
+    proposalStatusUpdatedEvents(where: { id_in: $eventIds }) {
+      ...ProposalStatusUpdatedEventFields
+    }
+  }
+  ${ProposalStatusUpdatedEventFields}
+`
+export const GetProposalDecisionMadeEventsByEventIds = gql`
+  query getProposalDecisionMadeEventsByEventIds($eventIds: [ID!]) {
+    proposalDecisionMadeEvents(where: { id_in: $eventIds }) {
+      ...ProposalDecisionMadeEventFields
+    }
+  }
+  ${ProposalDecisionMadeEventFields}
+`
+export const GetProposalExecutedEventsByEventIds = gql`
+  query getProposalExecutedEventsByEventIds($eventIds: [ID!]) {
+    proposalExecutedEvents(where: { id_in: $eventIds }) {
+      ...ProposalExecutedEventFields
+    }
+  }
+  ${ProposalExecutedEventFields}
+`
+export const GetProposalVotedEventsByEventIds = gql`
+  query getProposalVotedEventsByEventIds($eventIds: [ID!]) {
+    proposalVotedEvents(where: { id_in: $eventIds }) {
+      ...ProposalVotedEventFields
+    }
+  }
+  ${ProposalVotedEventFields}
+`
+export const GetProposalCancelledEventsByEventIds = gql`
+  query getProposalCancelledEventsByEventIds($eventIds: [ID!]) {
+    proposalCancelledEvents(where: { id_in: $eventIds }) {
+      ...ProposalCancelledEventFields
+    }
+  }
+  ${ProposalCancelledEventFields}
+`
 export const GetOpeningById = gql`
   query getOpeningById($openingId: ID!) {
     workingGroupOpeningByUniqueInput(where: { id: $openingId }) {

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 2931 - 81
tests/integration-tests/src/graphql/generated/schema.ts


+ 4 - 4
tests/integration-tests/src/graphql/queries/membershipEvents.graphql

@@ -131,8 +131,8 @@ fragment StakingAccountAddedEventFields on StakingAccountAddedEvent {
   account
 }
 
-query getStakingAccountAddedEventsByMemberId($memberId: ID!) {
-  stakingAccountAddedEvents(where: { member: { id_eq: $memberId } }) {
+query getStakingAccountAddedEventsByEventIds($ids: [ID!]) {
+  stakingAccountAddedEvents(where: { id_in: $ids }) {
     ...StakingAccountAddedEventFields
   }
 }
@@ -150,8 +150,8 @@ fragment StakingAccountConfirmedEventFields on StakingAccountConfirmedEvent {
   account
 }
 
-query getStakingAccountConfirmedEventsByMemberId($memberId: ID!) {
-  stakingAccountConfirmedEvents(where: { member: { id_eq: $memberId } }) {
+query getStakingAccountConfirmedEventsByEventIds($ids: [ID!]) {
+  stakingAccountConfirmedEvents(where: { id_in: $ids }) {
     ...StakingAccountConfirmedEventFields
   }
 }

+ 215 - 0
tests/integration-tests/src/graphql/queries/proposals.graphql

@@ -0,0 +1,215 @@
+fragment ProposalStatusFields on ProposalStatus {
+  __typename
+    ... on ProposalStatusDeciding {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusGracing {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusDormant {
+      proposalStatusUpdatedEvent {
+        id
+      }
+    }
+    ... on ProposalStatusVetoed {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+    ... on ProposalStatusExecuted {
+      proposalExecutedEvent {
+        id
+      }
+    }
+
+    ... on ProposalStatusExecutionFailed {
+      proposalExecutedEvent {
+        id
+      }
+      errorMessage
+    }
+    ... on ProposalStatusSlashed {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+
+    ... on ProposalStatusRejected {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+
+    ... on ProposalStatusExpired {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+
+    ... on ProposalStatusCancelled {
+      canelledInEvent {
+        id
+      }
+    }
+
+    ... on ProposalStatusCanceledByRuntime {
+      proposalDecisionMadeEvent {
+        id
+      }
+    }
+}
+
+fragment ProposalDetailsFields on ProposalDetails {
+    __typename
+    ... on SignalProposalDetails {
+      text
+    }
+    ... on RuntimeUpgradeProposalDetails {
+      wasmBytecode
+    }
+    ... on FundingRequestProposalDetails {
+      destinationsList {
+        destinations {
+          amount
+          account
+        }
+      }
+    }
+    ... on SetMaxValidatorCountProposalDetails {
+      newMaxValidatorCount
+    }
+
+    ... on CreateWorkingGroupLeadOpeningProposalDetails {
+      metadata {
+        ...OpeningMetadataFields
+      }
+      stakeAmount
+      unstakingPeriod
+      rewardPerBlock
+      group {
+        id
+      }
+    }
+    ... on FillWorkingGroupLeadOpeningProposalDetails {
+      opening {
+        id
+      }
+      application {
+        id
+      }
+    }
+    ... on UpdateWorkingGroupBudgetProposalDetails {
+      amount
+      group {
+        id
+      }
+    }
+    ... on DecreaseWorkingGroupLeadStakeProposalDetails {
+      lead {
+        id
+      }
+      amount
+    }
+
+    ... on SlashWorkingGroupLeadProposalDetails {
+      lead {
+        id
+      }
+      amount
+    }
+    ... on SetWorkingGroupLeadRewardProposalDetails {
+      lead {
+        id
+      }
+      newRewardPerBlock
+    }
+
+    ... on TerminateWorkingGroupLeadProposalDetails {
+      lead {
+        id
+      }
+      slashingAmount
+    }
+
+    ... on AmendConstitutionProposalDetails {
+      text
+    }
+
+    ... on CancelWorkingGroupLeadOpeningProposalDetails {
+      opening {
+        id
+      }
+    }
+
+    ... on SetMembershipPriceProposalDetails {
+      newPrice
+    }
+
+    ... on SetCouncilBudgetIncrementProposalDetails {
+      newAmount
+    }
+
+    ... on SetCouncilorRewardProposalDetails {
+      newRewardPerBlock
+    }
+
+    ... on SetInitialInvitationBalanceProposalDetails {
+      newInitialInvitationBalance
+    }
+
+    ... on SetInitialInvitationCountProposalDetails {
+      newInitialInvitationsCount
+    }
+
+    ... on SetMembershipLeadInvitationQuotaProposalDetails {
+      newLeadInvitationQuota
+    }
+
+    ... on SetReferralCutProposalDetails {
+      newReferralCut
+    }
+
+    # TODO: Blog proposals
+
+    ... on VetoProposalDetails {
+      proposal {
+        id
+      }
+    }
+}
+
+fragment ProposalFields on Proposal {
+  id
+  title
+  description
+  details {
+    ...ProposalDetailsFields
+  }
+  stakingAccount
+  creator {
+    id
+  }
+  exactExecutionBlock
+  councilApprovals
+  proposalStatusUpdates {
+    id
+  }
+  votes {
+    id
+  }
+  status {
+    ...ProposalStatusFields
+  }
+  statusSetAtBlock
+  statusSetAtTime
+}
+
+query getProposalsByIds($ids: [ID!]) {
+  proposals(where: { id_in: $ids }) {
+    ...ProposalFields
+  }
+}

+ 124 - 0
tests/integration-tests/src/graphql/queries/proposalsEvents.graphql

@@ -0,0 +1,124 @@
+fragment ProposalCreatedEventFields on ProposalCreatedEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  proposal {
+    id
+  }
+}
+
+query getProposalCreatedEventsByEventIds($eventIds: [ID!]) {
+  proposalCreatedEvents(where: { id_in: $eventIds }) {
+    ...ProposalCreatedEventFields
+  }
+}
+
+fragment ProposalStatusUpdatedEventFields on ProposalStatusUpdatedEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  proposal {
+    id
+  }
+  newStatus {
+    __typename
+  }
+}
+
+query getProposalStatusUpdatedEventsByEventIds($eventIds: [ID!]) {
+  proposalStatusUpdatedEvents(where: { id_in: $eventIds }) {
+    ...ProposalStatusUpdatedEventFields
+  }
+}
+
+fragment ProposalDecisionMadeEventFields on ProposalDecisionMadeEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  proposal {
+    id
+  }
+  decisionStatus {
+    __typename
+  }
+}
+
+query getProposalDecisionMadeEventsByEventIds($eventIds: [ID!]) {
+  proposalDecisionMadeEvents(where: { id_in: $eventIds }) {
+    ...ProposalDecisionMadeEventFields
+  }
+}
+
+fragment ProposalExecutedEventFields on ProposalExecutedEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  proposal {
+    id
+  }
+  executionStatus {
+    ... on ProposalStatusExecutionFailed {
+      errorMessage
+    }
+  }
+}
+
+query getProposalExecutedEventsByEventIds($eventIds: [ID!]) {
+  proposalExecutedEvents(where: { id_in: $eventIds }) {
+    ...ProposalExecutedEventFields
+  }
+}
+
+fragment ProposalVotedEventFields on ProposalVotedEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  voter {
+    id
+  }
+  voteKind
+  proposal {
+    id
+  }
+  rationale
+  votingRound
+}
+
+query getProposalVotedEventsByEventIds($eventIds: [ID!]) {
+  proposalVotedEvents(where: { id_in: $eventIds }) {
+    ...ProposalVotedEventFields
+  }
+}
+
+fragment ProposalCancelledEventFields on ProposalCancelledEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  proposal {
+   id
+  }
+}
+
+query getProposalCancelledEventsByEventIds($eventIds: [ID!]) {
+  proposalCancelledEvents(where: { id_in: $eventIds }) {
+    ...ProposalCancelledEventFields
+  }
+}

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

@@ -1,2 +1,3 @@
 import './memberships'
 import './workingGroups'
+import './proposals'

+ 6 - 0
tests/integration-tests/src/scenarios/proposals.ts

@@ -0,0 +1,6 @@
+import proposals from '../flows/proposals'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  job('proposals', proposals)
+})

+ 7 - 2
tests/integration-tests/src/sender.ts

@@ -134,11 +134,16 @@ export class Sender {
       try {
         await signedTx.send(handleEvents)
         if (this.logs === LogLevel.Verbose) {
-          this.debug('Submitted tx:', `${section}.${method}`)
+          this.debug('Submitted tx:', `${section}.${method} (nonce: ${nonce})`)
         }
       } catch (err) {
         if (this.logs === LogLevel.Debug) {
-          this.debug('Submitting tx failed:', sentTx, err)
+          this.debug(
+            'Submitting tx failed:',
+            sentTx,
+            err,
+            signedTx.method.args.map((a) => a.toHuman())
+          )
         }
         throw err
       }

+ 21 - 0
tests/integration-tests/src/types.ts

@@ -3,6 +3,8 @@ import { ApplicationId, OpeningId, WorkerId, ApplyOnOpeningParameters } from '@j
 import { Event } from '@polkadot/types/interfaces/system'
 import { BTreeMap } from '@polkadot/types'
 import { MembershipBoughtEvent } from './graphql/generated/schema'
+import { ProposalDetails, ProposalId } from '@joystream/types/proposals'
+import { CreateInterface } from '@joystream/types'
 
 export type MemberContext = {
   account: string
@@ -88,3 +90,22 @@ export type WorkingGroupModuleName =
   | 'contentDirectoryWorkingGroup'
   | 'forumWorkingGroup'
   | 'membershipWorkingGroup'
+
+// Proposals:
+
+export interface ProposalCreatedEventDetails extends EventDetails {
+  proposalId: ProposalId
+}
+
+export type ProposalsEngineEventName =
+  | 'ProposalCreated'
+  | 'ProposalStatusUpdated'
+  | 'ProposalDecisionMade'
+  | 'ProposalExecuted'
+  | 'Voted'
+  | 'ProposalCancelled'
+
+export type ProposalType = keyof typeof ProposalDetails.typeDefinitions
+export type ProposalDetailsJsonByType<T extends ProposalType = ProposalType> = CreateInterface<
+  InstanceType<ProposalDetails['typeDefinitions'][T]>
+>

+ 25 - 0
tests/integration-tests/src/utils.ts

@@ -6,6 +6,7 @@ import fs from 'fs'
 import { decodeAddress } from '@polkadot/keyring'
 import { Bytes } from '@polkadot/types'
 import { createType } from '@joystream/types'
+import Debugger from 'debug'
 
 export type AnyMessage<T> = T & {
   toJSON(): Record<string, unknown>
@@ -71,4 +72,28 @@ export class Utils {
       throw new Error(msg || 'Assertion failed')
     }
   }
+
+  public static async until(
+    name: string,
+    conditionFunc: (props: { debug: Debugger.Debugger }) => Promise<boolean>,
+    intervalMs = 6000,
+    timeoutMs = 10 * 60 * 1000
+  ): Promise<void> {
+    const debug = Debugger(`awaiting:${name}`)
+    return new Promise((resolve, reject) => {
+      const timeout = setTimeout(() => reject(new Error(`Awaiting ${name} - timoeut reached`)), timeoutMs)
+      const check = async () => {
+        if (await conditionFunc({ debug })) {
+          clearInterval(interval)
+          clearTimeout(timeout)
+          debug('Condition satisfied!')
+          resolve()
+          return
+        }
+        debug('Condition not satisfied, waiting...')
+      }
+      const interval = setInterval(check, intervalMs)
+      check()
+    })
+  }
 }

+ 29 - 29
types/augment-codec/augment-api-query.ts

@@ -61,7 +61,7 @@ declare module '@polkadot/api/types/storage' {
       initialized: AugmentedQuery<ApiType, () => Observable<Option<MaybeRandomness>>>;
       /**
        * How late the current block is compared to its parent.
-       *
+       * 
        * This entry is populated as part of block execution and is cleaned up
        * on block finalization. Querying this storage entry outside of block
        * execution context should always yield zero.
@@ -77,9 +77,9 @@ declare module '@polkadot/api/types/storage' {
       nextRandomness: AugmentedQuery<ApiType, () => Observable<Randomness>>;
       /**
        * The epoch randomness for the *current* epoch.
-       *
+       * 
        * # Security
-       *
+       * 
        * This MUST NOT be used for gambling, as it can be influenced by a
        * malicious validator in the short term. It MAY be used in many
        * cryptographic protocols, however, so long as one remembers that this
@@ -90,11 +90,11 @@ declare module '@polkadot/api/types/storage' {
       randomness: AugmentedQuery<ApiType, () => Observable<Randomness>>;
       /**
        * Randomness under construction.
-       *
+       * 
        * We make a tradeoff between storage accesses and list length.
        * We store the under-construction randomness in segments of up to
        * `UNDER_CONSTRUCTION_SEGMENT_LENGTH`.
-       *
+       * 
        * Once a segment reaches this length, we begin the next one.
        * We reset all segments and return to `0` at the beginning of every
        * epoch.
@@ -108,7 +108,7 @@ declare module '@polkadot/api/types/storage' {
     balances: {
       /**
        * The balance of an account.
-       *
+       * 
        * NOTE: This is only used in the case that this module is used to store balances.
        **/
       account: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<AccountData>>;
@@ -119,7 +119,7 @@ declare module '@polkadot/api/types/storage' {
       locks: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Vec<BalanceLock>>>;
       /**
        * Storage version of the pallet.
-       *
+       * 
        * This is set to v2.0.0 for new networks.
        **/
       storageVersion: AugmentedQuery<ApiType, () => Observable<Releases>>;
@@ -431,7 +431,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * A mapping from grandpa set ID to the index of the *most recent* session for which its
        * members were responsible.
-       *
+       * 
        * TWOX-NOTE: `SetId` is not under user control.
        **/
       setIdSession: AugmentedQuery<ApiType, (arg: SetId | AnyNumber | Uint8Array) => Observable<Option<SessionIndex>>>;
@@ -452,7 +452,7 @@ declare module '@polkadot/api/types/storage' {
       authoredBlocks: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: ValidatorId | string | Uint8Array) => Observable<u32>>;
       /**
        * The block number after which it's ok to send heartbeats in current session.
-       *
+       * 
        * At the beginning of each session we set this to a value that should
        * fall roughly in the middle of the session duration.
        * The idea is to first wait for the validators to produce a block
@@ -566,9 +566,9 @@ declare module '@polkadot/api/types/storage' {
       reports: AugmentedQuery<ApiType, (arg: ReportIdOf | string | Uint8Array) => Observable<Option<OffenceDetails>>>;
       /**
        * Enumerates all reports of a kind along with the time they happened.
-       *
+       * 
        * All reports are sorted by the time of offence.
-       *
+       * 
        * Note that the actual type of this mapping is `Vec<u8>`, this is because values of
        * different types are not supported at the moment so we are doing the manual serialization.
        **/
@@ -650,7 +650,7 @@ declare module '@polkadot/api/types/storage' {
       currentIndex: AugmentedQuery<ApiType, () => Observable<SessionIndex>>;
       /**
        * Indices of disabled validators.
-       *
+       * 
        * The set is cleared when `on_session_ending` returns a new set of identities.
        **/
       disabledValidators: AugmentedQuery<ApiType, () => Observable<Vec<u32>>>;
@@ -680,7 +680,7 @@ declare module '@polkadot/api/types/storage' {
     staking: {
       /**
        * The active era information, it holds index and start.
-       *
+       * 
        * The active era is the era currently rewarded.
        * Validator set of this era must be equal to `SessionInterface::validators`.
        **/
@@ -691,7 +691,7 @@ declare module '@polkadot/api/types/storage' {
       bonded: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Option<AccountId>>>;
       /**
        * A mapping from still-bonded eras to the first session index of that era.
-       *
+       * 
        * Must contains information for eras for the range:
        * `[active_era - bounding_duration; active_era]`
        **/
@@ -703,7 +703,7 @@ declare module '@polkadot/api/types/storage' {
       canceledSlashPayout: AugmentedQuery<ApiType, () => Observable<BalanceOf>>;
       /**
        * The current era index.
-       *
+       * 
        * This is the latest planned era, depending on how the Session pallet queues the validator
        * set, it might be active or not.
        **/
@@ -724,23 +724,23 @@ declare module '@polkadot/api/types/storage' {
       erasRewardPoints: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<EraRewardPoints>>;
       /**
        * Exposure of validator at era.
-       *
+       * 
        * This is keyed first by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
       erasStakers: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>>;
       /**
        * Clipped Exposure of validator at era.
-       *
+       * 
        * This is similar to [`ErasStakers`] but number of nominators exposed is reduced to the
        * `T::MaxNominatorRewardedPerValidator` biggest stakers.
        * (Note: the field `total` and `own` of the exposure remains unchanged).
        * This is used to limit the i/o cost for the nominator payout.
-       *
+       * 
        * This is keyed fist by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
@@ -756,15 +756,15 @@ declare module '@polkadot/api/types/storage' {
       erasTotalStake: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<BalanceOf>>;
       /**
        * Similar to `ErasStakers`, this holds the preferences of validators.
-       *
+       * 
        * This is keyed first by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        **/
       erasValidatorPrefs: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>>;
       /**
        * The total validator era payout for the last `HISTORY_DEPTH` eras.
-       *
+       * 
        * Eras that haven't finished yet or has been removed doesn't have reward.
        **/
       erasValidatorReward: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<Option<BalanceOf>>>;
@@ -774,9 +774,9 @@ declare module '@polkadot/api/types/storage' {
       forceEra: AugmentedQuery<ApiType, () => Observable<Forcing>>;
       /**
        * Number of eras to keep in history.
-       *
+       * 
        * Information is kept for eras in `[current_era - history_depth; current_era]`.
-       *
+       * 
        * Must be more than the number of eras delayed by session otherwise. I.e. active era must
        * always be in history. I.e. `active_era > current_era - history_depth` must be
        * guaranteed.
@@ -829,7 +829,7 @@ declare module '@polkadot/api/types/storage' {
       slashingSpans: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Option<SlashingSpans>>>;
       /**
        * The percentage of the slash that is distributed to reporters.
-       *
+       * 
        * The rest of the slashed value is handled by the `Slash`.
        **/
       slashRewardFraction: AugmentedQuery<ApiType, () => Observable<Perbill>>;
@@ -851,7 +851,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * True if network has been upgraded to this version.
        * Storage version of the pallet.
-       *
+       * 
        * This is set to v3.0.0 for new networks.
        **/
       storageVersion: AugmentedQuery<ApiType, () => Observable<Releases>>;
@@ -953,11 +953,11 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Mapping between a topic (represented by T::Hash) and a vector of indexes
        * of events in the `<Events<T>>` list.
-       *
+       * 
        * All topic vectors have deterministic storage locations depending on the topic. This
        * allows light-clients to leverage the changes trie storage tracking mechanism and
        * in case of changes fetch the list of events of interest.
-       *
+       * 
        * The value has the type `(T::BlockNumber, EventIndex)` because if we used only just
        * the `EventIndex` then in case if the topic has the same contents on the next block
        * no notification will be triggered thus the event might be lost.

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 157 - 157
types/augment-codec/augment-api-tx.ts


+ 5 - 4
types/augment/all/defs.json

@@ -67,7 +67,7 @@
         "_enum": {
             "Announcing": "CouncilStageAnnouncing",
             "Election": "CouncilStageElection",
-            "Idle": "u32"
+            "Idle": "Null"
         }
     },
     "Candidate": {
@@ -303,7 +303,7 @@
             "SetInitialInvitationBalance": "u128",
             "SetInitialInvitationCount": "u32",
             "SetMembershipLeadInvitationQuota": "u32",
-            "SetReferralCut": "u128",
+            "SetReferralCut": "u8",
             "CreateBlogPost": "(Text,Text)",
             "EditBlogPost": "(PostId,Option<Text>,Option<Text>)",
             "LockBlogPost": "PostId",
@@ -332,7 +332,7 @@
             "SetInitialInvitationBalance": "u128",
             "SetInitialInvitationCount": "u32",
             "SetMembershipLeadInvitationQuota": "u32",
-            "SetReferralCut": "u128",
+            "SetReferralCut": "u8",
             "CreateBlogPost": "(Text,Text)",
             "EditBlogPost": "(PostId,Option<Text>,Option<Text>)",
             "LockBlogPost": "PostId",
@@ -380,7 +380,7 @@
         "author_id": "u64"
     },
     "CreateOpeningParameters": {
-        "description": "Text",
+        "description": "Bytes",
         "stake_policy": "StakePolicy",
         "reward_per_block": "Option<u128>",
         "working_group": "WorkingGroup"
@@ -398,6 +398,7 @@
     "ProposalDecision": {
         "_enum": {
             "Canceled": "Null",
+            "CanceledByRuntime": "Null",
             "Vetoed": "Null",
             "Rejected": "Null",
             "Slashed": "Null",

+ 5 - 5
types/augment/all/types.ts

@@ -4,7 +4,7 @@
 import { ITuple } from '@polkadot/types/types';
 import { BTreeMap, BTreeSet, Enum, Option, Struct, U8aFixed, Vec } from '@polkadot/types/codec';
 import { GenericAccountId } from '@polkadot/types/generic';
-import { Bytes, Text, bool, i16, i32, i64, u128, u16, u32, u64 } from '@polkadot/types/primitive';
+import { Bytes, Text, bool, i16, i32, i64, u128, u16, u32, u64, u8 } from '@polkadot/types/primitive';
 import { AccountId, Balance, Hash } from '@polkadot/types/interfaces/runtime';
 
 /** @name Actor */
@@ -213,7 +213,6 @@ export interface CouncilStage extends Enum {
   readonly isElection: boolean;
   readonly asElection: CouncilStageElection;
   readonly isIdle: boolean;
-  readonly asIdle: u32;
 }
 
 /** @name CouncilStageAnnouncing */
@@ -239,7 +238,7 @@ export interface CreateEntityOperation extends Struct {
 
 /** @name CreateOpeningParameters */
 export interface CreateOpeningParameters extends Struct {
-  readonly description: Text;
+  readonly description: Bytes;
   readonly stake_policy: StakePolicy;
   readonly reward_per_block: Option<u128>;
   readonly working_group: WorkingGroup;
@@ -678,6 +677,7 @@ export interface PropertyTypeVector extends Struct {
 /** @name ProposalDecision */
 export interface ProposalDecision extends Enum {
   readonly isCanceled: boolean;
+  readonly isCanceledByRuntime: boolean;
   readonly isVetoed: boolean;
   readonly isRejected: boolean;
   readonly isSlashed: boolean;
@@ -727,7 +727,7 @@ export interface ProposalDetails extends Enum {
   readonly isSetMembershipLeadInvitationQuota: boolean;
   readonly asSetMembershipLeadInvitationQuota: u32;
   readonly isSetReferralCut: boolean;
-  readonly asSetReferralCut: u128;
+  readonly asSetReferralCut: u8;
   readonly isCreateBlogPost: boolean;
   readonly asCreateBlogPost: ITuple<[Text, Text]>;
   readonly isEditBlogPost: boolean;
@@ -781,7 +781,7 @@ export interface ProposalDetailsOf extends Enum {
   readonly isSetMembershipLeadInvitationQuota: boolean;
   readonly asSetMembershipLeadInvitationQuota: u32;
   readonly isSetReferralCut: boolean;
-  readonly asSetReferralCut: u128;
+  readonly asSetReferralCut: u8;
   readonly isCreateBlogPost: boolean;
   readonly asCreateBlogPost: ITuple<[Text, Text]>;
   readonly isEditBlogPost: boolean;

+ 29 - 29
types/augment/augment-api-query.ts

@@ -61,7 +61,7 @@ declare module '@polkadot/api/types/storage' {
       initialized: AugmentedQuery<ApiType, () => Observable<Option<MaybeRandomness>>>;
       /**
        * How late the current block is compared to its parent.
-       *
+       * 
        * This entry is populated as part of block execution and is cleaned up
        * on block finalization. Querying this storage entry outside of block
        * execution context should always yield zero.
@@ -77,9 +77,9 @@ declare module '@polkadot/api/types/storage' {
       nextRandomness: AugmentedQuery<ApiType, () => Observable<Randomness>>;
       /**
        * The epoch randomness for the *current* epoch.
-       *
+       * 
        * # Security
-       *
+       * 
        * This MUST NOT be used for gambling, as it can be influenced by a
        * malicious validator in the short term. It MAY be used in many
        * cryptographic protocols, however, so long as one remembers that this
@@ -90,11 +90,11 @@ declare module '@polkadot/api/types/storage' {
       randomness: AugmentedQuery<ApiType, () => Observable<Randomness>>;
       /**
        * Randomness under construction.
-       *
+       * 
        * We make a tradeoff between storage accesses and list length.
        * We store the under-construction randomness in segments of up to
        * `UNDER_CONSTRUCTION_SEGMENT_LENGTH`.
-       *
+       * 
        * Once a segment reaches this length, we begin the next one.
        * We reset all segments and return to `0` at the beginning of every
        * epoch.
@@ -108,7 +108,7 @@ declare module '@polkadot/api/types/storage' {
     balances: {
       /**
        * The balance of an account.
-       *
+       * 
        * NOTE: This is only used in the case that this module is used to store balances.
        **/
       account: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<AccountData>>;
@@ -119,7 +119,7 @@ declare module '@polkadot/api/types/storage' {
       locks: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Vec<BalanceLock>>>;
       /**
        * Storage version of the pallet.
-       *
+       * 
        * This is set to v2.0.0 for new networks.
        **/
       storageVersion: AugmentedQuery<ApiType, () => Observable<Releases>>;
@@ -431,7 +431,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * A mapping from grandpa set ID to the index of the *most recent* session for which its
        * members were responsible.
-       *
+       * 
        * TWOX-NOTE: `SetId` is not under user control.
        **/
       setIdSession: AugmentedQuery<ApiType, (arg: SetId | AnyNumber | Uint8Array) => Observable<Option<SessionIndex>>>;
@@ -452,7 +452,7 @@ declare module '@polkadot/api/types/storage' {
       authoredBlocks: AugmentedQueryDoubleMap<ApiType, (key1: SessionIndex | AnyNumber | Uint8Array, key2: ValidatorId | string | Uint8Array) => Observable<u32>>;
       /**
        * The block number after which it's ok to send heartbeats in current session.
-       *
+       * 
        * At the beginning of each session we set this to a value that should
        * fall roughly in the middle of the session duration.
        * The idea is to first wait for the validators to produce a block
@@ -566,9 +566,9 @@ declare module '@polkadot/api/types/storage' {
       reports: AugmentedQuery<ApiType, (arg: ReportIdOf | string | Uint8Array) => Observable<Option<OffenceDetails>>>;
       /**
        * Enumerates all reports of a kind along with the time they happened.
-       *
+       * 
        * All reports are sorted by the time of offence.
-       *
+       * 
        * Note that the actual type of this mapping is `Vec<u8>`, this is because values of
        * different types are not supported at the moment so we are doing the manual serialization.
        **/
@@ -650,7 +650,7 @@ declare module '@polkadot/api/types/storage' {
       currentIndex: AugmentedQuery<ApiType, () => Observable<SessionIndex>>;
       /**
        * Indices of disabled validators.
-       *
+       * 
        * The set is cleared when `on_session_ending` returns a new set of identities.
        **/
       disabledValidators: AugmentedQuery<ApiType, () => Observable<Vec<u32>>>;
@@ -680,7 +680,7 @@ declare module '@polkadot/api/types/storage' {
     staking: {
       /**
        * The active era information, it holds index and start.
-       *
+       * 
        * The active era is the era currently rewarded.
        * Validator set of this era must be equal to `SessionInterface::validators`.
        **/
@@ -691,7 +691,7 @@ declare module '@polkadot/api/types/storage' {
       bonded: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Option<AccountId>>>;
       /**
        * A mapping from still-bonded eras to the first session index of that era.
-       *
+       * 
        * Must contains information for eras for the range:
        * `[active_era - bounding_duration; active_era]`
        **/
@@ -703,7 +703,7 @@ declare module '@polkadot/api/types/storage' {
       canceledSlashPayout: AugmentedQuery<ApiType, () => Observable<BalanceOf>>;
       /**
        * The current era index.
-       *
+       * 
        * This is the latest planned era, depending on how the Session pallet queues the validator
        * set, it might be active or not.
        **/
@@ -724,23 +724,23 @@ declare module '@polkadot/api/types/storage' {
       erasRewardPoints: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<EraRewardPoints>>;
       /**
        * Exposure of validator at era.
-       *
+       * 
        * This is keyed first by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
       erasStakers: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<Exposure>>;
       /**
        * Clipped Exposure of validator at era.
-       *
+       * 
        * This is similar to [`ErasStakers`] but number of nominators exposed is reduced to the
        * `T::MaxNominatorRewardedPerValidator` biggest stakers.
        * (Note: the field `total` and `own` of the exposure remains unchanged).
        * This is used to limit the i/o cost for the nominator payout.
-       *
+       * 
        * This is keyed fist by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        * If stakers hasn't been set or has been removed then empty exposure is returned.
        **/
@@ -756,15 +756,15 @@ declare module '@polkadot/api/types/storage' {
       erasTotalStake: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<BalanceOf>>;
       /**
        * Similar to `ErasStakers`, this holds the preferences of validators.
-       *
+       * 
        * This is keyed first by the era index to allow bulk deletion and then the stash account.
-       *
+       * 
        * Is it removed after `HISTORY_DEPTH` eras.
        **/
       erasValidatorPrefs: AugmentedQueryDoubleMap<ApiType, (key1: EraIndex | AnyNumber | Uint8Array, key2: AccountId | string | Uint8Array) => Observable<ValidatorPrefs>>;
       /**
        * The total validator era payout for the last `HISTORY_DEPTH` eras.
-       *
+       * 
        * Eras that haven't finished yet or has been removed doesn't have reward.
        **/
       erasValidatorReward: AugmentedQuery<ApiType, (arg: EraIndex | AnyNumber | Uint8Array) => Observable<Option<BalanceOf>>>;
@@ -774,9 +774,9 @@ declare module '@polkadot/api/types/storage' {
       forceEra: AugmentedQuery<ApiType, () => Observable<Forcing>>;
       /**
        * Number of eras to keep in history.
-       *
+       * 
        * Information is kept for eras in `[current_era - history_depth; current_era]`.
-       *
+       * 
        * Must be more than the number of eras delayed by session otherwise. I.e. active era must
        * always be in history. I.e. `active_era > current_era - history_depth` must be
        * guaranteed.
@@ -829,7 +829,7 @@ declare module '@polkadot/api/types/storage' {
       slashingSpans: AugmentedQuery<ApiType, (arg: AccountId | string | Uint8Array) => Observable<Option<SlashingSpans>>>;
       /**
        * The percentage of the slash that is distributed to reporters.
-       *
+       * 
        * The rest of the slashed value is handled by the `Slash`.
        **/
       slashRewardFraction: AugmentedQuery<ApiType, () => Observable<Perbill>>;
@@ -851,7 +851,7 @@ declare module '@polkadot/api/types/storage' {
       /**
        * True if network has been upgraded to this version.
        * Storage version of the pallet.
-       *
+       * 
        * This is set to v3.0.0 for new networks.
        **/
       storageVersion: AugmentedQuery<ApiType, () => Observable<Releases>>;
@@ -953,11 +953,11 @@ declare module '@polkadot/api/types/storage' {
       /**
        * Mapping between a topic (represented by T::Hash) and a vector of indexes
        * of events in the `<Events<T>>` list.
-       *
+       * 
        * All topic vectors have deterministic storage locations depending on the topic. This
        * allows light-clients to leverage the changes trie storage tracking mechanism and
        * in case of changes fetch the list of events of interest.
-       *
+       * 
        * The value has the type `(T::BlockNumber, EventIndex)` because if we used only just
        * the `EventIndex` then in case if the topic has the same contents on the next block
        * no notification will be triggered thus the event might be lost.

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 157 - 157
types/augment/augment-api-tx.ts


+ 2 - 2
types/src/council/index.ts

@@ -1,6 +1,6 @@
 import { Option } from '@polkadot/types/codec'
 import { BlockNumber, Balance } from '@polkadot/types/interfaces'
-import { u32, u64, u128 } from '@polkadot/types'
+import { u32, u64, u128, Null } from '@polkadot/types'
 import { RegistryTypes } from '@polkadot/types/types'
 import { JoyStructDecorated } from '../JoyStruct'
 import { JoyEnum } from '../JoyEnum'
@@ -30,7 +30,7 @@ export class CouncilStageElection
 export class CouncilStage extends JoyEnum({
   Announcing: CouncilStageAnnouncing,
   Election: CouncilStageElection,
-  Idle: u32,
+  Idle: Null,
 } as const) {}
 
 export type ICouncilStageUpdate = {

+ 10 - 6
types/src/index.ts

@@ -1,4 +1,4 @@
-import { Codec, RegistryTypes } from '@polkadot/types/types'
+import { Codec, ITuple, RegistryTypes } from '@polkadot/types/types'
 import common from './common'
 import members from './members'
 import council from './council'
@@ -13,7 +13,7 @@ import referendum from './referendum'
 import constitution from './constitution'
 import bounty from './bounty'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
-import { TypeRegistry, Text, UInt, Null, bool, Option, Vec, BTreeSet, BTreeMap } from '@polkadot/types'
+import { TypeRegistry, Text, UInt, Null, bool, Option, Vec, BTreeSet, BTreeMap, Tuple } from '@polkadot/types'
 import { ExtendedEnum } from './JoyEnum'
 import { ExtendedStruct } from './JoyStruct'
 
@@ -68,19 +68,23 @@ type CreateInterface_NoOption<T extends Codec> =
       ? boolean
       : T extends Vec<infer S> | BTreeSet<infer S>
       ? CreateInterface<S>[]
+      : T extends ITuple<infer S>
+      ? S extends Tuple
+        ? any[]
+        : { [K in keyof S]: CreateInterface<T[K]> }
       : T extends BTreeMap<infer K, infer V>
       ? Map<K, V>
       : any)
 
 // Wrapper for CreateInterface_NoOption that includes resolving an Option
 // (nested Options like Option<Option<Codec>> will resolve to Option<any>, but there are very edge case)
-export type CreateInterface<T extends Codec> =
-  | T
-  | (T extends Option<infer S> ? undefined | null | S | CreateInterface_NoOption<S> : CreateInterface_NoOption<T>)
+export type CreateInterface<T> = T extends Codec
+  ? T | (T extends Option<infer S> ? undefined | null | S | CreateInterface_NoOption<S> : CreateInterface_NoOption<T>)
+  : any
 
 export function createType<TypeName extends keyof InterfaceTypes>(
   type: TypeName,
-  value: InterfaceTypes[TypeName] extends Codec ? CreateInterface<InterfaceTypes[TypeName]> : any
+  value: CreateInterface<InterfaceTypes[TypeName]>
 ): InterfaceTypes[TypeName] {
   return registry.createType(type, value)
 }

+ 33 - 10
types/src/proposals.ts

@@ -1,5 +1,6 @@
 import { Text, u32, Tuple, u8, u128, Vec, Option, Null, Bytes } from '@polkadot/types'
 import { BlockNumber, Balance } from '@polkadot/types/interfaces'
+import { Constructor, ITuple } from '@polkadot/types/types'
 import { AccountId, MemberId, WorkingGroup, JoyEnum, JoyStructDecorated, BalanceKind, PostId } from './common'
 import { ApplicationId, OpeningId, StakePolicy, WorkerId } from './working-group'
 
@@ -93,6 +94,7 @@ export class Approved extends ApprovedProposalDecision {}
 
 export const ProposalDecisionDef = {
   Canceled: Null,
+  CanceledByRuntime: Null,
   Vetoed: Null,
   Rejected: Null,
   Slashed: Null,
@@ -192,7 +194,7 @@ export class GeneralProposalParameters
   implements IGeneralProposalParameters {}
 
 export type ICreateOpeningParameters = {
-  description: Text
+  description: Bytes
   stake_policy: StakePolicy
   reward_per_block: Option<Balance>
   working_group: WorkingGroup
@@ -200,7 +202,7 @@ export type ICreateOpeningParameters = {
 
 export class CreateOpeningParameters
   extends JoyStructDecorated({
-    description: Text,
+    description: Bytes,
     stake_policy: StakePolicy,
     reward_per_block: Option.with(u128),
     working_group: WorkingGroup,
@@ -247,6 +249,27 @@ export class FundingRequestParameters
   })
   implements IFundingRequestParameters {}
 
+// Typesafe tuple workarounds
+const UpdateWorkingGroupBudget = (Tuple.with(['Balance', WorkingGroup, BalanceKind]) as unknown) as Constructor<
+  ITuple<[Balance, WorkingGroup, BalanceKind]>
+>
+const DecreaseWorkingGroupLeadStake = (Tuple.with([WorkerId, 'Balance', WorkingGroup]) as unknown) as Constructor<
+  ITuple<[WorkerId, Balance, WorkingGroup]>
+>
+const SlashWorkingGroupLead = (Tuple.with([WorkerId, 'Balance', WorkingGroup]) as unknown) as Constructor<
+  ITuple<[WorkerId, Balance, WorkingGroup]>
+>
+const SetWorkingGroupLeadReward = (Tuple.with([WorkerId, 'Option<Balance>', WorkingGroup]) as unknown) as Constructor<
+  ITuple<[WorkerId, Option<Balance>, WorkingGroup]>
+>
+const CancelWorkingGroupLeadOpening = (Tuple.with([OpeningId, WorkingGroup]) as unknown) as Constructor<
+  ITuple<[OpeningId, WorkingGroup]>
+>
+const CreateBlogPost = (Tuple.with([Text, Text]) as unknown) as Constructor<ITuple<[Text, Text]>>
+const EditBlogPost = (Tuple.with([PostId, 'Option<Text>', 'Option<Text>']) as unknown) as Constructor<
+  ITuple<[PostId, Option<Text>, Option<Text>]>
+>
+
 export class ProposalDetails extends JoyEnum({
   Signal: Text,
   RuntimeUpgrade: Bytes,
@@ -254,22 +277,22 @@ export class ProposalDetails extends JoyEnum({
   SetMaxValidatorCount: u32,
   CreateWorkingGroupLeadOpening: CreateOpeningParameters,
   FillWorkingGroupLeadOpening: FillOpeningParameters,
-  UpdateWorkingGroupBudget: Tuple.with(['Balance', WorkingGroup, BalanceKind]),
-  DecreaseWorkingGroupLeadStake: Tuple.with([WorkerId, 'Balance', WorkingGroup]),
-  SlashWorkingGroupLead: Tuple.with([WorkerId, 'Balance', WorkingGroup]),
-  SetWorkingGroupLeadReward: Tuple.with([WorkerId, 'Option<Balance>', WorkingGroup]),
+  UpdateWorkingGroupBudget,
+  DecreaseWorkingGroupLeadStake,
+  SlashWorkingGroupLead,
+  SetWorkingGroupLeadReward,
   TerminateWorkingGroupLead: TerminateRoleParameters,
   AmendConstitution: Text,
-  CancelWorkingGroupLeadOpening: Tuple.with([OpeningId, WorkingGroup]),
+  CancelWorkingGroupLeadOpening,
   SetMembershipPrice: u128,
   SetCouncilBudgetIncrement: u128,
   SetCouncilorReward: u128,
   SetInitialInvitationBalance: u128,
   SetInitialInvitationCount: u32,
   SetMembershipLeadInvitationQuota: u32,
-  SetReferralCut: u128,
-  CreateBlogPost: Tuple.with([Text, Text]),
-  EditBlogPost: Tuple.with([PostId, Option.with(Text), Option.with(Text)]),
+  SetReferralCut: u8,
+  CreateBlogPost,
+  EditBlogPost,
   LockBlogPost: PostId,
   UnlockBlogPost: PostId,
   VetoProposal: ProposalId,

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä