workingGroups.ts 38 KB

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