Browse Source

Merge branch 'olympia-proposals-mappings' into olympia-staging

Leszek Wiesner 3 years ago
parent
commit
52e8706419

+ 72 - 15
cli/src/graphql/generated/schema.ts

@@ -15,6 +15,8 @@ export type Scalars = {
   BigInt: any
   /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
   JSONObject: any
+  /** GraphQL representation of Bytes */
+  Bytes: any
 }
 
 export type AmendConstitutionProposalDetails = {
@@ -9984,6 +9986,9 @@ export type Query = {
   rewardPaidEvents: Array<RewardPaidEvent>
   rewardPaidEventByUniqueInput?: Maybe<RewardPaidEvent>
   rewardPaidEventsConnection: RewardPaidEventConnection
+  runtimeWasmBytecodes: Array<RuntimeWasmBytecode>
+  runtimeWasmBytecodeByUniqueInput?: Maybe<RuntimeWasmBytecode>
+  runtimeWasmBytecodesConnection: RuntimeWasmBytecodeConnection
   stakeDecreasedEvents: Array<StakeDecreasedEvent>
   stakeDecreasedEventByUniqueInput?: Maybe<StakeDecreasedEvent>
   stakeDecreasedEventsConnection: StakeDecreasedEventConnection
@@ -11329,6 +11334,26 @@ export type QueryRewardPaidEventsConnectionArgs = {
   orderBy?: Maybe<Array<RewardPaidEventOrderByInput>>
 }
 
+export type QueryRuntimeWasmBytecodesArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<RuntimeWasmBytecodeWhereInput>
+  orderBy?: Maybe<Array<RuntimeWasmBytecodeOrderByInput>>
+}
+
+export type QueryRuntimeWasmBytecodeByUniqueInputArgs = {
+  where: RuntimeWasmBytecodeWhereUniqueInput
+}
+
+export type QueryRuntimeWasmBytecodesConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<RuntimeWasmBytecodeWhereInput>
+  orderBy?: Maybe<Array<RuntimeWasmBytecodeOrderByInput>>
+}
+
 export type QueryStakeDecreasedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -12168,19 +12193,54 @@ export enum RewardPaymentType {
 }
 
 export type RuntimeUpgradeProposalDetails = {
-  /** Runtime upgrade WASM bytecode hash */
-  wasmBytecodeHash: Scalars['String']
+  /** Runtime upgrade WASM bytecode */
+  newRuntimeBytecode?: Maybe<RuntimeWasmBytecode>
 }
 
-export type RuntimeUpgradeProposalDetailsCreateInput = {
-  wasmBytecodeHash: Scalars['String']
+export type RuntimeWasmBytecode = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** The bytecode itself */
+  bytecode: Scalars['Bytes']
+}
+
+export type RuntimeWasmBytecodeConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<RuntimeWasmBytecodeEdge>
+  pageInfo: PageInfo
+}
+
+export type RuntimeWasmBytecodeCreateInput = {
+  bytecode: Scalars['Bytes']
+}
+
+export type RuntimeWasmBytecodeEdge = {
+  node: RuntimeWasmBytecode
+  cursor: Scalars['String']
+}
+
+export enum RuntimeWasmBytecodeOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  BytecodeAsc = 'bytecode_ASC',
+  BytecodeDesc = 'bytecode_DESC',
 }
 
