CreateProposalsFixture.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { Api } from '../../Api'
  2. import { QueryNodeApi } from '../../QueryNodeApi'
  3. import { ProposalCreatedEventDetails, ProposalDetailsJsonByType, ProposalType } from '../../types'
  4. import { SubmittableExtrinsic } from '@polkadot/api/types'
  5. import { Utils } from '../../utils'
  6. import { ISubmittableResult } from '@polkadot/types/types/'
  7. import { ProposalCreatedEventFieldsFragment, ProposalFieldsFragment } from '../../graphql/generated/queries'
  8. import { assert } from 'chai'
  9. import { ProposalId, ProposalParameters } from '@joystream/types/proposals'
  10. import { MemberId } from '@joystream/types/common'
  11. import { FixtureRunner, StandardizedFixture } from '../../Fixture'
  12. import { AddStakingAccountsHappyCaseFixture } from '../membership'
  13. import { getWorkingGroupModuleName } from '../../consts'
  14. import { assertQueriedOpeningMetadataIsValid } from '../workingGroups/utils'
  15. import { OpeningMetadata } from '@joystream/metadata-protobuf'
  16. import { blake2AsHex } from '@polkadot/util-crypto'
  17. export type ProposalCreationParams<T extends ProposalType = ProposalType> = {
  18. asMember: MemberId
  19. title: string
  20. description: string
  21. exactExecutionBlock?: number
  22. type: T
  23. details: ProposalDetailsJsonByType<T>
  24. }
  25. export class CreateProposalsFixture extends StandardizedFixture {
  26. protected events: ProposalCreatedEventDetails[] = []
  27. protected proposalsParams: ProposalCreationParams[]
  28. protected stakingAccounts: string[] = []
  29. public constructor(api: Api, query: QueryNodeApi, proposalsParams: ProposalCreationParams[]) {
  30. super(api, query)
  31. this.proposalsParams = proposalsParams
  32. }
  33. public getCreatedProposalsIds(): ProposalId[] {
  34. if (!this.events.length) {
  35. throw new Error('Trying to get created opening ids before they were created!')
  36. }
  37. return this.events.map((e) => e.proposalId)
  38. }
  39. protected proposalParams(i: number): ProposalParameters {
  40. const proposalType = this.proposalsParams[i].type
  41. return this.api.proposalParametersByType(proposalType)
  42. }
  43. protected async getSignerAccountOrAccounts(): Promise<string[]> {
  44. return this.api.getMemberSigners(this.proposalsParams)
  45. }
  46. protected async initStakingAccounts(): Promise<void> {
  47. const { api, query } = this
  48. const stakingAccounts = (await this.api.createKeyPairs(this.proposalsParams.length)).map((kp) => kp.address)
  49. const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
  50. api,
  51. query,
  52. this.proposalsParams.map(({ asMember }, i) => ({
  53. asMember,
  54. account: stakingAccounts[i],
  55. stakeAmount: this.proposalParams(i).requiredStake.unwrapOr(undefined),
  56. }))
  57. )
  58. await new FixtureRunner(addStakingAccountsFixture).run()
  59. this.stakingAccounts = stakingAccounts
  60. }
  61. public async execute(): Promise<void> {
  62. await this.initStakingAccounts()
  63. await super.execute()
  64. }
  65. protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
  66. return this.proposalsParams.map(({ asMember, description, title, exactExecutionBlock, details, type }, i) => {
  67. const proposalDetails = { [type]: details } as { [K in ProposalType]: ProposalDetailsJsonByType<K> }
  68. return this.api.tx.proposalsCodex.createProposal(
  69. {
  70. member_id: asMember,
  71. description: description,
  72. title: title,
  73. exact_execution_block: exactExecutionBlock,
  74. staking_account_id: this.stakingAccounts[i],
  75. },
  76. proposalDetails
  77. )
  78. })
  79. }
  80. protected async getEventFromResult(result: ISubmittableResult): Promise<ProposalCreatedEventDetails> {
  81. return this.api.retrieveProposalCreatedEventDetails(result)
  82. }
  83. protected assertProposalDetailsAreValid(
  84. params: ProposalCreationParams<ProposalType>,
  85. qProposal: ProposalFieldsFragment
  86. ): void {
  87. const proposalDetails = this.api.createType('ProposalDetails', { [params.type]: params.details })
  88. switch (params.type) {
  89. case 'AmendConstitution': {
  90. Utils.assert(qProposal.details.__typename === 'AmendConstitutionProposalDetails')
  91. const details = proposalDetails.asType('AmendConstitution')
  92. assert.equal(qProposal.details.text, details.toString())
  93. break
  94. }
  95. case 'CancelWorkingGroupLeadOpening': {
  96. Utils.assert(qProposal.details.__typename === 'CancelWorkingGroupLeadOpeningProposalDetails')
  97. const details = proposalDetails.asType('CancelWorkingGroupLeadOpening')
  98. const [openingId, workingGroup] = details
  99. const expectedId = `${getWorkingGroupModuleName(workingGroup)}-${openingId.toString()}`
  100. assert.equal(qProposal.details.opening?.id, expectedId)
  101. break
  102. }
  103. case 'CreateBlogPost': {
  104. Utils.assert(qProposal.details.__typename === 'CreateBlogPostProposalDetails')
  105. const details = proposalDetails.asType('CreateBlogPost')
  106. const [title, body] = details
  107. assert.equal(qProposal.details.title, title.toString())
  108. assert.equal(qProposal.details.body, body.toString())
  109. break
  110. }
  111. case 'CreateWorkingGroupLeadOpening': {
  112. Utils.assert(qProposal.details.__typename === 'CreateWorkingGroupLeadOpeningProposalDetails')
  113. const details = proposalDetails.asType('CreateWorkingGroupLeadOpening')
  114. assert.equal(qProposal.details.group?.id, getWorkingGroupModuleName(details.working_group))
  115. assert.equal(qProposal.details.rewardPerBlock, details.reward_per_block.toString())
  116. assert.equal(qProposal.details.stakeAmount, details.stake_policy.stake_amount.toString())
  117. assert.equal(qProposal.details.unstakingPeriod, details.stake_policy.leaving_unstaking_period.toNumber())
  118. Utils.assert(qProposal.details.metadata)
  119. assertQueriedOpeningMetadataIsValid(
  120. qProposal.details.metadata,
  121. Utils.metadataFromBytes(OpeningMetadata, details.description)
  122. )
  123. break
  124. }
  125. case 'DecreaseWorkingGroupLeadStake': {
  126. Utils.assert(qProposal.details.__typename === 'DecreaseWorkingGroupLeadStakeProposalDetails')
  127. const details = proposalDetails.asType('DecreaseWorkingGroupLeadStake')
  128. const [workerId, amount, group] = details
  129. const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
  130. assert.equal(qProposal.details.amount, amount.toString())
  131. assert.equal(qProposal.details.lead?.id, expectedId)
  132. break
  133. }
  134. case 'EditBlogPost': {
  135. Utils.assert(qProposal.details.__typename === 'EditBlogPostProposalDetails')
  136. const details = proposalDetails.asType('EditBlogPost')
  137. const [postId, newTitle, newBody] = details
  138. assert.equal(qProposal.details.blogPost, postId.toString())
  139. assert.equal(qProposal.details.newTitle, newTitle.unwrapOr(undefined)?.toString())
  140. assert.equal(qProposal.details.newBody, newBody.unwrapOr(undefined)?.toString())
  141. break
  142. }
  143. case 'FillWorkingGroupLeadOpening': {
  144. Utils.assert(qProposal.details.__typename === 'FillWorkingGroupLeadOpeningProposalDetails')
  145. const details = proposalDetails.asType('FillWorkingGroupLeadOpening')
  146. const expectedOpeningId = `${getWorkingGroupModuleName(details.working_group)}-${details.opening_id.toString()}`
  147. const expectedApplicationId = `${getWorkingGroupModuleName(
  148. details.working_group
  149. )}-${details.successful_application_id.toString()}`
  150. assert.equal(qProposal.details.opening?.id, expectedOpeningId)
  151. assert.equal(qProposal.details.application?.id, expectedApplicationId)
  152. break
  153. }
  154. case 'FundingRequest': {
  155. Utils.assert(qProposal.details.__typename === 'FundingRequestProposalDetails')
  156. const details = proposalDetails.asType('FundingRequest')
  157. assert.sameDeepMembers(
  158. qProposal.details.destinationsList?.destinations.map(({ amount, account }) => ({ amount, account })) || [],
  159. details.map((d) => ({ amount: d.amount.toString(), account: d.account.toString() }))
  160. )
  161. break
  162. }
  163. case 'LockBlogPost': {
  164. Utils.assert(qProposal.details.__typename === 'LockBlogPostProposalDetails')
  165. const postId = proposalDetails.asType('LockBlogPost')
  166. assert.equal(qProposal.details.blogPost, postId.toString())
  167. break
  168. }
  169. case 'RuntimeUpgrade': {
  170. Utils.assert(qProposal.details.__typename === 'RuntimeUpgradeProposalDetails')
  171. const details = proposalDetails.asType('RuntimeUpgrade')
  172. Utils.assert(qProposal.details.newRuntimeBytecode, 'Missing newRuntimeBytecode relationship')
  173. assert.equal(qProposal.details.newRuntimeBytecode.id, blake2AsHex(details.toU8a(true)))
  174. const expectedBytecode = '0x' + Buffer.from(details.toU8a(true)).toString('hex')
  175. const actualBytecode = qProposal.details.newRuntimeBytecode.bytecode
  176. if (actualBytecode !== expectedBytecode) {
  177. const diffStartPos = expectedBytecode.split('').findIndex((c, i) => actualBytecode[i] !== c)
  178. const diffSubExpected = expectedBytecode.slice(diffStartPos, diffStartPos + 10)
  179. const diffSubActual = actualBytecode.slice(diffStartPos, diffStartPos + 10)
  180. throw new Error(
  181. `Runtime bytecode doesn't match the expected one! Diff starts at pos ${diffStartPos}. ` +
  182. `Expected: ${diffSubExpected}.., Actual: ${diffSubActual}...`
  183. )
  184. }
  185. break
  186. }
  187. case 'SetCouncilBudgetIncrement': {
  188. Utils.assert(qProposal.details.__typename === 'SetCouncilBudgetIncrementProposalDetails')
  189. const details = proposalDetails.asType('SetCouncilBudgetIncrement')
  190. assert.equal(qProposal.details.newAmount, details.toString())
  191. break
  192. }
  193. case 'SetCouncilorReward': {
  194. Utils.assert(qProposal.details.__typename === 'SetCouncilorRewardProposalDetails')
  195. const details = proposalDetails.asType('SetCouncilorReward')
  196. assert.equal(qProposal.details.newRewardPerBlock, details.toString())
  197. break
  198. }
  199. case 'SetInitialInvitationBalance': {
  200. Utils.assert(qProposal.details.__typename === 'SetInitialInvitationBalanceProposalDetails')
  201. const details = proposalDetails.asType('SetInitialInvitationBalance')
  202. assert.equal(qProposal.details.newInitialInvitationBalance, details.toString())
  203. break
  204. }
  205. case 'SetInitialInvitationCount': {
  206. Utils.assert(qProposal.details.__typename === 'SetInitialInvitationCountProposalDetails')
  207. const details = proposalDetails.asType('SetInitialInvitationCount')
  208. assert.equal(qProposal.details.newInitialInvitationsCount, details.toNumber())
  209. break
  210. }
  211. case 'SetMaxValidatorCount': {
  212. Utils.assert(qProposal.details.__typename === 'SetMaxValidatorCountProposalDetails')
  213. const details = proposalDetails.asType('SetMaxValidatorCount')
  214. assert.equal(qProposal.details.newMaxValidatorCount, details.toNumber())
  215. break
  216. }
  217. case 'SetMembershipLeadInvitationQuota': {
  218. Utils.assert(qProposal.details.__typename === 'SetMembershipLeadInvitationQuotaProposalDetails')
  219. const details = proposalDetails.asType('SetMembershipLeadInvitationQuota')
  220. assert.equal(qProposal.details.newLeadInvitationQuota, details.toNumber())
  221. break
  222. }
  223. case 'SetMembershipPrice': {
  224. Utils.assert(qProposal.details.__typename === 'SetMembershipPriceProposalDetails')
  225. const details = proposalDetails.asType('SetMembershipPrice')
  226. assert.equal(qProposal.details.newPrice, details.toString())
  227. break
  228. }
  229. case 'SetReferralCut': {
  230. Utils.assert(qProposal.details.__typename === 'SetReferralCutProposalDetails')
  231. const details = proposalDetails.asType('SetReferralCut')
  232. assert.equal(qProposal.details.newReferralCut, details.toNumber())
  233. break
  234. }
  235. case 'SetWorkingGroupLeadReward': {
  236. Utils.assert(qProposal.details.__typename === 'SetWorkingGroupLeadRewardProposalDetails')
  237. const details = proposalDetails.asType('SetWorkingGroupLeadReward')
  238. const [workerId, reward, group] = details
  239. const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
  240. assert.equal(qProposal.details.newRewardPerBlock, reward.toString())
  241. assert.equal(qProposal.details.lead?.id, expectedId)
  242. break
  243. }
  244. case 'Signal': {
  245. Utils.assert(qProposal.details.__typename === 'SignalProposalDetails')
  246. const details = proposalDetails.asType('Signal')
  247. assert.equal(qProposal.details.text, details.toString())
  248. break
  249. }
  250. case 'SlashWorkingGroupLead': {
  251. Utils.assert(qProposal.details.__typename === 'SlashWorkingGroupLeadProposalDetails')
  252. const details = proposalDetails.asType('SlashWorkingGroupLead')
  253. const [workerId, amount, group] = details
  254. const expectedId = `${getWorkingGroupModuleName(group)}-${workerId.toString()}`
  255. assert.equal(qProposal.details.lead?.id, expectedId)
  256. assert.equal(qProposal.details.amount, amount.toString())
  257. break
  258. }
  259. case 'TerminateWorkingGroupLead': {
  260. Utils.assert(qProposal.details.__typename === 'TerminateWorkingGroupLeadProposalDetails')
  261. const details = proposalDetails.asType('TerminateWorkingGroupLead')
  262. const expectedId = `${getWorkingGroupModuleName(details.working_group)}-${details.worker_id.toString()}`
  263. assert.equal(qProposal.details.lead?.id, expectedId)
  264. assert.equal(qProposal.details.slashingAmount, details.slashing_amount.toString())
  265. break
  266. }
  267. case 'UnlockBlogPost': {
  268. Utils.assert(qProposal.details.__typename === 'UnlockBlogPostProposalDetails')
  269. const postId = proposalDetails.asType('UnlockBlogPost')
  270. assert.equal(qProposal.details.blogPost, postId.toString())
  271. break
  272. }
  273. case 'UpdateWorkingGroupBudget': {
  274. Utils.assert(qProposal.details.__typename === 'UpdateWorkingGroupBudgetProposalDetails')
  275. const details = proposalDetails.asType('UpdateWorkingGroupBudget')
  276. const [balance, group, balanceKind] = details
  277. assert.equal(qProposal.details.amount, (balanceKind.isOfType('Negative') ? '-' : '') + balance.toString())
  278. assert.equal(qProposal.details.group?.id, getWorkingGroupModuleName(group))
  279. break
  280. }
  281. case 'VetoProposal': {
  282. Utils.assert(qProposal.details.__typename === 'VetoProposalDetails')
  283. const details = proposalDetails.asType('VetoProposal')
  284. assert.equal(qProposal.details.proposal?.id, details.toString())
  285. break
  286. }
  287. }
  288. }
  289. protected assertQueriedProposalsAreValid(qProposals: ProposalFieldsFragment[]): void {
  290. this.events.map((e, i) => {
  291. const proposalParams = this.proposalsParams[i]
  292. const qProposal = qProposals.find((p) => p.id === e.proposalId.toString())
  293. Utils.assert(qProposal, 'Query node: Proposal not found')
  294. assert.equal(qProposal.councilApprovals, 0)
  295. assert.equal(qProposal.creator.id, proposalParams.asMember.toString())
  296. assert.equal(qProposal.description, proposalParams.description)
  297. assert.equal(qProposal.title, proposalParams.title)
  298. assert.equal(qProposal.stakingAccount, this.stakingAccounts[i].toString())
  299. assert.equal(qProposal.exactExecutionBlock, proposalParams.exactExecutionBlock)
  300. assert.equal(qProposal.status.__typename, 'ProposalStatusDeciding')
  301. assert.equal(qProposal.statusSetAtBlock, e.blockNumber)
  302. assert.equal(new Date(qProposal.statusSetAtTime).getTime(), e.blockTimestamp)
  303. assert.equal(qProposal.createdInEvent.inBlock, e.blockNumber)
  304. assert.equal(qProposal.createdInEvent.inExtrinsic, this.extrinsics[i].hash.toString())
  305. assert.equal(qProposal.isFinalized, false)
  306. assert.equal(qProposal.discussionThread.mode.__typename, 'ProposalDiscussionThreadModeOpen')
  307. this.assertProposalDetailsAreValid(proposalParams, qProposal)
  308. })
  309. }
  310. protected assertQueryNodeEventIsValid(qEvent: ProposalCreatedEventFieldsFragment, i: number): void {
  311. // TODO: https://github.com/Joystream/joystream/issues/2457
  312. }
  313. async runQueryNodeChecks(): Promise<void> {
  314. await super.runQueryNodeChecks()
  315. // Query the proposals
  316. await this.query.tryQueryWithTimeout(
  317. () => this.query.getProposalsByIds(this.events.map((e) => e.proposalId)),
  318. (result) => this.assertQueriedProposalsAreValid(result)
  319. )
  320. }
  321. }