workingGroups.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
  5. import { StorageWorkingGroup as WorkingGroups } from './generated/types'
  6. import {
  7. ApplicationMetadata,
  8. IAddUpcomingOpening,
  9. IOpeningMetadata,
  10. IRemoveUpcomingOpening,
  11. ISetGroupMetadata,
  12. IWorkingGroupMetadata,
  13. IWorkingGroupMetadataAction,
  14. OpeningMetadata,
  15. WorkingGroupMetadataAction,
  16. } from '@joystream/metadata-protobuf'
  17. import { Bytes } from '@polkadot/types'
  18. import { deserializeMetadata, bytesToString, genericEventFields, getWorker, WorkingGroupModuleName } from './common'
  19. import BN from 'bn.js'
  20. import {
  21. WorkingGroupOpening,
  22. OpeningAddedEvent,
  23. WorkingGroup,
  24. WorkingGroupOpeningMetadata,
  25. ApplicationFormQuestion,
  26. ApplicationFormQuestionType,
  27. OpeningStatusOpen,
  28. WorkingGroupOpeningType,
  29. WorkingGroupApplication,
  30. ApplicationFormQuestionAnswer,
  31. AppliedOnOpeningEvent,
  32. Membership,
  33. ApplicationStatusPending,
  34. ApplicationStatusAccepted,
  35. ApplicationStatusRejected,
  36. Worker,
  37. WorkerStatusActive,
  38. OpeningFilledEvent,
  39. OpeningStatusFilled,
  40. // LeaderSetEvent,
  41. OpeningCanceledEvent,
  42. OpeningStatusCancelled,
  43. ApplicationStatusCancelled,
  44. ApplicationWithdrawnEvent,
  45. ApplicationStatusWithdrawn,
  46. UpcomingWorkingGroupOpening,
  47. StatusTextChangedEvent,
  48. WorkingGroupMetadata,
  49. WorkingGroupMetadataSet,
  50. UpcomingOpeningRemoved,
  51. InvalidActionMetadata,
  52. WorkingGroupMetadataActionResult,
  53. UpcomingOpeningAdded,
  54. WorkerRoleAccountUpdatedEvent,
  55. WorkerRewardAccountUpdatedEvent,
  56. StakeIncreasedEvent,
  57. RewardPaidEvent,
  58. RewardPaymentType,
  59. NewMissedRewardLevelReachedEvent,
  60. WorkerExitedEvent,
  61. WorkerStatusLeft,
  62. WorkerStatusTerminated,
  63. TerminatedWorkerEvent,
  64. LeaderUnsetEvent,
  65. TerminatedLeaderEvent,
  66. WorkerRewardAmountUpdatedEvent,
  67. StakeSlashedEvent,
  68. StakeDecreasedEvent,
  69. WorkerStartedLeavingEvent,
  70. BudgetSetEvent,
  71. BudgetSpendingEvent,
  72. LeaderSetEvent,
  73. WorkerStatusLeaving,
  74. } from 'query-node/dist/model'
  75. import { createType } from '@joystream/types'
  76. // Reusable functions
  77. async function getWorkingGroup(
  78. store: DatabaseManager,
  79. event: SubstrateEvent,
  80. relations: string[] = []
  81. ): Promise<WorkingGroup> {
  82. const [groupName] = event.name.split('.')
  83. const group = await store.get(WorkingGroup, { where: { name: groupName }, relations })
  84. if (!group) {
  85. throw new Error(`Working group ${groupName} not found!`)
  86. }
  87. return group
  88. }
  89. async function getOpening(
  90. store: DatabaseManager,
  91. openingstoreId: string,
  92. relations: string[] = []
  93. ): Promise<WorkingGroupOpening> {
  94. const opening = await store.get(WorkingGroupOpening, { where: { id: openingstoreId }, relations })
  95. if (!opening) {
  96. throw new Error(`Opening not found by id ${openingstoreId}`)
  97. }
  98. return opening
  99. }
  100. async function getApplication(store: DatabaseManager, applicationstoreId: string): Promise<WorkingGroupApplication> {
  101. const application = await store.get(WorkingGroupApplication, { where: { id: applicationstoreId } })
  102. if (!application) {
  103. throw new Error(`Application not found by id ${applicationstoreId}`)
  104. }
  105. return application
  106. }
  107. async function getApplicationFormQuestions(
  108. store: DatabaseManager,
  109. openingstoreId: string
  110. ): Promise<ApplicationFormQuestion[]> {
  111. const openingWithQuestions = await getOpening(store, openingstoreId, [
  112. 'metadata',
  113. 'metadata.applicationFormQuestions',
  114. ])
  115. if (!openingWithQuestions) {
  116. throw new Error(`Opening not found by id: ${openingstoreId}`)
  117. }
  118. if (!openingWithQuestions.metadata.applicationFormQuestions) {
  119. throw new Error(`Application form questions not found for opening: ${openingstoreId}`)
  120. }
  121. return openingWithQuestions.metadata.applicationFormQuestions
  122. }
  123. const InputTypeToApplicationFormQuestionType = {
  124. [OpeningMetadata.ApplicationFormQuestion.InputType.TEXT]: ApplicationFormQuestionType.TEXT,
  125. [OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA]: ApplicationFormQuestionType.TEXTAREA,
  126. }
  127. function parseQuestionInputType(
  128. type?: OpeningMetadata.ApplicationFormQuestion.InputType | null
  129. ): ApplicationFormQuestionType {
  130. const validType: OpeningMetadata.ApplicationFormQuestion.InputType = type || 0
  131. return InputTypeToApplicationFormQuestionType[validType]
  132. }
  133. export async function createWorkingGroupOpeningMetadata(
  134. store: DatabaseManager,
  135. eventTime: Date,
  136. originalMeta: Bytes | IOpeningMetadata
  137. ): Promise<WorkingGroupOpeningMetadata> {
  138. let originallyValid: boolean
  139. let metadata: IOpeningMetadata
  140. if (originalMeta instanceof Bytes) {
  141. const deserializedMetadata = await deserializeMetadata(OpeningMetadata, originalMeta)
  142. metadata = deserializedMetadata || {}
  143. originallyValid = !!deserializedMetadata
  144. } else {
  145. metadata = originalMeta
  146. originallyValid = true
  147. }
  148. const {
  149. applicationFormQuestions,
  150. applicationDetails,
  151. description,
  152. expectedEndingTimestamp,
  153. hiringLimit,
  154. shortDescription,
  155. } = metadata
  156. const openingMetadata = new WorkingGroupOpeningMetadata({
  157. createdAt: eventTime,
  158. updatedAt: eventTime,
  159. originallyValid,
  160. applicationDetails: applicationDetails || undefined,
  161. description: description || undefined,
  162. shortDescription: shortDescription || undefined,
  163. hiringLimit: hiringLimit || undefined,
  164. expectedEnding: expectedEndingTimestamp ? new Date(expectedEndingTimestamp) : undefined,
  165. applicationFormQuestions: [],
  166. })
  167. await store.save<WorkingGroupOpeningMetadata>(openingMetadata)
  168. await Promise.all(
  169. (applicationFormQuestions || []).map(async ({ question, type }, index) => {
  170. const applicationFormQuestion = new ApplicationFormQuestion({
  171. createdAt: eventTime,
  172. updatedAt: eventTime,
  173. question: question || undefined,
  174. type: parseQuestionInputType(type),
  175. index,
  176. openingMetadata,
  177. })
  178. await store.save<ApplicationFormQuestion>(applicationFormQuestion)
  179. return applicationFormQuestion
  180. })
  181. )
  182. return openingMetadata
  183. }
  184. async function createApplicationQuestionAnswers(
  185. store: DatabaseManager,
  186. application: WorkingGroupApplication,
  187. metadataBytes: Bytes
  188. ) {
  189. const metadata = deserializeMetadata(ApplicationMetadata, metadataBytes)
  190. if (!metadata) {
  191. return
  192. }
  193. const questions = await getApplicationFormQuestions(store, application.opening.id)
  194. const { answers } = metadata
  195. await Promise.all(
  196. (answers || []).slice(0, questions.length).map(async (answer, index) => {
  197. const applicationFormQuestionAnswer = new ApplicationFormQuestionAnswer({
  198. createdAt: application.createdAt,
  199. updatedAt: application.updatedAt,
  200. application,
  201. question: questions[index],
  202. answer,
  203. })
  204. await store.save<ApplicationFormQuestionAnswer>(applicationFormQuestionAnswer)
  205. return applicationFormQuestionAnswer
  206. })
  207. )
  208. }
  209. async function handleAddUpcomingOpeningAction(
  210. store: DatabaseManager,
  211. event: SubstrateEvent,
  212. statusChangedEvent: StatusTextChangedEvent,
  213. action: IAddUpcomingOpening
  214. ): Promise<UpcomingOpeningAdded | InvalidActionMetadata> {
  215. const upcomingOpeningMeta = action.metadata || {}
  216. const group = await getWorkingGroup(store, event)
  217. const eventTime = new Date(event.blockTimestamp)
  218. const openingMeta = await await createWorkingGroupOpeningMetadata(
  219. store,
  220. eventTime,
  221. upcomingOpeningMeta.metadata || {}
  222. )
  223. const { rewardPerBlock, expectedStart, minApplicationStake } = upcomingOpeningMeta
  224. const upcomingOpening = new UpcomingWorkingGroupOpening({
  225. createdAt: eventTime,
  226. updatedAt: eventTime,
  227. metadata: openingMeta,
  228. group,
  229. rewardPerBlock: rewardPerBlock?.toNumber() ? new BN(rewardPerBlock.toString()) : undefined,
  230. expectedStart: expectedStart ? new Date(expectedStart) : undefined,
  231. stakeAmount: minApplicationStake?.toNumber() ? new BN(minApplicationStake.toString()) : undefined,
  232. createdInEvent: statusChangedEvent,
  233. })
  234. await store.save<UpcomingWorkingGroupOpening>(upcomingOpening)
  235. const result = new UpcomingOpeningAdded()
  236. result.upcomingOpeningId = upcomingOpening.id
  237. return result
  238. }
  239. async function handleRemoveUpcomingOpeningAction(
  240. store: DatabaseManager,
  241. action: IRemoveUpcomingOpening
  242. ): Promise<UpcomingOpeningRemoved | InvalidActionMetadata> {
  243. const { id } = action
  244. const upcomingOpening = await store.get(UpcomingWorkingGroupOpening, { where: { id } })
  245. let result: UpcomingOpeningRemoved | InvalidActionMetadata
  246. if (upcomingOpening) {
  247. result = new UpcomingOpeningRemoved()
  248. result.upcomingOpeningId = upcomingOpening.id
  249. await store.remove<UpcomingWorkingGroupOpening>(upcomingOpening)
  250. } else {
  251. const error = `Cannot remove upcoming opening: Entity by id ${id} not found!`
  252. console.error(error)
  253. result = new InvalidActionMetadata()
  254. result.reason = error
  255. }
  256. return result
  257. }
  258. async function handleSetWorkingGroupMetadataAction(
  259. store: DatabaseManager,
  260. event: SubstrateEvent,
  261. statusChangedEvent: StatusTextChangedEvent,
  262. action: ISetGroupMetadata
  263. ): Promise<WorkingGroupMetadataSet> {
  264. const { newMetadata } = action
  265. const group = await getWorkingGroup(store, event, ['metadata'])
  266. const oldMetadata = group.metadata
  267. const eventTime = new Date(event.blockTimestamp)
  268. const setNewOptionalString = (field: keyof IWorkingGroupMetadata) =>
  269. typeof newMetadata?.[field] === 'string' ? newMetadata[field] || undefined : oldMetadata?.[field]
  270. const newGroupMetadata = new WorkingGroupMetadata({
  271. createdAt: eventTime,
  272. updatedAt: eventTime,
  273. setInEvent: statusChangedEvent,
  274. group,
  275. status: setNewOptionalString('status'),
  276. statusMessage: setNewOptionalString('statusMessage'),
  277. about: setNewOptionalString('about'),
  278. description: setNewOptionalString('description'),
  279. })
  280. await store.save<WorkingGroupMetadata>(newGroupMetadata)
  281. group.metadata = newGroupMetadata
  282. group.updatedAt = eventTime
  283. await store.save<WorkingGroup>(group)
  284. const result = new WorkingGroupMetadataSet()
  285. result.metadataId = newGroupMetadata.id
  286. return result
  287. }
  288. async function handleWorkingGroupMetadataAction(
  289. store: DatabaseManager,
  290. event: SubstrateEvent,
  291. statusChangedEvent: StatusTextChangedEvent,
  292. action: IWorkingGroupMetadataAction
  293. ): Promise<typeof WorkingGroupMetadataActionResult> {
  294. if (action.addUpcomingOpening) {
  295. return handleAddUpcomingOpeningAction(store, event, statusChangedEvent, action.addUpcomingOpening)
  296. } else if (action.removeUpcomingOpening) {
  297. return handleRemoveUpcomingOpeningAction(store, action.removeUpcomingOpening)
  298. } else if (action.setGroupMetadata) {
  299. return handleSetWorkingGroupMetadataAction(store, event, statusChangedEvent, action.setGroupMetadata)
  300. } else {
  301. const result = new InvalidActionMetadata()
  302. result.reason = 'No known action was provided'
  303. return result
  304. }
  305. }
  306. async function handleTerminatedWorker({ store, event }: EventContext & StoreContext): Promise<void> {
  307. const [workerId, optPenalty, optRationale] = new WorkingGroups.TerminatedWorkerEvent(event).params
  308. const group = await getWorkingGroup(store, event)
  309. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  310. const eventTime = new Date(event.blockTimestamp)
  311. const EventConstructor = worker.isLead ? TerminatedLeaderEvent : TerminatedWorkerEvent
  312. const terminatedEvent = new EventConstructor({
  313. ...genericEventFields(event),
  314. group,
  315. worker,
  316. penalty: optPenalty.unwrapOr(undefined),
  317. rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
  318. })
  319. await store.save(terminatedEvent)
  320. const status = new WorkerStatusTerminated()
  321. status.terminatedWorkerEventId = terminatedEvent.id
  322. worker.status = status
  323. worker.stake = new BN(0)
  324. worker.rewardPerBlock = new BN(0)
  325. worker.updatedAt = eventTime
  326. await store.save<Worker>(worker)
  327. }
  328. export async function findLeaderSetEventByTxHash(store: DatabaseManager, txHash?: string): Promise<LeaderSetEvent> {
  329. const leaderSetEvent = await store.get(LeaderSetEvent, { where: { inExtrinsic: txHash } })
  330. if (!leaderSetEvent) {
  331. throw new Error(`LeaderSet event not found by tx hash: ${txHash}`)
  332. }
  333. return leaderSetEvent
  334. }
  335. // Mapping functions
  336. export async function workingGroups_OpeningAdded({ store, event }: EventContext & StoreContext): Promise<void> {
  337. const [
  338. openingRuntimeId,
  339. metadataBytes,
  340. openingType,
  341. stakePolicy,
  342. optRewardPerBlock,
  343. ] = new WorkingGroups.OpeningAddedEvent(event).params
  344. const group = await getWorkingGroup(store, event)
  345. const eventTime = new Date(event.blockTimestamp)
  346. const opening = new WorkingGroupOpening({
  347. createdAt: eventTime,
  348. updatedAt: eventTime,
  349. id: `${group.name}-${openingRuntimeId.toString()}`,
  350. runtimeId: openingRuntimeId.toNumber(),
  351. applications: [],
  352. group,
  353. rewardPerBlock: optRewardPerBlock.unwrapOr(new BN(0)),
  354. stakeAmount: stakePolicy.stake_amount,
  355. unstakingPeriod: stakePolicy.leaving_unstaking_period.toNumber(),
  356. status: new OpeningStatusOpen(),
  357. type: openingType.isLeader ? WorkingGroupOpeningType.LEADER : WorkingGroupOpeningType.REGULAR,
  358. })
  359. const metadata = await createWorkingGroupOpeningMetadata(store, eventTime, metadataBytes)
  360. opening.metadata = metadata
  361. await store.save<WorkingGroupOpening>(opening)
  362. const openingAddedEvent = new OpeningAddedEvent({
  363. ...genericEventFields(event),
  364. group,
  365. opening,
  366. })
  367. await store.save<OpeningAddedEvent>(openingAddedEvent)
  368. }
  369. export async function workingGroups_AppliedOnOpening({ store, event }: EventContext & StoreContext): Promise<void> {
  370. const eventTime = new Date(event.blockTimestamp)
  371. const [
  372. {
  373. opening_id: openingRuntimeId,
  374. description: metadataBytes,
  375. member_id: memberId,
  376. reward_account_id: rewardAccount,
  377. role_account_id: roleAccout,
  378. stake_parameters: { stake, staking_account_id: stakingAccount },
  379. },
  380. applicationRuntimeId,
  381. ] = new WorkingGroups.AppliedOnOpeningEvent(event).params
  382. const group = await getWorkingGroup(store, event)
  383. const openingstoreId = `${group.name}-${openingRuntimeId.toString()}`
  384. const application = new WorkingGroupApplication({
  385. createdAt: eventTime,
  386. updatedAt: eventTime,
  387. id: `${group.name}-${applicationRuntimeId.toString()}`,
  388. runtimeId: applicationRuntimeId.toNumber(),
  389. opening: new WorkingGroupOpening({ id: openingstoreId }),
  390. applicant: new Membership({ id: memberId.toString() }),
  391. rewardAccount: rewardAccount.toString(),
  392. roleAccount: roleAccout.toString(),
  393. stakingAccount: stakingAccount.toString(),
  394. status: new ApplicationStatusPending(),
  395. answers: [],
  396. stake,
  397. })
  398. await store.save<WorkingGroupApplication>(application)
  399. await createApplicationQuestionAnswers(store, application, metadataBytes)
  400. const appliedOnOpeningEvent = new AppliedOnOpeningEvent({
  401. ...genericEventFields(event),
  402. group,
  403. opening: new WorkingGroupOpening({ id: openingstoreId }),
  404. application,
  405. })
  406. await store.save<AppliedOnOpeningEvent>(appliedOnOpeningEvent)
  407. }
  408. export async function workingGroups_LeaderSet({ store, event }: EventContext & StoreContext): Promise<void> {
  409. const group = await getWorkingGroup(store, event)
  410. const leaderSetEvent = new LeaderSetEvent({
  411. ...genericEventFields(event),
  412. group,
  413. })
  414. await store.save<LeaderSetEvent>(leaderSetEvent)
  415. }
  416. export async function workingGroups_OpeningFilled({ store, event }: EventContext & StoreContext): Promise<void> {
  417. const eventTime = new Date(event.blockTimestamp)
  418. const [openingRuntimeId, applicationIdToWorkerIdMap, applicationIdsSet] = new WorkingGroups.OpeningFilledEvent(
  419. event
  420. ).params
  421. const group = await getWorkingGroup(store, event)
  422. const opening = await getOpening(store, `${group.name}-${openingRuntimeId.toString()}`, [
  423. 'applications',
  424. 'applications.applicant',
  425. ])
  426. const acceptedApplicationIds = createType('Vec<ApplicationId>', applicationIdsSet.toHex() as any)
  427. // Save the event
  428. const openingFilledEvent = new OpeningFilledEvent({
  429. ...genericEventFields(event),
  430. group,
  431. opening,
  432. })
  433. await store.save<OpeningFilledEvent>(openingFilledEvent)
  434. // Update applications and create new workers
  435. const hiredWorkers = (
  436. await Promise.all(
  437. (opening.applications || [])
  438. // Skip withdrawn applications
  439. .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
  440. .map(async (application) => {
  441. const isAccepted = acceptedApplicationIds.some((runtimeId) => runtimeId.toNumber() === application.runtimeId)
  442. const applicationStatus = isAccepted ? new ApplicationStatusAccepted() : new ApplicationStatusRejected()
  443. applicationStatus.openingFilledEventId = openingFilledEvent.id
  444. application.status = applicationStatus
  445. application.updatedAt = eventTime
  446. await store.save<WorkingGroupApplication>(application)
  447. if (isAccepted) {
  448. // Cannot use "applicationIdToWorkerIdMap.get" here,
  449. // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
  450. const [, workerRuntimeId] =
  451. Array.from(applicationIdToWorkerIdMap.entries()).find(
  452. ([applicationRuntimeId]) => applicationRuntimeId.toNumber() === application.runtimeId
  453. ) || []
  454. if (!workerRuntimeId) {
  455. throw new Error(
  456. `Fatal: No worker id found by accepted application id ${application.id} when handling OpeningFilled event!`
  457. )
  458. }
  459. const worker = new Worker({
  460. createdAt: eventTime,
  461. updatedAt: eventTime,
  462. id: `${group.name}-${workerRuntimeId.toString()}`,
  463. runtimeId: workerRuntimeId.toNumber(),
  464. application,
  465. group,
  466. isLead: opening.type === WorkingGroupOpeningType.LEADER,
  467. membership: application.applicant,
  468. stake: application.stake,
  469. roleAccount: application.roleAccount,
  470. rewardAccount: application.rewardAccount,
  471. stakeAccount: application.stakingAccount,
  472. payouts: [],
  473. status: new WorkerStatusActive(),
  474. entry: openingFilledEvent,
  475. rewardPerBlock: opening.rewardPerBlock,
  476. })
  477. await store.save<Worker>(worker)
  478. return worker
  479. }
  480. })
  481. )
  482. ).filter((w) => w !== undefined) as Worker[]
  483. // Set opening status
  484. const openingFilled = new OpeningStatusFilled()
  485. openingFilled.openingFilledEventId = openingFilledEvent.id
  486. opening.status = openingFilled
  487. opening.updatedAt = eventTime
  488. await store.save<WorkingGroupOpening>(opening)
  489. // Update working group and LeaderSetEvent if necessary
  490. if (opening.type === WorkingGroupOpeningType.LEADER && hiredWorkers.length) {
  491. group.leader = hiredWorkers[0]
  492. group.updatedAt = eventTime
  493. await store.save<WorkingGroup>(group)
  494. const leaderSetEvent = await findLeaderSetEventByTxHash(store, openingFilledEvent.inExtrinsic)
  495. leaderSetEvent.worker = hiredWorkers[0]
  496. leaderSetEvent.updatedAt = eventTime
  497. await store.save<LeaderSetEvent>(leaderSetEvent)
  498. }
  499. }
  500. export async function workingGroups_OpeningCanceled({ store, event }: EventContext & StoreContext): Promise<void> {
  501. const [openingRuntimeId] = new WorkingGroups.OpeningCanceledEvent(event).params
  502. const group = await getWorkingGroup(store, event)
  503. const opening = await getOpening(store, `${group.name}-${openingRuntimeId.toString()}`, ['applications'])
  504. const eventTime = new Date(event.blockTimestamp)
  505. // Create and save event
  506. const openingCanceledEvent = new OpeningCanceledEvent({
  507. ...genericEventFields(event),
  508. group,
  509. opening,
  510. })
  511. await store.save<OpeningCanceledEvent>(openingCanceledEvent)
  512. // Set opening status
  513. const openingCancelled = new OpeningStatusCancelled()
  514. openingCancelled.openingCanceledEventId = openingCanceledEvent.id
  515. opening.status = openingCancelled
  516. opening.updatedAt = eventTime
  517. await store.save<WorkingGroupOpening>(opening)
  518. // Set applications status
  519. const applicationCancelled = new ApplicationStatusCancelled()
  520. applicationCancelled.openingCanceledEventId = openingCanceledEvent.id
  521. await Promise.all(
  522. (opening.applications || [])
  523. // Skip withdrawn applications
  524. .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
  525. .map(async (application) => {
  526. application.status = applicationCancelled
  527. application.updatedAt = eventTime
  528. await store.save<WorkingGroupApplication>(application)
  529. })
  530. )
  531. }
  532. export async function workingGroups_ApplicationWithdrawn({ store, event }: EventContext & StoreContext): Promise<void> {
  533. const [applicationRuntimeId] = new WorkingGroups.ApplicationWithdrawnEvent(event).params
  534. const group = await getWorkingGroup(store, event)
  535. const application = await getApplication(store, `${group.name}-${applicationRuntimeId.toString()}`)
  536. const eventTime = new Date(event.blockTimestamp)
  537. // Create and save event
  538. const applicationWithdrawnEvent = new ApplicationWithdrawnEvent({
  539. ...genericEventFields(event),
  540. group,
  541. application,
  542. })
  543. await store.save<ApplicationWithdrawnEvent>(applicationWithdrawnEvent)
  544. // Set application status
  545. const statusWithdrawn = new ApplicationStatusWithdrawn()
  546. statusWithdrawn.applicationWithdrawnEventId = applicationWithdrawnEvent.id
  547. application.status = statusWithdrawn
  548. application.updatedAt = eventTime
  549. await store.save<WorkingGroupApplication>(application)
  550. }
  551. export async function workingGroups_StatusTextChanged({ store, event }: EventContext & StoreContext): Promise<void> {
  552. const [, optBytes] = new WorkingGroups.StatusTextChangedEvent(event).params
  553. const group = await getWorkingGroup(store, event)
  554. // Since result cannot be empty at this point, but we already need to have an existing StatusTextChangedEvent
  555. // in order to be able to create UpcomingOpening.createdInEvent relation, we use a temporary "mock" result
  556. const mockResult = new InvalidActionMetadata()
  557. mockResult.reason = 'Metadata not yet processed'
  558. const statusTextChangedEvent = new StatusTextChangedEvent({
  559. ...genericEventFields(event),
  560. group,
  561. metadata: optBytes.isSome ? optBytes.unwrap().toString() : undefined,
  562. result: mockResult,
  563. })
  564. await store.save<StatusTextChangedEvent>(statusTextChangedEvent)
  565. let result: typeof WorkingGroupMetadataActionResult
  566. if (optBytes.isSome) {
  567. const metadata = deserializeMetadata(WorkingGroupMetadataAction, optBytes.unwrap())
  568. if (metadata) {
  569. result = await handleWorkingGroupMetadataAction(store, event, statusTextChangedEvent, metadata)
  570. } else {
  571. result = new InvalidActionMetadata()
  572. result.reason = 'Invalid metadata: Cannot deserialize metadata binary'
  573. }
  574. } else {
  575. const error = 'No encoded metadata was provided'
  576. console.error(`StatusTextChanged event: ${error}`)
  577. result = new InvalidActionMetadata()
  578. result.reason = error
  579. }
  580. // Now we can set the "real" result
  581. statusTextChangedEvent.result = result
  582. await store.save<StatusTextChangedEvent>(statusTextChangedEvent)
  583. }
  584. export async function workingGroups_WorkerRoleAccountUpdated({
  585. store,
  586. event,
  587. }: EventContext & StoreContext): Promise<void> {
  588. const [workerId, accountId] = new WorkingGroups.WorkerRoleAccountUpdatedEvent(event).params
  589. const group = await getWorkingGroup(store, event)
  590. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  591. const eventTime = new Date(event.blockTimestamp)
  592. const workerRoleAccountUpdatedEvent = new WorkerRoleAccountUpdatedEvent({
  593. ...genericEventFields(event),
  594. group,
  595. worker,
  596. newRoleAccount: accountId.toString(),
  597. })
  598. await store.save<WorkerRoleAccountUpdatedEvent>(workerRoleAccountUpdatedEvent)
  599. worker.roleAccount = accountId.toString()
  600. worker.updatedAt = eventTime
  601. await store.save<Worker>(worker)
  602. }
  603. export async function workingGroups_WorkerRewardAccountUpdated({
  604. store,
  605. event,
  606. }: EventContext & StoreContext): Promise<void> {
  607. const [workerId, accountId] = new WorkingGroups.WorkerRewardAccountUpdatedEvent(event).params
  608. const group = await getWorkingGroup(store, event)
  609. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  610. const eventTime = new Date(event.blockTimestamp)
  611. const workerRewardAccountUpdatedEvent = new WorkerRewardAccountUpdatedEvent({
  612. ...genericEventFields(event),
  613. group,
  614. worker,
  615. newRewardAccount: accountId.toString(),
  616. })
  617. await store.save<WorkerRoleAccountUpdatedEvent>(workerRewardAccountUpdatedEvent)
  618. worker.rewardAccount = accountId.toString()
  619. worker.updatedAt = eventTime
  620. await store.save<Worker>(worker)
  621. }
  622. export async function workingGroups_StakeIncreased({ store, event }: EventContext & StoreContext): Promise<void> {
  623. const [workerId, increaseAmount] = new WorkingGroups.StakeIncreasedEvent(event).params
  624. const group = await getWorkingGroup(store, event)
  625. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  626. const eventTime = new Date(event.blockTimestamp)
  627. const stakeIncreasedEvent = new StakeIncreasedEvent({
  628. ...genericEventFields(event),
  629. group,
  630. worker,
  631. amount: increaseAmount,
  632. })
  633. await store.save<StakeIncreasedEvent>(stakeIncreasedEvent)
  634. worker.stake = worker.stake.add(increaseAmount)
  635. worker.updatedAt = eventTime
  636. await store.save<Worker>(worker)
  637. }
  638. export async function workingGroups_RewardPaid({ store, event }: EventContext & StoreContext): Promise<void> {
  639. const [workerId, rewardAccountId, amount, rewardPaymentType] = new WorkingGroups.RewardPaidEvent(event).params
  640. const group = await getWorkingGroup(store, event)
  641. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  642. const eventTime = new Date(event.blockTimestamp)
  643. const rewardPaidEvent = new RewardPaidEvent({
  644. ...genericEventFields(event),
  645. group,
  646. worker,
  647. amount,
  648. rewardAccount: rewardAccountId.toString(),
  649. paymentType: rewardPaymentType.isRegularReward ? RewardPaymentType.REGULAR : RewardPaymentType.MISSED,
  650. })
  651. await store.save<RewardPaidEvent>(rewardPaidEvent)
  652. // Update group budget
  653. group.budget = group.budget.sub(amount)
  654. group.updatedAt = eventTime
  655. await store.save<WorkingGroup>(group)
  656. }
  657. export async function workingGroups_NewMissedRewardLevelReached({
  658. store,
  659. event,
  660. }: EventContext & StoreContext): Promise<void> {
  661. const [workerId, newMissedRewardAmountOpt] = new WorkingGroups.NewMissedRewardLevelReachedEvent(event).params
  662. const group = await getWorkingGroup(store, event)
  663. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  664. const eventTime = new Date(event.blockTimestamp)
  665. const newMissedRewardLevelReachedEvent = new NewMissedRewardLevelReachedEvent({
  666. ...genericEventFields(event),
  667. group,
  668. worker,
  669. newMissedRewardAmount: newMissedRewardAmountOpt.unwrapOr(new BN(0)),
  670. })
  671. await store.save<NewMissedRewardLevelReachedEvent>(newMissedRewardLevelReachedEvent)
  672. // Update worker
  673. worker.missingRewardAmount = newMissedRewardAmountOpt.unwrapOr(undefined)
  674. worker.updatedAt = eventTime
  675. await store.save<Worker>(worker)
  676. }
  677. export async function workingGroups_WorkerExited({ store, event }: EventContext & StoreContext): Promise<void> {
  678. const [workerId] = new WorkingGroups.WorkerExitedEvent(event).params
  679. const group = await getWorkingGroup(store, event)
  680. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  681. const eventTime = new Date(event.blockTimestamp)
  682. const workerExitedEvent = new WorkerExitedEvent({
  683. ...genericEventFields(event),
  684. group,
  685. worker,
  686. })
  687. await store.save<WorkerExitedEvent>(workerExitedEvent)
  688. const newStatus = new WorkerStatusLeft()
  689. newStatus.workerStartedLeavingEventId = (worker.status as WorkerStatusLeaving).workerStartedLeavingEventId
  690. newStatus.workerExitedEventId = workerExitedEvent.id
  691. worker.status = newStatus
  692. worker.stake = new BN(0)
  693. worker.rewardPerBlock = new BN(0)
  694. worker.missingRewardAmount = undefined
  695. worker.updatedAt = eventTime
  696. await store.save<Worker>(worker)
  697. }
  698. export async function workingGroups_LeaderUnset({ store, event }: EventContext & StoreContext): Promise<void> {
  699. const group = await getWorkingGroup(store, event, ['leader'])
  700. const eventTime = new Date(event.blockTimestamp)
  701. const leaderUnsetEvent = new LeaderUnsetEvent({
  702. ...genericEventFields(event),
  703. group,
  704. leader: group.leader,
  705. })
  706. await store.save<LeaderUnsetEvent>(leaderUnsetEvent)
  707. group.leader = undefined
  708. group.updatedAt = eventTime
  709. await store.save<WorkingGroup>(group)
  710. }
  711. export async function workingGroups_TerminatedWorker(ctx: EventContext & StoreContext): Promise<void> {
  712. await handleTerminatedWorker(ctx)
  713. }
  714. export async function workingGroups_TerminatedLeader(ctx: EventContext & StoreContext): Promise<void> {
  715. await handleTerminatedWorker(ctx)
  716. }
  717. export async function workingGroups_WorkerRewardAmountUpdated({
  718. store,
  719. event,
  720. }: EventContext & StoreContext): Promise<void> {
  721. const [workerId, newRewardPerBlockOpt] = new WorkingGroups.WorkerRewardAmountUpdatedEvent(event).params
  722. const group = await getWorkingGroup(store, event)
  723. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  724. const eventTime = new Date(event.blockTimestamp)
  725. const workerRewardAmountUpdatedEvent = new WorkerRewardAmountUpdatedEvent({
  726. ...genericEventFields(event),
  727. group,
  728. worker,
  729. newRewardPerBlock: newRewardPerBlockOpt.unwrapOr(new BN(0)),
  730. })
  731. await store.save<WorkerRewardAmountUpdatedEvent>(workerRewardAmountUpdatedEvent)
  732. worker.rewardPerBlock = newRewardPerBlockOpt.unwrapOr(new BN(0))
  733. worker.updatedAt = eventTime
  734. await store.save<Worker>(worker)
  735. }
  736. export async function workingGroups_StakeSlashed({ store, event }: EventContext & StoreContext): Promise<void> {
  737. const [workerId, slashedAmount, requestedAmount, optRationale] = new WorkingGroups.StakeSlashedEvent(event).params
  738. const group = await getWorkingGroup(store, event)
  739. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  740. const eventTime = new Date(event.blockTimestamp)
  741. const workerStakeSlashedEvent = new StakeSlashedEvent({
  742. ...genericEventFields(event),
  743. group,
  744. worker,
  745. requestedAmount,
  746. slashedAmount,
  747. rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
  748. })
  749. await store.save<StakeSlashedEvent>(workerStakeSlashedEvent)
  750. worker.stake = worker.stake.sub(slashedAmount)
  751. worker.updatedAt = eventTime
  752. await store.save<Worker>(worker)
  753. }
  754. export async function workingGroups_StakeDecreased({ store, event }: EventContext & StoreContext): Promise<void> {
  755. const [workerId, amount] = new WorkingGroups.StakeDecreasedEvent(event).params
  756. const group = await getWorkingGroup(store, event)
  757. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  758. const eventTime = new Date(event.blockTimestamp)
  759. const workerStakeDecreasedEvent = new StakeDecreasedEvent({
  760. ...genericEventFields(event),
  761. group,
  762. worker,
  763. amount,
  764. })
  765. await store.save<StakeDecreasedEvent>(workerStakeDecreasedEvent)
  766. worker.stake = worker.stake.sub(amount)
  767. worker.updatedAt = eventTime
  768. await store.save<Worker>(worker)
  769. }
  770. export async function workingGroups_WorkerStartedLeaving({ store, event }: EventContext & StoreContext): Promise<void> {
  771. const [workerId, optRationale] = new WorkingGroups.WorkerStartedLeavingEvent(event).params
  772. const group = await getWorkingGroup(store, event)
  773. const worker = await getWorker(store, group.name as WorkingGroupModuleName, workerId)
  774. const eventTime = new Date(event.blockTimestamp)
  775. const workerStartedLeavingEvent = new WorkerStartedLeavingEvent({
  776. ...genericEventFields(event),
  777. group,
  778. worker,
  779. rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
  780. })
  781. await store.save<WorkerStartedLeavingEvent>(workerStartedLeavingEvent)
  782. const status = new WorkerStatusLeaving()
  783. status.workerStartedLeavingEventId = workerStartedLeavingEvent.id
  784. worker.status = status
  785. worker.updatedAt = eventTime
  786. await store.save<Worker>(worker)
  787. }
  788. export async function workingGroups_BudgetSet({ store, event }: EventContext & StoreContext): Promise<void> {
  789. const [newBudget] = new WorkingGroups.BudgetSetEvent(event).params
  790. const group = await getWorkingGroup(store, event)
  791. const eventTime = new Date(event.blockTimestamp)
  792. const budgetSetEvent = new BudgetSetEvent({
  793. ...genericEventFields(event),
  794. group,
  795. newBudget,
  796. })
  797. await store.save<BudgetSetEvent>(budgetSetEvent)
  798. group.budget = newBudget
  799. group.updatedAt = eventTime
  800. await store.save<WorkingGroup>(group)
  801. }
  802. export async function workingGroups_BudgetSpending({ store, event }: EventContext & StoreContext): Promise<void> {
  803. const [reciever, amount, optRationale] = new WorkingGroups.BudgetSpendingEvent(event).params
  804. const group = await getWorkingGroup(store, event)
  805. const eventTime = new Date(event.blockTimestamp)
  806. const budgetSpendingEvent = new BudgetSpendingEvent({
  807. ...genericEventFields(event),
  808. group,
  809. amount,
  810. reciever: reciever.toString(),
  811. rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
  812. })
  813. await store.save<BudgetSpendingEvent>(budgetSpendingEvent)
  814. group.budget = group.budget.sub(amount)
  815. group.updatedAt = eventTime
  816. await store.save<WorkingGroup>(group)
  817. }