proposals.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { SubstrateEvent, DatabaseManager } from '@dzlzv/hydra-common'
  5. import { ProposalDetails as RuntimeProposalDetails, ProposalId } from '@joystream/types/augment/all'
  6. import BN from 'bn.js'
  7. import {
  8. Proposal,
  9. SignalProposalDetails,
  10. RuntimeUpgradeProposalDetails,
  11. FundingRequestProposalDetails,
  12. SetMaxValidatorCountProposalDetails,
  13. CreateWorkingGroupLeadOpeningProposalDetails,
  14. FillWorkingGroupLeadOpeningProposalDetails,
  15. UpdateWorkingGroupBudgetProposalDetails,
  16. DecreaseWorkingGroupLeadStakeProposalDetails,
  17. SlashWorkingGroupLeadProposalDetails,
  18. SetWorkingGroupLeadRewardProposalDetails,
  19. TerminateWorkingGroupLeadProposalDetails,
  20. AmendConstitutionProposalDetails,
  21. CancelWorkingGroupLeadOpeningProposalDetails,
  22. SetMembershipPriceProposalDetails,
  23. SetCouncilBudgetIncrementProposalDetails,
  24. SetCouncilorRewardProposalDetails,
  25. SetInitialInvitationBalanceProposalDetails,
  26. SetInitialInvitationCountProposalDetails,
  27. SetMembershipLeadInvitationQuotaProposalDetails,
  28. SetReferralCutProposalDetails,
  29. CreateBlogPostProposalDetails,
  30. EditBlogPostProposalDetails,
  31. LockBlogPostProposalDetails,
  32. UnlockBlogPostProposalDetails,
  33. VetoProposalDetails,
  34. ProposalDetails,
  35. FundingRequestDestinationsList,
  36. FundingRequestDestination,
  37. Membership,
  38. ProposalStatusDeciding,
  39. ProposalIntermediateStatus,
  40. ProposalStatusDormant,
  41. ProposalStatusGracing,
  42. ProposalStatusUpdatedEvent,
  43. ProposalDecisionStatus,
  44. ProposalStatusCancelled,
  45. ProposalStatusExpired,
  46. ProposalStatusRejected,
  47. ProposalStatusSlashed,
  48. ProposalStatusVetoed,
  49. ProposalDecisionMadeEvent,
  50. ProposalStatusCanceledByRuntime,
  51. } from 'query-node/dist/model'
  52. import { genericEventFields, getWorkingGroupModuleName, perpareString } from './common'
  53. import { ProposalsEngine, ProposalsCodex } from './generated/types'
  54. import { createWorkingGroupOpeningMetadata } from './workingGroups'
  55. // FIXME: https://github.com/Joystream/joystream/issues/2457
  56. type ProposalsMappingsMemoryCache = {
  57. lastCreatedProposalId: ProposalId | null
  58. }
  59. const proposalsMappingsMemoryCache: ProposalsMappingsMemoryCache = {
  60. lastCreatedProposalId: null,
  61. }
  62. async function getProposal(db: DatabaseManager, id: string) {
  63. const proposal = await db.get(Proposal, { where: { id } })
  64. if (!proposal) {
  65. throw new Error(`Proposal not found by id: ${id}`)
  66. }
  67. return proposal
  68. }
  69. async function parseProposalDetails(
  70. event_: SubstrateEvent,
  71. db: DatabaseManager,
  72. proposalDetails: RuntimeProposalDetails
  73. ): Promise<typeof ProposalDetails> {
  74. const eventTime = new Date(event_.blockTimestamp)
  75. // SignalProposalDetails:
  76. if (proposalDetails.isSignal) {
  77. const details = new SignalProposalDetails()
  78. const specificDetails = proposalDetails.asSignal
  79. details.text = perpareString(specificDetails.toString())
  80. return details
  81. }
  82. // RuntimeUpgradeProposalDetails:
  83. else if (proposalDetails.isRuntimeUpgrade) {
  84. const details = new RuntimeUpgradeProposalDetails()
  85. const specificDetails = proposalDetails.asRuntimeUpgrade
  86. details.wasmBytecode = Buffer.from(specificDetails.toU8a(true))
  87. return details
  88. }
  89. // FundingRequestProposalDetails:
  90. else if (proposalDetails.isFundingRequest) {
  91. const destinationsList = new FundingRequestDestinationsList()
  92. const specificDetails = proposalDetails.asFundingRequest
  93. await db.save<FundingRequestDestinationsList>(destinationsList)
  94. await Promise.all(
  95. specificDetails.map(({ account, amount }) =>
  96. db.save(
  97. new FundingRequestDestination({
  98. createdAt: eventTime,
  99. updatedAt: eventTime,
  100. account: account.toString(),
  101. amount: new BN(amount.toString()),
  102. list: destinationsList,
  103. })
  104. )
  105. )
  106. )
  107. const details = new FundingRequestProposalDetails()
  108. details.destinationsListId = destinationsList.id
  109. return details
  110. }
  111. // SetMaxValidatorCountProposalDetails:
  112. else if (proposalDetails.isSetMaxValidatorCount) {
  113. const details = new SetMaxValidatorCountProposalDetails()
  114. const specificDetails = proposalDetails.asSetMaxValidatorCount
  115. details.newMaxValidatorCount = specificDetails.toNumber()
  116. return details
  117. }
  118. // CreateWorkingGroupLeadOpeningProposalDetails:
  119. else if (proposalDetails.isCreateWorkingGroupLeadOpening) {
  120. const details = new CreateWorkingGroupLeadOpeningProposalDetails()
  121. const specificDetails = proposalDetails.asCreateWorkingGroupLeadOpening
  122. const metadata = await createWorkingGroupOpeningMetadata(db, eventTime, specificDetails.description)
  123. details.groupId = getWorkingGroupModuleName(specificDetails.working_group)
  124. details.metadataId = metadata.id
  125. details.rewardPerBlock = new BN(specificDetails.reward_per_block.unwrapOr(0).toString())
  126. details.stakeAmount = new BN(specificDetails.stake_policy.stake_amount.toString())
  127. details.unstakingPeriod = specificDetails.stake_policy.leaving_unstaking_period.toNumber()
  128. return details
  129. }
  130. // FillWorkingGroupLeadOpeningProposalDetails:
  131. else if (proposalDetails.isFillWorkingGroupLeadOpening) {
  132. const details = new FillWorkingGroupLeadOpeningProposalDetails()
  133. const specificDetails = proposalDetails.asFillWorkingGroupLeadOpening
  134. const groupModuleName = getWorkingGroupModuleName(specificDetails.working_group)
  135. details.openingId = `${groupModuleName}-${specificDetails.opening_id.toString()}`
  136. details.applicationId = `${groupModuleName}-${specificDetails.successful_application_id.toString()}`
  137. return details
  138. }
  139. // UpdateWorkingGroupBudgetProposalDetails:
  140. else if (proposalDetails.isUpdateWorkingGroupBudget) {
  141. const details = new UpdateWorkingGroupBudgetProposalDetails()
  142. const specificDetails = proposalDetails.asUpdateWorkingGroupBudget
  143. const [amount, workingGroup, balanceKind] = specificDetails
  144. details.groupId = getWorkingGroupModuleName(workingGroup)
  145. details.amount = amount.muln(balanceKind.isNegative ? -1 : 1)
  146. return details
  147. }
  148. // DecreaseWorkingGroupLeadStakeProposalDetails:
  149. else if (proposalDetails.isDecreaseWorkingGroupLeadStake) {
  150. const details = new DecreaseWorkingGroupLeadStakeProposalDetails()
  151. const specificDetails = proposalDetails.asDecreaseWorkingGroupLeadStake
  152. const [workerId, amount, workingGroup] = specificDetails
  153. details.amount = new BN(amount.toString())
  154. details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
  155. return details
  156. }
  157. // SlashWorkingGroupLeadProposalDetails:
  158. else if (proposalDetails.isSlashWorkingGroupLead) {
  159. const details = new SlashWorkingGroupLeadProposalDetails()
  160. const specificDetails = proposalDetails.asSlashWorkingGroupLead
  161. const [workerId, amount, workingGroup] = specificDetails
  162. details.amount = new BN(amount.toString())
  163. details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
  164. return details
  165. }
  166. // SetWorkingGroupLeadRewardProposalDetails:
  167. else if (proposalDetails.isSetWorkingGroupLeadReward) {
  168. const details = new SetWorkingGroupLeadRewardProposalDetails()
  169. const specificDetails = proposalDetails.asSetWorkingGroupLeadReward
  170. const [workerId, reward, workingGroup] = specificDetails
  171. details.newRewardPerBlock = new BN(reward.unwrapOr(0).toString())
  172. details.leadId = `${getWorkingGroupModuleName(workingGroup)}-${workerId.toString()}`
  173. return details
  174. }
  175. // TerminateWorkingGroupLeadProposalDetails:
  176. else if (proposalDetails.isTerminateWorkingGroupLead) {
  177. const details = new TerminateWorkingGroupLeadProposalDetails()
  178. const specificDetails = proposalDetails.asTerminateWorkingGroupLead
  179. details.leadId = `${getWorkingGroupModuleName(
  180. specificDetails.working_group
  181. )}-${specificDetails.worker_id.toString()}`
  182. details.slashingAmount = specificDetails.slashing_amount.isSome
  183. ? new BN(specificDetails.slashing_amount.unwrap().toString())
  184. : undefined
  185. return details
  186. }
  187. // AmendConstitutionProposalDetails:
  188. else if (proposalDetails.isAmendConstitution) {
  189. const details = new AmendConstitutionProposalDetails()
  190. const specificDetails = proposalDetails.asAmendConstitution
  191. details.text = perpareString(specificDetails.toString())
  192. return details
  193. }
  194. // CancelWorkingGroupLeadOpeningProposalDetails:
  195. else if (proposalDetails.isCancelWorkingGroupLeadOpening) {
  196. const details = new CancelWorkingGroupLeadOpeningProposalDetails()
  197. const [openingId, workingGroup] = proposalDetails.asCancelWorkingGroupLeadOpening
  198. details.openingId = `${getWorkingGroupModuleName(workingGroup)}-${openingId.toString()}`
  199. return details
  200. }
  201. // SetCouncilBudgetIncrementProposalDetails:
  202. else if (proposalDetails.isSetCouncilBudgetIncrement) {
  203. const details = new SetCouncilBudgetIncrementProposalDetails()
  204. const specificDetails = proposalDetails.asSetCouncilBudgetIncrement
  205. details.newAmount = new BN(specificDetails.toString())
  206. return details
  207. }
  208. // SetMembershipPriceProposalDetails:
  209. else if (proposalDetails.isSetMembershipPrice) {
  210. const details = new SetMembershipPriceProposalDetails()
  211. const specificDetails = proposalDetails.asSetMembershipPrice
  212. details.newPrice = new BN(specificDetails.toString())
  213. return details
  214. }
  215. // SetCouncilorRewardProposalDetails:
  216. else if (proposalDetails.isSetCouncilorReward) {
  217. const details = new SetCouncilorRewardProposalDetails()
  218. const specificDetails = proposalDetails.asSetCouncilorReward
  219. details.newRewardPerBlock = new BN(specificDetails.toString())
  220. return details
  221. }
  222. // SetInitialInvitationBalanceProposalDetails:
  223. else if (proposalDetails.isSetInitialInvitationBalance) {
  224. const details = new SetInitialInvitationBalanceProposalDetails()
  225. const specificDetails = proposalDetails.asSetInitialInvitationBalance
  226. details.newInitialInvitationBalance = new BN(specificDetails.toString())
  227. return details
  228. }
  229. // SetInitialInvitationCountProposalDetails:
  230. else if (proposalDetails.isSetInitialInvitationCount) {
  231. const details = new SetInitialInvitationCountProposalDetails()
  232. const specificDetails = proposalDetails.asSetInitialInvitationCount
  233. details.newInitialInvitationsCount = specificDetails.toNumber()
  234. return details
  235. }
  236. // SetMembershipLeadInvitationQuotaProposalDetails:
  237. else if (proposalDetails.isSetMembershipLeadInvitationQuota) {
  238. const details = new SetMembershipLeadInvitationQuotaProposalDetails()
  239. const specificDetails = proposalDetails.asSetMembershipLeadInvitationQuota
  240. details.newLeadInvitationQuota = specificDetails.toNumber()
  241. return details
  242. }
  243. // SetReferralCutProposalDetails:
  244. else if (proposalDetails.isSetReferralCut) {
  245. const details = new SetReferralCutProposalDetails()
  246. const specificDetails = proposalDetails.asSetReferralCut
  247. details.newReferralCut = specificDetails.toNumber()
  248. return details
  249. }
  250. // CreateBlogPostProposalDetails:
  251. else if (proposalDetails.isCreateBlogPost) {
  252. const details = new CreateBlogPostProposalDetails()
  253. const specificDetails = proposalDetails.asCreateBlogPost
  254. // TODO:
  255. }
  256. // EditBlogPostProposalDetails:
  257. else if (proposalDetails.isEditBlogPost) {
  258. const details = new EditBlogPostProposalDetails()
  259. const specificDetails = proposalDetails.asEditBlogPost
  260. // TODO:
  261. }
  262. // LockBlogPostProposalDetails:
  263. else if (proposalDetails.isLockBlogPost) {
  264. const details = new LockBlogPostProposalDetails()
  265. const specificDetails = proposalDetails.asLockBlogPost
  266. // TODO:
  267. }
  268. // UnlockBlogPostProposalDetails:
  269. else if (proposalDetails.isUnlockBlogPost) {
  270. const details = new UnlockBlogPostProposalDetails()
  271. const specificDetails = proposalDetails.asUnlockBlogPost
  272. // TODO:
  273. }
  274. // VetoProposalDetails:
  275. else if (proposalDetails.isVetoProposal) {
  276. const details = new VetoProposalDetails()
  277. const specificDetails = proposalDetails.asVetoProposal
  278. details.proposalId = specificDetails.toString()
  279. return details
  280. }
  281. throw new Error(`Unspported proposal details type: ${proposalDetails.type}`)
  282. }
  283. export async function proposalsEngine_ProposalCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  284. const [, proposalId] = new ProposalsEngine.ProposalCreatedEvent(event_).params
  285. // Cache the id
  286. proposalsMappingsMemoryCache.lastCreatedProposalId = proposalId
  287. }
  288. export async function proposalsCodex_ProposalCreated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  289. const [generalProposalParameters, runtimeProposalDetails] = new ProposalsCodex.ProposalCreatedEvent(event_).params
  290. const eventTime = new Date(event_.blockTimestamp)
  291. const proposalDetails = await parseProposalDetails(event_, db, runtimeProposalDetails)
  292. if (!proposalsMappingsMemoryCache.lastCreatedProposalId) {
  293. throw new Error('Unexpected state: proposalsMappingsMemoryCache.lastCreatedProposalId is empty')
  294. }
  295. const proposal = new Proposal({
  296. id: proposalsMappingsMemoryCache.lastCreatedProposalId.toString(),
  297. createdAt: eventTime,
  298. updatedAt: eventTime,
  299. details: proposalDetails,
  300. councilApprovals: 0,
  301. creator: new Membership({ id: generalProposalParameters.member_id.toString() }),
  302. title: perpareString(generalProposalParameters.title.toString()),
  303. description: perpareString(generalProposalParameters.description.toString()),
  304. exactExecutionBlock: generalProposalParameters.exact_execution_block.unwrapOr(undefined)?.toNumber(),
  305. stakingAccount: generalProposalParameters.staking_account_id.toString(),
  306. status: new ProposalStatusDeciding(),
  307. statusSetAtBlock: event_.blockNumber,
  308. statusSetAtTime: eventTime,
  309. })
  310. await db.save<Proposal>(proposal)
  311. }
  312. export async function proposalsEngine_ProposalStatusUpdated(
  313. db: DatabaseManager,
  314. event_: SubstrateEvent
  315. ): Promise<void> {
  316. const [proposalId, status] = new ProposalsEngine.ProposalStatusUpdatedEvent(event_).params
  317. const proposal = await getProposal(db, proposalId.toString())
  318. const eventTime = new Date(event_.blockTimestamp)
  319. let newStatus: typeof ProposalIntermediateStatus
  320. if (status.isActive) {
  321. newStatus = new ProposalStatusDeciding()
  322. } else if (status.isPendingConstitutionality) {
  323. newStatus = new ProposalStatusDormant()
  324. } else if (status.isPendingExecution) {
  325. newStatus = new ProposalStatusGracing()
  326. } else {
  327. throw new Error(`Unexpected proposal status: ${status.type}`)
  328. }
  329. const proposalStatusUpdatedEvent = new ProposalStatusUpdatedEvent({
  330. ...genericEventFields(event_),
  331. newStatus,
  332. proposal,
  333. })
  334. await db.save<ProposalStatusUpdatedEvent>(proposalStatusUpdatedEvent)
  335. newStatus.proposalStatusUpdatedEventId = proposalStatusUpdatedEvent.id
  336. proposal.updatedAt = eventTime
  337. proposal.status = newStatus
  338. await db.save<Proposal>(proposal)
  339. }
  340. export async function proposalsEngine_ProposalDecisionMade(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  341. const [proposalId, decision] = new ProposalsEngine.ProposalDecisionMadeEvent(event_).params
  342. const proposal = await getProposal(db, proposalId.toString())
  343. const eventTime = new Date(event_.blockTimestamp)
  344. let decisionStatus: typeof ProposalDecisionStatus
  345. if (decision.isApproved) {
  346. if (decision.asApproved.isPendingConstitutionality) {
  347. decisionStatus = new ProposalStatusDormant()
  348. } else {
  349. decisionStatus = new ProposalStatusGracing()
  350. }
  351. } else if (decision.isCanceled) {
  352. decisionStatus = new ProposalStatusCancelled()
  353. } else if (decision.isCanceledByRuntime) {
  354. decisionStatus = new ProposalStatusCanceledByRuntime()
  355. } else if (decision.isExpired) {
  356. decisionStatus = new ProposalStatusExpired()
  357. } else if (decision.isRejected) {
  358. decisionStatus = new ProposalStatusRejected()
  359. } else if (decision.isSlashed) {
  360. decisionStatus = new ProposalStatusSlashed()
  361. } else if (decision.isVetoed) {
  362. decisionStatus = new ProposalStatusVetoed()
  363. } else {
  364. throw new Error(`Unexpected proposal decision: ${decision.type}`)
  365. }
  366. const proposalDecisionMadeEvent = new ProposalDecisionMadeEvent({
  367. ...genericEventFields(event_),
  368. decisionStatus,
  369. proposal,
  370. })
  371. await db.save<ProposalDecisionMadeEvent>(proposalDecisionMadeEvent)
  372. // We don't handle Cancelled, Dormant and Gracing statuses here, since they emit separate events
  373. if (
  374. [
  375. 'ProposalStatusCanceledByRuntime',
  376. 'ProposalStatusExpired',
  377. 'ProposalStatusRejected',
  378. 'ProposalStatusSlashed',
  379. 'ProposalStatusVetoed',
  380. ].includes(decisionStatus.isTypeOf)
  381. ) {
  382. ;(decisionStatus as
  383. | ProposalStatusCanceledByRuntime
  384. | ProposalStatusExpired
  385. | ProposalStatusRejected
  386. | ProposalStatusSlashed
  387. | ProposalStatusVetoed).proposalDecisionMadeEventId = proposalDecisionMadeEvent.id
  388. proposal.status = decisionStatus
  389. proposal.updatedAt = eventTime
  390. await db.save<Proposal>(proposal)
  391. }
  392. }
  393. export async function proposalsEngine_ProposalExecuted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  394. // TODO
  395. }
  396. export async function proposalsEngine_Voted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  397. // TODO
  398. }
  399. export async function proposalsEngine_ProposalCancelled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  400. // TODO
  401. }