From 8c1a10fa119b76db6c52a77794248498145fabb2 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Wed, 29 Jul 2026 11:36:48 +0300 Subject: [PATCH 1/4] refactor: job discovery models --- .../server/src/common/enums/global-common.ts | 15 --- .../server/src/common/utils/pageable.model.ts | 3 + .../exchange-oracle.gateway.ts | 28 ++-- .../exchange-oracle.mapper.profile.ts | 16 +-- .../model/exchange-oracle.model.ts | 95 ++++++++++++++ .../spec/exchange-oracle.fixtures.ts | 100 ++++++++++++++ .../spec/exchange-oracle.gateway.spec.ts | 48 +++---- .../src/modules/cron-job/cron-job.service.ts | 72 +++++----- .../cron-job/spec/cron-job.service.spec.ts | 30 ++--- .../jobs-discovery.controller.ts | 28 ++-- .../jobs-discovery.mapper.profile.ts | 27 ++-- .../jobs-discovery/jobs-discovery.module.ts | 4 +- .../jobs-discovery/jobs-discovery.service.ts | 99 ++++++-------- .../model/jobs-discovery.model.ts | 123 +++++++++--------- .../spec/jobs-discovery.controller.spec.ts | 19 ++- .../spec/jobs-discovery.fixtures.ts | 86 +++++------- .../spec/jobs-discovery.service.mock.ts | 2 +- .../spec/jobs-discovery.service.spec.ts | 48 +++---- 18 files changed, 494 insertions(+), 349 deletions(-) create mode 100644 packages/apps/human-app/server/src/integrations/exchange-oracle/model/exchange-oracle.model.ts create mode 100644 packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.fixtures.ts diff --git a/packages/apps/human-app/server/src/common/enums/global-common.ts b/packages/apps/human-app/server/src/common/enums/global-common.ts index 386300c11c..bf16bddec1 100644 --- a/packages/apps/human-app/server/src/common/enums/global-common.ts +++ b/packages/apps/human-app/server/src/common/enums/global-common.ts @@ -1,11 +1,3 @@ -export enum JobDiscoveryFieldName { - JobDescription = 'job_description', - RewardAmount = 'reward_amount', - RewardToken = 'reward_token', - CreatedAt = 'created_at', - UpdatedAt = 'updated_at', -} - export enum JobStatus { ACTIVE = 'active', COMPLETED = 'completed', @@ -21,13 +13,6 @@ export enum AssignmentStatus { REJECTED = 'rejected', } -export enum JobDiscoverySortField { - CHAIN_ID = 'chain_id', - JOB_TYPE = 'job_type', - REWARD_AMOUNT = 'reward_amount', - CREATED_AT = 'created_at', -} - export enum SortOrder { ASC = 'asc', DESC = 'desc', diff --git a/packages/apps/human-app/server/src/common/utils/pageable.model.ts b/packages/apps/human-app/server/src/common/utils/pageable.model.ts index 586b60804d..c6c1330238 100644 --- a/packages/apps/human-app/server/src/common/utils/pageable.model.ts +++ b/packages/apps/human-app/server/src/common/utils/pageable.model.ts @@ -40,6 +40,7 @@ export abstract class PageableDto { @IsOptional() abstract sort_field?: any; } + export abstract class PageableParams { @AutoMap() page?: number; @@ -52,6 +53,7 @@ export abstract class PageableParams { abstract sortField?: any; } + export abstract class PageableData { @AutoMap() page?: number; @@ -64,6 +66,7 @@ export abstract class PageableData { abstract sort_field?: any; } + export abstract class PageableResponse { page: number; page_size: number; diff --git a/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.gateway.ts b/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.gateway.ts index ef1d4f2316..c38742e779 100644 --- a/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.gateway.ts +++ b/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.gateway.ts @@ -5,8 +5,8 @@ import { Injectable } from '@nestjs/common'; import { AxiosError, AxiosRequestConfig } from 'axios'; import { lastValueFrom } from 'rxjs'; import { HttpMethod } from '../../common/enums/http-method'; -import { toCleanObjParams } from '../../common/utils/gateway-common.utils'; import * as errorUtils from '../../common/utils/error'; +import { toCleanObjParams } from '../../common/utils/gateway-common.utils'; import logger from '../../logger'; import { JobAssignmentCommand, @@ -20,12 +20,6 @@ import { ResignJobCommand, ResignJobData, } from '../../modules/job-assignment/model/job-assignment.model'; -import { - JobsDiscoveryParams, - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, - JobsDiscoveryResponse, -} from '../../modules/jobs-discovery/model/jobs-discovery.model'; import { OracleStatisticsCommand, OracleStatisticsResponse, @@ -40,6 +34,12 @@ import { } from '../../modules/user-worker/model/worker-registration.model'; import { EscrowUtilsGateway } from '../escrow/escrow-utils-gateway.service'; import { KvStoreGateway } from '../kv-store/kv-store.gateway'; +import { + FetchJobsCommand, + FetchJobsParams, + FetchJobsParamsData, + FetchJobsResponse, +} from './model/exchange-oracle.model'; @Injectable() export class ExchangeOracleGateway { @@ -169,15 +169,13 @@ export class ExchangeOracleGateway { return this.callExternalHttpUtilRequest(options); } - async fetchJobs( - command: JobsDiscoveryParamsCommand, - ): Promise { - const jobsDiscoveryParamsData = this.mapper.map( + async fetchJobs(command: FetchJobsCommand): Promise { + const fetchJobsParamsData = this.mapper.map( command.data, - JobsDiscoveryParams, - JobsDiscoveryParamsData, + FetchJobsParams, + FetchJobsParamsData, ); - const reducedParams = toCleanObjParams(jobsDiscoveryParamsData); + const reducedParams = toCleanObjParams(fetchJobsParamsData); const options: AxiosRequestConfig = { method: HttpMethod.GET, url: `${await this.kvStoreGateway.getExchangeOracleUrlByAddress( @@ -189,7 +187,7 @@ export class ExchangeOracleGateway { Accept: 'application/json', }, }; - return this.callExternalHttpUtilRequest(options); + return this.callExternalHttpUtilRequest(options); } async sendRegistrationInExchangeOracle( diff --git a/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.mapper.profile.ts b/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.mapper.profile.ts index 9709289dd4..7132456fc3 100644 --- a/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.mapper.profile.ts +++ b/packages/apps/human-app/server/src/integrations/exchange-oracle/exchange-oracle.mapper.profile.ts @@ -1,5 +1,3 @@ -import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; import { CamelCaseNamingConvention, createMap, @@ -9,6 +7,8 @@ import { namingConventions, SnakeCaseNamingConvention, } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; import { JobAssignmentData, JobAssignmentParams, @@ -17,14 +17,14 @@ import { ResignJobCommand, ResignJobData, } from '../../modules/job-assignment/model/job-assignment.model'; -import { - JobsDiscoveryParams, - JobsDiscoveryParamsData, -} from '../../modules/jobs-discovery/model/jobs-discovery.model'; import { RegistrationInExchangeOracleCommand, RegistrationInExchangeOracleData, } from '../../modules/user-worker/model/worker-registration.model'; +import { + FetchJobsParams, + FetchJobsParamsData, +} from './model/exchange-oracle.model'; @Injectable() export class ExchangeOracleProfile extends AutomapperProfile { @@ -63,8 +63,8 @@ export class ExchangeOracleProfile extends AutomapperProfile { ); createMap( mapper, - JobsDiscoveryParams, - JobsDiscoveryParamsData, + FetchJobsParams, + FetchJobsParamsData, // Automapper has problem with mapping arrays, thus explicit conversion forMember( (destination) => destination.fields, diff --git a/packages/apps/human-app/server/src/integrations/exchange-oracle/model/exchange-oracle.model.ts b/packages/apps/human-app/server/src/integrations/exchange-oracle/model/exchange-oracle.model.ts new file mode 100644 index 0000000000..f5d5d151a8 --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/exchange-oracle/model/exchange-oracle.model.ts @@ -0,0 +1,95 @@ +import { AutoMap } from '@automapper/classes'; +import { JobStatus } from '../../../common/enums/global-common'; +import { + PageableData, + PageableParams, + PageableResponse, +} from '../../../common/utils/pageable.model'; + +export enum FetchJobsFieldName { + JobDescription = 'job_description', + RewardAmount = 'reward_amount', + RewardToken = 'reward_token', + CreatedAt = 'created_at', +} + +export enum FetchJobsSortField { + CHAIN_ID = 'chain_id', + JOB_TYPE = 'job_type', + REWARD_AMOUNT = 'reward_amount', + CREATED_AT = 'created_at', +} + +export class FetchJobsParams extends PageableParams { + @AutoMap() + escrowAddress?: string; + + @AutoMap() + chainId?: number; + + @AutoMap() + sortField?: FetchJobsSortField; + + @AutoMap() + jobType?: string; + + @AutoMap() + fields: FetchJobsFieldName[]; + + @AutoMap() + status: JobStatus; + + @AutoMap() + updatedAfter?: string; + + qualifications?: string[]; +} + +export class FetchJobsCommand { + @AutoMap() + oracleAddress: string; + @AutoMap() + token: string; + @AutoMap() + data: FetchJobsParams; +} + +export class FetchJobsParamsData extends PageableData { + @AutoMap() + escrow_address?: string; + + @AutoMap() + chain_id?: number; + + @AutoMap() + sort_field?: FetchJobsSortField; + + @AutoMap() + job_type?: string; + + @AutoMap() + fields: FetchJobsFieldName[]; + + @AutoMap() + status: JobStatus; + + @AutoMap() + updated_after?: string; +} + +export type FetchJobsResponseItem = { + escrow_address: string; + chain_id: number; + job_type: string; + status: JobStatus; + job_description?: string; + reward_amount?: string; + reward_token?: string; + created_at?: string; + updated_at?: string; + qualifications: string[]; +}; + +export class FetchJobsResponse extends PageableResponse { + results: FetchJobsResponseItem[]; +} diff --git a/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.fixtures.ts b/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.fixtures.ts new file mode 100644 index 0000000000..82e4fd37dc --- /dev/null +++ b/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.fixtures.ts @@ -0,0 +1,100 @@ +import { JobStatus, SortOrder } from '../../../common/enums/global-common'; +import { + FetchJobsCommand, + FetchJobsFieldName, + FetchJobsParamsData, + FetchJobsResponse, + FetchJobsResponseItem, + FetchJobsSortField, +} from '../model/exchange-oracle.model'; + +const ESCROW_ADDRESS = 'test_address'; +const CHAIN_ID = 1; +const PAGE_SIZE = 10; +const PAGE = 0; +const SORT = SortOrder.ASC; +const SORT_FIELD = FetchJobsSortField.CREATED_AT; +const JOB_TYPE = 'FORTUNE'; +const EXCHANGE_ORACLE_ADDRESS = '0x3dfa342'; +const STATUS = JobStatus.ACTIVE; +const FIELDS = [ + FetchJobsFieldName.CreatedAt, + FetchJobsFieldName.JobDescription, +]; + +export const fetchJobsCommandFixture: FetchJobsCommand = { + data: { + chainId: CHAIN_ID, + pageSize: PAGE_SIZE, + page: PAGE, + sort: SORT, + sortField: SORT_FIELD, + jobType: JOB_TYPE, + status: STATUS, + fields: FIELDS, + }, + token: 'jwt-token', + oracleAddress: EXCHANGE_ORACLE_ADDRESS, +}; + +export const paramsDataFixture: FetchJobsParamsData = { + chain_id: CHAIN_ID, + page_size: PAGE_SIZE, + page: PAGE, + sort: SORT, + sort_field: SORT_FIELD, + job_type: JOB_TYPE, + fields: FIELDS, + status: STATUS, +}; +export const paramsDataFixtureAsString = `?escrow_address=${paramsDataFixture.escrow_address}&chain_id=${paramsDataFixture.chain_id}&page_size=${paramsDataFixture.page_size}&page=${paramsDataFixture.page}&sort=${paramsDataFixture.sort}&sort_field=${paramsDataFixture.sort_field}&job_type=${paramsDataFixture.job_type}&fields=${paramsDataFixture.fields.join(',')}`; + +export const responseItemFixture1: FetchJobsResponseItem = { + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + status: JobStatus.ACTIVE, + created_at: '2025-03-18T03:00:00.000Z', + qualifications: [], + job_description: 'Response item fixture 1 description', + reward_amount: '42.1', + reward_token: 'hmt', + updated_at: '2025-03-18T03:00:00.000Z', +}; +export const responseItemFixture2: FetchJobsResponseItem = { + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + status: JobStatus.COMPLETED, + created_at: '2025-03-18T02:00:00.000Z', + qualifications: [], + job_description: 'Response item fixture 2 description', + reward_amount: '42.2', + reward_token: 'hmt', + updated_at: '2025-03-18T02:00:00.000Z', +}; +export const responseItemFixture3: FetchJobsResponseItem = { + escrow_address: ESCROW_ADDRESS, + chain_id: CHAIN_ID, + job_type: JOB_TYPE, + status: JobStatus.ACTIVE, + created_at: '2025-03-18T01:00:00.000Z', + qualifications: [], + job_description: 'Response item fixture 3 description', + reward_amount: '42.3', + reward_token: 'hmt', + updated_at: '2025-03-18T01:00:00.000Z', +}; +export const responseItemsFixture: FetchJobsResponseItem[] = [ + responseItemFixture1, + responseItemFixture2, + responseItemFixture3, +]; + +export const responseFixture: FetchJobsResponse = { + results: responseItemsFixture, + page: PAGE, + page_size: PAGE_SIZE, + total_pages: 34, + total_results: 4, +}; diff --git a/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.gateway.spec.ts b/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.gateway.spec.ts index f52aea4d11..d6558f25b7 100644 --- a/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.gateway.spec.ts +++ b/packages/apps/human-app/server/src/integrations/exchange-oracle/spec/exchange-oracle.gateway.spec.ts @@ -1,15 +1,13 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService } from '@nestjs/axios'; -import { ExchangeOracleGateway } from '../exchange-oracle.gateway'; -import { - statisticsExchangeOracleUrl, - generalUserStatsCommandFixture, - oracleStatsCommandFixture, -} from '../../../modules/statistics/spec/statistics.fixtures'; -import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { HttpService } from '@nestjs/axios'; +import { GoneException, HttpException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AxiosResponse } from 'axios'; import nock, { RequestBodyMatcher } from 'nock'; import { of, throwError } from 'rxjs'; +import { HttpMethod } from '../../../common/enums/http-method'; +import { ResignJobData } from '../../../modules/job-assignment/model/job-assignment.model'; import { jobAssignmentCommandFixture, jobAssignmentDataFixture, @@ -19,26 +17,28 @@ import { jobsFetchParamsDataFixtureAsString, workerRegisterUrl, } from '../../../modules/job-assignment/spec/job-assignment.fixtures'; -import { ExchangeOracleProfile } from '../exchange-oracle.mapper.profile'; import { - jobsDiscoveryParamsCommandFixture, - paramsDataFixture, - paramsDataFixtureAsString, - responseFixture, -} from '../../../modules/jobs-discovery/spec/jobs-discovery.fixtures'; -import { GoneException, HttpException } from '@nestjs/common'; -import { HttpMethod } from '../../../common/enums/http-method'; -import { KvStoreGateway } from '../../kv-store/kv-store.gateway'; -import { EscrowUtilsGateway } from '../../escrow/escrow-utils-gateway.service'; -import { ResignJobData } from '../../../modules/job-assignment/model/job-assignment.model'; -import { JobsDiscoveryParamsData } from '../../../modules/jobs-discovery/model/jobs-discovery.model'; -import { AxiosResponse } from 'axios'; + generalUserStatsCommandFixture, + oracleStatsCommandFixture, + statisticsExchangeOracleUrl, +} from '../../../modules/statistics/spec/statistics.fixtures'; import { RegistrationInExchangeOracleData } from '../../../modules/user-worker/model/worker-registration.model'; import { registerWorkerCommandFixture, registerWorkerDataFixture, responseWorkerFixture, } from '../../../modules/user-worker/spec/worker.fixtures'; +import { EscrowUtilsGateway } from '../../escrow/escrow-utils-gateway.service'; +import { KvStoreGateway } from '../../kv-store/kv-store.gateway'; +import { ExchangeOracleGateway } from '../exchange-oracle.gateway'; +import { ExchangeOracleProfile } from '../exchange-oracle.mapper.profile'; +import { FetchJobsParamsData } from '../model/exchange-oracle.model'; +import { + fetchJobsCommandFixture, + paramsDataFixture, + paramsDataFixtureAsString, + responseFixture, +} from './exchange-oracle.fixtures'; describe('ExchangeOracleApiGateway', () => { let gateway: ExchangeOracleGateway; @@ -232,8 +232,8 @@ describe('ExchangeOracleApiGateway', () => { config: {}, } as AxiosResponse), ); - const command = jobsDiscoveryParamsCommandFixture; - const expectedMappedData: JobsDiscoveryParamsData = paramsDataFixture; + const command = fetchJobsCommandFixture; + const expectedMappedData: FetchJobsParamsData = paramsDataFixture; nock(jobAssignmentOracleUrl) .get(`/assignment${paramsDataFixtureAsString}`) .reply(200, responseFixture); diff --git a/packages/apps/human-app/server/src/modules/cron-job/cron-job.service.ts b/packages/apps/human-app/server/src/modules/cron-job/cron-job.service.ts index 154173b4ae..549b1f1744 100644 --- a/packages/apps/human-app/server/src/modules/cron-job/cron-job.service.ts +++ b/packages/apps/human-app/server/src/modules/cron-job/cron-job.service.ts @@ -3,41 +3,51 @@ import { SchedulerRegistry } from '@nestjs/schedule'; import { AxiosError } from 'axios'; import { CronJob } from 'cron'; import { EnvironmentConfigService } from '../../common/config/environment-config.service'; -import { - JobDiscoveryFieldName, - JobStatus, -} from '../../common/enums/global-common'; +import { JobStatus } from '../../common/enums/global-common'; import * as errorUtils from '../../common/utils/error'; import { ExchangeOracleGateway } from '../../integrations/exchange-oracle/exchange-oracle.gateway'; +import { + FetchJobsCommand, + FetchJobsFieldName, + FetchJobsParams, + FetchJobsResponse, +} from '../../integrations/exchange-oracle/model/exchange-oracle.model'; import { ReputationOracleGateway } from '../../integrations/reputation-oracle/reputation-oracle.gateway'; import logger from '../../logger'; import { JobsDiscoveryService } from '../jobs-discovery/jobs-discovery.service'; -import { - DiscoveredJob, - JobsDiscoveryParams, - JobsDiscoveryParamsCommand, - JobsDiscoveryResponse, -} from '../jobs-discovery/model/jobs-discovery.model'; +import { DiscoveredJob } from '../jobs-discovery/model/jobs-discovery.model'; import { DiscoveredOracle } from '../oracle-discovery/model/oracle-discovery.model'; import { OracleDiscoveryService } from '../oracle-discovery/oracle-discovery.service'; -function assertJobsDiscoveryResponseItemsFormat( - items: JobsDiscoveryResponse['results'], +const DISCOVERED_JOB_REQUIRED_KEYS = [ + 'escrow_address', + 'chain_id', + 'job_type', + 'status', + 'job_description', + 'reward_amount', + 'reward_token', + 'created_at', +] as const; + +function assertDiscoveredJobsFormat( + items: FetchJobsResponse['results'], ): asserts items is DiscoveredJob[] { if (items.length === 0) { return; } - const item = items[0]; - if ( - [ - item.job_description, - item.reward_amount, - item.reward_token, - item.created_at, - ].includes(undefined) - ) { - throw new Error('Job discovery response items missing expected fields'); + const missingKeys: string[] = []; + for (const requiredKey of DISCOVERED_JOB_REQUIRED_KEYS) { + if (items[0][requiredKey] === undefined) { + missingKeys.push(requiredKey); + } + } + + if (missingKeys.length) { + throw new Error( + `Jobs response items missing expected fields: ${missingKeys.join()}`, + ); } } @@ -117,23 +127,23 @@ export class CronJobService { let allResults: DiscoveredJob[] = []; // Initial fetch to determine the total number of pages - const command = new JobsDiscoveryParamsCommand(); + const command = new FetchJobsCommand(); command.oracleAddress = oracle.address; command.token = token; - command.data = new JobsDiscoveryParams(); + command.data = new FetchJobsParams(); command.data.page = 0; - command.data.pageSize = command.data.pageSize || 10; // Max value for Exchange Oracle + command.data.pageSize = 10; // Max value for Exchange Oracle command.data.fields = [ - JobDiscoveryFieldName.JobDescription, - JobDiscoveryFieldName.RewardAmount, - JobDiscoveryFieldName.RewardToken, - JobDiscoveryFieldName.CreatedAt, + FetchJobsFieldName.JobDescription, + FetchJobsFieldName.RewardAmount, + FetchJobsFieldName.RewardToken, + FetchJobsFieldName.CreatedAt, ]; command.data.status = JobStatus.ACTIVE; const initialResponse = await this.exchangeOracleGateway.fetchJobs(command); - assertJobsDiscoveryResponseItemsFormat(initialResponse.results); + assertDiscoveredJobsFormat(initialResponse.results); allResults = this.mergeJobs(allResults, initialResponse.results); @@ -148,7 +158,7 @@ export class CronJobService { const remainingResponses = await Promise.all(pageFetches); for (const response of remainingResponses) { - assertJobsDiscoveryResponseItemsFormat(response.results); + assertDiscoveredJobsFormat(response.results); allResults = this.mergeJobs(allResults, response.results); } diff --git a/packages/apps/human-app/server/src/modules/cron-job/spec/cron-job.service.spec.ts b/packages/apps/human-app/server/src/modules/cron-job/spec/cron-job.service.spec.ts index d05258f95b..5b44372733 100644 --- a/packages/apps/human-app/server/src/modules/cron-job/spec/cron-job.service.spec.ts +++ b/packages/apps/human-app/server/src/modules/cron-job/spec/cron-job.service.spec.ts @@ -1,22 +1,22 @@ import { ChainId } from '@human-protocol/sdk'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test, TestingModule } from '@nestjs/testing'; -import { CronJobService } from '../cron-job.service'; -import { ExchangeOracleGateway } from '../../../integrations/exchange-oracle/exchange-oracle.gateway'; -import { ReputationOracleGateway } from '../../../integrations/reputation-oracle/reputation-oracle.gateway'; -import { OracleDiscoveryService } from '../../../modules/oracle-discovery/oracle-discovery.service'; import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; -import { - DiscoveredJob, - JobsDiscoveryParamsCommand, - JobsDiscoveryResponse, - JobsDiscoveryResponseItem, -} from '../../../modules/jobs-discovery/model/jobs-discovery.model'; import { JobStatus } from '../../../common/enums/global-common'; +import { ExchangeOracleGateway } from '../../../integrations/exchange-oracle/exchange-oracle.gateway'; +import { ReputationOracleGateway } from '../../../integrations/reputation-oracle/reputation-oracle.gateway'; import { JobsDiscoveryService } from '../../../modules/jobs-discovery/jobs-discovery.service'; +import { DiscoveredJob } from '../../../modules/jobs-discovery/model/jobs-discovery.model'; +import { OracleDiscoveryService } from '../../../modules/oracle-discovery/oracle-discovery.service'; import { generateOracleDiscoveryResponseBody } from '../../../modules/oracle-discovery/spec/oracle-discovery.fixture'; +import { CronJobService } from '../cron-job.service'; import { HMT_TOKEN_SYMBOL } from '../../../common/constants/hmt'; +import { + FetchJobsCommand, + FetchJobsResponse, + FetchJobsResponseItem, +} from '../../../integrations/exchange-oracle/model/exchange-oracle.model'; jest.mock('cron', () => { return { @@ -174,7 +174,7 @@ describe('CronJobService', () => { it('should fetch all jobs and update the cache', async () => { const now = new Date(); - const initialResponse: JobsDiscoveryResponse = { + const initialResponse: FetchJobsResponse = { results: [ { escrow_address: '0xabc', @@ -201,7 +201,7 @@ describe('CronJobService', () => { await service.updateJobsListCache(oracle, token); expect(exchangeOracleGatewayMock.fetchJobs).toHaveBeenCalledWith( - expect.any(JobsDiscoveryParamsCommand), + expect.any(FetchJobsCommand), ); expect(jobDiscoveryServiceMock.setCachedJobs).toHaveBeenCalledWith( oracle.address, @@ -236,7 +236,7 @@ describe('CronJobService', () => { it('should reset retries count after successful job fetch', async () => { const now = new Date(); - const initialResponse: JobsDiscoveryResponse = { + const initialResponse: FetchJobsResponse = { results: [ { escrow_address: '0xabc', @@ -290,7 +290,7 @@ describe('CronJobService', () => { }, ]; - const newJobs: JobsDiscoveryResponseItem[] = [ + const newJobs: FetchJobsResponseItem[] = [ { escrow_address: '0xdef', chain_id: 1, @@ -329,7 +329,7 @@ describe('CronJobService', () => { qualifications: [], }, ]; - const newJobs: JobsDiscoveryResponseItem[] = [ + const newJobs: FetchJobsResponseItem[] = [ { escrow_address: '0xabc', chain_id: 1, diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts index 50650861b6..993cf5e55b 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts @@ -18,9 +18,9 @@ import { EnvironmentConfigService } from '../../common/config/environment-config import { RequestWithUser } from '../../common/interfaces/jwt'; import { JobsDiscoveryService } from './jobs-discovery.service'; import { - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsDto, - JobsDiscoveryResponse, + GetJobsCommand, + GetJobsQueryDto, + GetJobsResponseDto, } from './model/jobs-discovery.model'; @Controller() @@ -37,11 +37,11 @@ export class JobsDiscoveryController { @ApiOperation({ summary: 'Retrieve a list of jobs for given Exchange Oracle', }) - @ApiOkResponse({ type: JobsDiscoveryResponse, description: 'List of jobs' }) + @ApiOkResponse({ type: GetJobsResponseDto, description: 'List of jobs' }) public async getJobs( - @Query() jobsDiscoveryParamsDto: JobsDiscoveryParamsDto, + @Query() query: GetJobsQueryDto, @Request() req: RequestWithUser, - ): Promise { + ): Promise { if (!this.environmentConfigService.jobsDiscoveryFlag) { throw new HttpException( 'Jobs discovery is disabled', @@ -49,14 +49,12 @@ export class JobsDiscoveryController { ); } - const jobsDiscoveryParamsCommand: JobsDiscoveryParamsCommand = - this.mapper.map( - jobsDiscoveryParamsDto, - JobsDiscoveryParamsDto, - JobsDiscoveryParamsCommand, - ); - jobsDiscoveryParamsCommand.token = req.token; - jobsDiscoveryParamsCommand.data.qualifications = req.user.qualifications; - return await this.service.processJobsDiscovery(jobsDiscoveryParamsCommand); + const getJobsCommand: GetJobsCommand = this.mapper.map( + query, + GetJobsQueryDto, + GetJobsCommand, + ); + getJobsCommand.data.qualifications = req.user.qualifications; + return await this.service.getJobs(getJobsCommand); } } diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.profile.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.profile.ts index cab001c971..ca417f7782 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.profile.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.mapper.profile.ts @@ -1,5 +1,3 @@ -import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; import { CamelCaseNamingConvention, createMap, @@ -9,10 +7,12 @@ import { namingConventions, SnakeCaseNamingConvention, } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; import { - JobsDiscoveryParams, - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsDto, + GetJobsCommand, + GetJobsParams, + GetJobsQueryDto, } from './model/jobs-discovery.model'; @Injectable() @@ -25,8 +25,8 @@ export class JobsDiscoveryProfile extends AutomapperProfile { return (mapper: Mapper) => { createMap( mapper, - JobsDiscoveryParamsDto, - JobsDiscoveryParams, + GetJobsQueryDto, + GetJobsParams, // forMember usage cause: https://github.com/nartc/mapper/issues/583 forMember( (destination) => destination.pageSize, @@ -36,11 +36,6 @@ export class JobsDiscoveryProfile extends AutomapperProfile { (destination) => destination.sortField, mapFrom((source) => source.sort_field), ), - // Automapper has problem with mapping arrays, thus explicit conversion - forMember( - (destination) => destination.fields, - mapFrom((source) => source.fields), - ), namingConventions({ source: new SnakeCaseNamingConvention(), destination: new CamelCaseNamingConvention(), @@ -48,12 +43,12 @@ export class JobsDiscoveryProfile extends AutomapperProfile { ); createMap( mapper, - JobsDiscoveryParamsDto, - JobsDiscoveryParamsCommand, + GetJobsQueryDto, + GetJobsCommand, forMember( (destination) => destination.data, - mapFrom((source: JobsDiscoveryParamsDto) => - mapper.map(source, JobsDiscoveryParamsDto, JobsDiscoveryParams), + mapFrom((source: GetJobsQueryDto) => + mapper.map(source, GetJobsQueryDto, GetJobsParams), ), ), namingConventions({ diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts index 1117c638e3..1888fec479 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.module.ts @@ -1,7 +1,7 @@ -import { JobsDiscoveryService } from './jobs-discovery.service'; -import { JobsDiscoveryProfile } from './jobs-discovery.mapper.profile'; import { Module } from '@nestjs/common'; import { ExchangeOracleModule } from '../../integrations/exchange-oracle/exchange-oracle.module'; +import { JobsDiscoveryProfile } from './jobs-discovery.mapper.profile'; +import { JobsDiscoveryService } from './jobs-discovery.service'; @Module({ imports: [ExchangeOracleModule], diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts index ee87a582cb..8cad1dc79c 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts @@ -1,21 +1,18 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable } from '@nestjs/common'; +import { Cache } from 'cache-manager'; +import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { JOB_DISCOVERY_CACHE_KEY } from '../../common/constants/cache'; import { Iteratee, paginateAndSortResults, } from '../../common/utils/pagination.utils'; import { DiscoveredJob, - JobsDiscoveryParamsCommand, - JobsDiscoveryResponse, -} from './model/jobs-discovery.model'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; -import { EnvironmentConfigService } from '../../common/config/environment-config.service'; -import { JOB_DISCOVERY_CACHE_KEY } from '../../common/constants/cache'; -import { - JobDiscoveryFieldName, + GetJobsCommand, + GetJobsResponseDto, JobDiscoverySortField, -} from '../../common/enums/global-common'; +} from './model/jobs-discovery.model'; @Injectable() export class JobsDiscoveryService { @@ -24,9 +21,7 @@ export class JobsDiscoveryService { private configService: EnvironmentConfigService, ) {} - async processJobsDiscovery( - command: JobsDiscoveryParamsCommand, - ): Promise { + async getJobs(command: GetJobsCommand): Promise { const allJobs = await this.getCachedJobs(command.oracleAddress); let filteredJobs = this.applyFilters(allJobs, command.data); filteredJobs = filteredJobs.filter((job) => @@ -58,54 +53,42 @@ export class JobsDiscoveryService { private applyFilters( jobs: DiscoveredJob[], - filters: JobsDiscoveryParamsCommand['data'], + filters: GetJobsCommand['data'], ): DiscoveredJob[] { - const difference = Object.values(JobDiscoveryFieldName).filter( - (value) => !filters.fields?.includes(value), - ); - return jobs - .filter((job) => { - let matches = true; - - if (filters.escrowAddress) { - matches = matches && job.escrow_address === filters.escrowAddress; - } - - if (filters.chainId !== undefined && filters.chainId !== null) { - matches = matches && job.chain_id === filters.chainId; - } - - if (filters.jobType) { - matches = matches && job.job_type === filters.jobType; + return jobs.filter((job) => { + let matches = true; + + if (filters.escrowAddress) { + matches = matches && job.escrow_address === filters.escrowAddress; + } + + if (filters.chainId !== undefined && filters.chainId !== null) { + matches = matches && job.chain_id === filters.chainId; + } + + if (filters.jobType) { + matches = matches && job.job_type === filters.jobType; + } + + if (filters.status !== undefined && filters.status !== null) { + matches = matches && job.status === filters.status; + } + + if ( + filters.qualifications !== undefined && + filters.qualifications !== null + ) { + if (job.qualifications && job.qualifications.length > 0) { + matches = + matches && + job.qualifications.every((qualification) => + filters.qualifications?.includes(qualification), + ); } + } - if (filters.status !== undefined && filters.status !== null) { - matches = matches && job.status === filters.status; - } - - if ( - filters.qualifications !== undefined && - filters.qualifications !== null - ) { - if (job.qualifications && job.qualifications.length > 0) { - matches = - matches && - job.qualifications.every((qualification) => - filters.qualifications?.includes(qualification), - ); - } - } - - return matches; - }) - .map((job) => { - if (difference && difference.length > 0) { - difference.forEach((field) => { - delete job[field]; - }); - } - return job; - }); + return matches; + }); } static makeCacheKeyForOracle(oracleAddress: string): string { diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/model/jobs-discovery.model.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/model/jobs-discovery.model.ts index cc63a65da0..8d3a75ca66 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/model/jobs-discovery.model.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/model/jobs-discovery.model.ts @@ -1,115 +1,122 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { AutoMap } from '@automapper/classes'; -import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsEnumCaseInsensitive } from '../../../common/decorators'; +import { JobStatus } from '../../../common/enums/global-common'; import { - JobDiscoveryFieldName, - JobDiscoverySortField, - JobStatus, -} from '../../../common/enums/global-common'; -import { - PageableData, PageableDto, PageableParams, PageableResponse, } from '../../../common/utils/pageable.model'; -import { IsEnumCaseInsensitive } from '../../../common/decorators'; +import { FetchJobsResponseItem } from '../../../integrations/exchange-oracle/model/exchange-oracle.model'; + +export enum JobDiscoverySortField { + REWARD_AMOUNT = 'reward_amount', + CREATED_AT = 'created_at', +} -export class JobsDiscoveryParamsDto extends PageableDto { +export class GetJobsQueryDto extends PageableDto { @AutoMap() @IsString() @ApiProperty() - oracle_address?: string; + oracle_address: string; + @AutoMap() @IsOptional() @IsString() @ApiPropertyOptional() escrow_address?: string; + @AutoMap() @Type(() => Number) @IsNumber() @IsOptional() @ApiPropertyOptional() chain_id?: number; - @AutoMap() - @IsOptional() - @IsEnumCaseInsensitive(JobDiscoverySortField) - @ApiPropertyOptional({ enum: JobDiscoverySortField }) - sort_field?: JobDiscoverySortField; + @AutoMap() @IsString() @IsOptional() @ApiPropertyOptional() job_type?: string; - @AutoMap() - @IsOptional() - @IsEnumCaseInsensitive(JobDiscoveryFieldName, { each: true }) - @ApiPropertyOptional({ enum: JobDiscoveryFieldName, isArray: true }) - fields: JobDiscoveryFieldName[]; + @AutoMap() @ApiPropertyOptional({ enum: JobStatus }) @IsEnumCaseInsensitive(JobStatus) @IsOptional() status: JobStatus; + + @AutoMap() + @IsOptional() + @IsEnumCaseInsensitive(JobDiscoverySortField) + @ApiPropertyOptional({ enum: JobDiscoverySortField }) + sort_field?: JobDiscoverySortField; } -export class JobsDiscoveryParams extends PageableParams { +export class GetJobsParams extends PageableParams { @AutoMap() escrowAddress?: string; + @AutoMap() chainId?: number; - @AutoMap() - sortField?: JobDiscoverySortField; + @AutoMap() jobType?: string; - @AutoMap() - fields: JobDiscoveryFieldName[]; + @AutoMap() status: JobStatus; + @AutoMap() - updatedAfter?: string; + sortField?: JobDiscoverySortField; + qualifications?: string[]; } -export class JobsDiscoveryParamsData extends PageableData { - @AutoMap() - escrow_address?: string; - @AutoMap() - chain_id?: number; - @AutoMap() - sort_field?: JobDiscoverySortField; - @AutoMap() - job_type?: string; - @AutoMap() - fields: JobDiscoveryFieldName[]; - @AutoMap() - status: JobStatus; - @AutoMap() - updated_after?: string; -} -export class JobsDiscoveryParamsCommand { + +export class GetJobsCommand { @AutoMap() oracleAddress: string; + @AutoMap() - token: string; - @AutoMap() - data: JobsDiscoveryParams; + data: GetJobsParams; } -export type JobsDiscoveryResponseItem = { +export class DiscoveredJob implements Required { + @ApiProperty() escrow_address: string; + + @ApiProperty() chain_id: number; + + @ApiProperty() job_type: string; + + @ApiProperty() status: JobStatus; - job_description?: string; - reward_amount?: string; - reward_token?: string; - created_at?: string; - updated_at?: string; - qualifications: string[]; -}; -export class JobsDiscoveryResponse extends PageableResponse { - results: JobsDiscoveryResponseItem[]; + @ApiProperty() + job_description: string; + + @ApiProperty() + reward_amount: string; + + @ApiProperty() + reward_token: string; + + @ApiProperty() + created_at: string; + + @ApiProperty() + updated_at: string; + + @ApiProperty() + qualifications: string[]; } -export type DiscoveredJob = Required; +export class GetJobsResponseDto extends PageableResponse { + @ApiProperty({ + type: DiscoveredJob, + isArray: true, + }) + results: DiscoveredJob[]; +} diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts index c1780290c0..c6a6f66e17 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts @@ -11,8 +11,8 @@ import { JobsDiscoveryController } from '../jobs-discovery.controller'; import { JobsDiscoveryProfile } from '../jobs-discovery.mapper.profile'; import { JobsDiscoveryService } from '../jobs-discovery.service'; import { - dtoFixture, - jobsDiscoveryParamsCommandFixture, + queryDtoFixture, + getJobsCommandFixture, responseFixture, } from './jobs-discovery.fixtures'; import { jobsDiscoveryServiceMock } from './jobs-discovery.service.mock'; @@ -68,22 +68,19 @@ describe('JobsDiscoveryController', () => { expect(controller).toBeDefined(); }); - describe('processJobsDiscovery', () => { - it('should call service processJobsDiscovery method with proper fields set', async () => { - const dto = dtoFixture; - const command = jobsDiscoveryParamsCommandFixture; + describe('getJobs', () => { + it('should call service getJobs method with proper fields set', async () => { + const dto = queryDtoFixture; + const command = getJobsCommandFixture; await controller.getJobs(dto, { user: { qualifications: [] }, - token: command.token, } as any); command.data.qualifications = []; - expect(jobsDiscoveryService.processJobsDiscovery).toHaveBeenCalledWith( - command, - ); + expect(jobsDiscoveryService.getJobs).toHaveBeenCalledWith(command); }); it('should throw an error if jobsDiscoveryFlag is disabled', async () => { - const dto = dtoFixture; + const dto = queryDtoFixture; (configServiceMock as any).jobsDiscoveryFlag = false; await expect( controller.getJobs(dto, { user: { qualifications: [] } } as any), diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts index bdd0b0fcb6..2ab227433a 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.fixtures.ts @@ -1,20 +1,12 @@ import { - JobsDiscoveryParams, - JobsDiscoveryParamsCommand, - JobsDiscoveryParamsData, - JobsDiscoveryParamsDto, - JobsDiscoveryResponse, - JobsDiscoveryResponseItem, + GetJobsCommand, + GetJobsQueryDto, + GetJobsResponseDto, DiscoveredJob, -} from '../model/jobs-discovery.model'; -import { - JobDiscoveryFieldName, JobDiscoverySortField, - JobStatus, - SortOrder, -} from '../../../common/enums/global-common'; +} from '../model/jobs-discovery.model'; +import { JobStatus, SortOrder } from '../../../common/enums/global-common'; -const EXCHANGE_ORACLE_URL = 'https://www.test_url.org'; const ESCROW_ADDRESS1 = 'test_address1'; const ESCROW_ADDRESS2 = 'test_address2'; const ESCROW_ADDRESS3 = 'test_address3'; @@ -29,78 +21,68 @@ const TOTAL_PAGES = 4; const SORT = SortOrder.ASC; const SORT_FIELD = JobDiscoverySortField.CREATED_AT; const JOB_TYPE = 'FORTUNE'; -const FIELDS = [ - JobDiscoveryFieldName.CreatedAt, - JobDiscoveryFieldName.JobDescription, -]; -const TOKEN = 'test-token'; const EXCHANGE_ORACLE_ADDRESS = '0x3dfa342'; const STATUS = JobStatus.ACTIVE; -export const jobsDiscoveryOracleUrlFixture = EXCHANGE_ORACLE_URL; -export const jobDiscoveryToken = TOKEN; -export const dtoFixture: JobsDiscoveryParamsDto = { - oracle_address: EXCHANGE_ORACLE_ADDRESS, - chain_id: CHAIN_ID, - page_size: PAGE_SIZE, - page: PAGE, - sort: SORT, - sort_field: SORT_FIELD, - job_type: JOB_TYPE, - fields: FIELDS, - status: STATUS, -}; -const dataFixture: JobsDiscoveryParams = { - chainId: CHAIN_ID, - pageSize: PAGE_SIZE, - page: PAGE, - sort: SORT, - sortField: SORT_FIELD, - jobType: JOB_TYPE, - fields: FIELDS, - status: STATUS, -}; -export const paramsDataFixture: JobsDiscoveryParamsData = { +export const queryDtoFixture: GetJobsQueryDto = { + oracle_address: EXCHANGE_ORACLE_ADDRESS, chain_id: CHAIN_ID, page_size: PAGE_SIZE, page: PAGE, sort: SORT, sort_field: SORT_FIELD, job_type: JOB_TYPE, - fields: FIELDS, status: STATUS, }; -export const paramsDataFixtureAsString = `?escrow_address=${paramsDataFixture.escrow_address}&chain_id=${paramsDataFixture.chain_id}&page_size=${paramsDataFixture.page_size}&page=${paramsDataFixture.page}&sort=${paramsDataFixture.sort}&sort_field=${paramsDataFixture.sort_field}&job_type=${paramsDataFixture.job_type}&fields=${paramsDataFixture.fields.join(',')}`; -export const jobsDiscoveryParamsCommandFixture: JobsDiscoveryParamsCommand = { - data: dataFixture, +export const getJobsCommandFixture: GetJobsCommand = { + data: { + chainId: CHAIN_ID, + pageSize: PAGE_SIZE, + page: PAGE, + sort: SORT, + sortField: SORT_FIELD, + jobType: JOB_TYPE, + status: STATUS, + }, oracleAddress: EXCHANGE_ORACLE_ADDRESS, - token: TOKEN, }; -export const responseItemFixture1: JobsDiscoveryResponseItem = { +export const responseItemFixture1: DiscoveredJob = { escrow_address: ESCROW_ADDRESS1, chain_id: CHAIN_ID, job_type: JOB_TYPE, status: JobStatus.ACTIVE, created_at: '2025-03-18T03:00:00.000Z', qualifications: [], + job_description: 'Response item fixture 1 description', + reward_amount: '42.1', + reward_token: 'hmt', + updated_at: '2025-03-18T03:00:00.000Z', }; -export const responseItemFixture2: JobsDiscoveryResponseItem = { +export const responseItemFixture2: DiscoveredJob = { escrow_address: ESCROW_ADDRESS2, chain_id: CHAIN_ID, job_type: JOB_TYPE, status: JobStatus.COMPLETED, created_at: '2025-03-18T02:00:00.000Z', qualifications: [], + job_description: 'Response item fixture 2 description', + reward_amount: '42.2', + reward_token: 'hmt', + updated_at: '2025-03-18T02:00:00.000Z', }; -export const responseItemFixture3: JobsDiscoveryResponseItem = { +export const responseItemFixture3: DiscoveredJob = { escrow_address: ESCROW_ADDRESS3, chain_id: CHAIN_ID, job_type: JOB_TYPE, status: JobStatus.ACTIVE, created_at: '2025-03-18T01:00:00.000Z', qualifications: [], + job_description: 'Response item fixture 3 description', + reward_amount: '42.3', + reward_token: 'hmt', + updated_at: '2025-03-18T01:00:00.000Z', }; -export const responseItemsFixture: JobsDiscoveryResponseItem[] = [ +export const responseItemsFixture: DiscoveredJob[] = [ responseItemFixture1, responseItemFixture2, responseItemFixture3, @@ -141,7 +123,7 @@ export const validRewardAmountResponseItemFixture: DiscoveredJob = { reward_token: REWARD_TOKEN_HMT, updated_at: '2025-03-18T01:00:00.000Z', }; -export const responseFixture: JobsDiscoveryResponse = { +export const responseFixture: GetJobsResponseDto = { results: responseItemsFixture, page: PAGE, page_size: PAGE_SIZE, diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.mock.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.mock.ts index b16d952239..d6ac491a58 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.mock.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.mock.ts @@ -1,5 +1,5 @@ import { responseFixture } from './jobs-discovery.fixtures'; export const jobsDiscoveryServiceMock = { - processJobsDiscovery: jest.fn().mockReturnValue(responseFixture), + getJobs: jest.fn().mockReturnValue(responseFixture), }; diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.spec.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.spec.ts index d3d45b6cff..1bd81a9920 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.spec.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.service.spec.ts @@ -1,24 +1,21 @@ import { ChainId } from '@human-protocol/sdk'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { JobsDiscoveryService } from '../jobs-discovery.service'; -import { ExchangeOracleGateway } from '../../../integrations/exchange-oracle/exchange-oracle.gateway'; import { Test, TestingModule } from '@nestjs/testing'; +import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; +import { SortOrder } from '../../../common/enums/global-common'; +import { ExchangeOracleGateway } from '../../../integrations/exchange-oracle/exchange-oracle.gateway'; +import { JobsDiscoveryService } from '../jobs-discovery.service'; import { hmtRewardAmountResponseItemFixture, invalidRewardAmountResponseItemFixture, - jobsDiscoveryParamsCommandFixture, - responseItemsFixture, + getJobsCommandFixture, responseItemFixture1, responseItemFixture3, + responseItemsFixture, usdcRewardAmountResponseItemFixture, validRewardAmountResponseItemFixture, } from './jobs-discovery.fixtures'; -import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; -import { - JobDiscoveryFieldName, - JobDiscoverySortField, - SortOrder, -} from '../../../common/enums/global-common'; +import { JobDiscoverySortField } from '../model/jobs-discovery.model'; describe('JobsDiscoveryService', () => { let service: JobsDiscoveryService; @@ -51,20 +48,23 @@ describe('JobsDiscoveryService', () => { service = module.get(JobsDiscoveryService); }); + it('should be defined', () => { expect(service).toBeDefined(); }); - describe('processJobsDiscovery', () => { + + describe('getJobs', () => { it('should get oracle url and call api for jobs fetch', async () => { - const command = jobsDiscoveryParamsCommandFixture; + const command = getJobsCommandFixture; jest .spyOn(service as any, 'getCachedJobs') .mockReturnValue(responseItemsFixture); - const result = await service.processJobsDiscovery(command); + const result = await service.getJobs(command); + expect(service.getCachedJobs).toHaveBeenCalledWith( - jobsDiscoveryParamsCommandFixture.oracleAddress, + getJobsCommandFixture.oracleAddress, ); expect(result.results).toEqual([ responseItemFixture3, @@ -74,13 +74,9 @@ describe('JobsDiscoveryService', () => { it('should sort reward amounts using human-readable units', async () => { const command = { - ...jobsDiscoveryParamsCommandFixture, + ...getJobsCommandFixture, data: { - ...jobsDiscoveryParamsCommandFixture.data, - fields: [ - JobDiscoveryFieldName.RewardAmount, - JobDiscoveryFieldName.RewardToken, - ], + ...getJobsCommandFixture.data, sort: SortOrder.DESC, sortField: JobDiscoverySortField.REWARD_AMOUNT, }, @@ -93,7 +89,7 @@ describe('JobsDiscoveryService', () => { usdcRewardAmountResponseItemFixture, ]); - const result = await service.processJobsDiscovery(command); + const result = await service.getJobs(command); expect(result.results).toEqual([ usdcRewardAmountResponseItemFixture, @@ -103,13 +99,9 @@ describe('JobsDiscoveryService', () => { it('should use zero for invalid reward amounts', async () => { const command = { - ...jobsDiscoveryParamsCommandFixture, + ...getJobsCommandFixture, data: { - ...jobsDiscoveryParamsCommandFixture.data, - fields: [ - JobDiscoveryFieldName.RewardAmount, - JobDiscoveryFieldName.RewardToken, - ], + ...getJobsCommandFixture.data, sort: SortOrder.DESC, sortField: JobDiscoverySortField.REWARD_AMOUNT, }, @@ -122,7 +114,7 @@ describe('JobsDiscoveryService', () => { validRewardAmountResponseItemFixture, ]); - const result = await service.processJobsDiscovery(command); + const result = await service.getJobs(command); expect(result.results).toEqual([ validRewardAmountResponseItemFixture, From 3dc4cee128e602afaec72a8ddd6bd4c7eadf46ac Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Wed, 29 Jul 2026 11:41:21 +0300 Subject: [PATCH 2/4] fix: use chain id for oracle job types key --- .../server/src/integrations/kv-store/kv-store.gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts b/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts index 492dc6ebdd..64176e72e3 100644 --- a/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts +++ b/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts @@ -65,7 +65,7 @@ export class KvStoreGateway { chainId: ChainId, address: string, ): Promise { - const key = `jobTypes:${address}`; + const key = `jobTypes:${chainId}:${address}`; const cachedData: string | undefined = await this.cacheManager.get(key); if (cachedData) { return cachedData; From cf1d7b93da5d0cd6725765e7e515f7bda4b387ec Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Wed, 29 Jul 2026 12:26:51 +0300 Subject: [PATCH 3/4] fix: typo in redis env example --- packages/apps/human-app/server/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/human-app/server/.env.example b/packages/apps/human-app/server/.env.example index d009192d3c..8ec8f2c40b 100644 --- a/packages/apps/human-app/server/.env.example +++ b/packages/apps/human-app/server/.env.example @@ -18,7 +18,7 @@ HUMAN_APP_SECRET_KEY=sk_example_1VwUpBMO8H0v4Pmu4TPiWFEwuMguW4PkozSban4Rfbc # Redis REDIS_HOST=0.0.0.0 REDIS_PORT=6380 -REIDS_DB=1 +REDIS_DB=1 # Flush DB on startup IS_CACHE_TO_RESTART=false From 02b89ee470f5a1b33e538cf9b0a72230a342d2a2 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Wed, 29 Jul 2026 15:02:57 +0300 Subject: [PATCH 4/4] feat: oracle total tasks and rewards range --- .../jobs-discovery/jobs-discovery.service.ts | 2 +- .../model/oracle-discovery.model.ts | 112 ++++++++---------- .../oracle-discovery.controller.ts | 50 +++++++- .../oracle-discovery.module.ts | 3 +- .../spec/oracle-discovery.controller.spec.ts | 88 -------------- .../spec/oracle-discovery.fixture.ts | 8 +- 6 files changed, 103 insertions(+), 160 deletions(-) delete mode 100644 packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.controller.spec.ts diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts index 8cad1dc79c..83e5b194f8 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.service.ts @@ -92,7 +92,7 @@ export class JobsDiscoveryService { } static makeCacheKeyForOracle(oracleAddress: string): string { - return `${JOB_DISCOVERY_CACHE_KEY}:${oracleAddress}`; + return `${JOB_DISCOVERY_CACHE_KEY}:${oracleAddress.toLowerCase()}`; } async getCachedJobs(oracleAddress: string): Promise { diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/model/oracle-discovery.model.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/model/oracle-discovery.model.ts index e8914a8d7e..683fe9974f 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/model/oracle-discovery.model.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/model/oracle-discovery.model.ts @@ -2,7 +2,7 @@ import { ChainId } from '@human-protocol/sdk'; import { AutoMap } from '@automapper/classes'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional } from 'class-validator'; -import { Exclude, Transform } from 'class-transformer'; +import { Transform } from 'class-transformer'; type DiscoveredOracleCreateProps = { id: string; @@ -29,89 +29,28 @@ type DiscoveredOracleCreateProps = { }; export class DiscoveredOracle { - @ApiProperty({ description: 'Unique identifier of the oracle operator' }) id: string; - - @ApiProperty({ description: 'Address of the oracle operator' }) address: string; - - @ApiProperty({ description: 'Chain ID where the oracle is registered' }) chainId: ChainId; - - @ApiPropertyOptional({ description: 'Amount staked by the operator' }) stakedAmount?: string; - - @ApiPropertyOptional({ - description: 'Amount currently locked by the operator', - }) lockedAmount?: string; - - @ApiPropertyOptional({ description: 'Timestamp until funds are locked' }) lockedUntilTimestamp?: string; - - @ApiPropertyOptional({ - description: 'Total amount withdrawn by the operator', - }) withdrawnAmount?: string; - - @ApiPropertyOptional({ - description: 'Total amount slashed from the operator', - }) slashedAmount?: string; - - @ApiPropertyOptional({ - description: 'Number of jobs processed by the operator', - }) amountJobsProcessed?: string; - - @ApiPropertyOptional({ description: 'Fee charged by the operator' }) fee?: string; - - @ApiPropertyOptional({ description: 'Public key of the operator' }) publicKey?: string; - - @ApiPropertyOptional({ description: 'Webhook URL of the operator' }) webhookUrl?: string; - - @ApiPropertyOptional({ description: 'Website of the operator' }) website?: string; - - @ApiProperty({ description: 'URL of the oracle operator' }) url: string; - - @ApiProperty({ description: 'Role of the oracle operator' }) role: string; - - @ApiPropertyOptional({ - type: [String], - description: 'Types of jobs the oracle supports', - }) jobTypes: string[]; - - @ApiPropertyOptional({ description: 'Indicates if registration is needed' }) registrationNeeded?: boolean; - - @ApiPropertyOptional({ - description: 'Instructions for registration, if needed', - }) registrationInstructions?: string; - - @ApiPropertyOptional({ - type: [String], - description: 'Reputation networks the operator belongs to', - }) reputationNetworks?: string[]; - - @ApiProperty({ description: 'Name of the operator' }) name: string; - - @ApiPropertyOptional({ description: 'Category of the operator' }) category?: string; - - @Exclude() retriesCount = 0; - - @Exclude() executionsToSkip = 0; constructor(props: DiscoveredOracleCreateProps) { @@ -152,3 +91,52 @@ export class GetOraclesCommand { @Transform(({ value }) => (Array.isArray(value) ? value : [value])) selectedJobTypes?: string[]; } + +export class GetOraclesResponseItem { + @ApiProperty({ description: 'Unique identifier of the oracle operator' }) + id: string; + + @ApiProperty({ description: 'Address of the oracle operator' }) + address: string; + + @ApiProperty({ description: 'Chain ID where the oracle is registered' }) + chainId: ChainId; + + @ApiProperty({ description: 'Role of the oracle operator' }) + role: string; + + @ApiProperty({ description: 'URL of the oracle operator' }) + url: string; + + @ApiPropertyOptional({ + type: [String], + description: 'Types of jobs the oracle supports', + }) + jobTypes: string[]; + + @ApiPropertyOptional({ description: 'Indicates if registration is needed' }) + registrationNeeded?: boolean; + + @ApiPropertyOptional({ + description: 'Instructions for registration, if needed', + }) + registrationInstructions?: string; + + @ApiProperty({ description: 'Name of the operator' }) + name: string; + + @ApiPropertyOptional({ description: 'Category of the operator' }) + category?: string; + + @ApiProperty() + nTasks: number; + + @ApiProperty() + minRewardAmount: string; + + @ApiProperty() + maxRewardAmount: string; + + @ApiProperty() + rewardToken: string; +} diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts index 5adaa96875..02bfad380b 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts @@ -16,11 +16,12 @@ import { } from '@nestjs/swagger'; import { EnvironmentConfigService } from '../../common/config/environment-config.service'; import { - DiscoveredOracle, + GetOraclesResponseItem, GetOraclesCommand, GetOraclesQuery, } from './model/oracle-discovery.model'; import { OracleDiscoveryService } from './oracle-discovery.service'; +import { JobsDiscoveryService } from '../jobs-discovery/jobs-discovery.service'; @ApiTags('Oracle-Discovery') @ApiBearerAuth() @@ -29,19 +30,20 @@ export class OracleDiscoveryController { constructor( private readonly oracleDiscoveryService: OracleDiscoveryService, private readonly environmentConfigService: EnvironmentConfigService, + private readonly jobsDiscoveryService: JobsDiscoveryService, @InjectMapper() private readonly mapper: Mapper, ) {} @ApiOperation({ summary: 'Oracles discovery' }) @ApiOkResponse({ - type: Array, + type: Array, description: 'List of oracles', }) @Header('Cache-Control', 'private, max-age=60') @Get('/oracles') public async getOracles( @Query() query: GetOraclesQuery, - ): Promise { + ): Promise { if (!this.environmentConfigService.jobsDiscoveryFlag) { throw new HttpException( 'Oracles discovery is disabled', @@ -51,6 +53,46 @@ export class OracleDiscoveryController { const command = this.mapper.map(query, GetOraclesQuery, GetOraclesCommand); const oracles = await this.oracleDiscoveryService.getOracles(command); - return oracles; + const responseItems: GetOraclesResponseItem[] = []; + for (const oracle of oracles) { + const oracleJobs = await this.jobsDiscoveryService.getCachedJobs( + oracle.address, + ); + + let rewardToken: string; + let minRewardAmount: number; + let maxRewardAmount: number; + if (oracleJobs.length) { + rewardToken = oracleJobs[0].reward_token; + minRewardAmount = Number.MAX_VALUE; + maxRewardAmount = Number.MIN_VALUE; + + for (const oracleJob of oracleJobs) { + const jobRewardAmount = Number(oracleJob.reward_amount); + minRewardAmount = Math.min(minRewardAmount, jobRewardAmount); + maxRewardAmount = Math.max(maxRewardAmount, jobRewardAmount); + } + } else { + rewardToken = 'HMT'; + minRewardAmount = 0; + maxRewardAmount = 0; + } + + responseItems.push({ + id: oracle.id, + address: oracle.address, + chainId: oracle.chainId, + role: oracle.role, + url: oracle.url, + jobTypes: oracle.jobTypes, + name: oracle.name, + nTasks: oracleJobs.length, + minRewardAmount: minRewardAmount.toString(), + maxRewardAmount: maxRewardAmount.toString(), + rewardToken, + }); + } + + return responseItems; } } diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts index 67b8eb8f6d..6c470714b4 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { OracleDiscoveryService } from './oracle-discovery.service'; import { OracleDiscoveryProfile } from './oracle-discovery.mapper.profile'; import { KvStoreModule } from '../../integrations/kv-store/kv-store.module'; +import { JobsDiscoveryModule } from '../jobs-discovery/jobs-discovery.module'; @Module({ - imports: [KvStoreModule], + imports: [JobsDiscoveryModule, KvStoreModule], providers: [OracleDiscoveryService, OracleDiscoveryProfile], exports: [OracleDiscoveryService], }) diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.controller.spec.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.controller.spec.ts deleted file mode 100644 index 58df22e460..0000000000 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.controller.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AutomapperModule } from '@automapper/nestjs'; -import { classes } from '@automapper/classes'; -import { OracleDiscoveryController } from '../oracle-discovery.controller'; -import { OracleDiscoveryService } from '../oracle-discovery.service'; -import { oracleDiscoveryServiceMock } from './oracle-discovery.service.mock'; -import { - GetOraclesQuery, - DiscoveredOracle, -} from '../model/oracle-discovery.model'; -import { generateOracleDiscoveryResponseBody } from './oracle-discovery.fixture'; -import { OracleDiscoveryProfile } from '../oracle-discovery.mapper.profile'; -import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; -import { CommonConfigModule } from '../../../common/config/common-config.module'; -import { ConfigModule } from '@nestjs/config'; -import { HttpException, HttpStatus } from '@nestjs/common'; -import { ChainId } from '@human-protocol/sdk'; - -describe('OracleDiscoveryController', () => { - let controller: OracleDiscoveryController; - let serviceMock: OracleDiscoveryService; - const configServiceMock: Partial = { - cacheTtlOracleDiscovery: 600, - chainIdsEnabled: [ChainId.POLYGON, ChainId.MAINNET], - jobsDiscoveryFlag: true, - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [OracleDiscoveryController], - imports: [ - AutomapperModule.forRoot({ - strategyInitializer: classes(), - }), - CommonConfigModule, - ConfigModule.forRoot({ - envFilePath: '.env', - isGlobal: true, - }), - ], - providers: [ - OracleDiscoveryService, - OracleDiscoveryProfile, - { provide: EnvironmentConfigService, useValue: configServiceMock }, - ], - }) - .overrideProvider(OracleDiscoveryService) - .useValue(oracleDiscoveryServiceMock) - .compile(); - - controller = module.get( - OracleDiscoveryController, - ); - serviceMock = module.get(OracleDiscoveryService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('oracle discovery', () => { - it('should return discovered oracles', async () => { - const dtoFixture = { - selected_job_types: ['job-type-1', 'job-type-2'], - } as GetOraclesQuery; - const result: DiscoveredOracle[] = - await controller.getOracles(dtoFixture); - const expectedResponse = generateOracleDiscoveryResponseBody(); - expect(serviceMock.getOracles).toHaveBeenCalled(); - expect(result).toEqual(expectedResponse); - }); - - it('should throw an error if jobsDiscoveryFlag is disabled', async () => { - const dtoFixture = { - selected_job_types: ['job-type-1', 'job-type-2'], - } as GetOraclesQuery; - - (configServiceMock as any).jobsDiscoveryFlag = false; - - await expect(controller.getOracles(dtoFixture)).rejects.toThrow( - new HttpException( - 'Oracles discovery is disabled', - HttpStatus.FORBIDDEN, - ), - ); - }); - }); -}); diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.fixture.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.fixture.ts index de9ca168c8..b343545384 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.fixture.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/spec/oracle-discovery.fixture.ts @@ -4,7 +4,7 @@ import { DiscoveredOracle, } from '../model/oracle-discovery.model'; -export const response1: DiscoveredOracle = { +const response1: DiscoveredOracle = { id: '1', address: '0xd06eac24a0c47c776Ce6826A93162c4AfC029047', chainId: ChainId.POLYGON_AMOY, @@ -24,7 +24,7 @@ export const response1: DiscoveredOracle = { amountJobsProcessed: '0', }; -export const response2: DiscoveredOracle = { +const response2: DiscoveredOracle = { id: '2', address: '0xd10c3402155c058D78e4D5fB5f50E125F06eb39d', chainId: ChainId.POLYGON_AMOY, @@ -44,7 +44,7 @@ export const response2: DiscoveredOracle = { amountJobsProcessed: '0', }; -export const response3: DiscoveredOracle = { +const response3: DiscoveredOracle = { id: '3', address: '0xd83422155c058D78e4D5fB5f50E125F06eb39d', chainId: ChainId.POLYGON_AMOY, @@ -64,7 +64,7 @@ export const response3: DiscoveredOracle = { amountJobsProcessed: '0', }; -export const response4: DiscoveredOracle = { +const response4: DiscoveredOracle = { id: '4', address: '0xd83422155c058D78e4D5fB5f50E125F06eb39d', chainId: ChainId.BSC_TESTNET,