diff --git a/packages/apps/fortune/recording-oracle/src/common/interfaces/job.ts b/packages/apps/fortune/recording-oracle/src/common/interfaces/job.ts index 05b023fec6..59b5561357 100644 --- a/packages/apps/fortune/recording-oracle/src/common/interfaces/job.ts +++ b/packages/apps/fortune/recording-oracle/src/common/interfaces/job.ts @@ -11,9 +11,16 @@ export interface ISolution { workerAddress: string; solution: string; error?: boolean | SolutionError; + verificationResult?: VerificationResult; + rejectionReason?: SolutionError; } export interface ISolutionsFile { exchangeAddress: string; solutions: ISolution[]; } + +export enum VerificationResult { + Accepted = 'accepted', + Rejected = 'rejected', +} diff --git a/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts b/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts index 5d348d5057..9b6fb308f2 100644 --- a/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts +++ b/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts @@ -16,7 +16,11 @@ import { ErrorJob } from '../../common/constants/errors'; import { JobRequestType, SolutionError } from '../../common/enums/job'; import { EventType } from '../../common/enums/webhook'; import { ConflictError, ValidationError } from '../../common/errors'; -import { IManifest, ISolution } from '../../common/interfaces/job'; +import { + IManifest, + ISolution, + VerificationResult, +} from '../../common/interfaces/job'; import { checkCurseWords } from '../../common/utils/curseWords'; import { sendWebhook } from '../../common/utils/webhook'; import { StorageService } from '../storage/storage.service'; @@ -48,8 +52,8 @@ export class JobService { const errorSolutions: ISolution[] = []; const uniqueSolutions: ISolution[] = []; - const filteredExchangeSolution = exchangeSolutions.filter( - (exchangeSolution) => !exchangeSolution.error, + const filteredExchangeSolution = exchangeSolutions.filter((solution) => + this.isAcceptedSolution(solution), ); filteredExchangeSolution.forEach((exchangeSolution) => { @@ -88,6 +92,30 @@ export class JobService { return { errorSolutions, uniqueSolutions }; } + private isAcceptedSolution(solution: ISolution): boolean { + return ( + !solution.error && + solution.verificationResult !== VerificationResult.Rejected + ); + } + + private toFinalResult(solution: ISolution): ISolution { + const rejectionReason = + solution.error || + solution.verificationResult === VerificationResult.Rejected + ? solution.rejectionReason || (solution.error as SolutionError) + : undefined; + + return { + workerAddress: solution.workerAddress, + solution: solution.solution, + verificationResult: rejectionReason + ? VerificationResult.Rejected + : VerificationResult.Accepted, + ...(rejectionReason ? { rejectionReason } : {}), + }; + } + async processJobSolution(webhook: WebhookDto): Promise { const logger = this.logger.child({ action: 'processJobSolution', @@ -149,7 +177,11 @@ export class JobService { ); } - if (existingJobSolutions.length >= submissionsRequired) { + if ( + existingJobSolutions.filter((solution) => + this.isAcceptedSolution(solution), + ).length >= submissionsRequired + ) { logger.warn(ErrorJob.AllSolutionsHaveAlreadyBeenSent, { submissionsRequired, nExistingJobSolutions: existingJobSolutions.length, @@ -175,7 +207,7 @@ export class JobService { const jobSolutionUploaded = await this.storageService.uploadJobSolutions( webhook.escrowAddress, webhook.chainId, - recordingOracleSolutions, + recordingOracleSolutions.map((solution) => this.toFinalResult(solution)), ); const lastExchangeSolution = @@ -206,8 +238,9 @@ export class JobService { ); if ( - recordingOracleSolutions.filter((solution) => !solution.error).length >= - submissionsRequired + recordingOracleSolutions.filter((solution) => + this.isAcceptedSolution(solution), + ).length >= submissionsRequired ) { const reputationOracleWebhook = await KVStoreUtils.get( webhook.chainId, diff --git a/packages/apps/job-launcher/client/src/pages/Job/JobDetail/index.tsx b/packages/apps/job-launcher/client/src/pages/Job/JobDetail/index.tsx index bbae6dd16e..ba351ecf2f 100644 --- a/packages/apps/job-launcher/client/src/pages/Job/JobDetail/index.tsx +++ b/packages/apps/job-launcher/client/src/pages/Job/JobDetail/index.tsx @@ -248,11 +248,14 @@ export default function JobDetail() { }, { id: 'solution', label: 'Fortune' }, { - id: 'error', + id: 'verificationResult', label: 'Status', - render: ({ error }) => (error ? 'Refused' : 'Accepted'), + render: ({ verificationResult }) => + verificationResult === 'rejected' + ? 'Rejected' + : 'Accepted', }, - { id: 'error', label: 'Refused reason' }, + { id: 'rejectionReason', label: 'Rejection reason' }, ]} data={data} page={page} diff --git a/packages/apps/job-launcher/client/src/types/index.ts b/packages/apps/job-launcher/client/src/types/index.ts index 2ca082c754..9e240856be 100644 --- a/packages/apps/job-launcher/client/src/types/index.ts +++ b/packages/apps/job-launcher/client/src/types/index.ts @@ -290,9 +290,10 @@ export type JobDetailsResults = JobDetailsResponse & { }; export type FortuneFinalResult = { - exchangeAddress: string; workerAddress: string; solution: string; + verificationResult: 'accepted' | 'rejected'; + rejectionReason?: string; }; export type Qualification = { 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 dc5822aa8d..2fd27bb044 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 @@ -228,10 +228,18 @@ export class FortuneFinalResultDto { @IsString() public solution: string; - @ApiProperty() + @ApiProperty({ + name: 'verification_result', + enum: ['accepted', 'rejected'], + }) + @IsNotEmpty() + @IsIn(['accepted', 'rejected']) + public verificationResult: string; + + @ApiPropertyOptional({ name: 'rejection_reason' }) @IsOptional() @IsString() - public error?: string; + public rejectionReason?: string; } export class JobListDto { 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 1ac1487f52..b9bb5312a8 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 @@ -950,11 +950,13 @@ describe('JobService', () => { { workerAddress: faker.finance.ethereumAddress(), solution: 'good', + verificationResult: 'accepted', }, { workerAddress: faker.finance.ethereumAddress(), solution: 'bad', - error: 'wrong answer', + verificationResult: 'rejected', + rejectionReason: 'wrong answer', }, ]; diff --git a/packages/apps/reputation-oracle/server/src/common/types/job-result.ts b/packages/apps/reputation-oracle/server/src/common/types/job-result.ts index cab3caad0e..7ded2e0df1 100644 --- a/packages/apps/reputation-oracle/server/src/common/types/job-result.ts +++ b/packages/apps/reputation-oracle/server/src/common/types/job-result.ts @@ -1,20 +1,21 @@ -export type FortuneFinalResult = { - workerAddress: string; - solution: string; - error?: 'duplicated' | 'curse_word'; -}; - export enum VerificationResult { Accepted = 'accepted', Rejected = 'rejected', } -export type MarketingFinalResult = { - workerAddress: string; - postUrl: string; - verificationResult: VerificationResult; +export class BaseFinalResult { + workerAddress!: string; + verificationResult!: VerificationResult; rejectionReason?: string; -}; +} + +export class FortuneFinalResult extends BaseFinalResult { + solution!: string; +} + +export class MarketingFinalResult extends BaseFinalResult { + postUrl!: string; +} type CvatAnnotationMetaJob = { job_id: number; diff --git a/packages/apps/reputation-oracle/server/src/common/types/manifest.ts b/packages/apps/reputation-oracle/server/src/common/types/manifest.ts index 4aefc89961..f5ca9efdba 100644 --- a/packages/apps/reputation-oracle/server/src/common/types/manifest.ts +++ b/packages/apps/reputation-oracle/server/src/common/types/manifest.ts @@ -5,16 +5,18 @@ import { MarketingJobType, } from '@/common/enums'; -export type FortuneManifest = { +export interface BaseManifest< + TJobType extends FortuneJobType | MarketingJobType, +> { submissionsRequired: number; - requestType: FortuneJobType; -}; + jobType: TJobType; +} -export type MarketingManifest = { - job_type: MarketingJobType; - submissions_required: number; - end_date?: string; -}; +export type FortuneManifest = BaseManifest; + +export interface MarketingManifest extends BaseManifest { + endDate?: string; +} export type CvatManifest = { annotation: { diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts index 916f276d6a..90ede4a9ff 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts @@ -49,13 +49,11 @@ import { } from './fixtures/escrow-completion'; import { CvatPayoutsCalculator, - FortunePayoutsCalculator, - MarketingPayoutsCalculator, + DefaultPayoutsCalculator, } from './payouts-calculation'; import { CvatResultsProcessor, - FortuneResultsProcessor, - MarketingResultsProcessor, + DefaultResultsProcessor, } from './results-processing'; const mockServerConfigService = { @@ -69,12 +67,10 @@ const mockWeb3Service = createMock(); const mockStorageService = createMock(); const mockOutgoingWebhookService = createMock(); const mockReputationService = createMock(); -const mockFortuneResultsProcessor = createMock(); const mockCvatResultsProcessor = createMock(); -const mockMarketingResultsProcessor = createMock(); -const mockFortunePayoutsCalculator = createMock(); +const mockDefaultResultsProcessor = createMock(); const mockCvatPayoutsCalculator = createMock(); -const mockMarketingPayoutsCalculator = createMock(); +const mockDefaultPayoutsCalculator = createMock(); const mockedEscrowClient = jest.mocked(EscrowClient); const mockedEscrowUtils = jest.mocked(EscrowUtils); @@ -120,29 +116,21 @@ describe('EscrowCompletionService', () => { useValue: mockReputationService, }, { - provide: FortuneResultsProcessor, - useValue: mockFortuneResultsProcessor, + provide: DefaultResultsProcessor, + useValue: mockDefaultResultsProcessor, }, { provide: CvatResultsProcessor, useValue: mockCvatResultsProcessor, }, { - provide: MarketingResultsProcessor, - useValue: mockMarketingResultsProcessor, - }, - { - provide: FortunePayoutsCalculator, - useValue: mockFortunePayoutsCalculator, + provide: DefaultPayoutsCalculator, + useValue: mockDefaultPayoutsCalculator, }, { provide: CvatPayoutsCalculator, useValue: mockCvatPayoutsCalculator, }, - { - provide: MarketingPayoutsCalculator, - useValue: mockMarketingPayoutsCalculator, - }, ], }).compile(); @@ -332,7 +320,7 @@ describe('EscrowCompletionService', () => { ); const finalResultsUrl = faker.internet.url(); const finalResultsHash = faker.string.hexadecimal({ length: 42 }); - mockFortuneResultsProcessor.storeResults.mockResolvedValueOnce({ + mockDefaultResultsProcessor.storeResults.mockResolvedValueOnce({ url: finalResultsUrl, hash: finalResultsHash, }); @@ -342,7 +330,7 @@ describe('EscrowCompletionService', () => { amount: faker.number.bigInt(), }, ]; - mockFortunePayoutsCalculator.calculate.mockResolvedValueOnce( + mockDefaultPayoutsCalculator.calculate.mockResolvedValueOnce( calculatedPayouts, ); @@ -355,8 +343,8 @@ describe('EscrowCompletionService', () => { expect(mockStorageService.downloadJsonLikeData).toHaveBeenCalledWith( manifestUrl, ); - expect(mockFortuneResultsProcessor.storeResults).toHaveBeenCalledTimes(1); - expect(mockFortuneResultsProcessor.storeResults).toHaveBeenCalledWith( + expect(mockDefaultResultsProcessor.storeResults).toHaveBeenCalledTimes(1); + expect(mockDefaultResultsProcessor.storeResults).toHaveBeenCalledWith( pendingRecord.chainId, pendingRecord.escrowAddress, fortuneManifest, @@ -369,8 +357,8 @@ describe('EscrowCompletionService', () => { }), ); - expect(mockFortunePayoutsCalculator.calculate).toHaveBeenCalledTimes(1); - expect(mockFortunePayoutsCalculator.calculate).toHaveBeenCalledWith({ + expect(mockDefaultPayoutsCalculator.calculate).toHaveBeenCalledTimes(1); + expect(mockDefaultPayoutsCalculator.calculate).toHaveBeenCalledWith({ manifest: fortuneManifest, finalResultsUrl, chainId: pendingRecord.chainId, @@ -425,7 +413,7 @@ describe('EscrowCompletionService', () => { amount: faker.number.bigInt(), }; - mockFortunePayoutsCalculator.calculate.mockResolvedValueOnce( + mockDefaultPayoutsCalculator.calculate.mockResolvedValueOnce( faker.helpers.shuffle([ firstAddressPayout, secondAddressPayout, @@ -729,7 +717,7 @@ describe('EscrowCompletionService', () => { ); const manifest = generateFortuneManifest(); - jobRequestType = manifest.requestType; + jobRequestType = manifest.jobType; mockedEscrowUtils.getEscrow.mockResolvedValue({ manifest: faker.internet.url(), } as unknown as IEscrow); @@ -1233,14 +1221,6 @@ describe('EscrowCompletionService', () => { }); describe('getEscrowResultsProcessor', () => { - it.each(Object.values(FortuneJobType))( - 'should return fortune processor for "%s" job type', - (jobRequestType) => { - expect(service['getEscrowResultsProcessor'](jobRequestType)).toBe( - mockFortuneResultsProcessor, - ); - }, - ); it.each(Object.values(CvatJobType))( 'should return cvat processor for "%s" job type', (jobRequestType) => { @@ -1249,25 +1229,20 @@ describe('EscrowCompletionService', () => { ); }, ); - it.each(Object.values(MarketingJobType))( - 'should return marketing processor for "%s" job type', + it.each([ + ...Object.values(FortuneJobType), + ...Object.values(MarketingJobType), + ])( + 'should return default processor for "%s" job type', (jobRequestType) => { expect(service['getEscrowResultsProcessor'](jobRequestType)).toBe( - mockMarketingResultsProcessor, + mockDefaultResultsProcessor, ); }, ); }); describe('getEscrowPayoutsCalculator', () => { - it.each(Object.values(FortuneJobType))( - 'should return fortune calculator for "%s" job type', - (jobRequestType) => { - expect(service['getEscrowPayoutsCalculator'](jobRequestType)).toBe( - mockFortunePayoutsCalculator, - ); - }, - ); it.each(Object.values(CvatJobType))( 'should return cvat calculator for "%s" job type', (jobRequestType) => { @@ -1276,11 +1251,14 @@ describe('EscrowCompletionService', () => { ); }, ); - it.each(Object.values(MarketingJobType))( - 'should return marketing calculator for "%s" job type', + it.each([ + ...Object.values(FortuneJobType), + ...Object.values(MarketingJobType), + ])( + 'should return default calculator for "%s" job type', (jobRequestType) => { expect(service['getEscrowPayoutsCalculator'](jobRequestType)).toBe( - mockMarketingPayoutsCalculator, + mockDefaultPayoutsCalculator, ); }, ); diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts index cadc222b3a..31f3fb9a9b 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts @@ -38,16 +38,14 @@ import { EscrowPayoutsBatchEntity } from './escrow-payouts-batch.entity'; import { EscrowPayoutsBatchRepository } from './escrow-payouts-batch.repository'; import { CvatPayoutsCalculator, - FortunePayoutsCalculator, + DefaultPayoutsCalculator, EscrowPayoutsCalculator, CalculatedPayout, - MarketingPayoutsCalculator, } from './payouts-calculation'; import { CvatResultsProcessor, + DefaultResultsProcessor, EscrowResultsProcessor, - FortuneResultsProcessor, - MarketingResultsProcessor, } from './results-processing'; @Injectable() @@ -66,11 +64,9 @@ export class EscrowCompletionService { private readonly outgoingWebhookService: OutgoingWebhookService, private readonly reputationService: ReputationService, private readonly cvatResultsProcessor: CvatResultsProcessor, - private readonly fortuneResultsProcessor: FortuneResultsProcessor, - private readonly marketingResultsProcessor: MarketingResultsProcessor, + private readonly defaultResultsProcessor: DefaultResultsProcessor, private readonly cvatPayoutsCalculator: CvatPayoutsCalculator, - private readonly fortunePayoutsCalculator: FortunePayoutsCalculator, - private readonly marketingPayoutsCalculator: MarketingPayoutsCalculator, + private readonly defaultPayoutsCalculator: DefaultPayoutsCalculator, ) {} async createEscrowCompletion( @@ -544,40 +540,20 @@ export class EscrowCompletionService { private getEscrowResultsProcessor( jobRequestType: JobRequestType, ): EscrowResultsProcessor { - if (manifestUtils.isFortuneJobType(jobRequestType)) { - return this.fortuneResultsProcessor; - } - - if (manifestUtils.isMarketingJobType(jobRequestType)) { - return this.marketingResultsProcessor; - } - if (manifestUtils.isCvatJobType(jobRequestType)) { return this.cvatResultsProcessor; } - throw new Error( - `No escrow results processor defined for '${jobRequestType}' jobs`, - ); + return this.defaultResultsProcessor; } private getEscrowPayoutsCalculator( jobRequestType: JobRequestType, ): EscrowPayoutsCalculator { - if (manifestUtils.isFortuneJobType(jobRequestType)) { - return this.fortunePayoutsCalculator; - } - - if (manifestUtils.isMarketingJobType(jobRequestType)) { - return this.marketingPayoutsCalculator; - } - if (manifestUtils.isCvatJobType(jobRequestType)) { return this.cvatPayoutsCalculator; } - throw new Error( - `No escrow payouts calculator defined for '${jobRequestType}' jobs`, - ); + return this.defaultPayoutsCalculator; } } diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/fortune.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/fortune.ts index 7978fdce20..c7312abe58 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/fortune.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/fortune.ts @@ -1,19 +1,28 @@ import { faker } from '@faker-js/faker'; import { FortuneJobType } from '@/common/enums'; -import { FortuneFinalResult, FortuneManifest } from '@/common/types'; +import { + FortuneFinalResult, + FortuneManifest, + VerificationResult, +} from '@/common/types'; export function generateFortuneManifest(): FortuneManifest { return { - requestType: FortuneJobType.FORTUNE, + jobType: FortuneJobType.FORTUNE, submissionsRequired: faker.number.int({ min: 2, max: 5 }), }; } -export function generateFortuneSolution(error?: string): FortuneFinalResult { +export function generateFortuneSolution( + rejectionReason?: string, +): FortuneFinalResult { return { workerAddress: faker.finance.ethereumAddress(), solution: faker.string.sample(), - error: error as FortuneFinalResult['error'], + verificationResult: rejectionReason + ? VerificationResult.Rejected + : VerificationResult.Accepted, + ...(rejectionReason ? { rejectionReason } : {}), }; } diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/marketing.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/marketing.ts index 46f2da3b14..2e7b7ece8b 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/marketing.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/fixtures/marketing.ts @@ -9,8 +9,8 @@ import { export function generateMarketingManifest(): MarketingManifest { return { - job_type: MarketingJobType.SOCIAL_MEDIA_PROMOTION, - submissions_required: faker.number.int({ min: 2, max: 5 }), + jobType: MarketingJobType.SOCIAL_MEDIA_PROMOTION, + submissionsRequired: faker.number.int({ min: 2, max: 5 }), }; } diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts index 75d14dcc23..bde421dbca 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts @@ -9,13 +9,13 @@ import { StorageService } from '@/modules/storage'; import { Web3Service } from '@/modules/web3'; import { - CalclulatePayoutsInput, CalculatedPayout, + CalculatePayoutsInput, EscrowPayoutsCalculator, } from './types'; type CalculateCvatPayoutsInput = OverrideProperties< - CalclulatePayoutsInput, + CalculatePayoutsInput, { manifest: CvatManifest } >; diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.spec.ts new file mode 100644 index 0000000000..e3ea54c319 --- /dev/null +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.spec.ts @@ -0,0 +1,137 @@ +jest.mock('@human-protocol/sdk'); + +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { EscrowClient } from '@human-protocol/sdk'; +import { Test } from '@nestjs/testing'; + +import { MarketingJobType } from '@/common/enums'; +import { + BaseFinalResult, + BaseManifest, + VerificationResult, +} from '@/common/types'; +import { StorageService } from '@/modules/storage'; +import { Web3Service } from '@/modules/web3'; +import { generateTestnetChainId } from '@/modules/web3/fixtures'; + +import { DefaultPayoutsCalculator } from './default-payouts-calculator'; + +const mockedStorageService = createMock(); +const mockedWeb3Service = createMock(); +const mockedEscrowClient = jest.mocked(EscrowClient); + +describe('DefaultPayoutsCalculator', () => { + let calculator: DefaultPayoutsCalculator; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + DefaultPayoutsCalculator, + { + provide: StorageService, + useValue: mockedStorageService, + }, + { + provide: Web3Service, + useValue: mockedWeb3Service, + }, + ], + }).compile(); + + calculator = moduleRef.get( + DefaultPayoutsCalculator, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('calculate', () => { + const reservedFunds = BigInt(faker.number.int({ min: 1000 }).toString()); + const mockedGetReservedFunds = jest + .fn() + .mockImplementation(async () => reservedFunds); + + beforeAll(() => { + mockedEscrowClient.build.mockResolvedValue({ + getReservedFunds: mockedGetReservedFunds, + } as unknown as EscrowClient); + }); + + it('should calculate equal payouts for accepted results', async () => { + const acceptedResults = [ + generateFinalResult(VerificationResult.Accepted), + generateFinalResult(VerificationResult.Accepted), + ]; + const rejectedResult = generateFinalResult(VerificationResult.Rejected); + const results = faker.helpers.shuffle([ + ...acceptedResults, + rejectedResult, + ]); + const manifest: BaseManifest = { + jobType: MarketingJobType.SOCIAL_MEDIA_PROMOTION, + submissionsRequired: faker.number.int({ min: 2, max: 5 }), + }; + + mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce(results); + + const payouts = await calculator.calculate({ + chainId: generateTestnetChainId(), + escrowAddress: faker.finance.ethereumAddress(), + finalResultsUrl: faker.internet.url(), + manifest, + }); + + const expectedPayouts = acceptedResults.map((result) => ({ + address: result.workerAddress, + amount: reservedFunds / BigInt(manifest.submissionsRequired), + })); + + expect(normalizePayouts(payouts)).toEqual( + normalizePayouts(expectedPayouts), + ); + }); + + it('should return an empty list when there are no accepted results', async () => { + mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce([ + generateFinalResult(VerificationResult.Rejected), + generateFinalResult(VerificationResult.Rejected), + ]); + + const payouts = await calculator.calculate({ + chainId: generateTestnetChainId(), + escrowAddress: faker.finance.ethereumAddress(), + finalResultsUrl: faker.internet.url(), + manifest: { + jobType: MarketingJobType.SOCIAL_MEDIA_PROMOTION, + submissionsRequired: faker.number.int({ min: 2, max: 5 }), + }, + }); + + expect(payouts).toEqual([]); + }); + }); +}); + +function generateFinalResult( + verificationResult: VerificationResult, +): BaseFinalResult { + return { + workerAddress: faker.finance.ethereumAddress(), + verificationResult, + ...(verificationResult === VerificationResult.Rejected + ? { rejectionReason: faker.lorem.word() } + : {}), + }; +} + +function normalizePayouts(items: { address: string; amount: bigint }[]) { + return items + .map((item) => ({ + address: item.address.toLowerCase(), + amount: item.amount.toString(), + })) + .sort((a, b) => a.address.localeCompare(b.address)); +} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.ts similarity index 72% rename from packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.ts rename to packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.ts index 3c60ad7206..5bb3c8e06c 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/default-payouts-calculator.ts @@ -1,9 +1,9 @@ import { EscrowClient } from '@human-protocol/sdk'; import { Injectable } from '@nestjs/common'; -import type { OverrideProperties } from 'type-fest'; import { - MarketingFinalResult, + BaseFinalResult, + FortuneManifest, MarketingManifest, VerificationResult, } from '@/common/types'; @@ -11,18 +11,15 @@ import { StorageService } from '@/modules/storage'; import { Web3Service } from '@/modules/web3'; import { - CalclulatePayoutsInput, CalculatedPayout, + CalculatePayoutsInput, EscrowPayoutsCalculator, } from './types'; -type CalculateMarketingPayoutsInput = OverrideProperties< - CalclulatePayoutsInput, - { manifest: MarketingManifest } ->; +type DefaultPayoutsManifest = FortuneManifest | MarketingManifest; @Injectable() -export class MarketingPayoutsCalculator implements EscrowPayoutsCalculator { +export class DefaultPayoutsCalculator implements EscrowPayoutsCalculator { constructor( private readonly storageService: StorageService, private readonly web3Service: Web3Service, @@ -33,11 +30,13 @@ export class MarketingPayoutsCalculator implements EscrowPayoutsCalculator { chainId, escrowAddress, finalResultsUrl, - }: CalculateMarketingPayoutsInput): Promise { + }: CalculatePayoutsInput & { + manifest: DefaultPayoutsManifest; + }): Promise { const signer = this.web3Service.getSigner(chainId); const escrowClient = await EscrowClient.build(signer); const finalResults = - await this.storageService.downloadJsonLikeData( + await this.storageService.downloadJsonLikeData( finalResultsUrl, ); @@ -52,7 +51,7 @@ export class MarketingPayoutsCalculator implements EscrowPayoutsCalculator { } const reservedFunds = await escrowClient.getReservedFunds(escrowAddress); - const payoutAmount = reservedFunds / BigInt(manifest.submissions_required); + const payoutAmount = reservedFunds / BigInt(manifest.submissionsRequired); return recipients.map((recipient) => ({ address: recipient, diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.spec.ts deleted file mode 100644 index d44c26dc29..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -jest.mock('@human-protocol/sdk'); - -import { faker } from '@faker-js/faker'; -import { createMock } from '@golevelup/ts-jest'; -import { EscrowClient } from '@human-protocol/sdk'; -import { Test } from '@nestjs/testing'; - -import { StorageService } from '@/modules/storage'; -import { Web3Service } from '@/modules/web3'; -import { generateTestnetChainId } from '@/modules/web3/fixtures'; - -import { generateFortuneManifest, generateFortuneSolution } from '../fixtures'; -import { FortunePayoutsCalculator } from './fortune-payouts-calculator'; - -const mockedStorageService = createMock(); -const mockedWeb3Service = createMock(); -const mockedEscrowClient = jest.mocked(EscrowClient); - -describe('FortunePayoutsCalculator', () => { - let calculator: FortunePayoutsCalculator; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - FortunePayoutsCalculator, - { - provide: StorageService, - useValue: mockedStorageService, - }, - { - provide: Web3Service, - useValue: mockedWeb3Service, - }, - ], - }).compile(); - - calculator = moduleRef.get( - FortunePayoutsCalculator, - ); - - const mockedGetTokenAddress = jest.fn().mockImplementation(async () => { - return faker.finance.ethereumAddress(); - }); - mockedEscrowClient.build.mockResolvedValue({ - getTokenAddress: mockedGetTokenAddress, - } as unknown as EscrowClient); - }); - - describe('calculate', () => { - const balance = BigInt(faker.number.int({ min: 1000 }).toString()); - const mockedGetReservedFunds = jest - .fn() - .mockImplementation(async () => balance); - - beforeAll(() => { - mockedEscrowClient.build.mockResolvedValue({ - getReservedFunds: mockedGetReservedFunds, - } as unknown as EscrowClient); - }); - it('should properly calculate payouts', async () => { - const validSolutions = [ - generateFortuneSolution(), - generateFortuneSolution(), - ]; - const results = faker.helpers.shuffle([ - ...validSolutions, - generateFortuneSolution('curse_word'), - generateFortuneSolution('duplicated'), - generateFortuneSolution(faker.string.sample()), - ]); - mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce(results); - const resultsUrl = faker.internet.url(); - const manifest = generateFortuneManifest(); - - const tokenDecimals = BigInt(faker.number.int({ min: 6, max: 18 })); - mockedWeb3Service.getTokenDecimals.mockResolvedValueOnce(tokenDecimals); - - const payouts = await calculator.calculate({ - chainId: generateTestnetChainId(), - escrowAddress: faker.finance.ethereumAddress(), - finalResultsUrl: resultsUrl, - manifest, - }); - - const expectedPayouts = validSolutions.map((s) => ({ - address: s.workerAddress, - amount: balance / BigInt(validSolutions.length), - })); - - const normalize = (arr: { address: string; amount: bigint }[]) => - arr - .map((p) => ({ - address: p.address.toLowerCase(), - amount: p.amount.toString(), - })) - .sort((a, b) => a.address.localeCompare(b.address)); - - expect(normalize(payouts)).toEqual(normalize(expectedPayouts)); - - expect(mockedStorageService.downloadJsonLikeData).toHaveBeenCalledWith( - resultsUrl, - ); - }); - }); -}); diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.ts deleted file mode 100644 index f2d5221d24..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/fortune-payouts-calculator.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { EscrowClient } from '@human-protocol/sdk'; -import { Injectable } from '@nestjs/common'; -import type { OverrideProperties } from 'type-fest'; - -import { FortuneFinalResult, FortuneManifest } from '@/common/types'; -import { StorageService } from '@/modules/storage'; -import { Web3Service } from '@/modules/web3'; - -import { - CalclulatePayoutsInput, - CalculatedPayout, - EscrowPayoutsCalculator, -} from './types'; - -type CalculateFortunePayoutsInput = OverrideProperties< - CalclulatePayoutsInput, - { manifest: FortuneManifest } ->; - -@Injectable() -export class FortunePayoutsCalculator implements EscrowPayoutsCalculator { - constructor( - private readonly storageService: StorageService, - private readonly web3Service: Web3Service, - ) {} - - async calculate({ - chainId, - escrowAddress, - finalResultsUrl, - }: CalculateFortunePayoutsInput): Promise { - const signer = this.web3Service.getSigner(chainId); - const escrowClient = await EscrowClient.build(signer); - const finalResults = - await this.storageService.downloadJsonLikeData( - finalResultsUrl, - ); - - const recipients = finalResults - .filter((result) => !result.error) - .map((item) => item.workerAddress); - - const reservedFunds = await escrowClient.getReservedFunds(escrowAddress); - const payoutAmount = reservedFunds / BigInt(recipients.length); - - return recipients.map((recipient) => ({ - address: recipient, - amount: payoutAmount, - })); - } -} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/index.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/index.ts index abd815d57a..d2f90bae68 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/index.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/index.ts @@ -1,4 +1,3 @@ export { CvatPayoutsCalculator } from './cvat-payouts-calculator'; -export { FortunePayoutsCalculator } from './fortune-payouts-calculator'; -export { MarketingPayoutsCalculator } from './marketing-payouts-calculator'; +export { DefaultPayoutsCalculator } from './default-payouts-calculator'; export * from './types'; diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.spec.ts deleted file mode 100644 index cb8222d9a7..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/marketing-payouts-calculator.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -jest.mock('@human-protocol/sdk'); - -import { faker } from '@faker-js/faker'; -import { createMock } from '@golevelup/ts-jest'; -import { EscrowClient } from '@human-protocol/sdk'; -import { Test } from '@nestjs/testing'; - -import { VerificationResult } from '@/common/types'; -import { StorageService } from '@/modules/storage'; -import { Web3Service } from '@/modules/web3'; -import { generateTestnetChainId } from '@/modules/web3/fixtures'; - -import { - generateMarketingManifest, - generateMarketingResult, -} from '../fixtures'; -import { MarketingPayoutsCalculator } from './marketing-payouts-calculator'; - -const mockedStorageService = createMock(); -const mockedWeb3Service = createMock(); -const mockedEscrowClient = jest.mocked(EscrowClient); - -describe('MarketingPayoutsCalculator', () => { - let calculator: MarketingPayoutsCalculator; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - MarketingPayoutsCalculator, - { - provide: StorageService, - useValue: mockedStorageService, - }, - { - provide: Web3Service, - useValue: mockedWeb3Service, - }, - ], - }).compile(); - - calculator = moduleRef.get( - MarketingPayoutsCalculator, - ); - }); - - describe('calculate', () => { - const balance = BigInt(faker.number.int({ min: 1000 }).toString()); - const mockedGetReservedFunds = jest - .fn() - .mockImplementation(async () => balance); - - beforeAll(() => { - mockedEscrowClient.build.mockResolvedValue({ - getReservedFunds: mockedGetReservedFunds, - } as unknown as EscrowClient); - }); - - it('should properly calculate payouts for accepted results only', async () => { - const acceptedResults = [ - generateMarketingResult(VerificationResult.Accepted), - generateMarketingResult(VerificationResult.Accepted), - ]; - mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce( - faker.helpers.shuffle([ - ...acceptedResults, - generateMarketingResult(VerificationResult.Rejected), - ]), - ); - - const manifest = generateMarketingManifest(); - const payouts = await calculator.calculate({ - chainId: generateTestnetChainId(), - escrowAddress: faker.finance.ethereumAddress(), - finalResultsUrl: faker.internet.url(), - manifest: manifest, - }); - - const expectedPayouts = acceptedResults.map((result) => ({ - address: result.workerAddress, - amount: balance / BigInt(manifest.submissions_required), - })); - - const normalize = (items: { address: string; amount: bigint }[]) => - items - .map((item) => ({ - address: item.address.toLowerCase(), - amount: item.amount.toString(), - })) - .sort((a, b) => a.address.localeCompare(b.address)); - - expect(normalize(payouts)).toEqual(normalize(expectedPayouts)); - }); - - it('should return an empty list when there are no accepted results', async () => { - mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce([ - generateMarketingResult(VerificationResult.Rejected), - generateMarketingResult(VerificationResult.Rejected), - ]); - - const payouts = await calculator.calculate({ - chainId: generateTestnetChainId(), - escrowAddress: faker.finance.ethereumAddress(), - finalResultsUrl: faker.internet.url(), - manifest: generateMarketingManifest(), - }); - - expect(payouts).toEqual([]); - }); - }); -}); diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/module.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/module.ts index 31147e9502..30eebe33a4 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/module.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/module.ts @@ -4,20 +4,11 @@ import { StorageModule } from '@/modules/storage'; import { Web3Module } from '@/modules/web3'; import { CvatPayoutsCalculator } from './cvat-payouts-calculator'; -import { FortunePayoutsCalculator } from './fortune-payouts-calculator'; -import { MarketingPayoutsCalculator } from './marketing-payouts-calculator'; +import { DefaultPayoutsCalculator } from './default-payouts-calculator'; @Module({ imports: [StorageModule, Web3Module], - providers: [ - CvatPayoutsCalculator, - FortunePayoutsCalculator, - MarketingPayoutsCalculator, - ], - exports: [ - CvatPayoutsCalculator, - FortunePayoutsCalculator, - MarketingPayoutsCalculator, - ], + providers: [CvatPayoutsCalculator, DefaultPayoutsCalculator], + exports: [CvatPayoutsCalculator, DefaultPayoutsCalculator], }) export class EscrowPayoutsCalculationModule {} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/types.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/types.ts index 272d8c1dc9..861093d2cf 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/types.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/types.ts @@ -7,7 +7,7 @@ export type CalculatedPayout = { amount: bigint; }; -export type CalclulatePayoutsInput = { +export type CalculatePayoutsInput = { manifest: JobManifest; chainId: ChainId; escrowAddress: string; @@ -15,5 +15,5 @@ export type CalclulatePayoutsInput = { }; export interface EscrowPayoutsCalculator { - calculate(input: CalclulatePayoutsInput): Promise; + calculate(input: CalculatePayoutsInput): Promise; } diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.spec.ts similarity index 58% rename from packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.spec.ts rename to packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.spec.ts index 9cba041828..4b30ab0d81 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.spec.ts @@ -8,23 +8,25 @@ import { StorageService } from '@/modules/storage'; import { Web3Service } from '@/modules/web3'; import { + generateFortuneManifest, + generateFortuneSolution, generateMarketingManifest, generateMarketingResult, } from '../fixtures'; +import { DefaultResultsProcessor } from './default-results-processor'; import { BaseEscrowResultsProcessor } from './escrow-results-processor'; -import { MarketingResultsProcessor } from './marketing-results-processor'; const mockedStorageService = createMock(); const mockedPgpEncryptionService = createMock(); const mockedWeb3Service = createMock(); -describe('MarketingResultsProcessor', () => { - let processor: MarketingResultsProcessor; +describe('DefaultResultsProcessor', () => { + let processor: DefaultResultsProcessor; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ providers: [ - MarketingResultsProcessor, + DefaultResultsProcessor, { provide: StorageService, useValue: mockedStorageService, @@ -40,9 +42,7 @@ describe('MarketingResultsProcessor', () => { ], }).compile(); - processor = moduleRef.get( - MarketingResultsProcessor, - ); + processor = moduleRef.get(DefaultResultsProcessor); }); it('should be properly initialized', () => { @@ -61,13 +61,13 @@ describe('MarketingResultsProcessor', () => { }); describe('assertResultsComplete', () => { - const testManifest = generateMarketingManifest(); + const marketingManifest = generateMarketingManifest(); it('throws if results is not json', async () => { await expect( processor['assertResultsComplete']( Buffer.from(faker.lorem.words()), - testManifest, + marketingManifest, ), ).rejects.toThrow('Failed to parse results data'); }); @@ -76,7 +76,7 @@ describe('MarketingResultsProcessor', () => { await expect( processor['assertResultsComplete']( Buffer.from(JSON.stringify({})), - testManifest, + marketingManifest, ), ).rejects.toThrow('No final results found'); }); @@ -85,36 +85,64 @@ describe('MarketingResultsProcessor', () => { await expect( processor['assertResultsComplete']( Buffer.from(JSON.stringify([])), - testManifest, + marketingManifest, ), ).rejects.toThrow('No final results found'); }); - it('passes when there are accepted and rejected results', async () => { + it('passes for marketing when results are not empty', async () => { await expect( processor['assertResultsComplete']( Buffer.from( JSON.stringify([ - generateMarketingResult(VerificationResult.Accepted), generateMarketingResult(VerificationResult.Rejected), ]), ), - testManifest, + marketingManifest, ), ).resolves.not.toThrow(); }); - it('passes when there are only rejected results', async () => { + it('throws for fortune if accepted results are below required submissions', async () => { + const fortuneManifest = generateFortuneManifest(); + const results = Array.from( + { length: fortuneManifest.submissionsRequired }, + () => generateFortuneSolution(faker.string.sample()), + ); + results.push(generateFortuneSolution()); + await expect( processor['assertResultsComplete']( - Buffer.from( - JSON.stringify([ - generateMarketingResult(VerificationResult.Rejected), - ]), - ), - testManifest, + Buffer.from(JSON.stringify(results)), + fortuneManifest, + ), + ).rejects.toThrow('Not all required results have been sent'); + }); + + it('passes for fortune when required accepted results are present', async () => { + const fortuneManifest = generateFortuneManifest(); + const results = Array.from( + { length: fortuneManifest.submissionsRequired }, + () => generateFortuneSolution(), + ); + results.push(generateFortuneSolution(faker.string.sample())); + + await expect( + processor['assertResultsComplete']( + Buffer.from(JSON.stringify(results)), + fortuneManifest, ), ).resolves.not.toThrow(); }); }); + + describe('getFinalResultsFileName', () => { + it('should return hash with extension', () => { + const hash = faker.string.hexadecimal({ prefix: '', length: 40 }); + + const name = processor['getFinalResultsFileName'](hash); + + expect(name).toBe(`${hash}.json`); + }); + }); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.ts new file mode 100644 index 0000000000..2d28d93801 --- /dev/null +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/default-results-processor.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; + +import { FortuneJobType } from '@/common/enums'; +import { + BaseFinalResult, + FortuneManifest, + MarketingManifest, + VerificationResult, +} from '@/common/types'; + +import { BaseEscrowResultsProcessor } from './escrow-results-processor'; + +type DefaultResultsManifest = FortuneManifest | MarketingManifest; + +@Injectable() +export class DefaultResultsProcessor extends BaseEscrowResultsProcessor { + protected constructIntermediateResultsUrl(baseUrl: string): string { + return baseUrl; + } + + protected async assertResultsComplete( + resultsFileContent: Buffer, + manifest: DefaultResultsManifest, + ): Promise { + let finalResults: BaseFinalResult[]; + try { + finalResults = JSON.parse(resultsFileContent.toString()); + } catch { + throw new Error('Failed to parse results data'); + } + + if (!Array.isArray(finalResults) || !finalResults.length) { + throw new Error('No final results found'); + } + + if (manifest.jobType !== FortuneJobType.FORTUNE) { + return; + } + + const acceptedResults = finalResults.filter( + (result) => result.verificationResult === VerificationResult.Accepted, + ); + if (acceptedResults.length < manifest.submissionsRequired) { + throw new Error('Not all required results have been sent'); + } + } + + protected getFinalResultsFileName(hash: string): string { + return `${hash}.json`; + } +} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.spec.ts deleted file mode 100644 index ddfc763a10..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { createMock } from '@golevelup/ts-jest'; -import { Test } from '@nestjs/testing'; - -import { FortuneFinalResult } from '@/common/types'; -import { PgpEncryptionService } from '@/modules/encryption'; -import { StorageService } from '@/modules/storage'; -import { Web3Service } from '@/modules/web3'; - -import { generateFortuneManifest, generateFortuneSolution } from '../fixtures'; -import { BaseEscrowResultsProcessor } from './escrow-results-processor'; -import { FortuneResultsProcessor } from './fortune-results-processor'; - -const mockedStorageService = createMock(); -const mockedPgpEncryptionService = createMock(); -const mockedWeb3Service = createMock(); - -describe('FortuneResultsProcessor', () => { - let processor: FortuneResultsProcessor; - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - FortuneResultsProcessor, - { - provide: StorageService, - useValue: mockedStorageService, - }, - { - provide: PgpEncryptionService, - useValue: mockedPgpEncryptionService, - }, - { - provide: Web3Service, - useValue: mockedWeb3Service, - }, - ], - }).compile(); - - processor = moduleRef.get(FortuneResultsProcessor); - }); - - it('should be properly initialized', () => { - expect(processor).toBeDefined(); - expect(processor).toBeInstanceOf(BaseEscrowResultsProcessor); - }); - - describe('constructIntermediateResultsUrl', () => { - it('should return intermediate results url as is', () => { - const baseUrl = faker.internet.url(); - const url = processor['constructIntermediateResultsUrl'](baseUrl); - - expect(url).toBe(baseUrl); - }); - }); - - describe('assertResultsComplete', () => { - const testManifest = generateFortuneManifest(); - - it('throws if results is not json', async () => { - await expect( - processor['assertResultsComplete']( - Buffer.from(faker.lorem.words()), - testManifest, - ), - ).rejects.toThrow('Failed to parse results data'); - }); - - it('throws if results is not array', async () => { - await expect( - processor['assertResultsComplete']( - Buffer.from(JSON.stringify({})), - testManifest, - ), - ).rejects.toThrow('No intermediate results found'); - }); - - it('throws if results is empty array', async () => { - await expect( - processor['assertResultsComplete']( - Buffer.from(JSON.stringify([])), - testManifest, - ), - ).rejects.toThrow('No intermediate results found'); - }); - - it('throws if not all submissions sent', async () => { - const solutions: FortuneFinalResult[] = Array.from( - { length: testManifest.submissionsRequired }, - () => generateFortuneSolution(), - ); - solutions.pop(); - solutions.push(generateFortuneSolution(faker.string.sample())); - - await expect( - processor['assertResultsComplete']( - Buffer.from(JSON.stringify(solutions)), - testManifest, - ), - ).rejects.toThrow('Not all required solutions have been sent'); - }); - - it('passes when all solutions sent', async () => { - const solutions: FortuneFinalResult[] = Array.from( - { length: testManifest.submissionsRequired * 2 }, - (i: number) => - generateFortuneSolution( - i % 2 === 0 ? faker.string.sample() : undefined, - ), - ); - - await expect( - processor['assertResultsComplete']( - Buffer.from(JSON.stringify(solutions)), - testManifest, - ), - ).resolves.not.toThrow(); - }); - }); - - describe('getFinalResultsFileName', () => { - it('should return hash with extension', () => { - const hash = faker.string.hexadecimal({ prefix: '', length: 40 }); - - const name = processor['getFinalResultsFileName'](hash); - - expect(name).toBe(`${hash}.json`); - }); - }); -}); diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.ts deleted file mode 100644 index c11bbe6cc9..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { FortuneFinalResult, FortuneManifest } from '@/common/types'; - -import { BaseEscrowResultsProcessor } from './escrow-results-processor'; - -@Injectable() -export class FortuneResultsProcessor extends BaseEscrowResultsProcessor { - protected constructIntermediateResultsUrl(baseUrl: string): string { - return baseUrl; - } - - protected async assertResultsComplete( - resultsFileContent: Buffer, - manifest: FortuneManifest, - ): Promise { - let intermediateResults: FortuneFinalResult[]; - try { - intermediateResults = JSON.parse(resultsFileContent.toString()); - } catch { - throw new Error('Failed to parse results data'); - } - - if (!intermediateResults.length) { - throw new Error('No intermediate results found'); - } - - const validResults = intermediateResults.filter((result) => !result.error); - if (validResults.length < manifest.submissionsRequired) { - throw new Error('Not all required solutions have been sent'); - } - } - - protected getFinalResultsFileName(hash: string): string { - return `${hash}.json`; - } -} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/index.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/index.ts index c6c989a922..6e86b4e4b5 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/index.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/index.ts @@ -1,4 +1,3 @@ export { CvatResultsProcessor } from './cvat-results-processor'; +export { DefaultResultsProcessor } from './default-results-processor'; export { type EscrowResultsProcessor } from './escrow-results-processor'; -export { FortuneResultsProcessor } from './fortune-results-processor'; -export { MarketingResultsProcessor } from './marketing-results-processor'; diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.ts deleted file mode 100644 index eeca140661..0000000000 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/marketing-results-processor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { MarketingFinalResult, MarketingManifest } from '@/common/types'; - -import { BaseEscrowResultsProcessor } from './escrow-results-processor'; - -@Injectable() -export class MarketingResultsProcessor extends BaseEscrowResultsProcessor { - protected constructIntermediateResultsUrl(baseUrl: string): string { - return baseUrl; - } - - protected async assertResultsComplete( - resultsFileContent: Buffer, - _manifest: MarketingManifest, - ): Promise { - let finalResults: MarketingFinalResult[]; - try { - finalResults = JSON.parse(resultsFileContent.toString()); - } catch { - throw new Error('Failed to parse results data'); - } - - if (!Array.isArray(finalResults) || !finalResults.length) { - throw new Error('No final results found'); - } - } - - protected getFinalResultsFileName(hash: string): string { - return `${hash}.json`; - } -} diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/module.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/module.ts index dbde327c09..b91f5f62d2 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/module.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/module.ts @@ -5,20 +5,11 @@ import { StorageModule } from '@/modules/storage'; import { Web3Module } from '@/modules/web3'; import { CvatResultsProcessor } from './cvat-results-processor'; -import { FortuneResultsProcessor } from './fortune-results-processor'; -import { MarketingResultsProcessor } from './marketing-results-processor'; +import { DefaultResultsProcessor } from './default-results-processor'; @Module({ imports: [EncryptionModule, StorageModule, Web3Module], - providers: [ - CvatResultsProcessor, - FortuneResultsProcessor, - MarketingResultsProcessor, - ], - exports: [ - CvatResultsProcessor, - FortuneResultsProcessor, - MarketingResultsProcessor, - ], + providers: [CvatResultsProcessor, DefaultResultsProcessor], + exports: [CvatResultsProcessor, DefaultResultsProcessor], }) export class EscrowResultsProcessingModule {} diff --git a/packages/apps/reputation-oracle/server/src/utils/manifest.ts b/packages/apps/reputation-oracle/server/src/utils/manifest.ts index bd148288bc..4e1bfb9541 100644 --- a/packages/apps/reputation-oracle/server/src/utils/manifest.ts +++ b/packages/apps/reputation-oracle/server/src/utils/manifest.ts @@ -38,10 +38,8 @@ function assertValidJobRequestType( export function getJobRequestType(manifest: JobManifest): JobRequestType { let jobRequestType: string | undefined; - if ('requestType' in manifest) { - jobRequestType = manifest.requestType; - } else if ('job_type' in manifest) { - jobRequestType = manifest.job_type; + if ('jobType' in manifest) { + jobRequestType = manifest.jobType; } else if ('annotation' in manifest) { jobRequestType = manifest.annotation.type; }