-export type RuntimeUpgradeProposalDetailsUpdateInput = {
-  wasmBytecodeHash?: Maybe<Scalars['String']>
+export type RuntimeWasmBytecodeUpdateInput = {
+  bytecode?: Maybe<Scalars['Bytes']>
 }
 
-export type RuntimeUpgradeProposalDetailsWhereInput = {
+export type RuntimeWasmBytecodeWhereInput = {
   id_eq?: Maybe<Scalars['ID']>
   id_in?: Maybe<Array<Scalars['ID']>>
   createdAt_eq?: Maybe<Scalars['DateTime']>
@@ -12205,16 +12265,13 @@ export type RuntimeUpgradeProposalDetailsWhereInput = {
   deletedAt_gte?: Maybe<Scalars['DateTime']>
   deletedById_eq?: Maybe<Scalars['ID']>
   deletedById_in?: Maybe<Array<Scalars['ID']>>
-  wasmBytecodeHash_eq?: Maybe<Scalars['String']>
-  wasmBytecodeHash_contains?: Maybe<Scalars['String']>
-  wasmBytecodeHash_startsWith?: Maybe<Scalars['String']>
-  wasmBytecodeHash_endsWith?: Maybe<Scalars['String']>
-  wasmBytecodeHash_in?: Maybe<Array<Scalars['String']>>
-  AND?: Maybe<Array<RuntimeUpgradeProposalDetailsWhereInput>>
-  OR?: Maybe<Array<RuntimeUpgradeProposalDetailsWhereInput>>
+  bytecode_eq?: Maybe<Scalars['Bytes']>
+  bytecode_in?: Maybe<Array<Scalars['Bytes']>>
+  AND?: Maybe<Array<RuntimeWasmBytecodeWhereInput>>
+  OR?: Maybe<Array<RuntimeWasmBytecodeWhereInput>>
 }
 
-export type RuntimeUpgradeProposalDetailsWhereUniqueInput = {
+export type RuntimeWasmBytecodeWhereUniqueInput = {
   id: Scalars['ID']
 }
 

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

@@ -1,3 +1,10 @@
+import BN from 'bn.js'
+
+// Workaround for invalid BN variant fields serialization
+BN.prototype.toJSON = function () {
+  return this.toString()
+}
+
 export * from './membership'
 export * from './workingGroups'
 export * from './proposals'

+ 18 - 2
query-node/mappings/proposals.ts

@@ -56,11 +56,13 @@ import {
   ProposalVoteKind,
   ProposalCancelledEvent,
   ProposalCreatedEvent,
+  RuntimeWasmBytecode,
 } from 'query-node/dist/model'
 import { bytesToString, genericEventFields, getWorkingGroupModuleName, perpareString } from './common'
 import { ProposalsEngine, ProposalsCodex } from './generated/types'
 import { createWorkingGroupOpeningMetadata } from './workingGroups'
 import { blake2AsHex } from '@polkadot/util-crypto'
+import { Bytes } from '@polkadot/types'
 
 // FIXME: https://github.com/Joystream/joystream/issues/2457
 type ProposalsMappingsMemoryCache = {
@@ -79,6 +81,19 @@ async function getProposal(store: DatabaseManager, id: string) {
   return proposal
 }
 
+async function getOrCreateRuntimeWasmBytecode(store: DatabaseManager, bytecode: Bytes) {
+  const bytecodeHash = blake2AsHex(bytecode.toU8a(true))
+  let wasmBytecode = await store.get(RuntimeWasmBytecode, { where: { id: bytecodeHash } })
+  if (!wasmBytecode) {
+    wasmBytecode = new RuntimeWasmBytecode({
+      id: bytecodeHash,
+      bytecode: Buffer.from(bytecode.toU8a(true)),
+    })
+    await store.save<RuntimeWasmBytecode>(wasmBytecode)
+  }
+  return wasmBytecode
+}
+
 async function parseProposalDetails(
   event: SubstrateEvent,
   store: DatabaseManager,
@@ -96,8 +111,8 @@ async function parseProposalDetails(
   // RuntimeUpgradeProposalDetails:
   else if (proposalDetails.isRuntimeUpgrade) {
     const details = new RuntimeUpgradeProposalDetails()
-    const specificDetails = proposalDetails.asRuntimeUpgrade
-    details.wasmBytecodeHash = blake2AsHex(Buffer.from(specificDetails.toU8a(true)))
+    const runtimeBytecode = proposalDetails.asRuntimeUpgrade
+    details.newRuntimeBytecodeId = (await getOrCreateRuntimeWasmBytecode(store, runtimeBytecode)).id
     return details
   }
   // FundingRequestProposalDetails:
@@ -216,6 +231,7 @@ async function parseProposalDetails(
   else if (proposalDetails.isSetCouncilBudgetIncrement) {
     const details = new SetCouncilBudgetIncrementProposalDetails()
     const specificDetails = proposalDetails.asSetCouncilBudgetIncrement
+    console.log('SetCouncilBudgetIncrement specificDetails.toString():', new BN(specificDetails.toString()).toString())
     details.newAmount = new BN(specificDetails.toString())
     return details
   }

+ 10 - 2
query-node/schemas/proposals.graphql

@@ -152,9 +152,17 @@ type SignalProposalDetails @variant {
   text: String!
 }
 
+type RuntimeWasmBytecode @entity {
+  "Blake2b hash of the runtime bytecode"
+  id: ID!
+
+  "The bytecode itself"
+  bytecode: Bytes!
+}
+
 type RuntimeUpgradeProposalDetails @variant {
-  "Runtime upgrade WASM bytecode hash"
-  wasmBytecodeHash: String!
+  "Runtime upgrade WASM bytecode"
+  newRuntimeBytecode: RuntimeWasmBytecode!
 }
 
 type FundingRequestDestination @entity {

+ 6 - 2
tests/integration-tests/src/fixtures/proposals/AllProposalsOutcomesFixture.ts

@@ -13,6 +13,7 @@ import { BuyMembershipHappyCaseFixture } from '../membership'
 import { CreateProposalsFixture, ProposalCreationParams } from './CreateProposalsFixture'
 import { ElectCouncilFixture } from '../council/ElectCouncilFixture'
 import _ from 'lodash'
+import { Resource, ResourceLocker } from '../../Resources'
 
 export type TestedProposal = { details: CreateInterface<ProposalDetails>; expectExecutionFailure?: boolean }
 export type ProposalTestCase = {
@@ -25,10 +26,12 @@ export type ProposalTestCase = {
 export class AllProposalsOutcomesFixture extends BaseFixture {
   protected testedProposals: TestedProposal[]
   protected query: QueryNodeApi
+  protected lock: ResourceLocker
 
-  public constructor(api: Api, query: QueryNodeApi, testedProposals: TestedProposal[]) {
+  public constructor(api: Api, query: QueryNodeApi, lock: ResourceLocker, testedProposals: TestedProposal[]) {
     super(api)
     this.query = query
+    this.lock = lock
     this.testedProposals = testedProposals
   }
 
@@ -62,7 +65,7 @@ export class AllProposalsOutcomesFixture extends BaseFixture {
     let batch: ProposalTestCase[]
     let n = 0
     while ((batch = testCases.slice(n * proposalsPerBatch, (n + 1) * proposalsPerBatch)).length) {
-      await api.untilProposalsCanBeCreated(batch.length)
+      const unlocks = await Promise.all(batch.map(() => this.lock(Resource.Proposals)))
       const createProposalsParams: ProposalCreationParams[] = batch.map(({ type, details, decisionStatus }, i) => ({
         asMember: memberIds[i],
         title: `${_.startCase(type)}`,
@@ -115,6 +118,7 @@ export class AllProposalsOutcomesFixture extends BaseFixture {
         await new FixtureRunner(approveProposalsFixture).runWithQueryNodeChecks()
         dormantProposals = approveProposalsFixture.getDormantProposalsIds()
       }
+      unlocks.forEach((unlock) => unlock())
       ++n
     }
   }

+ 16 - 3
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -97,7 +97,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
     params: ProposalCreationParams<ProposalType>,
     qProposal: ProposalFieldsFragment
   ): void {
-    const proposalDetails = this.api.createType('ProposalDetails', params.details)
+    const proposalDetails = this.api.createType('ProposalDetails', { [params.type]: params.details })
     switch (params.type) {
       case 'AmendConstitution': {
         Utils.assert(qProposal.details.__typename === 'AmendConstitutionProposalDetails')
@@ -167,7 +167,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
       case 'FundingRequest': {
         Utils.assert(qProposal.details.__typename === 'FundingRequestProposalDetails')
         const details = proposalDetails.asType('FundingRequest')
-        assert.sameMembers(
+        assert.sameDeepMembers(
           qProposal.details.destinationsList?.destinations.map(({ amount, account }) => ({ amount, account })) || [],
           details.map((d) => ({ amount: d.amount.toString(), account: d.account.toString() }))
         )
@@ -182,7 +182,19 @@ export class CreateProposalsFixture extends StandardizedFixture {
       case 'RuntimeUpgrade': {
         Utils.assert(qProposal.details.__typename === 'RuntimeUpgradeProposalDetails')
         const details = proposalDetails.asType('RuntimeUpgrade')
-        assert.equal(qProposal.details.wasmBytecodeHash, blake2AsHex(details))
+        Utils.assert(qProposal.details.newRuntimeBytecode, 'Missing newRuntimeBytecode relationship')
+        assert.equal(qProposal.details.newRuntimeBytecode.id, blake2AsHex(details.toU8a(true)))
+        const expectedBytecode = '0x' + Buffer.from(details.toU8a(true)).toString('hex')
+        const actualBytecode = qProposal.details.newRuntimeBytecode.bytecode
+        if (actualBytecode !== expectedBytecode) {
+          const diffStartPos = expectedBytecode.split('').findIndex((c, i) => actualBytecode[i] !== c)
+          const diffSubExpected = expectedBytecode.slice(diffStartPos, diffStartPos + 10)
+          const diffSubActual = actualBytecode.slice(diffStartPos, diffStartPos + 10)
+          throw new Error(
+            `Runtime bytecode doesn't match the expected one! Diff starts at pos ${diffStartPos}. ` +
+              `Expected: ${diffSubExpected}.., Actual: ${diffSubActual}...`
+          )
+        }
         break
       }
       case 'SetCouncilBudgetIncrement': {
@@ -304,6 +316,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
       assert.equal(new Date(qProposal.statusSetAtTime).getTime(), e.blockTimestamp)
       assert.equal(qProposal.createdInEvent.inBlock, e.blockNumber)
       assert.equal(qProposal.createdInEvent.inExtrinsic, this.extrinsics[i].hash.toString())
+      this.assertProposalDetailsAreValid(proposalParams, qProposal)
     })
   }
 

+ 24 - 7
tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts

@@ -98,7 +98,7 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
     if (params.status === 'Approved') {
       if (proposal.parameters.constitutionality.toNumber() > proposal.nrOfCouncilConfirmations.toNumber() + 1) {
         return 'ProposalStatusDormant'
-      } else if (proposal.parameters.gracePeriod.toNumber()) {
+      } else if (proposal.parameters.gracePeriod.toNumber() || proposal.exactExecutionBlock.isSome) {
         return 'ProposalStatusGracing'
       } else {
         return params.expectExecutionFailure ? 'ProposalStatusExecutionFailed' : 'ProposalStatusExecuted'
@@ -141,6 +141,24 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
     })
   }
 
+  protected assertProposalExecutedAsExpected(qProposal: ProposalFieldsFragment, i: number): void {
+    const params = this.params[i]
+    const proposal = this.proposals[i]
+
+    assert.equal(
+      qProposal.status.__typename,
+      params.expectExecutionFailure ? 'ProposalStatusExecutionFailed' : 'ProposalStatusExecuted'
+    )
+    if (proposal.exactExecutionBlock.isSome) {
+      assert.equal(qProposal.statusSetAtBlock, proposal.exactExecutionBlock.unwrap().toNumber())
+    } else if (proposal.parameters.gracePeriod.toNumber()) {
+      const gracePriodStartedAt = qProposal.proposalStatusUpdates.find(
+        (u) => u.newStatus.__typename === 'ProposalStatusGracing'
+      )?.inBlock
+      assert.equal(qProposal.statusSetAtBlock, (gracePriodStartedAt || 0) + proposal.parameters.gracePeriod.toNumber())
+    }
+  }
+
   public async execute(): Promise<void> {
     const { api, query } = this
     this.proposals = await this.api.query.proposalsEngine.proposals.multi<Proposal>(
@@ -176,14 +194,13 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
       this.proposals.map(async (proposal, i) => {
         let qProposal = qProposals[i]
         if (this.getExpectedProposalStatus(i) === 'ProposalStatusGracing') {
-          await this.api.untilBlock(qProposal.statusSetAtBlock + proposal.parameters.gracePeriod.toNumber())
+          const proposalExecutionBlock = proposal.exactExecutionBlock.isSome
+            ? proposal.exactExecutionBlock.unwrap().toNumber()
+            : qProposal.statusSetAtBlock + proposal.parameters.gracePeriod.toNumber()
+          await this.api.untilBlock(proposalExecutionBlock)
           ;[qProposal] = await this.query.tryQueryWithTimeout(
             () => this.query.getProposalsByIds([this.params[i].proposalId]),
-            ([p]) =>
-              assert.equal(
-                p.status.__typename,
-                this.params[i].expectExecutionFailure ? 'ProposalStatusExecutionFailed' : 'ProposalStatusExecuted'
-              )
+            ([p]) => this.assertProposalExecutedAsExpected(p, i)
           )
           await this.postExecutionChecks(qProposal)
         }

+ 50 - 0
tests/integration-tests/src/fixtures/proposals/ExpireProposalsFixture.ts

@@ -0,0 +1,50 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { Utils } from '../../utils'
+import { Proposal, ProposalId } from '@joystream/types/proposals'
+import { BaseQueryNodeFixture } from '../../Fixture'
+import { ProposalFieldsFragment } from '../../graphql/generated/queries'
+import { assert } from 'chai'
+
+export class ExpireProposalsFixture extends BaseQueryNodeFixture {
+  protected proposalIds: ProposalId[]
+  protected proposals: Proposal[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, proposalIds: ProposalId[]) {
+    super(api, query)
+    this.proposalIds = proposalIds
+  }
+
+  public async execute(): Promise<void> {
+    const { api } = this
+    this.proposals = await this.api.query.proposalsEngine.proposals.multi<Proposal>(this.proposalIds)
+    await Promise.all(
+      this.proposals.map(async (p) => {
+        const activatedAt = p.activatedAt.toNumber()
+        const expiresAt = activatedAt + p.parameters.votingPeriod.toNumber()
+        await api.untilBlock(expiresAt)
+      })
+    )
+  }
+
+  protected assertQueriedProposalsStatusesAreValid(qProposals: ProposalFieldsFragment[]): void {
+    this.proposalIds.forEach((id) => {
+      const qProposal = qProposals.find((p) => p.id === id.toString())
+      Utils.assert(qProposal, 'Query node: Proposal not found')
+      Utils.assert(
+        qProposal.status.__typename === 'ProposalStatusExpired',
+        `Unexpected proposal status: ${qProposal.status.__typename}`
+      )
+      Utils.assert(qProposal.status.proposalDecisionMadeEvent, 'Missing proposalDecisionMadeEvent relation')
+      assert.equal(qProposal.status.proposalDecisionMadeEvent.decisionStatus.__typename, 'ProposalStatusExpired')
+    })
+  }
+
+  public async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getProposalsByIds(this.proposalIds),
+      (res) => this.assertQueriedProposalsStatusesAreValid(res)
+    )
+  }
+}

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

@@ -5,3 +5,6 @@ export {
   DecideOnProposalStatusParams,
   DecisionStatus,
 } from './DecideOnProposalStatusFixture'
+export { AllProposalsOutcomesFixture, TestedProposal } from './AllProposalsOutcomesFixture'
+export { CancelProposalsFixture } from './CancelProposalsFixture'
+export { ExpireProposalsFixture } from './ExpireProposalsFixture'

+ 7 - 3
tests/integration-tests/src/flows/proposals/cancellingProposal.ts

@@ -2,14 +2,16 @@ import { FlowProps } from '../../Flow'
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
-import { CreateProposalsFixture } from '../../fixtures/proposals'
-import { CancelProposalsFixture } from '../../fixtures/proposals/CancelProposalsFixture'
+import { CreateProposalsFixture, CancelProposalsFixture } from '../../fixtures/proposals'
+import { Resource } from '../../Resources'
 
-export default async function cancellingProposals({ api, query }: FlowProps): Promise<void> {
+export default async function cancellingProposals({ api, query, lock }: FlowProps): Promise<void> {
   const debug = Debugger('flow:cancelling-proposals')
   debug('Started')
   api.enableDebugTxLogs()
 
+  const unlock = await lock(Resource.Proposals)
+
   const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
@@ -30,5 +32,7 @@ export default async function cancellingProposals({ api, query }: FlowProps): Pr
   const cancelProposalsFixture = new CancelProposalsFixture(api, query, [proposalId])
   await new FixtureRunner(cancelProposalsFixture).runWithQueryNodeChecks()
 
+  unlock()
+
   debug('Done')
 }

+ 41 - 0
tests/integration-tests/src/flows/proposals/exactExecutionBlock.ts

@@ -0,0 +1,41 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { CreateProposalsFixture, DecideOnProposalStatusFixture } from '../../fixtures/proposals'
+import { Resource } from '../../Resources'
+
+export default async function exactExecutionBlock({ api, query, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:proposal-exact-execution-block')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const unlock = await lock(Resource.Proposals)
+
+  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
+  await new FixtureRunner(buyMembershipFixture).run()
+  const [memberId] = buyMembershipFixture.getCreatedMembers()
+
+  const currentBlock = (await api.getBestBlock()).toNumber()
+  const exactExecutionBlock = currentBlock + 50
+  const createProposalFixture = new CreateProposalsFixture(api, query, [
+    {
+      type: 'Signal',
+      details: `Proposal to be executed at block ${exactExecutionBlock}`,
+      asMember: memberId,
+      title: `Executes at #${exactExecutionBlock}`,
+      description: `Proposal to be executed at block ${exactExecutionBlock}`,
+      exactExecutionBlock,
+    },
+  ])
+  await new FixtureRunner(createProposalFixture).run()
+  const [proposalId] = createProposalFixture.getCreatedProposalsIds()
+
+  const approveProposalFixture = new DecideOnProposalStatusFixture(api, query, [{ proposalId, status: 'Approved' }])
+  await new FixtureRunner(approveProposalFixture).runWithQueryNodeChecks()
+
+  unlock()
+
+  debug('Done')
+}

+ 38 - 0
tests/integration-tests/src/flows/proposals/expireProposal.ts

@@ -0,0 +1,38 @@
+import { FlowProps } from '../../Flow'
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
+import { CreateProposalsFixture, ExpireProposalsFixture } from '../../fixtures/proposals'
+import { Resource } from '../../Resources'
+
+export default async function expireProposal({ api, query, lock }: FlowProps): Promise<void> {
+  const debug = Debugger('flow:expire-proposal')
+  debug('Started')
+  api.enableDebugTxLogs()
+
+  const unlock = await lock(Resource.Proposals)
+
+  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
+  await new FixtureRunner(buyMembershipFixture).run()
+  const [memberId] = buyMembershipFixture.getCreatedMembers()
+
+  const createProposalFixture = new CreateProposalsFixture(api, query, [
+    {
+      type: 'Signal',
+      details: `Proposal to be Expired`,
+      asMember: memberId,
+      title: `Proposal to be Expired`,
+      description: `Proposal to be Expired`,
+    },
+  ])
+  await new FixtureRunner(createProposalFixture).run()
+  const [proposalId] = createProposalFixture.getCreatedProposalsIds()
+
+  const approveProposalFixture = new ExpireProposalsFixture(api, query, [proposalId])
+  await new FixtureRunner(approveProposalFixture).runWithQueryNodeChecks()
+
+  unlock()
+
+  debug('Done')
+}

+ 5 - 5
tests/integration-tests/src/flows/proposals/index.ts

@@ -9,9 +9,9 @@ import {
   DEFAULT_OPENING_PARAMS,
 } from '../../fixtures/workingGroups'
 import { OpeningMetadata } from '@joystream/metadata-protobuf'
-import { AllProposalsOutcomesFixture, TestedProposal } from '../../fixtures/proposals/AllProposalsOutcomesFixture'
+import { AllProposalsOutcomesFixture, TestedProposal } from '../../fixtures/proposals'
 
-export default async function creatingProposals({ api, query }: FlowProps): Promise<void> {
+export default async function creatingProposals({ api, query, lock }: FlowProps): Promise<void> {
   const debug = Debugger('flow:creating-proposals')
   debug('Started')
   api.enableDebugTxLogs()
@@ -105,7 +105,7 @@ export default async function creatingProposals({ api, query }: FlowProps): Prom
     { details: { UnlockBlogPost: 999 }, expectExecutionFailure: true },
   ]
 
-  const testAllOutcomesFixture = new AllProposalsOutcomesFixture(api, query, proposalsToTest)
+  const testAllOutcomesFixture = new AllProposalsOutcomesFixture(api, query, lock, proposalsToTest)
   await new FixtureRunner(testAllOutcomesFixture).run()
 
   // The membership lead should be hired at this point, so we can test lead-related proposals
@@ -117,10 +117,10 @@ export default async function creatingProposals({ api, query }: FlowProps): Prom
     { details: { SetWorkingGroupLeadReward: [leadId, 50, 'Membership'] } },
     { details: { SlashWorkingGroupLead: [leadId, 100, 'Membership'] } },
   ]
-  const leadProposalsOutcomesFixture = new AllProposalsOutcomesFixture(api, query, leadProposalsToTest)
+  const leadProposalsOutcomesFixture = new AllProposalsOutcomesFixture(api, query, lock, leadProposalsToTest)
   await new FixtureRunner(leadProposalsOutcomesFixture).run()
 
-  const terminateLeadProposalOutcomesFixture = new AllProposalsOutcomesFixture(api, query, [
+  const terminateLeadProposalOutcomesFixture = new AllProposalsOutcomesFixture(api, query, lock, [
     {
       details: { TerminateWorkingGroupLead: { worker_id: leadId, working_group: 'Membership', slashing_amount: 100 } },
     },

+ 13 - 4
tests/integration-tests/src/flows/proposals/runtimeUpgradeProposal.ts

@@ -1,18 +1,25 @@
 import { FlowProps } from '../../Flow'
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
-import { AllProposalsOutcomesFixture, TestedProposal } from '../../fixtures/proposals/AllProposalsOutcomesFixture'
 import { Utils } from '../../utils'
 import fs from 'fs'
-import { CreateProposalsFixture, DecideOnProposalStatusFixture } from '../../fixtures/proposals'
+import {
+  CreateProposalsFixture,
+  DecideOnProposalStatusFixture,
+  AllProposalsOutcomesFixture,
+  TestedProposal,
+} from '../../fixtures/proposals'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
 import { assert } from 'chai'
+import { Resource } from '../../Resources'
 
-export default async function runtimeUpgradeProposal({ api, query, env }: FlowProps): Promise<void> {
+export default async function runtimeUpgradeProposal({ api, query, lock, env }: FlowProps): Promise<void> {
   const debug = Debugger('flow:runtime-upgrade-proposal')
   debug('Started')
   api.enableVerboseTxLogs()
 
+  const unlocks = await Promise.all(Array.from({ length: 2 }, () => lock(Resource.Proposals)))
+
   const runtimeUpgradeWasmPath = env.RUNTIME_UPGRADE_TARGET_WASM_PATH
 
   Utils.assert(
@@ -47,7 +54,7 @@ export default async function runtimeUpgradeProposal({ api, query, env }: FlowPr
   const testedProposals: TestedProposal[] = [
     { details: { RuntimeUpgrade: Utils.readRuntimeFromFile(runtimeUpgradeWasmPath) } },
   ]
-  const testAllOutcomesFixture = new AllProposalsOutcomesFixture(api, query, testedProposals)
+  const testAllOutcomesFixture = new AllProposalsOutcomesFixture(api, query, lock, testedProposals)
   await new FixtureRunner(testAllOutcomesFixture).run()
 
   // Check the "CancelledByRuntime" proposal status
@@ -66,5 +73,7 @@ export default async function runtimeUpgradeProposal({ api, query, env }: FlowPr
     }
   )
 
+  unlocks.map((unlock) => unlock())
+
   debug('Done')
 }

+ 6 - 1
tests/integration-tests/src/flows/proposals/vetoProposal.ts

@@ -3,12 +3,15 @@ import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 import { BuyMembershipHappyCaseFixture } from '../../fixtures/membership'
 import { CreateProposalsFixture, DecideOnProposalStatusFixture } from '../../fixtures/proposals'
+import { Resource } from '../../Resources'
 
-export default async function vetoProposal({ api, query }: FlowProps): Promise<void> {
+export default async function vetoProposal({ api, query, lock }: FlowProps): Promise<void> {
   const debug = Debugger('flow:creating-proposals')
   debug('Started')
   api.enableDebugTxLogs()
 
+  const unlocks = await Promise.all(Array.from({ length: 2 }, () => lock(Resource.Proposals)))
+
   const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
@@ -43,5 +46,7 @@ export default async function vetoProposal({ api, query }: FlowProps): Promise<v
   ])
   await new FixtureRunner(decideOnProposalStatusFixture).runWithQueryNodeChecks()
 
+  unlocks.forEach((unlock) => unlock())
+
   debug('Done')
 }

+ 17 - 3
tests/integration-tests/src/graphql/generated/queries.ts

@@ -818,7 +818,7 @@ type ProposalDetailsFields_SignalProposalDetails_Fragment = { __typename: 'Signa
 
 type ProposalDetailsFields_RuntimeUpgradeProposalDetails_Fragment = {
   __typename: 'RuntimeUpgradeProposalDetails'
-  wasmBytecodeHash: string
+  newRuntimeBytecode?: Types.Maybe<{ id: string; bytecode: any }>
 }
 
 type ProposalDetailsFields_FundingRequestProposalDetails_Fragment = {
@@ -1012,7 +1012,14 @@ export type ProposalFieldsFragment = {
     | ProposalDetailsFields_UnlockBlogPostProposalDetails_Fragment
     | ProposalDetailsFields_VetoProposalDetails_Fragment
   creator: { id: string }
-  proposalStatusUpdates: Array<{ id: string }>
+  proposalStatusUpdates: Array<{
+    id: string
+    inBlock: number
+    newStatus:
+      | { __typename: 'ProposalStatusDeciding' }
+      | { __typename: 'ProposalStatusGracing' }
+      | { __typename: 'ProposalStatusDormant' }
+  }>
   votes: Array<{ id: string }>
   status:
     | ProposalStatusFields_ProposalStatusDeciding_Fragment
@@ -2397,7 +2404,10 @@ export const ProposalDetailsFields = gql`
       text
     }
     ... on RuntimeUpgradeProposalDetails {
-      wasmBytecodeHash
+      newRuntimeBytecode {
+        id
+        bytecode
+      }
     }
     ... on FundingRequestProposalDetails {
       destinationsList {
@@ -2621,6 +2631,10 @@ export const ProposalFields = gql`
     councilApprovals
     proposalStatusUpdates {
       id
+      inBlock
+      newStatus {
+        __typename
+      }
     }
     votes {
       id

+ 72 - 15
tests/integration-tests/src/graphql/generated/schema.ts

@@ -15,6 +15,8 @@ export type Scalars = {
   BigInt: any
   /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
   JSONObject: any
+  /** GraphQL representation of Bytes */
+  Bytes: any
 }
 
 export type AmendConstitutionProposalDetails = {
@@ -9984,6 +9986,9 @@ export type Query = {
   rewardPaidEvents: Array<RewardPaidEvent>
   rewardPaidEventByUniqueInput?: Maybe<RewardPaidEvent>
   rewardPaidEventsConnection: RewardPaidEventConnection
+  runtimeWasmBytecodes: Array<RuntimeWasmBytecode>
+  runtimeWasmBytecodeByUniqueInput?: Maybe<RuntimeWasmBytecode>
+  runtimeWasmBytecodesConnection: RuntimeWasmBytecodeConnection
   stakeDecreasedEvents: Array<StakeDecreasedEvent>
   stakeDecreasedEventByUniqueInput?: Maybe<StakeDecreasedEvent>
   stakeDecreasedEventsConnection: StakeDecreasedEventConnection
@@ -11329,6 +11334,26 @@ export type QueryRewardPaidEventsConnectionArgs = {
   orderBy?: Maybe<Array<RewardPaidEventOrderByInput>>
 }
 
+export type QueryRuntimeWasmBytecodesArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<RuntimeWasmBytecodeWhereInput>
+  orderBy?: Maybe<Array<RuntimeWasmBytecodeOrderByInput>>
+}
+
+export type QueryRuntimeWasmBytecodeByUniqueInputArgs = {
+  where: RuntimeWasmBytecodeWhereUniqueInput
+}
+
+export type QueryRuntimeWasmBytecodesConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<RuntimeWasmBytecodeWhereInput>
+  orderBy?: Maybe<Array<RuntimeWasmBytecodeOrderByInput>>
+}
+
 export type QueryStakeDecreasedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -12168,19 +12193,54 @@ export enum RewardPaymentType {
 }
 
 export type RuntimeUpgradeProposalDetails = {
-  /** Runtime upgrade WASM bytecode hash */
-  wasmBytecodeHash: Scalars['String']
+  /** Runtime upgrade WASM bytecode */
+  newRuntimeBytecode?: Maybe<RuntimeWasmBytecode>
 }
 
-export type RuntimeUpgradeProposalDetailsCreateInput = {
-  wasmBytecodeHash: Scalars['String']
+export type RuntimeWasmBytecode = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** The bytecode itself */
+  bytecode: Scalars['Bytes']
+}
+
+export type RuntimeWasmBytecodeConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<RuntimeWasmBytecodeEdge>
+  pageInfo: PageInfo
+}
+
+export type RuntimeWasmBytecodeCreateInput = {
+  bytecode: Scalars['Bytes']
+}
+
+export type RuntimeWasmBytecodeEdge = {
+  node: RuntimeWasmBytecode
+  cursor: Scalars['String']
+}
+
+export enum RuntimeWasmBytecodeOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  BytecodeAsc = 'bytecode_ASC',
+  BytecodeDesc = 'bytecode_DESC',
 }
 
-export type RuntimeUpgradeProposalDetailsUpdateInput = {
-  wasmBytecodeHash?: Maybe<Scalars['String']>
+export type RuntimeWasmBytecodeUpdateInput = {
+  bytecode?: Maybe<Scalars['Bytes']>
 }
 
-export type RuntimeUpgradeProposalDetailsWhereInput = {
+export type RuntimeWasmBytecodeWhereInput = {
   id_eq?: Maybe<Scalars['ID']>
   id_in?: Maybe<Array<Scalars['ID']>>
   createdAt_eq?: Maybe<Scalars['DateTime']>
@@ -12205,16 +12265,13 @@ export type RuntimeUpgradeProposalDetailsWhereInput = {
   deletedAt_gte?: Maybe<Scalars['DateTime']>
   deletedById_eq?: Maybe<Scalars['ID']>
   deletedById_in?: Maybe<Array<Scalars['ID']>>
-  wasmBytecodeHash_eq?: Maybe<Scalars['String']>
-  wasmBytecodeHash_contains?: Maybe<Scalars['String']>
-  wasmBytecodeHash_startsWith?: Maybe<Scalars['String']>
-  wasmBytecodeHash_endsWith?: Maybe<Scalars['String']>
-  wasmBytecodeHash_in?: Maybe<Array<Scalars['String']>>
-  AND?: Maybe<Array<RuntimeUpgradeProposalDetailsWhereInput>>
-  OR?: Maybe<Array<RuntimeUpgradeProposalDetailsWhereInput>>
+  bytecode_eq?: Maybe<Scalars['Bytes']>
+  bytecode_in?: Maybe<Array<Scalars['Bytes']>>
+  AND?: Maybe<Array<RuntimeWasmBytecodeWhereInput>>
+  OR?: Maybe<Array<RuntimeWasmBytecodeWhereInput>>
 }
 
-export type RuntimeUpgradeProposalDetailsWhereUniqueInput = {
+export type RuntimeWasmBytecodeWhereUniqueInput = {
   id: Scalars['ID']
 }
 

+ 8 - 1
tests/integration-tests/src/graphql/queries/proposals.graphql

@@ -99,7 +99,10 @@ fragment ProposalDetailsFields on ProposalDetails {
     text
   }
   ... on RuntimeUpgradeProposalDetails {
-    wasmBytecodeHash
+    newRuntimeBytecode {
+      id
+      bytecode
+    }
   }
   ... on FundingRequestProposalDetails {
     destinationsList {
@@ -248,6 +251,10 @@ fragment ProposalFields on Proposal {
   councilApprovals
   proposalStatusUpdates {
     id
+    inBlock
+    newStatus {
+      __typename
+    }
   }
   votes {
     id

+ 10 - 2
tests/integration-tests/src/scenarios/full.ts

@@ -21,13 +21,15 @@ import cancellingProposals from '../flows/proposals/cancellingProposal'
 import vetoProposal from '../flows/proposals/vetoProposal'
 import electCouncil from '../flows/council/elect'
 import runtimeUpgradeProposal from '../flows/proposals/runtimeUpgradeProposal'
+import exactExecutionBlock from '../flows/proposals/exactExecutionBlock'
+import expireProposal from '../flows/proposals/expireProposal'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job, env }) => {
   // Runtime upgrade should always be first job
   // (except councilJob, which is required for voting and should probably depend on the "source" runtime)
   const councilJob = job('electing council', electCouncil)
-  const runtimeUpgradeProposalJob = env.RUNTIME_UPGRADE_TARGET_IMAGE_TAG
+  const runtimeUpgradeProposalJob = env.RUNTIME_UPGRADE_TARGET_WASM_PATH
     ? job('runtime upgrade proposal', runtimeUpgradeProposal).requires(councilJob)
     : undefined
 
@@ -47,7 +49,13 @@ scenario(async ({ job, env }) => {
   job('managing staking accounts', managingStakingAccounts).after(membershipSystemJob)
 
   // Proposals:
-  const proposalsJob = job('proposals', [proposals, cancellingProposals, vetoProposal]).requires(membershipSystemJob)
+  const proposalsJob = job('proposals', [
+    proposals,
+    cancellingProposals,
+    vetoProposal,
+    exactExecutionBlock,
+    expireProposal,
+  ]).requires(membershipSystemJob)
 
   // Working groups
   const sudoHireLead = job('sudo lead opening', leadOpening).after(proposalsJob)

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

@@ -3,12 +3,16 @@ import cancellingProposals from '../flows/proposals/cancellingProposal'
 import vetoProposal from '../flows/proposals/vetoProposal'
 import electCouncil from '../flows/council/elect'
 import runtimeUpgradeProposal from '../flows/proposals/runtimeUpgradeProposal'
+import exactExecutionBlock from '../flows/proposals/exactExecutionBlock'
+import expireProposal from '../flows/proposals/expireProposal'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job, env }) => {
   const councilJob = job('electing council', electCouncil)
-  const runtimeUpgradeProposalJob = env.RUNTIME_UPGRADE_TARGET_IMAGE_TAG
+  const runtimeUpgradeProposalJob = env.RUNTIME_UPGRADE_TARGET_WASM_PATH
     ? job('runtime upgrade proposal', runtimeUpgradeProposal).requires(councilJob)
     : undefined
-  job('proposals', [proposals, cancellingProposals, vetoProposal]).requires(runtimeUpgradeProposalJob || councilJob)
+  job('proposals', [proposals, cancellingProposals, vetoProposal, exactExecutionBlock, expireProposal]).requires(
+    runtimeUpgradeProposalJob || councilJob
+  )
 })