diff --git a/packages/apps/job-launcher/server/src/common/config/env-schema.ts b/packages/apps/job-launcher/server/src/common/config/env-schema.ts index 1304d47ade..edd5b41a68 100644 --- a/packages/apps/job-launcher/server/src/common/config/env-schema.ts +++ b/packages/apps/job-launcher/server/src/common/config/env-schema.ts @@ -30,7 +30,6 @@ export const envValidator = Joi.object({ GAS_PRICE_MULTIPLIER: Joi.number(), APPROVE_AMOUNT_USD: Joi.number(), REPUTATION_ORACLE_ADDRESS: Joi.string().required(), - REPUTATION_ORACLES: Joi.string().required(), CVAT_EXCHANGE_ORACLE_ADDRESS: Joi.string().required(), CVAT_RECORDING_ORACLE_ADDRESS: Joi.string().required(), HCAPTCHA_ORACLE_ADDRESS: Joi.string().required(), diff --git a/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts b/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts index 9c5806f0f4..41011149fb 100644 --- a/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts +++ b/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts @@ -37,14 +37,6 @@ export class Web3ConfigService { return this.configService.getOrThrow('REPUTATION_ORACLE_ADDRESS'); } - /** - * List of reputation oracle addresses, typically comma-separated. - * Required - */ - get reputationOracles(): string { - return this.configService.getOrThrow('REPUTATION_ORACLES'); - } - /** * URI for the hCaptcha recording oracle service. * Required diff --git a/packages/apps/job-launcher/server/src/common/constants/errors.ts b/packages/apps/job-launcher/server/src/common/constants/errors.ts index 29c15d826f..e1e59e2a90 100644 --- a/packages/apps/job-launcher/server/src/common/constants/errors.ts +++ b/packages/apps/job-launcher/server/src/common/constants/errors.ts @@ -149,15 +149,6 @@ export enum ErrorWeb3 { ReputationOracleUrlNotSet = 'Reputation oracle URL not set', } -/** - * Represents error messages related to routing protocol. - */ -export enum ErrorRoutingProtocol { - ReputationOracleNotFound = 'The specified Reputation Oracle address is not found in the set of available oracles. Ensure the address is correct and check available oracles for this network.', - ExchangeOracleNotFound = 'The specified Exchange Oracle address is not found in the set of available oracles. Ensure the address is correct and part of the available oracle pool.', - RecordingOracleNotFound = 'The specified Recording Oracle address is not found in the set of available oracles. Ensure the address is correct and part of the available oracle pool.', -} - /** * Represents error messages related to send grid. */ diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts index 530b86e3e5..74d298b02a 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts @@ -47,7 +47,6 @@ import { PaymentRepository } from '../payment/payment.repository'; import { PaymentService } from '../payment/payment.service'; import { QualificationService } from '../qualification/qualification.service'; import { RateService } from '../rate/rate.service'; -import { RoutingProtocolService } from '../routing-protocol/routing-protocol.service'; import { StorageService } from '../storage/storage.service'; import { Web3Service } from '../web3/web3.service'; import { WebhookEntity } from '../webhook/webhook.entity'; @@ -124,10 +123,6 @@ describe('CronJobService', () => { { provide: PaymentService, useValue: createMock() }, { provide: WhitelistService, useValue: createMock() }, { provide: ConfigService, useValue: mockConfigService }, - { - provide: RoutingProtocolService, - useValue: createMock(), - }, { provide: WebhookRepository, useValue: createMock(), diff --git a/packages/apps/job-launcher/server/src/modules/job/job.controller.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.controller.spec.ts index bc2cb7a1df..f4820d64fd 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.controller.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.controller.spec.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@human-protocol/sdk'; import { Test, TestingModule } from '@nestjs/testing'; import { JobController } from './job.controller'; import { JobService } from './job.service'; @@ -84,6 +85,7 @@ describe('JobController', () => { describe('quickLaunch', () => { it('should create a job and return job ID', async () => { const jobDto: JobQuickLaunchDto = { + chainId: ChainId.POLYGON_AMOY, requestType: 'type_a' as JobRequestType, manifestUrl: MOCK_FILE_URL, manifestHash: MOCK_FILE_HASH, @@ -119,6 +121,7 @@ describe('JobController', () => { it('should throw a conflict error if mutex manager fails', async () => { const jobDto: JobQuickLaunchDto = { + chainId: ChainId.POLYGON_AMOY, requestType: 'type_a' as JobRequestType, manifestUrl: MOCK_FILE_URL, manifestHash: MOCK_FILE_HASH, @@ -159,6 +162,7 @@ describe('JobController', () => { it('should return unauthorized error if user is not authenticated', async () => { const jobDto: JobQuickLaunchDto = { + chainId: ChainId.POLYGON_AMOY, requestType: 'type_a' as JobRequestType, manifestUrl: MOCK_FILE_URL, manifestHash: MOCK_FILE_HASH, @@ -185,6 +189,7 @@ describe('JobController', () => { describe('createFortuneJob', () => { const jobFortuneDto: JobFortuneDto = { + chainId: ChainId.POLYGON_AMOY, requesterTitle: MOCK_REQUESTER_TITLE, requesterDescription: MOCK_REQUESTER_DESCRIPTION, submissionsRequired: 10, diff --git a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts index 919396f901..efd406286c 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.dto.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.dto.ts @@ -32,10 +32,9 @@ import { IsValidToken } from '../../common/validators/tokens'; import { ManifestDetails } from '../manifest/manifest.dto'; export class JobDto { - @ApiProperty({ enum: ChainId, required: false, name: 'chain_id' }) + @ApiProperty({ enum: ChainId, name: 'chain_id' }) @IsEnumCaseInsensitive(ChainId) - @IsOptional() - public chainId?: ChainId; + public chainId: ChainId; @ApiPropertyOptional() @IsArray() diff --git a/packages/apps/job-launcher/server/src/modules/job/job.module.ts b/packages/apps/job-launcher/server/src/modules/job/job.module.ts index 531a5e8beb..9ebe2c27ca 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.module.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.module.ts @@ -15,7 +15,6 @@ import { WebhookRepository } from '../webhook/webhook.repository'; import { MutexManagerService } from '../mutex/mutex-manager.service'; import { QualificationModule } from '../qualification/qualification.module'; import { WhitelistModule } from '../whitelist/whitelist.module'; -import { RoutingProtocolModule } from '../routing-protocol/routing-protocol.module'; import { RateModule } from '../rate/rate.module'; import { ManifestModule } from '../manifest/manifest.module'; @@ -29,7 +28,6 @@ import { ManifestModule } from '../manifest/manifest.module'; StorageModule, QualificationModule, WhitelistModule, - RoutingProtocolModule, RateModule, ManifestModule, ], diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts index 3d69a1c594..3b15ccc1f3 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts @@ -10,6 +10,7 @@ import { IEscrow, KVStoreUtils, NETWORKS, + Role, } from '@human-protocol/sdk'; import { Test } from '@nestjs/testing'; import { ethers, ZeroAddress } from 'ethers'; @@ -45,7 +46,6 @@ import { PaymentRepository } from '../payment/payment.repository'; import { PaymentService } from '../payment/payment.service'; import { QualificationService } from '../qualification/qualification.service'; import { RateService } from '../rate/rate.service'; -import { RoutingProtocolService } from '../routing-protocol/routing-protocol.service'; import { StorageService } from '../storage/storage.service'; import { createUser } from '../user/fixtures'; import { Web3Service } from '../web3/web3.service'; @@ -71,11 +71,14 @@ const mockPaymentRepository = createMock(); const mockStorageService = createMock(); const mockPaymentService = createMock(); const mockRateService = createMock(); -const mockRoutingProtocolService = createMock(); const mockManifestService = createMock(); const mockWhitelistService = createMock(); const mockWeb3ConfigService = { txTimeoutMs: faker.number.int({ min: 30000, max: 120000 }), + reputationOracleAddress: faker.finance.ethereumAddress(), + cvatExchangeOracleAddress: faker.finance.ethereumAddress(), + cvatRecordingOracleAddress: faker.finance.ethereumAddress(), + hCaptchaOracleAddress: faker.finance.ethereumAddress(), }; const mockedEscrowClient = jest.mocked(EscrowClient); @@ -116,10 +119,6 @@ describe('JobService', () => { { provide: PaymentService, useValue: mockPaymentService }, { provide: StorageService, useValue: mockStorageService }, { provide: WhitelistService, useValue: mockWhitelistService }, - { - provide: RoutingProtocolService, - useValue: mockRoutingProtocolService, - }, { provide: ManifestService, useValue: mockManifestService, @@ -188,14 +187,7 @@ describe('JobService', () => { expect(mockWeb3Service.validateChainId).toHaveBeenCalledWith( fortuneJobDto.chainId, ); - expect(mockRoutingProtocolService.selectOracles).not.toHaveBeenCalled(); - expect(mockRoutingProtocolService.validateOracles).toHaveBeenCalledWith( - fortuneJobDto.chainId, - FortuneJobType.FORTUNE, - fortuneJobDto.reputationOracle, - fortuneJobDto.exchangeOracle, - fortuneJobDto.recordingOracle, - ); + expect(mockWeb3Service.findAvailableOracles).not.toHaveBeenCalled(); expect(mockManifestService.createManifest).toHaveBeenCalledWith( fortuneJobDto, FortuneJobType.FORTUNE, @@ -288,14 +280,7 @@ describe('JobService', () => { expect(mockWeb3Service.validateChainId).toHaveBeenCalledWith( fortuneJobDto.chainId, ); - expect(mockRoutingProtocolService.selectOracles).not.toHaveBeenCalled(); - expect(mockRoutingProtocolService.validateOracles).toHaveBeenCalledWith( - fortuneJobDto.chainId, - FortuneJobType.FORTUNE, - fortuneJobDto.reputationOracle, - fortuneJobDto.exchangeOracle, - fortuneJobDto.recordingOracle, - ); + expect(mockWeb3Service.findAvailableOracles).not.toHaveBeenCalled(); expect(mockManifestService.createManifest).toHaveBeenCalledWith( fortuneJobDto, FortuneJobType.FORTUNE, @@ -379,13 +364,20 @@ describe('JobService', () => { const mockOracles = { recordingOracle: faker.finance.ethereumAddress(), exchangeOracle: faker.finance.ethereumAddress(), - reputationOracle: faker.finance.ethereumAddress(), + reputationOracle: mockWeb3ConfigService.reputationOracleAddress, }; - mockRoutingProtocolService.selectOracles.mockResolvedValueOnce({ - recordingOracle: mockOracles.recordingOracle, - exchangeOracle: mockOracles.exchangeOracle, - reputationOracle: mockOracles.reputationOracle, - }); + mockWeb3Service.findAvailableOracles.mockResolvedValueOnce([ + { + address: mockOracles.exchangeOracle, + role: Role.ExchangeOracle, + url: null, + }, + { + address: mockOracles.recordingOracle, + role: Role.RecordingOracle, + url: null, + }, + ]); mockedKVStoreUtils.get.mockResolvedValueOnce('1'); const result = await jobService.createJob( @@ -405,13 +397,11 @@ describe('JobService', () => { expect(mockWeb3Service.validateChainId).toHaveBeenCalledWith( fortuneJobDto.chainId, ); - expect(mockRoutingProtocolService.selectOracles).toHaveBeenCalledWith( + expect(mockWeb3Service.findAvailableOracles).toHaveBeenCalledWith( fortuneJobDto.chainId, FortuneJobType.FORTUNE, + mockOracles.reputationOracle, ); - expect( - mockRoutingProtocolService.validateOracles, - ).not.toHaveBeenCalled(); expect(mockManifestService.createManifest).toHaveBeenCalledWith( fortuneJobDto, FortuneJobType.FORTUNE, @@ -514,14 +504,7 @@ describe('JobService', () => { expect(mockWeb3Service.validateChainId).toHaveBeenCalledWith( jobQuickLaunchDto.chainId, ); - expect(mockRoutingProtocolService.selectOracles).not.toHaveBeenCalled(); - expect(mockRoutingProtocolService.validateOracles).toHaveBeenCalledWith( - jobQuickLaunchDto.chainId, - HCaptchaJobType.HCAPTCHA, - jobQuickLaunchDto.reputationOracle, - jobQuickLaunchDto.exchangeOracle, - jobQuickLaunchDto.recordingOracle, - ); + expect(mockWeb3Service.findAvailableOracles).not.toHaveBeenCalled(); expect(mockManifestService.createManifest).not.toHaveBeenCalled(); expect(mockManifestService.uploadManifest).not.toHaveBeenCalled(); expect(mockPaymentService.createWithdrawalPayment).toHaveBeenCalledWith( diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 385dd3961a..b10716443f 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -6,6 +6,7 @@ import { KVStoreKeys, KVStoreUtils, NETWORKS, + Role, } from '@human-protocol/sdk'; import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; @@ -61,7 +62,6 @@ import { ManifestService } from '../manifest/manifest.service'; import { PaymentService } from '../payment/payment.service'; import { QualificationService } from '../qualification/qualification.service'; import { RateService } from '../rate/rate.service'; -import { RoutingProtocolService } from '../routing-protocol/routing-protocol.service'; import { StorageService } from '../storage/storage.service'; import { UserEntity } from '../user/user.entity'; import { Web3Service } from '../web3/web3.service'; @@ -94,7 +94,6 @@ export class JobService { private readonly webhookRepository: WebhookRepository, private readonly paymentService: PaymentService, private readonly serverConfigService: ServerConfigService, - private readonly routingProtocolService: RoutingProtocolService, private readonly storageService: StorageService, private readonly rateService: RateService, private readonly whitelistService: WhitelistService, @@ -124,10 +123,8 @@ export class JobService { throw new ValidationError(ErrorPayment.HMTTokenDisabled); } - let { chainId, reputationOracle, exchangeOracle, recordingOracle } = dto; - - // Select network - chainId = chainId || this.routingProtocolService.selectNetwork(); + const { chainId } = dto; + let { reputationOracle, exchangeOracle, recordingOracle } = dto; this.web3Service.validateChainId(chainId); // Check if not whitelisted user has an active payment method @@ -197,25 +194,11 @@ export class JobService { ).toFixed(fundTokenDecimals), ); - // Select oracles if (!reputationOracle || !exchangeOracle || !recordingOracle) { - const selectedOracles = await this.routingProtocolService.selectOracles( - chainId, - requestType, - ); - - exchangeOracle = exchangeOracle || selectedOracles.exchangeOracle; - recordingOracle = recordingOracle || selectedOracles.recordingOracle; - reputationOracle = reputationOracle || selectedOracles.reputationOracle; - } else { - // Validate if all oracles are provided - await this.routingProtocolService.validateOracles( - chainId, - requestType, - reputationOracle, - exchangeOracle, - recordingOracle, - ); + const defaultOracles = await this.getDefaultOracles(chainId, requestType); + reputationOracle = reputationOracle ?? defaultOracles.reputationOracle; + exchangeOracle = exchangeOracle ?? defaultOracles.exchangeOracle; + recordingOracle = recordingOracle ?? defaultOracles.recordingOracle; } if (dto.qualifications) { @@ -292,6 +275,49 @@ export class JobService { return jobEntity.id; } + private async getDefaultOracles( + chainId: ChainId, + requestType: JobRequestType, + ): Promise<{ + reputationOracle: string; + exchangeOracle: string; + recordingOracle: string; + }> { + if (requestType === HCaptchaJobType.HCAPTCHA) { + const oracleAddress = this.web3ConfigService.hCaptchaOracleAddress; + return { + reputationOracle: oracleAddress, + exchangeOracle: oracleAddress, + recordingOracle: oracleAddress, + }; + } + + if (Object.values(CvatJobType).includes(requestType as CvatJobType)) { + return { + reputationOracle: this.web3ConfigService.reputationOracleAddress, + exchangeOracle: this.web3ConfigService.cvatExchangeOracleAddress, + recordingOracle: this.web3ConfigService.cvatRecordingOracleAddress, + }; + } + + const reputationOracle = this.web3ConfigService.reputationOracleAddress; + const availableOracles = await this.web3Service.findAvailableOracles( + chainId, + requestType, + reputationOracle, + ); + + return { + reputationOracle, + exchangeOracle: + availableOracles.find((oracle) => oracle.role === Role.ExchangeOracle) + ?.address || '', + recordingOracle: + availableOracles.find((oracle) => oracle.role === Role.RecordingOracle) + ?.address || '', + }; + } + public async createEscrow(jobEntity: JobEntity): Promise { const signer = this.web3Service.getSigner(jobEntity.chainId); diff --git a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts index 21e93dfa4e..747e5c79ee 100644 --- a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts @@ -40,6 +40,7 @@ describe('ManifestService', () => { describe('createManifest', () => { it('should create a fortune manifest', async () => { const dto: JobFortuneDto = { + chainId: faker.number.int({ min: 1, max: 100 }), requesterTitle: faker.lorem.sentence(), requesterDescription: faker.lorem.sentence(), submissionsRequired: faker.number.int({ min: 1, max: 100 }), diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.interface.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.interface.ts deleted file mode 100644 index 5ccc44a7ee..0000000000 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.interface.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface OracleOrder { - [chainId: number]: { - [reputationOracle: string]: { - [oracleType: string]: { - [jobType: string]: string[]; - }; - }; - }; -} - -export interface OracleIndex { - [chainId: number]: { - [reputationOracle: string]: { - [oracleType: string]: { - [jobType: string]: number; - }; - }; - }; -} - -export interface OracleHash { - [chainId: number]: { - [reputationOracle: string]: { - [oracleType: string]: { - [jobType: string]: string; - }; - }; - }; -} diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.module.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.module.ts deleted file mode 100644 index 1ae2b644ba..0000000000 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { RoutingProtocolService } from './routing-protocol.service'; -import { Web3Module } from '../web3/web3.module'; -import { ConfigModule } from '@nestjs/config'; - -@Module({ - imports: [Web3Module, ConfigModule], - providers: [RoutingProtocolService], - exports: [RoutingProtocolService], -}) -export class RoutingProtocolModule {} diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts deleted file mode 100644 index 01d3e64cbd..0000000000 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts +++ /dev/null @@ -1,498 +0,0 @@ -jest.mock('../../common/utils', () => ({ - ...jest.requireActual('../../common/utils'), - hashString: jest.fn(), -})); - -jest.mock('@human-protocol/sdk', () => ({ - ...jest.requireActual('@human-protocol/sdk'), - EscrowClient: { - build: jest.fn().mockImplementation(() => ({})), - }, -})); - -import { ChainId, Role } from '@human-protocol/sdk'; -import { ConfigService } from '@nestjs/config'; -import { Test } from '@nestjs/testing'; -import { MOCK_REPUTATION_ORACLE_1, mockConfig } from '../../../test/constants'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ErrorRoutingProtocol } from '../../common/constants/errors'; -import { FortuneJobType } from '../../common/enums/job'; -import { ServerError } from '../../common/errors'; -import { hashString } from '../../common/utils'; -import { Web3Service } from '../web3/web3.service'; -import { RoutingProtocolService } from './routing-protocol.service'; - -describe('RoutingProtocolService', () => { - let web3Service: Web3Service; - let routingProtocolService: RoutingProtocolService; - - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - { - provide: ConfigService, - useValue: { - get: jest.fn((key: string) => mockConfig[key]), - getOrThrow: jest.fn((key: string) => { - if (!mockConfig[key]) { - throw new Error(`Configuration key "${key}" does not exist`); - } - return mockConfig[key]; - }), - }, - }, - Web3ConfigService, - NetworkConfigService, - { - provide: Web3Service, - useValue: { - findAvailableOracles: jest.fn(), - }, - }, - RoutingProtocolService, - ], - }).compile(); - - web3Service = moduleRef.get(Web3Service); - routingProtocolService = moduleRef.get(RoutingProtocolService); - }); - - describe('constructor', () => { - it('should initialize chains and reputation oracles from config', () => { - const chains = routingProtocolService['chains']; - const reputationOracles = routingProtocolService['reputationOracles']; - - expect(chains).toHaveLength(routingProtocolService['chains'].length); - expect(reputationOracles).toEqual( - mockConfig['REPUTATION_ORACLES'] - .split(',') - .map((address: string) => address.trim()), - ); - }); - - it('should shuffle chains and reputation oracles', () => { - const chainPriorityOrder = routingProtocolService['chainPriorityOrder']; - const reputationOraclePriorityOrder = - routingProtocolService['reputationOraclePriorityOrder']; - - expect(chainPriorityOrder).toHaveLength( - routingProtocolService['chains'].length, - ); - expect(reputationOraclePriorityOrder).toHaveLength( - routingProtocolService['reputationOracles'].length, - ); - }); - }); - - describe('selectNetwork', () => { - it('should select a network in a random order', () => { - const chainIds = []; - for (let i = 0; i < routingProtocolService['chains'].length; i++) { - chainIds.push(routingProtocolService.selectNetwork()); - } - expect(chainIds).toHaveLength(routingProtocolService['chains'].length); - }); - - it('should cycle back to the first network after cycling through all', () => { - const firstCycle = routingProtocolService.selectNetwork(); - const chainLength = routingProtocolService['chains'].length; - - for (let i = 1; i < chainLength; i++) { - routingProtocolService.selectNetwork(); - } - - const secondCycle = routingProtocolService.selectNetwork(); - expect(firstCycle).toBe(secondCycle); - }); - }); - - describe('selectReputationOracle', () => { - it('should select a reputation oracle in shuffled order', () => { - const selectedOracles = []; - const oracleLength = routingProtocolService['reputationOracles'].length; - - for (let i = 0; i < oracleLength; i++) { - selectedOracles.push(routingProtocolService.selectReputationOracle()); - } - - expect(selectedOracles).toHaveLength(oracleLength); - expect(new Set(selectedOracles).size).toBe(oracleLength); // Ensure all oracles are unique - }); - - it('should cycle back to the first reputation oracle after cycling through all', () => { - const firstCycle = routingProtocolService.selectReputationOracle(); - const oracleLength = routingProtocolService['reputationOracles'].length; - - for (let i = 1; i < oracleLength; i++) { - routingProtocolService.selectReputationOracle(); - } - - const secondCycle = routingProtocolService.selectReputationOracle(); - expect(firstCycle).toBe(secondCycle); - }); - }); - - describe('selectOracleFromAvailable', () => { - it('should return empty string if no oracles of the specified type are available', () => { - const result = routingProtocolService.selectOracleFromAvailable( - [], - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - expect(result).toBe(''); - }); - - it('should select the first available oracle of specified type', async () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { role: Role.ExchangeOracle, address: '0xExchangeOracle2', url: null }, - ]; - - const result = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - expect(result).toEqual(expect.stringContaining('0xExchangeOracle')); // 0xExchangeOraclex; - }); - - it('should shuffle oracles and return the first oracle from the shuffled list', () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { role: Role.ExchangeOracle, address: '0xExchangeOracle2', url: null }, - ]; - - jest - .spyOn(routingProtocolService, 'shuffleArray') - .mockReturnValue(['0xExchangeOracle2', '0xExchangeOracle1']); - - const result = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - expect(result).toBe('0xExchangeOracle2'); - }); - - it('should update oracle order and select from the newly shuffled list if jobType changes', () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { role: Role.ExchangeOracle, address: '0xExchangeOracle2', url: null }, - ]; - - routingProtocolService.oracleOrder = { - [ChainId.POLYGON_AMOY]: { - [MOCK_REPUTATION_ORACLE_1]: { - [Role.ExchangeOracle]: { - oldJobType: ['0xExchangeOracle1'], - }, - }, - }, - }; - - const result = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'newJobType', - ); - - // The jobType changed, so the order should have been shuffled - expect( - routingProtocolService.oracleOrder[ChainId.POLYGON_AMOY][ - MOCK_REPUTATION_ORACLE_1 - ][Role.ExchangeOracle]['newJobType'], - ).toEqual( - expect.arrayContaining(['0xExchangeOracle1', '0xExchangeOracle2']), - ); - expect(result).toBeDefined(); - }); - - it('should not shuffle if the oracle hash has not changed for the same jobType', () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - ]; - - const latestOraclesHash = 'hash123'; - (hashString as jest.Mock).mockReturnValue(latestOraclesHash); - - routingProtocolService.oracleOrder = { - [ChainId.POLYGON_AMOY]: { - [MOCK_REPUTATION_ORACLE_1]: { - [Role.ExchangeOracle]: { jobType: ['0xExchangeOracle1'] }, - }, - }, - }; - - routingProtocolService.oracleHashes = { - [ChainId.POLYGON_AMOY]: { - [MOCK_REPUTATION_ORACLE_1]: { - [Role.ExchangeOracle]: { jobType: latestOraclesHash }, - }, - }, - }; - - jest.spyOn(routingProtocolService, 'shuffleArray'); - - const result = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - // Shuffle should not be called if the oracle hash is unchanged - expect(routingProtocolService.shuffleArray).not.toHaveBeenCalled(); - expect(result).toBe('0xExchangeOracle1'); - }); - - it('should update the oracle order and hash if the list of available oracles changes', () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { role: Role.ExchangeOracle, address: '0xExchangeOracle2', url: null }, - ]; - - const previousHash = 'oldHash'; - routingProtocolService.oracleHashes = { - [ChainId.POLYGON_AMOY]: { - [MOCK_REPUTATION_ORACLE_1]: { - [Role.ExchangeOracle]: { jobType: previousHash }, - }, - }, - }; - - jest - .spyOn(routingProtocolService, 'shuffleArray') - .mockReturnValue(availableOracles.map((oracle) => oracle.address)); - const latestOraclesHash = 'newHash'; - (hashString as jest.Mock).mockReturnValue(latestOraclesHash); - - const result = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - expect( - routingProtocolService.oracleHashes[ChainId.POLYGON_AMOY][ - MOCK_REPUTATION_ORACLE_1 - ][Role.ExchangeOracle]['jobType'], - ).toBe(latestOraclesHash); - expect(result).toBe('0xExchangeOracle1'); - }); - - it('should select the oracle from available ones and rotate index', async () => { - const availableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { role: Role.ExchangeOracle, address: '0xExchangeOracle2', url: null }, - { - role: Role.RecordingOracle, - address: '0xRecordingOracle1', - url: null, - }, - { - role: Role.RecordingOracle, - address: '0xRecordingOracle2', - url: null, - }, - ]; - - const result1 = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - const result2 = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.RecordingOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - const result3 = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - const result4 = routingProtocolService.selectOracleFromAvailable( - availableOracles, - Role.RecordingOracle, - ChainId.POLYGON_AMOY, - MOCK_REPUTATION_ORACLE_1, - 'jobType', - ); - - expect(result1).toEqual(expect.stringContaining('0xExchangeOracle')); // 0xExchangeOraclex; - expect(result2).toEqual(expect.stringContaining('0xRecordingOracle')); // 0xRecordingOraclex - expect(result3).toEqual(expect.stringContaining('0xExchangeOracle')); // 0xExchangeOraclex; - expect(result4).toEqual(expect.stringContaining('0xRecordingOracle')); // 0xRecordingOraclex - }); - }); - - describe('selectOracles', () => { - it('should select reputation oracle and find available oracles', async () => { - const mockAvailableOracles = [ - { role: Role.ExchangeOracle, address: '0xExchangeOracle1', url: null }, - { - role: Role.RecordingOracle, - address: '0xRecordingOracle1', - url: null, - }, - ]; - - web3Service.findAvailableOracles = jest - .fn() - .mockResolvedValue(mockAvailableOracles); - - const result = await routingProtocolService.selectOracles( - ChainId.POLYGON_AMOY, - FortuneJobType.FORTUNE, - ); - expect(result.reputationOracle).toBeDefined(); - expect(result.exchangeOracle).toBe('0xExchangeOracle1'); - expect(result.recordingOracle).toBe('0xRecordingOracle1'); - }); - - it('should return null for exchange and recording oracles if none available', async () => { - web3Service.findAvailableOracles = jest.fn().mockResolvedValue([]); - - const result = await routingProtocolService.selectOracles( - ChainId.POLYGON_AMOY, - FortuneJobType.FORTUNE, - ); - expect(result.exchangeOracle).toBe(''); - expect(result.recordingOracle).toBe(''); - }); - }); - - describe('validateOracles', () => { - it('should validate oracles successfully', async () => { - const chainId = ChainId.POLYGON_AMOY; - const reputationOracle = '0xReputationOracle'; - const exchangeOracle = '0xExchangeOracle'; - const recordingOracle = '0xRecordingOracle'; - - jest - .spyOn( - routingProtocolService.web3ConfigService, - 'reputationOracles', - 'get', - ) - .mockReturnValue(`${reputationOracle},otherOracle`); - jest.spyOn(web3Service, 'findAvailableOracles').mockResolvedValue([ - { address: exchangeOracle, role: Role.ExchangeOracle, url: null }, - { address: recordingOracle, role: Role.RecordingOracle, url: null }, - ]); - - await expect( - routingProtocolService.validateOracles( - chainId, - FortuneJobType.FORTUNE, - reputationOracle, - exchangeOracle, - recordingOracle, - ), - ).resolves.not.toThrow(); - }); - - it('should throw error if reputation oracle not found', async () => { - const chainId = ChainId.POLYGON_AMOY; - const invalidReputationOracle = 'invalidOracle'; - - jest - .spyOn( - routingProtocolService.web3ConfigService, - 'reputationOracles', - 'get', - ) - .mockReturnValue('validReputationOracle,otherOracle'); - - await expect( - routingProtocolService.validateOracles( - chainId, - FortuneJobType.FORTUNE, - invalidReputationOracle, - ), - ).rejects.toThrow( - new ServerError(ErrorRoutingProtocol.ReputationOracleNotFound), - ); - }); - - it('should throw error if exchange oracle not found', async () => { - const chainId = ChainId.POLYGON_AMOY; - const reputationOracle = '0xReputationOracle'; - - jest - .spyOn( - routingProtocolService.web3ConfigService, - 'reputationOracles', - 'get', - ) - .mockReturnValue(reputationOracle); - jest - .spyOn(web3Service, 'findAvailableOracles') - .mockResolvedValue([ - { address: 'anotherOracle', role: Role.ExchangeOracle, url: null }, - ]); - - await expect( - routingProtocolService.validateOracles( - chainId, - FortuneJobType.FORTUNE, - reputationOracle, - 'invalidExchangeOracle', - ), - ).rejects.toThrow( - new ServerError(ErrorRoutingProtocol.ExchangeOracleNotFound), - ); - }); - - it('should throw error if recording oracle not found', async () => { - const chainId = ChainId.POLYGON_AMOY; - const reputationOracle = '0xReputationOracle'; - - jest - .spyOn( - routingProtocolService.web3ConfigService, - 'reputationOracles', - 'get', - ) - .mockReturnValue(reputationOracle); - jest - .spyOn(web3Service, 'findAvailableOracles') - .mockResolvedValue([ - { address: 'anotherOracle', role: Role.RecordingOracle, url: null }, - ]); - - await expect( - routingProtocolService.validateOracles( - chainId, - FortuneJobType.FORTUNE, - reputationOracle, - undefined, - 'invalidRecordingOracle', - ), - ).rejects.toThrow( - new ServerError(ErrorRoutingProtocol.RecordingOracleNotFound), - ); - }); - }); -}); diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts deleted file mode 100644 index b08e1f6819..0000000000 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { ChainId, Role } from '@human-protocol/sdk'; -import { Injectable } from '@nestjs/common'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ErrorRoutingProtocol } from '../../common/constants/errors'; -import { - CvatJobType, - HCaptchaJobType, - JobRequestType, -} from '../../common/enums/job'; -import { ServerError } from '../../common/errors'; -import { hashString } from '../../common/utils'; -import { Web3Service } from '../web3/web3.service'; -import { - OracleHash, - OracleIndex, - OracleOrder, -} from './routing-protocol.interface'; -import { OracleDataDto } from '../web3/web3.dto'; - -type OracleValue = { - [reputationOracle: string]: { - [oracleType: string]: { [jobType: string]: T }; - }; -}; - -@Injectable() -export class RoutingProtocolService { - private readonly chains: ChainId[]; - private readonly reputationOracles: string[]; - private readonly chainPriorityOrder: number[]; - private readonly reputationOraclePriorityOrder: number[]; - private chainCurrentIndex = 0; - private reputationOracleIndex = 0; - private oracleIndexes: OracleIndex = {}; - public oracleHashes: OracleHash = {}; - public oracleOrder: OracleOrder = {}; - - constructor( - public readonly web3Service: Web3Service, - public readonly web3ConfigService: Web3ConfigService, - private readonly networkConfigService: NetworkConfigService, - ) { - this.chains = this.networkConfigService.networks.map( - (network) => network.chainId, - ); - this.reputationOracles = this.web3ConfigService.reputationOracles - .split(',') - .map((address) => address.trim()); - - this.chainPriorityOrder = this.shuffleArray(this.chains); - this.reputationOraclePriorityOrder = this.shuffleArray( - this.reputationOracles.map((_, i) => i), - ); - - this.oracleOrder = this.createOracleStructure( - this.chains, - this.reputationOracles, - ); - this.oracleIndexes = this.createOracleStructure( - this.chains, - this.reputationOracles, - ); - this.oracleHashes = this.createOracleStructure( - this.chains, - this.reputationOracles, - ); - } - - private createOracleStructure( - chains: ChainId[], - reputationOracles: string[], - ): { [chainId: string]: OracleValue } { - return chains.reduce( - (acc: { [chainId: string]: OracleValue }, chainId) => { - acc[chainId] = reputationOracles.reduce( - (oracleAcc: OracleValue, reputationOracle) => { - oracleAcc[reputationOracle] = { - [Role.ExchangeOracle]: {}, - [Role.RecordingOracle]: {}, - }; - return oracleAcc; - }, - {} as OracleValue, - ); - return acc; - }, - {} as { [chainId: string]: OracleValue }, - ); - } - - public shuffleArray(array: T[]): T[] { - return array.sort(() => Math.random() - 0.5); - } - - public selectNetwork(): ChainId { - const chainId = - this.chains[this.chainPriorityOrder[this.chainCurrentIndex]]; - this.chainCurrentIndex = (this.chainCurrentIndex + 1) % this.chains.length; - return chainId; - } - - public selectReputationOracle(): string { - const reputationOracle = - this.reputationOracles[ - this.reputationOraclePriorityOrder[this.reputationOracleIndex] - ]; - - this.reputationOracleIndex = - (this.reputationOracleIndex + 1) % this.reputationOracles.length; - return reputationOracle; - } - - public selectOracleFromAvailable( - availableOracles: OracleDataDto[], - oracleType: string, - chainId: ChainId, - reputationOracle: string, - jobType: string, - ): string { - const oraclesOfType = availableOracles - .filter((oracle) => oracle.role === oracleType) - .map((oracle) => oracle.address); - - if (!oraclesOfType.length) return ''; - - const latestOraclesHash = hashString( - JSON.stringify(availableOracles, (_, value) => - typeof value === 'bigint' ? value.toString() : value, - ), - ); - - if ( - !this.oracleOrder[chainId][reputationOracle][oracleType][jobType] || - this.oracleHashes[chainId][reputationOracle][oracleType][jobType] !== - latestOraclesHash - ) { - this.oracleHashes[chainId][reputationOracle][oracleType][jobType] = - latestOraclesHash; - - const shuffledOracles = this.shuffleArray(oraclesOfType); - this.oracleOrder[chainId][reputationOracle][oracleType][jobType] = - shuffledOracles; - this.oracleIndexes[chainId][reputationOracle][oracleType][jobType] = 0; - } - - const orderedOracles = - this.oracleOrder[chainId][reputationOracle][oracleType][jobType]; - const currentIndex = - this.oracleIndexes[chainId][reputationOracle][oracleType][jobType] || 0; - const selectedOracle = orderedOracles[currentIndex]; - - this.oracleIndexes[chainId][reputationOracle][oracleType][jobType] = - (currentIndex + 1) % orderedOracles.length; - return selectedOracle; - } - - public async selectOracles( - chainId: ChainId, - jobType: JobRequestType, - ): Promise<{ - reputationOracle: string; - exchangeOracle: string; - recordingOracle: string; - }> { - if (jobType === HCaptchaJobType.HCAPTCHA) { - return { - reputationOracle: this.web3ConfigService.hCaptchaOracleAddress, - exchangeOracle: this.web3ConfigService.hCaptchaOracleAddress, - recordingOracle: this.web3ConfigService.hCaptchaOracleAddress, - }; - } else if (Object.values(CvatJobType).includes(jobType as CvatJobType)) { - return { - reputationOracle: this.web3ConfigService.reputationOracleAddress, - exchangeOracle: this.web3ConfigService.cvatExchangeOracleAddress, - recordingOracle: this.web3ConfigService.cvatRecordingOracleAddress, - }; - } - - const reputationOracle = this.selectReputationOracle(); - const availableOracles = await this.web3Service.findAvailableOracles( - chainId, - jobType, - reputationOracle, - ); - - const exchangeOracle = this.selectOracleFromAvailable( - availableOracles, - Role.ExchangeOracle, - chainId, - reputationOracle, - jobType, - ); - const recordingOracle = this.selectOracleFromAvailable( - availableOracles, - Role.RecordingOracle, - chainId, - reputationOracle, - jobType, - ); - - return { reputationOracle, exchangeOracle, recordingOracle }; - } - - public async validateOracles( - chainId: ChainId, - jobType: JobRequestType, - reputationOracle: string, - exchangeOracle?: string | null, - recordingOracle?: string | null, - ) { - const reputationOracles = this.web3ConfigService.reputationOracles - .split(',') - .map((address) => address.trim()); - - if (!reputationOracles.includes(reputationOracle)) { - throw new ServerError(ErrorRoutingProtocol.ReputationOracleNotFound); - } - - const availableOracles = await this.web3Service.findAvailableOracles( - chainId, - jobType, - reputationOracle, - ); - - if ( - exchangeOracle && - !this.isOracleAvailable( - availableOracles, - exchangeOracle, - Role.ExchangeOracle, - ) - ) { - throw new ServerError(ErrorRoutingProtocol.ExchangeOracleNotFound); - } - - if ( - recordingOracle && - !this.isOracleAvailable( - availableOracles, - recordingOracle, - Role.RecordingOracle, - ) - ) { - throw new ServerError(ErrorRoutingProtocol.RecordingOracleNotFound); - } - } - - private isOracleAvailable( - availableOracles: any[], - oracle: string, - role: string, - ): boolean { - return availableOracles.some( - (o) => - o.address.toLowerCase() === oracle.toLowerCase() && o.role === role, - ); - } -} diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts index dcaaadd94b..d0b7bc5688 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts @@ -15,7 +15,6 @@ import { MOCK_ADDRESS, MOCK_EXCHANGE_ORACLE_URL, MOCK_RECORDING_ORACLE_URL, - MOCK_REPUTATION_ORACLES, mockConfig, } from './../../../test/constants'; import { RateService } from '../rate/rate.service'; @@ -359,12 +358,6 @@ describe('Web3Service', () => { }); describe('getReputationOraclesByJobType', () => { - beforeEach(async () => { - jest - .spyOn(web3Service.web3ConfigService, 'reputationOracles', 'get') - .mockReturnValue(MOCK_REPUTATION_ORACLES); - }); - afterEach(() => { jest.clearAllMocks(); }); @@ -494,20 +487,6 @@ describe('Web3Service', () => { expect(result).toEqual([]); expect(OperatorUtils.getOperator).toHaveBeenCalledTimes(1); }); - - it('should return an empty array if no reputation oracles are configured', async () => { - jest - .spyOn(web3Service.web3ConfigService, 'reputationOracles', 'get') - .mockReturnValue(''); - - const result = await web3Service.getReputationOraclesByJobType( - ChainId.POLYGON_AMOY, - 'Points', - ); - - expect(result).toEqual([]); - expect(OperatorUtils.getOperator).toHaveBeenCalledTimes(1); - }); }); describe('ensureEscrowAllowance', () => { diff --git a/packages/apps/job-launcher/server/test/constants.ts b/packages/apps/job-launcher/server/test/constants.ts index 438e96be3b..46608d4029 100644 --- a/packages/apps/job-launcher/server/test/constants.ts +++ b/packages/apps/job-launcher/server/test/constants.ts @@ -9,10 +9,6 @@ export const MOCK_FILE_URL = 'http://mockedFileUrl.test/bucket/file.json'; export const MOCK_FILE_HASH = 'mockedFileHash'; export const MOCK_PRIVATE_KEY = 'd334daf65a631f40549cc7de126d5a0016f32a2d00c49f94563f9737f7135e55'; -export const MOCK_REPUTATION_ORACLES = - '0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,0x0000000000000000000000000000000000000003'; -export const MOCK_REPUTATION_ORACLE_1 = - '0x0000000000000000000000000000000000000001'; export const MOCK_WEB3_RPC_URL = 'http://localhost:8545'; export const MOCK_EXCHANGE_ORACLE_ADDRESS = '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e'; @@ -117,7 +113,6 @@ export const mockConfig: any = { SENDGRID_API_KEY: MOCK_SENDGRID_API_KEY, SENDGRID_FROM_EMAIL: MOCK_SENDGRID_FROM_EMAIL, SENDGRID_FROM_NAME: MOCK_SENDGRID_FROM_NAME, - REPUTATION_ORACLES: MOCK_REPUTATION_ORACLES, WEB3_ENV: Web3Env.TESTNET, FE_URL: MOCK_FE_URL, };