workingGroups.ts 38 KB

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