workingGroups.ts 36 KB

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