workingGroups.ts 38 KB

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