From bc7b7a150d2ac402bcb9ca9049e25b9711c19d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Fri, 29 Sep 2023 17:38:55 +0200 Subject: [PATCH 1/3] Upload file to Exchange Oracle bucket --- .../exchange-oracle/server/.env.example | 18 +++++ .../fortune/exchange-oracle/server/.gitignore | 2 +- .../exchange-oracle/server/docker-compose.yml | 32 +++++++++ .../server/src/common/config/env.ts | 13 ++++ .../server/src/common/config/index.ts | 1 + .../server/src/common/config/s3.ts | 13 ++++ .../server/src/common/interfaces/job.ts | 5 ++ .../server/src/common/utils/storage.ts | 58 +++++++++++++++ .../exchange-oracle/server/src/main.ts | 4 +- .../src/modules/job/job.controller.spec.ts | 27 +++++++ .../server/src/modules/job/job.module.ts | 8 ++- .../src/modules/job/job.service.spec.ts | 72 +++++++++++++++---- .../server/src/modules/job/job.service.ts | 46 ++++++------ .../exchange-oracle/server/test/constants.ts | 8 +++ 14 files changed, 271 insertions(+), 36 deletions(-) create mode 100644 packages/apps/fortune/exchange-oracle/server/.env.example create mode 100644 packages/apps/fortune/exchange-oracle/server/docker-compose.yml create mode 100644 packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts create mode 100644 packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts create mode 100644 packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts diff --git a/packages/apps/fortune/exchange-oracle/server/.env.example b/packages/apps/fortune/exchange-oracle/server/.env.example new file mode 100644 index 0000000000..546787d6c2 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/.env.example @@ -0,0 +1,18 @@ +# General +NODE_ENV=development +PORT= + +S3_ENDPOINT= +S3_PORT= +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_REGION= + +WEB3_PRIVATE_KEY= + +# Reputation Level +REPUTATION_LEVEL_LOW= +REPUTATION_LEVEL_HIGH= + +# Oracles +REPUTATION_ORACLE_URL= \ No newline at end of file diff --git a/packages/apps/fortune/exchange-oracle/server/.gitignore b/packages/apps/fortune/exchange-oracle/server/.gitignore index 9510a5e5ba..d9749b200b 100644 --- a/packages/apps/fortune/exchange-oracle/server/.gitignore +++ b/packages/apps/fortune/exchange-oracle/server/.gitignore @@ -13,7 +13,7 @@ lerna-debug.log* # OS .DS_Store -.env* +.env.development # Tests /coverage diff --git a/packages/apps/fortune/exchange-oracle/server/docker-compose.yml b/packages/apps/fortune/exchange-oracle/server/docker-compose.yml new file mode 100644 index 0000000000..f9dc699676 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.7' + +services: + minio: + container_name: minio + image: minio/minio:RELEASE.2022-05-26T05-48-41Z + ports: + - 9001:9001 + - 9000:9000 + environment: + MINIO_ROOT_USER: dev + MINIO_ROOT_PASSWORD: devdevdev + entrypoint: 'sh' + command: + -c "minio server /data --console-address ':9001'" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 5s + timeout: 5s + retries: 3 + minio-mc: + container_name: minio-mc + image: minio/mc + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host add myminio http://minio:9000 dev devdevdev; + /usr/bin/mc mb myminio/solution; + /usr/bin/mc anonymous set public myminio/solution; + " diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts b/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts index 50c8812e64..8f1b394994 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts @@ -5,10 +5,23 @@ export const ConfigNames = { PORT: 'PORT', WEB3_PRIVATE_KEY: 'WEB3_PRIVATE_KEY', REPUTATION_ORACLE_URL: 'REPUTATION_ORACLE_URL', + S3_ENDPOINT: 'S3_ENDPOINT', + S3_PORT: 'S3_PORT', + S3_ACCESS_KEY: 'S3_ACCESS_KEY', + S3_SECRET_KEY: 'S3_SECRET_KEY', + S3_BACKET: 'S3_BACKET', + S3_USE_SSL: 'S3_USE_SSL', }; export const envValidator = Joi.object({ HOST: Joi.string().default('localhost'), PORT: Joi.string().default(3002), WEB3_PRIVATE_KEY: Joi.string().required(), + // S3 + S3_ENDPOINT: Joi.string().default('127.0.0.1'), + S3_PORT: Joi.string().default(9000), + S3_ACCESS_KEY: Joi.string().required(), + S3_SECRET_KEY: Joi.string().required(), + S3_BACKET: Joi.string().default('solution'), + S3_USE_SSL: Joi.string().default(false), }); diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/config/index.ts b/packages/apps/fortune/exchange-oracle/server/src/common/config/index.ts index b257a909e5..20c54299ce 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/config/index.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/config/index.ts @@ -1,2 +1,3 @@ export * from './env'; export * from './networks'; +export * from './s3'; diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts b/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts new file mode 100644 index 0000000000..e243d9f52d --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts @@ -0,0 +1,13 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +export const s3Config = registerAs('s3', () => ({ + endPoint: process.env.S3_ENDPOINT!, + port: +process.env.S3_PORT!, + accessKey: process.env.S3_ACCESS_KEY!, + secretKey: process.env.S3_SECRET_KEY!, + bucket: process.env.S3_BACKET!, + useSSL: process.env.S3_USE_SSL === 'true', +})); + +export const s3ConfigKey = s3Config.KEY; +export type S3ConfigType = ConfigType; diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts b/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts new file mode 100644 index 0000000000..640104dce0 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts @@ -0,0 +1,5 @@ +export interface ISolution { + exchangeAddress: string; + workerAddress: string; + solution: string; +} diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts b/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts new file mode 100644 index 0000000000..7db172b0e0 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts @@ -0,0 +1,58 @@ +import * as Minio from 'minio'; +import { ISolution } from '../interfaces/job'; +import { StorageClient } from '@human-protocol/sdk'; +import { BadRequestException } from '@nestjs/common'; + +export async function uploadJobSolutions( + client: Minio.Client, + chainId: number, + escrowAddress: string, + workerAddress: string, + exchangeAddress: string, + solution: string, + bucket: string, +): Promise { + if (!(await client.bucketExists(bucket))) { + throw new BadRequestException('Bucket not found'); + } + const key = `${escrowAddress}-${chainId}.json`; + const url = `${(client as any).protocol}//${(client as any).host}:${ + (client as any).port + }/${bucket}/${key}`; + + let existingJobSolutions: ISolution[]; + try { + existingJobSolutions = await StorageClient.downloadFileFromUrl(url); + } catch { + existingJobSolutions = []; + } + + if ( + existingJobSolutions.find( + (solution) => solution.workerAddress === workerAddress, + ) + ) { + throw new BadRequestException('User has already submitted a solution'); + } + + const newJobSolutions: ISolution[] = [ + ...existingJobSolutions, + { + exchangeAddress: exchangeAddress, + workerAddress: workerAddress, + solution: solution, + }, + ]; + + const content = JSON.stringify(newJobSolutions); + + try { + await client.putObject(bucket, key, content, { + 'Content-Type': 'application/json', + }); + + return url; + } catch (e) { + throw new BadRequestException('File not uploaded'); + } +} diff --git a/packages/apps/fortune/exchange-oracle/server/src/main.ts b/packages/apps/fortune/exchange-oracle/server/src/main.ts index 2952756989..b58ec80f15 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/main.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/main.ts @@ -1,15 +1,15 @@ import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { json, urlencoded } from 'body-parser'; import { useContainer } from 'class-validator'; import { AppModule } from './app.module'; import { ConfigNames } from './common/config'; +import { INestApplication } from '@nestjs/common'; async function bootstrap() { - const app = await NestFactory.create(AppModule, { + const app = await NestFactory.create(AppModule, { cors: true, }); diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts index b2bae20534..1a7ff14b76 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts @@ -6,6 +6,16 @@ import { JobDetailsDto, SolveJobDto } from './job.dto'; import { Web3Service } from '../web3/web3.service'; import { HttpService } from '@nestjs/axios'; import { of } from 'rxjs'; +import { ConfigModule, registerAs } from '@nestjs/config'; +import { + MOCK_REPUTATION_ORACLE_WEBHOOK_URL, + MOCK_S3_ACCESS_KEY, + MOCK_S3_BUCKET, + MOCK_S3_ENDPOINT, + MOCK_S3_PORT, + MOCK_S3_SECRET_KEY, + MOCK_S3_USE_SSL, +} from '../../../test/constants'; describe('JobController', () => { let jobController: JobController; @@ -22,6 +32,23 @@ describe('JobController', () => { beforeAll(async () => { const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forFeature( + registerAs('s3', () => ({ + accessKey: MOCK_S3_ACCESS_KEY, + secretKey: MOCK_S3_SECRET_KEY, + endPoint: MOCK_S3_ENDPOINT, + port: MOCK_S3_PORT, + useSSL: MOCK_S3_USE_SSL, + bucket: MOCK_S3_BUCKET, + })), + ), + ConfigModule.forFeature( + registerAs('server', () => ({ + reputationOracleWebhookUrl: MOCK_REPUTATION_ORACLE_WEBHOOK_URL, + })), + ), + ], controllers: [JobController], providers: [ JobService, diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts index 2922e80e93..7746b87505 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts @@ -4,9 +4,15 @@ import { JobController } from './job.controller'; import { JobService } from './job.service'; import { HttpModule } from '@nestjs/axios'; import { Web3Module } from '../web3/web3.module'; +import { s3Config } from 'src/common/config'; @Module({ - imports: [ConfigModule, HttpModule, Web3Module], + imports: [ + ConfigModule.forFeature(s3Config), + ConfigModule, + HttpModule, + Web3Module, + ], controllers: [JobController], providers: [JobService], }) diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts index b6fd26d011..78c903ac29 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts @@ -4,11 +4,25 @@ import { Test } from '@nestjs/testing'; import { of } from 'rxjs'; import { Web3Service } from '../web3/web3.service'; import { JobService } from './job.service'; -import { EscrowClient, KVStoreClient, EscrowUtils } from '@human-protocol/sdk'; -import { MOCK_PRIVATE_KEY } from '../../../test/constants'; +import { + EscrowClient, + KVStoreClient, + StorageClient, + EscrowUtils, +} from '@human-protocol/sdk'; +import { + MOCK_PRIVATE_KEY, + MOCK_S3_ACCESS_KEY, + MOCK_S3_BUCKET, + MOCK_S3_ENDPOINT, + MOCK_S3_PORT, + MOCK_S3_SECRET_KEY, + MOCK_S3_USE_SSL, +} from '../../../test/constants'; import { EventType } from '../../common/enums/webhook'; import { HEADER_SIGNATURE_KEY } from '../../common/constant'; import { signMessage } from '../../common/utils/signature'; +import { ConfigModule, registerAs } from '@nestjs/config'; jest.mock('@human-protocol/sdk', () => ({ ...jest.requireActual('@human-protocol/sdk'), @@ -22,13 +36,21 @@ jest.mock('@human-protocol/sdk', () => ({ downloadFileFromUrl: jest.fn(), }, })); - -jest.mock('axios', () => ({ - post: jest.fn(), -})); +jest.mock('minio', () => { + class Client { + putObject = jest.fn(); + bucketExists = jest.fn().mockResolvedValue(true); + constructor() { + (this as any).protocol = 'http:'; + (this as any).host = 'localhost'; + (this as any).port = 9000; + } + } + + return { Client }; +}); describe('JobService', () => { - let configService: ConfigService; let jobService: JobService; let web3Service: Web3Service; let httpService: HttpService; @@ -60,6 +82,18 @@ describe('JobService', () => { beforeAll(async () => { const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forFeature( + registerAs('s3', () => ({ + accessKey: MOCK_S3_ACCESS_KEY, + secretKey: MOCK_S3_SECRET_KEY, + endPoint: MOCK_S3_ENDPOINT, + port: MOCK_S3_PORT, + useSSL: MOCK_S3_USE_SSL, + bucket: MOCK_S3_BUCKET, + })), + ), + ], providers: [ JobService, { @@ -84,7 +118,6 @@ describe('JobService', () => { ], }).compile(); - configService = moduleRef.get(ConfigService); jobService = moduleRef.get(JobService); web3Service = moduleRef.get(Web3Service); httpService = moduleRef.get(HttpService); @@ -153,7 +186,11 @@ describe('JobService', () => { }); it('should fail if reputation oracle url is empty', async () => { - configService.get = jest.fn().mockReturnValue(''); + (configServiceMock as any).get.mockImplementation((key: string) => { + if (key === 'REPUTATION_ORACLE_URL') { + return ''; + } + }); await expect( jobService.getDetails(chainId, escrowAddress), @@ -209,7 +246,8 @@ describe('JobService', () => { describe('solveJob', () => { it('should solve a job', async () => { - const solution = 'job-solution'; + const solutionUrl = + 'http://localhost:9000/solution/0x1234567890123456789012345678901234567890-1.json'; const recordingOracleURLMock = 'https://example.com/recordingoracle'; @@ -222,11 +260,13 @@ describe('JobService', () => { get: jest.fn().mockResolvedValue(recordingOracleURLMock), })); + StorageClient.downloadFileFromUrl = jest.fn().mockResolvedValue([]); + const result = await jobService.solveJob( chainId, escrowAddress, workerAddress, - solution, + 'solution', ); expect(result).toBe(true); @@ -238,7 +278,7 @@ describe('JobService', () => { chainId, exchangeAddress: signerMock.address, workerAddress, - solution, + solutionUrl, }), ); }); @@ -288,6 +328,14 @@ describe('JobService', () => { get: jest.fn().mockResolvedValue('https://example.com/recordingoracle'), })); + StorageClient.downloadFileFromUrl = jest.fn().mockResolvedValue([ + { + exchangeAddress: '0x1234567890123456789012345678901234567892', + workerAddress: '0x1234567890123456789012345678901234567891', + solution: 'test', + }, + ]); + jobService['storage'][escrowAddress] = [workerAddress]; await expect( diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts index 55d9880188..1a1dc2979d 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts @@ -6,34 +6,41 @@ import { KVStoreKeys, } from '@human-protocol/sdk'; import { HttpService } from '@nestjs/axios'; -import { - BadRequestException, - Inject, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; +import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { ConfigNames } from '../../common/config'; +import { ConfigNames, S3ConfigType, s3ConfigKey } from '../../common/config'; import { Web3Service } from '../web3/web3.service'; import { EscrowFailedWebhookDto, JobDetailsDto } from './job.dto'; import { EventType } from '../../common/enums/webhook'; import { signMessage } from '../../common/utils/signature'; import { HEADER_SIGNATURE_KEY } from '../../common/constant'; +import * as Minio from 'minio'; +import { uploadJobSolutions } from '../../common/utils/storage'; @Injectable() export class JobService { public readonly logger = new Logger(JobService.name); + public readonly minioClient: Minio.Client; private storage: { [key: string]: string[]; } = {}; constructor( + @Inject(s3ConfigKey) + private s3Config: S3ConfigType, private readonly configService: ConfigService, @Inject(Web3Service) private readonly web3Service: Web3Service, private readonly httpService: HttpService, - ) {} + ) { + this.minioClient = new Minio.Client({ + endPoint: this.s3Config.endPoint, + port: this.s3Config.port, + accessKey: this.s3Config.accessKey, + secretKey: this.s3Config.secretKey, + useSSL: this.s3Config.useSSL, + }); + } public async getDetails( chainId: number, @@ -131,23 +138,22 @@ export class JobService { if (!recordingOracleWebhookUrl) throw new NotFoundException('Unable to get Recording Oracle webhook URL'); - if ( - this.storage[escrowAddress] && - this.storage[escrowAddress].includes(workerAddress) - ) - throw new BadRequestException('User has already submitted a solution'); - - if (!this.storage[escrowAddress]) { - this.storage[escrowAddress] = []; - } - this.storage[escrowAddress].push(workerAddress); + const solutionUrl = await uploadJobSolutions( + this.minioClient, + chainId, + escrowAddress, + workerAddress, + signer.address, + solution, + this.s3Config.bucket, + ); await this.httpService.post(recordingOracleWebhookUrl, { escrowAddress: escrowAddress, chainId: chainId, exchangeAddress: signer.address, workerAddress: workerAddress, - solution: solution, + solutionUrl: solutionUrl, }); return true; diff --git a/packages/apps/fortune/exchange-oracle/server/test/constants.ts b/packages/apps/fortune/exchange-oracle/server/test/constants.ts index c814128def..1c48ae34ce 100644 --- a/packages/apps/fortune/exchange-oracle/server/test/constants.ts +++ b/packages/apps/fortune/exchange-oracle/server/test/constants.ts @@ -1,2 +1,10 @@ export const MOCK_PRIVATE_KEY = 'd334daf65a631f40549cc7de126d5a0016f32a2d00c49f94563f9737f7135e55'; + +export const MOCK_S3_ENDPOINT = 'localhost'; +export const MOCK_S3_PORT = 9000; +export const MOCK_S3_ACCESS_KEY = 'access_key'; +export const MOCK_S3_SECRET_KEY = 'secret_key'; +export const MOCK_S3_BUCKET = 'solution'; +export const MOCK_S3_USE_SSL = false; +export const MOCK_REPUTATION_ORACLE_WEBHOOK_URL = 'http://localhost:3000'; From adcff5eb7740f15c7e7b305853829df9b02ed808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= <50665615+flopez7@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:19:09 +0200 Subject: [PATCH 2/3] Add and endpoint to receive solutions marked as invalid (#995) * Add and endpoint to receive solutions marked as invalid * Create storage service and resolve comments --- .../server/src/common/config/env.ts | 4 +- .../server/src/common/config/s3.ts | 2 +- .../server/src/common/interfaces/job.ts | 7 +- .../server/src/common/utils/storage.ts | 58 ----- .../src/modules/job/job.controller.spec.ts | 26 ++- .../server/src/modules/job/job.controller.ts | 9 +- .../server/src/modules/job/job.dto.ts | 18 ++ .../server/src/modules/job/job.module.ts | 2 + .../src/modules/job/job.service.spec.ts | 76 ++++++- .../server/src/modules/job/job.service.ts | 116 +++++++--- .../src/modules/storage/storage.module.ts | 11 + .../modules/storage/storage.service.spec.ts | 199 ++++++++++++++++++ .../src/modules/storage/storage.service.ts | 73 +++++++ 13 files changed, 507 insertions(+), 94 deletions(-) delete mode 100644 packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts create mode 100644 packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.module.ts create mode 100644 packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.spec.ts create mode 100644 packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts b/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts index 8f1b394994..7df0874e88 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/config/env.ts @@ -9,7 +9,7 @@ export const ConfigNames = { S3_PORT: 'S3_PORT', S3_ACCESS_KEY: 'S3_ACCESS_KEY', S3_SECRET_KEY: 'S3_SECRET_KEY', - S3_BACKET: 'S3_BACKET', + S3_BUCKET: 'S3_BUCKET', S3_USE_SSL: 'S3_USE_SSL', }; @@ -22,6 +22,6 @@ export const envValidator = Joi.object({ S3_PORT: Joi.string().default(9000), S3_ACCESS_KEY: Joi.string().required(), S3_SECRET_KEY: Joi.string().required(), - S3_BACKET: Joi.string().default('solution'), + S3_BUCKET: Joi.string().default('solution'), S3_USE_SSL: Joi.string().default(false), }); diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts b/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts index e243d9f52d..b2d4e3731b 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/config/s3.ts @@ -5,7 +5,7 @@ export const s3Config = registerAs('s3', () => ({ port: +process.env.S3_PORT!, accessKey: process.env.S3_ACCESS_KEY!, secretKey: process.env.S3_SECRET_KEY!, - bucket: process.env.S3_BACKET!, + bucket: process.env.S3_BUCKET!, useSSL: process.env.S3_USE_SSL === 'true', })); diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts b/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts index 640104dce0..d71a6c524f 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/interfaces/job.ts @@ -1,5 +1,10 @@ export interface ISolution { - exchangeAddress: string; workerAddress: string; solution: string; + invalid?: boolean; +} + +export interface ISolutionsFile { + exchangeAddress: string; + solutions: ISolution[]; } diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts b/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts deleted file mode 100644 index 7db172b0e0..0000000000 --- a/packages/apps/fortune/exchange-oracle/server/src/common/utils/storage.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as Minio from 'minio'; -import { ISolution } from '../interfaces/job'; -import { StorageClient } from '@human-protocol/sdk'; -import { BadRequestException } from '@nestjs/common'; - -export async function uploadJobSolutions( - client: Minio.Client, - chainId: number, - escrowAddress: string, - workerAddress: string, - exchangeAddress: string, - solution: string, - bucket: string, -): Promise { - if (!(await client.bucketExists(bucket))) { - throw new BadRequestException('Bucket not found'); - } - const key = `${escrowAddress}-${chainId}.json`; - const url = `${(client as any).protocol}//${(client as any).host}:${ - (client as any).port - }/${bucket}/${key}`; - - let existingJobSolutions: ISolution[]; - try { - existingJobSolutions = await StorageClient.downloadFileFromUrl(url); - } catch { - existingJobSolutions = []; - } - - if ( - existingJobSolutions.find( - (solution) => solution.workerAddress === workerAddress, - ) - ) { - throw new BadRequestException('User has already submitted a solution'); - } - - const newJobSolutions: ISolution[] = [ - ...existingJobSolutions, - { - exchangeAddress: exchangeAddress, - workerAddress: workerAddress, - solution: solution, - }, - ]; - - const content = JSON.stringify(newJobSolutions); - - try { - await client.putObject(bucket, key, content, { - 'Content-Type': 'application/json', - }); - - return url; - } catch (e) { - throw new BadRequestException('File not uploaded'); - } -} diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts index 1a7ff14b76..fb7d2e37e8 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts @@ -2,7 +2,7 @@ import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { JobController } from './job.controller'; import { JobService } from './job.service'; -import { JobDetailsDto, SolveJobDto } from './job.dto'; +import { InvalidJobDto, JobDetailsDto, SolveJobDto } from './job.dto'; import { Web3Service } from '../web3/web3.service'; import { HttpService } from '@nestjs/axios'; import { of } from 'rxjs'; @@ -16,6 +16,7 @@ import { MOCK_S3_SECRET_KEY, MOCK_S3_USE_SSL, } from '../../../test/constants'; +import { StorageService } from '../storage/storage.service'; describe('JobController', () => { let jobController: JobController; @@ -65,6 +66,7 @@ describe('JobController', () => { }), }, }, + StorageService, { provide: HttpService, useValue: { @@ -146,4 +148,26 @@ describe('JobController', () => { ); }); }); + + describe('invalidJobSolution-solution', () => { + it('should mark a job solution as invalid', async () => { + const solveJobDto: InvalidJobDto = { + chainId, + escrowAddress, + workerAddress, + }; + const expectedResult = true; + + jest + .spyOn(jobService, 'processInvalidJobSolution') + .mockResolvedValue(expectedResult); + + const result = await jobController.invalidJobSolution(solveJobDto); + + expect(result).toBe(expectedResult); + expect(jobService.processInvalidJobSolution).toHaveBeenCalledWith( + solveJobDto, + ); + }); + }); }); diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.ts index 521d845caa..d85dee52dc 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { JobService } from './job.service'; -import { JobDetailsDto, SolveJobDto } from './job.dto'; +import { InvalidJobDto, JobDetailsDto, SolveJobDto } from './job.dto'; @ApiTags('Job') @Controller('job') @@ -33,4 +33,9 @@ export class JobController { body.solution, ); } + + @Patch('invalid-solution') + invalidJobSolution(@Body() body: InvalidJobDto): Promise { + return this.jobService.processInvalidJobSolution(body); + } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.dto.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.dto.ts index 980121cd0f..c8696cd0bc 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.dto.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.dto.ts @@ -39,6 +39,24 @@ export class SolveJobDto { public solution: string; } +export class InvalidJobDto { + @ApiProperty() + @IsString() + @IsValidEthereumAddress() + public escrowAddress: string; + + @ApiProperty({ + enum: ChainId, + }) + @IsEnum(ChainId) + public chainId: ChainId; + + @ApiProperty() + @IsString() + @IsValidEthereumAddress() + workerAddress: string; +} + export class EscrowFailedWebhookDto { public chain_id: ChainId; public escrow_address: string; diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts index 7746b87505..2c9a4528a0 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.module.ts @@ -5,6 +5,7 @@ import { JobService } from './job.service'; import { HttpModule } from '@nestjs/axios'; import { Web3Module } from '../web3/web3.module'; import { s3Config } from 'src/common/config'; +import { StorageModule } from '../storage/storage.module'; @Module({ imports: [ @@ -12,6 +13,7 @@ import { s3Config } from 'src/common/config'; ConfigModule, HttpModule, Web3Module, + StorageModule, ], controllers: [JobController], providers: [JobService], diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts index 78c903ac29..bb47db0317 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts @@ -23,6 +23,7 @@ import { EventType } from '../../common/enums/webhook'; import { HEADER_SIGNATURE_KEY } from '../../common/constant'; import { signMessage } from '../../common/utils/signature'; import { ConfigModule, registerAs } from '@nestjs/config'; +import { StorageService } from '../storage/storage.service'; jest.mock('@human-protocol/sdk', () => ({ ...jest.requireActual('@human-protocol/sdk'), @@ -54,6 +55,7 @@ describe('JobService', () => { let jobService: JobService; let web3Service: Web3Service; let httpService: HttpService; + let storageService: StorageService; const chainId = 1; const escrowAddress = '0x1234567890123456789012345678901234567890'; @@ -96,6 +98,7 @@ describe('JobService', () => { ], providers: [ JobService, + StorageService, { provide: ConfigService, useValue: configServiceMock, @@ -121,6 +124,7 @@ describe('JobService', () => { jobService = moduleRef.get(JobService); web3Service = moduleRef.get(Web3Service); httpService = moduleRef.get(HttpService); + storageService = moduleRef.get(StorageService); }); describe('getDetails', () => { @@ -246,7 +250,7 @@ describe('JobService', () => { describe('solveJob', () => { it('should solve a job', async () => { - const solutionUrl = + const solutionsUrl = 'http://localhost:9000/solution/0x1234567890123456789012345678901234567890-1.json'; const recordingOracleURLMock = 'https://example.com/recordingoracle'; @@ -276,9 +280,7 @@ describe('JobService', () => { expect.objectContaining({ escrowAddress, chainId, - exchangeAddress: signerMock.address, - workerAddress, - solutionUrl, + solutionsUrl, }), ); }); @@ -344,4 +346,70 @@ describe('JobService', () => { expect(web3Service.getSigner).toHaveBeenCalledWith(chainId); }); }); + + describe('processInvalidJob', () => { + it('should mark a job solution as invalid', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + const solution = 'test'; + + const jobSolution = { + workerAddress, + solution, + }; + const existingJobSolutions = [jobSolution]; + StorageClient.downloadFileFromUrl = jest + .fn() + .mockResolvedValue(existingJobSolutions); + const result = await jobService.processInvalidJobSolution({ + chainId, + escrowAddress, + workerAddress, + }); + + expect(result).toBe(true); + expect(storageService.minioClient.putObject).toHaveBeenCalledWith( + MOCK_S3_BUCKET, + `${escrowAddress}-${chainId}.json`, + JSON.stringify({ + exchangeAddress, + solutions: [ + { + workerAddress, + solution, + invalid: true, + }, + ], + }), + + { + 'Content-Type': 'application/json', + }, + ); + }); + + it('should throw an error if solution was not previously in S3', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + + const existingJobSolutions = [ + { + exchangeAddress, + workerAddress: '0x1234567890123456789012345678901234567892', + solution: 'test', + }, + ]; + StorageClient.downloadFileFromUrl = jest + .fn() + .mockResolvedValue(existingJobSolutions); + + await expect( + jobService.processInvalidJobSolution({ + chainId, + escrowAddress, + workerAddress, + }), + ).rejects.toThrow(`Solution not found in Escrow: ${escrowAddress}`); + }); + }); }); diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts index 1a1dc2979d..22ea9d8415 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts @@ -1,4 +1,5 @@ import { + ChainId, EscrowClient, EscrowStatus, EscrowUtils, @@ -6,41 +7,42 @@ import { KVStoreKeys, } from '@human-protocol/sdk'; import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { ConfigNames, S3ConfigType, s3ConfigKey } from '../../common/config'; -import { Web3Service } from '../web3/web3.service'; -import { EscrowFailedWebhookDto, JobDetailsDto } from './job.dto'; +import { ISolution } from 'src/common/interfaces/job'; +import { ConfigNames } from '../../common/config'; +import { HEADER_SIGNATURE_KEY } from '../../common/constant'; import { EventType } from '../../common/enums/webhook'; import { signMessage } from '../../common/utils/signature'; -import { HEADER_SIGNATURE_KEY } from '../../common/constant'; -import * as Minio from 'minio'; -import { uploadJobSolutions } from '../../common/utils/storage'; +import { StorageService } from '../storage/storage.service'; +import { Web3Service } from '../web3/web3.service'; +import { + EscrowFailedWebhookDto, + InvalidJobDto, + JobDetailsDto, +} from './job.dto'; @Injectable() export class JobService { public readonly logger = new Logger(JobService.name); - public readonly minioClient: Minio.Client; private storage: { [key: string]: string[]; } = {}; constructor( - @Inject(s3ConfigKey) - private s3Config: S3ConfigType, private readonly configService: ConfigService, @Inject(Web3Service) private readonly web3Service: Web3Service, + @Inject(StorageService) + private readonly storageService: StorageService, private readonly httpService: HttpService, - ) { - this.minioClient = new Minio.Client({ - endPoint: this.s3Config.endPoint, - port: this.s3Config.port, - accessKey: this.s3Config.accessKey, - secretKey: this.s3Config.secretKey, - useSSL: this.s3Config.useSSL, - }); - } + ) {} public async getDetails( chainId: number, @@ -138,24 +140,88 @@ export class JobService { if (!recordingOracleWebhookUrl) throw new NotFoundException('Unable to get Recording Oracle webhook URL'); - const solutionUrl = await uploadJobSolutions( - this.minioClient, + const solutionsUrl = await this.addSolution( chainId, escrowAddress, workerAddress, signer.address, solution, - this.s3Config.bucket, ); await this.httpService.post(recordingOracleWebhookUrl, { escrowAddress: escrowAddress, chainId: chainId, - exchangeAddress: signer.address, - workerAddress: workerAddress, - solutionUrl: solutionUrl, + solutionsUrl: solutionsUrl, }); return true; } + + private async addSolution( + chainId: ChainId, + escrowAddress: string, + workerAddress: string, + exchangeAddress: string, + solution: string, + ) { + const existingJobSolutions = await this.storageService.downloadJobSolutions( + escrowAddress, + chainId, + ); + + if ( + existingJobSolutions.find( + (solution) => solution.workerAddress === workerAddress, + ) + ) { + throw new BadRequestException('User has already submitted a solution'); + } + + const newJobSolutions: ISolution[] = [ + ...existingJobSolutions, + { + workerAddress: workerAddress, + solution: solution, + }, + ]; + + const url = await this.storageService.uploadJobSolutions( + exchangeAddress, + escrowAddress, + chainId, + newJobSolutions, + ); + + return url; + } + + public async processInvalidJobSolution( + invalidJobSolution: InvalidJobDto, + ): Promise { + const existingJobSolutions = await this.storageService.downloadJobSolutions( + invalidJobSolution.escrowAddress, + invalidJobSolution.chainId, + ); + + const foundSolution = existingJobSolutions.find( + (sol) => sol.workerAddress === invalidJobSolution.workerAddress, + ); + + if (foundSolution) { + foundSolution.invalid = true; + } else { + throw new BadRequestException( + `Solution not found in Escrow: ${invalidJobSolution.escrowAddress}`, + ); + } + + await this.storageService.uploadJobSolutions( + this.web3Service.getSigner(invalidJobSolution.chainId).address, + invalidJobSolution.escrowAddress, + invalidJobSolution.chainId, + existingJobSolutions, + ); + + return true; + } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.module.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.module.ts new file mode 100644 index 0000000000..f9cf1659df --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { StorageService } from './storage.service'; +import { ConfigModule } from '@nestjs/config'; +import { s3Config } from '../../common/config'; + +@Module({ + imports: [ConfigModule.forFeature(s3Config)], + providers: [StorageService], + exports: [StorageService], +}) +export class StorageModule {} diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.spec.ts new file mode 100644 index 0000000000..4f4b781632 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.spec.ts @@ -0,0 +1,199 @@ +import { ChainId, StorageClient } from '@human-protocol/sdk'; +import { ConfigModule, registerAs } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { + MOCK_S3_ACCESS_KEY, + MOCK_S3_BUCKET, + MOCK_S3_ENDPOINT, + MOCK_S3_PORT, + MOCK_S3_SECRET_KEY, + MOCK_S3_USE_SSL, +} from '../../../test/constants'; +import { StorageService } from './storage.service'; + +jest.mock('@human-protocol/sdk', () => ({ + ...jest.requireActual('@human-protocol/sdk'), + StorageClient: { + downloadFileFromUrl: jest.fn(), + }, +})); + +jest.mock('minio', () => { + class Client { + putObject = jest.fn(); + bucketExists = jest.fn(); + constructor() { + (this as any).protocol = 'http:'; + (this as any).host = 'localhost'; + (this as any).port = 9000; + } + } + + return { Client }; +}); + +describe('Web3Service', () => { + let storageService: StorageService; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forFeature( + registerAs('s3', () => ({ + accessKey: MOCK_S3_ACCESS_KEY, + secretKey: MOCK_S3_SECRET_KEY, + endPoint: MOCK_S3_ENDPOINT, + port: MOCK_S3_PORT, + useSSL: MOCK_S3_USE_SSL, + bucket: MOCK_S3_BUCKET, + })), + ), + ], + providers: [StorageService], + }).compile(); + + storageService = moduleRef.get(StorageService); + }); + + describe('uploadJobSolutions', () => { + it('should upload the solutions correctly', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + const escrowAddress = '0x1234567890123456789012345678901234567890'; + const chainId = ChainId.LOCALHOST; + const solution = 'test'; + + storageService.minioClient.bucketExists = jest + .fn() + .mockResolvedValue(true); + + const jobSolution = { + workerAddress, + solution, + }; + const fileUrl = await storageService.uploadJobSolutions( + exchangeAddress, + escrowAddress, + chainId, + [jobSolution], + ); + expect(fileUrl).toBe( + `http://${MOCK_S3_ENDPOINT}:${MOCK_S3_PORT}/${MOCK_S3_BUCKET}/${escrowAddress}-${chainId}.json`, + ); + expect(storageService.minioClient.putObject).toHaveBeenCalledWith( + MOCK_S3_BUCKET, + `${escrowAddress}-${chainId}.json`, + JSON.stringify({ + exchangeAddress, + solutions: [ + { + workerAddress, + solution, + }, + ], + }), + + { + 'Content-Type': 'application/json', + }, + ); + }); + + it('should fail if the bucket does not exist', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + const escrowAddress = '0x1234567890123456789012345678901234567890'; + const chainId = ChainId.LOCALHOST; + const solution = 'test'; + + storageService.minioClient.bucketExists = jest + .fn() + .mockResolvedValue(false); + + const jobSolution = { + workerAddress, + solution, + }; + await expect( + storageService.uploadJobSolutions( + exchangeAddress, + escrowAddress, + chainId, + [jobSolution], + ), + ).rejects.toThrow('Bucket not found'); + }); + it('should fail if the file cannot be uploaded', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + const escrowAddress = '0x1234567890123456789012345678901234567890'; + const chainId = ChainId.LOCALHOST; + const solution = 'test'; + + storageService.minioClient.bucketExists = jest + .fn() + .mockResolvedValue(true); + storageService.minioClient.putObject = jest + .fn() + .mockRejectedValue('Network error'); + + const jobSolution = { + workerAddress, + solution, + }; + + await expect( + storageService.uploadJobSolutions( + exchangeAddress, + escrowAddress, + chainId, + [jobSolution], + ), + ).rejects.toThrow('File not uploaded'); + }); + }); + + describe('downloadJobSolutions', () => { + it('should download the file correctly', async () => { + const exchangeAddress = '0x1234567890123456789012345678901234567892'; + const workerAddress = '0x1234567890123456789012345678901234567891'; + const escrowAddress = '0x1234567890123456789012345678901234567890'; + const chainId = ChainId.LOCALHOST; + const solution = 'test'; + + const expectedJobFile = { + exchangeAddress, + solutions: [ + { + workerAddress, + solution, + }, + ], + }; + + StorageClient.downloadFileFromUrl = jest + .fn() + .mockResolvedValue(expectedJobFile); + const solutionsFile = await storageService.downloadJobSolutions( + escrowAddress, + chainId, + ); + expect(solutionsFile).toBe(expectedJobFile); + }); + + it('should return empty array when file cannot be downloaded', async () => { + const escrowAddress = '0x1234567890123456789012345678901234567890'; + const chainId = ChainId.LOCALHOST; + + StorageClient.downloadFileFromUrl = jest + .fn() + .mockRejectedValue('Network error'); + + const solutionsFile = await storageService.downloadJobSolutions( + escrowAddress, + chainId, + ); + expect(solutionsFile).toStrictEqual([]); + }); + }); +}); diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts new file mode 100644 index 0000000000..2b50bf4336 --- /dev/null +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts @@ -0,0 +1,73 @@ +import { ChainId, StorageClient } from '@human-protocol/sdk'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import * as Minio from 'minio'; +import { S3ConfigType, s3ConfigKey } from '../../common/config'; +import { ISolution, ISolutionsFile } from '../../common/interfaces/job'; + +@Injectable() +export class StorageService { + public readonly minioClient: Minio.Client; + + constructor( + @Inject(s3ConfigKey) + private s3Config: S3ConfigType, + ) { + this.minioClient = new Minio.Client({ + endPoint: this.s3Config.endPoint, + port: this.s3Config.port, + accessKey: this.s3Config.accessKey, + secretKey: this.s3Config.secretKey, + useSSL: this.s3Config.useSSL, + }); + } + public getJobUrl(escrowAddress: string, chainId: ChainId): string { + return `${this.s3Config.useSSL ? 'https' : 'http'}://${ + this.s3Config.endPoint + }:${this.s3Config.port}/${ + this.s3Config.bucket + }/${escrowAddress}-${chainId}.json`; + } + + public async downloadJobSolutions( + escrowAddress: string, + chainId: ChainId, + ): Promise { + const url = this.getJobUrl(escrowAddress, chainId); + try { + return await StorageClient.downloadFileFromUrl(url); + } catch { + return []; + } + } + + public async uploadJobSolutions( + exchangeAddress: string, + escrowAddress: string, + chainId: ChainId, + solutions: ISolution[], + ): Promise { + if (!(await this.minioClient.bucketExists(this.s3Config.bucket))) { + throw new BadRequestException('Bucket not found'); + } + + const content: ISolutionsFile = { + exchangeAddress, + solutions, + }; + + try { + await this.minioClient.putObject( + this.s3Config.bucket, + `${escrowAddress}-${chainId}.json`, + JSON.stringify(content), + { + 'Content-Type': 'application/json', + }, + ); + + return this.getJobUrl(escrowAddress, chainId); + } catch (e) { + throw new BadRequestException('File not uploaded'); + } + } +} From 32e02e9a860fbf4537a413e931224b8e3f4c5a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Tue, 10 Oct 2023 12:37:55 +0200 Subject: [PATCH 3/3] Remove boolean from void methods --- .../server/src/modules/job/job.controller.spec.ts | 14 ++++---------- .../server/src/modules/job/job.service.spec.ts | 6 ++---- .../server/src/modules/job/job.service.ts | 10 +++------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts index fb7d2e37e8..89781b8ca1 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.controller.spec.ts @@ -133,13 +133,11 @@ describe('JobController', () => { workerAddress, solution, }; - const expectedResult = true; - jest.spyOn(jobService, 'solveJob').mockResolvedValue(expectedResult); + jest.spyOn(jobService, 'solveJob').mockResolvedValue(); - const result = await jobController.solveJob(solveJobDto); + await jobController.solveJob(solveJobDto); - expect(result).toBe(expectedResult); expect(jobService.solveJob).toHaveBeenCalledWith( solveJobDto.chainId, solveJobDto.escrowAddress, @@ -156,15 +154,11 @@ describe('JobController', () => { escrowAddress, workerAddress, }; - const expectedResult = true; - jest - .spyOn(jobService, 'processInvalidJobSolution') - .mockResolvedValue(expectedResult); + jest.spyOn(jobService, 'processInvalidJobSolution').mockResolvedValue(); - const result = await jobController.invalidJobSolution(solveJobDto); + await jobController.invalidJobSolution(solveJobDto); - expect(result).toBe(expectedResult); expect(jobService.processInvalidJobSolution).toHaveBeenCalledWith( solveJobDto, ); diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts index bb47db0317..1ac8531493 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts @@ -266,14 +266,13 @@ describe('JobService', () => { StorageClient.downloadFileFromUrl = jest.fn().mockResolvedValue([]); - const result = await jobService.solveJob( + await jobService.solveJob( chainId, escrowAddress, workerAddress, 'solution', ); - expect(result).toBe(true); expect(web3Service.getSigner).toHaveBeenCalledWith(chainId); expect(httpServicePostMock).toHaveBeenCalledWith( recordingOracleURLMock, @@ -361,13 +360,12 @@ describe('JobService', () => { StorageClient.downloadFileFromUrl = jest .fn() .mockResolvedValue(existingJobSolutions); - const result = await jobService.processInvalidJobSolution({ + await jobService.processInvalidJobSolution({ chainId, escrowAddress, workerAddress, }); - expect(result).toBe(true); expect(storageService.minioClient.putObject).toHaveBeenCalledWith( MOCK_S3_BUCKET, `${escrowAddress}-${chainId}.json`, diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts index 22ea9d8415..122c4410db 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts @@ -124,7 +124,7 @@ export class JobService { escrowAddress: string, workerAddress: string, solution: string, - ): Promise { + ): Promise { const signer = this.web3Service.getSigner(chainId); const escrowClient = await EscrowClient.build(signer); const recordingOracleAddress = await escrowClient.getRecordingOracleAddress( @@ -153,8 +153,6 @@ export class JobService { chainId: chainId, solutionsUrl: solutionsUrl, }); - - return true; } private async addSolution( @@ -163,7 +161,7 @@ export class JobService { workerAddress: string, exchangeAddress: string, solution: string, - ) { + ): Promise { const existingJobSolutions = await this.storageService.downloadJobSolutions( escrowAddress, chainId, @@ -197,7 +195,7 @@ export class JobService { public async processInvalidJobSolution( invalidJobSolution: InvalidJobDto, - ): Promise { + ): Promise { const existingJobSolutions = await this.storageService.downloadJobSolutions( invalidJobSolution.escrowAddress, invalidJobSolution.chainId, @@ -221,7 +219,5 @@ export class JobService { invalidJobSolution.chainId, existingJobSolutions, ); - - return true; } }