workingGroups.ts 38 KB

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