workingGroups.ts 33 KB

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