Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds filtering to oracle discovery #154

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@ export class JobAssignmentProfile extends AutomapperProfile {
);
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { IOperator } from '@human-protocol/sdk';
import { AutoMap } from '@automapper/classes';
import { ApiPropertyOptional } from '@nestjs/swagger';

export class OracleDiscoveryResponse implements IOperator {
address: string;
Expand All @@ -12,3 +14,12 @@ export class OracleDiscoveryResponse implements IOperator {
this.jobTypes = jobTypes;
}
}
export class OracleDiscoveryDto {
@AutoMap()
@ApiPropertyOptional()
selected_job_types?: string[];
}
export class OracleDiscoveryCommand {
@AutoMap()
selectedJobTypes?: string[];
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import { Controller, Get, UsePipes, ValidationPipe } from '@nestjs/common';
import {
Controller,
Get,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { OracleDiscoveryService } from './oracle-discovery.service';
import { OracleDiscoveryResponse } from './model/oracle-discovery.model';
import {
OracleDiscoveryCommand,
OracleDiscoveryDto,
OracleDiscoveryResponse,
} from './model/oracle-discovery.model';
import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core';

@Controller()
export class OracleDiscoveryController {
constructor(private readonly service: OracleDiscoveryService) {}
constructor(
private readonly service: OracleDiscoveryService,
@InjectMapper() private readonly mapper: Mapper,
) {}
@ApiTags('Oracle-Discovery')
@Get('/oracles')
@ApiOperation({ summary: 'Oracles discovery' })
@UsePipes(new ValidationPipe())
public getOracles(): Promise<OracleDiscoveryResponse[]> {
return this.service.processOracleDiscovery();
public getOracles(
@Query() dto?: OracleDiscoveryDto,
): Promise<OracleDiscoveryResponse[]> {
const command = this.mapper.map(
dto,
OracleDiscoveryDto,
OracleDiscoveryCommand,
);
return this.service.processOracleDiscovery(command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import {
CamelCaseNamingConvention,
createMap, forMember, mapFrom,
Mapper,
namingConventions,
SnakeCaseNamingConvention,
} from '@automapper/core';
import {
OracleDiscoveryCommand,
OracleDiscoveryDto,
} from './model/oracle-discovery.model';

@Injectable()
export class OracleDiscoveryProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}

override get profile() {
return (mapper: Mapper) => {
createMap(
mapper,
OracleDiscoveryDto,
OracleDiscoveryCommand,
forMember(
(destination) => destination.selectedJobTypes,
mapFrom((source) => source.selected_job_types),
),
);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { OracleDiscoveryService } from './oracle-discovery.service';
import { OracleDiscoveryProfile } from './oracle-discovery.mapper.profile';

@Module({
providers: [OracleDiscoveryService],
providers: [OracleDiscoveryService, OracleDiscoveryProfile],
exports: [OracleDiscoveryService],
})
export class OracleDiscoveryModule {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { OracleDiscoveryResponse } from './model/oracle-discovery.model';
import {
OracleDiscoveryCommand,
OracleDiscoveryResponse,
} from './model/oracle-discovery.model';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { OperatorUtils } from '@human-protocol/sdk';
Expand All @@ -14,34 +17,70 @@ export class OracleDiscoveryService {
private configService: EnvironmentConfigService,
) {}

async processOracleDiscovery(): Promise<OracleDiscoveryResponse[]> {
async processOracleDiscovery(
command: OracleDiscoveryCommand,
): Promise<OracleDiscoveryResponse[]> {
const address = this.configService.reputationOracleAddress;
const chainIds = this.configService.chainIdsEnabled;

const allData = await Promise.all(
const filteredOracles = await Promise.all(
chainIds.map(async (chainId) => {
let data: OracleDiscoveryResponse[] | undefined =
await this.cacheManager.get(chainId);
if (!data) {
try {
data = await OperatorUtils.getReputationNetworkOperators(
Number(chainId),
address,
this.EXCHANGE_ORACLE,
);
await this.cacheManager.set(
chainId,
data,
this.configService.cacheTtlOracleDiscovery,
);
} catch (error) {
this.logger.error(`Error processing chainId ${chainId}:`, error);
}
}
return data;
return this.findOraclesByChainId(chainId, address).then((oracles) =>
this.getOraclesWithSelectedJobTypes(
oracles,
command.selectedJobTypes,
),
);
}),
);

return allData.flat().filter(Boolean) as OracleDiscoveryResponse[];
return filteredOracles.flat().filter(Boolean) as OracleDiscoveryResponse[];
}
private async findOraclesByChainId(
chainId: string,
address: string,
): Promise<OracleDiscoveryResponse[] | undefined> {
let receivedOracles: OracleDiscoveryResponse[] | undefined =
await this.cacheManager.get(chainId);
if (!receivedOracles) {
try {
receivedOracles = await OperatorUtils.getReputationNetworkOperators(
Number(chainId),
address,
this.EXCHANGE_ORACLE,
);
await this.cacheManager.set(
chainId,
receivedOracles,
this.configService.cacheTtlOracleDiscovery,
);
} catch (error) {
this.logger.error(`Error processing chainId ${chainId}:`, error);
}
}
return receivedOracles;
}
private getOraclesWithSelectedJobTypes(
foundOracles: OracleDiscoveryResponse[] | undefined,
selectedJobTypes: string[] | undefined,
) {
if (
!selectedJobTypes ||
selectedJobTypes.length === 0 ||
!foundOracles ||
foundOracles.length === 0
) {
return foundOracles;
}
return foundOracles.filter((oracle) =>
oracle.jobTypes && oracle.jobTypes.length > 0
? this.areJobTypeSetsIntersect(oracle.jobTypes, selectedJobTypes)
: false,
);
}
private areJobTypeSetsIntersect(
oracleJobTypes: string[],
requiredJobTypes: string[],
) {
return oracleJobTypes.some((job) => requiredJobTypes.includes(job));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ 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 { OracleDiscoveryResponse } from '../model/oracle-discovery.model';
import {
OracleDiscoveryCommand,
OracleDiscoveryDto,
OracleDiscoveryResponse,
} from '../model/oracle-discovery.model';
import { generateOracleDiscoveryResponseBody } from './oracle-discovery.fixture';
import { OracleDiscoveryProfile } from '../oracle-discovery.mapper.profile';

describe('OracleDiscoveryController', () => {
let controller: OracleDiscoveryController;
let serviceMock: OracleDiscoveryService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -18,7 +24,7 @@ describe('OracleDiscoveryController', () => {
strategyInitializer: classes(),
}),
],
providers: [OracleDiscoveryService],
providers: [OracleDiscoveryService, OracleDiscoveryProfile],
})
.overrideProvider(OracleDiscoveryService)
.useValue(oracleDiscoveryServiceMock)
Expand All @@ -27,6 +33,7 @@ describe('OracleDiscoveryController', () => {
controller = module.get<OracleDiscoveryController>(
OracleDiscoveryController,
);
serviceMock = module.get<OracleDiscoveryService>(OracleDiscoveryService);
});

it('should be defined', () => {
Expand All @@ -35,8 +42,18 @@ describe('OracleDiscoveryController', () => {

describe('oracle discovery', () => {
it('oracle discovery should be return OracleDiscoveryData', async () => {
const result: OracleDiscoveryResponse[] = await controller.getOracles();
const dtoFixture = {
selected_job_types: ['job-type-1', 'job-type-2'],
} as OracleDiscoveryDto;
const commandFixture = {
selectedJobTypes: ['job-type-1', 'job-type-2'],
} as OracleDiscoveryCommand;
const result: OracleDiscoveryResponse[] =
await controller.getOracles(dtoFixture);
const expectedResponse = generateOracleDiscoveryResponseBody();
expect(serviceMock.processOracleDiscovery).toHaveBeenCalledWith(
commandFixture,
);
expect(result).toEqual(expectedResponse);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { OracleDiscoveryResponse } from '../model/oracle-discovery.model';
import {
OracleDiscoveryCommand,
OracleDiscoveryResponse,
} from '../model/oracle-discovery.model';

export function generateOracleDiscoveryResponseBody() {
const response1: OracleDiscoveryResponse = {
address: '0xd06eac24a0c47c776Ce6826A93162c4AfC029047',
role: 'role1',
jobTypes: ['job-type-3'],
};
const response2: OracleDiscoveryResponse = {
address: '0xd10c3402155c058D78e4D5fB5f50E125F06eb39d',
role: 'role2',
jobTypes: ['job-type-1', 'job-type-3', 'job-type-4'],
};
return [response1, response2];
}

export const filledCommandFixture = {
selectedJobTypes: ['job-type-1', 'job-type-2'],
} as OracleDiscoveryCommand;
export const emptyCommandFixture = {
selectedJobTypes: [],
} as OracleDiscoveryCommand;
export const notSetCommandFixture = {} as OracleDiscoveryCommand;
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { Cache } from 'cache-manager';
import { OracleDiscoveryService } from '../oracle-discovery.service';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { OperatorUtils } from '@human-protocol/sdk';
import { OracleDiscoveryResponse } from '../model/oracle-discovery.model';
import { OracleDiscoveryCommand, OracleDiscoveryResponse } from '../model/oracle-discovery.model';
import { EnvironmentConfigService } from '../../../common/config/environment-config.service';
import { CommonConfigModule } from '../../../common/config/common-config.module';
import { ConfigModule } from '@nestjs/config';
import {
emptyCommandFixture, filledCommandFixture,
generateOracleDiscoveryResponseBody,
notSetCommandFixture,
} from './oracle-discovery.fixture';

jest.mock('@human-protocol/sdk', () => ({
OperatorUtils: {
Expand Down Expand Up @@ -72,7 +77,8 @@ describe('OracleDiscoveryService', () => {
];
jest.spyOn(cacheManager, 'get').mockResolvedValue(mockData);

const result = await oracleDiscoveryService.processOracleDiscovery();
const result =
await oracleDiscoveryService.processOracleDiscovery(notSetCommandFixture);

expect(result).toEqual(mockData);
expect(OperatorUtils.getReputationNetworkOperators).not.toHaveBeenCalled();
Expand All @@ -89,21 +95,32 @@ describe('OracleDiscoveryService', () => {
.spyOn(OperatorUtils, 'getReputationNetworkOperators')
.mockResolvedValue(mockData);

const result = await oracleDiscoveryService.processOracleDiscovery();
const result =
await oracleDiscoveryService.processOracleDiscovery(emptyCommandFixture);

expect(result).toEqual(mockData);
EXPECTED_CHAIN_IDS.forEach((chainId) => {
expect(cacheManager.get).toHaveBeenCalledWith(chainId);
expect(cacheManager.set).toHaveBeenCalledWith(
chainId,
mockData,
TTL,
);
expect(cacheManager.set).toHaveBeenCalledWith(chainId, mockData, TTL);
expect(OperatorUtils.getReputationNetworkOperators).toHaveBeenCalledWith(
Number(chainId),
REPUTATION_ORACLE_ADDRESS,
EXCHANGE_ORACLE,
);
});
});
it('should filter responses if selectedJobTypes not empty', async () => {
const mockData: OracleDiscoveryResponse[] =
generateOracleDiscoveryResponseBody();

jest.spyOn(cacheManager, 'get').mockResolvedValue(undefined);
jest
.spyOn(OperatorUtils, 'getReputationNetworkOperators')
.mockResolvedValue(mockData);

const result =
await oracleDiscoveryService.processOracleDiscovery(filledCommandFixture);

expect(result).toEqual([mockData[1]]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
UserStatisticsDto,
UserStatisticsResponse,
} from './model/user-statistics.model';
import { Authorization, JwtPayload } from '../../common/config/params-decorators';
import {
Authorization,
JwtPayload,
} from '../../common/config/params-decorators';
import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core';
import { JwtUserData } from '../../common/utils/jwt-token.model';
Expand Down
Loading