From 18f7abb3bcc5ae5e2c2022b9172b67eb770cff08 Mon Sep 17 00:00:00 2001 From: portuu3 Date: Tue, 5 May 2026 15:26:39 +0200 Subject: [PATCH 1/2] account workers reputation and add job request type --- .../1777986717078-reputationJobType.ts | 29 ++++++++ .../escrow-completion.service.spec.ts | 38 +++++++++++ .../escrow-completion.service.ts | 67 +++++++++++++++++++ .../src/modules/reputation/fixtures/index.ts | 2 + .../reputation/reputation.controller.ts | 2 + .../src/modules/reputation/reputation.dto.ts | 41 +++++++++++- .../modules/reputation/reputation.entity.ts | 7 +- .../reputation/reputation.repository.ts | 7 ++ .../reputation/reputation.service.spec.ts | 17 +++++ .../modules/reputation/reputation.service.ts | 16 +++-- .../server/src/modules/reputation/types.ts | 4 ++ 11 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 packages/apps/reputation-oracle/server/src/database/1777986717078-reputationJobType.ts diff --git a/packages/apps/reputation-oracle/server/src/database/1777986717078-reputationJobType.ts b/packages/apps/reputation-oracle/server/src/database/1777986717078-reputationJobType.ts new file mode 100644 index 0000000000..376e812134 --- /dev/null +++ b/packages/apps/reputation-oracle/server/src/database/1777986717078-reputationJobType.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ReputationJobType1777986717078 implements MigrationInterface { + name = 'ReputationJobType1777986717078'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "hmt"."IDX_5012dff596f037415a1370a0cb"`, + ); + await queryRunner.query( + `ALTER TABLE "hmt"."reputation" ADD "job_request_type" character varying NOT NULL DEFAULT 'image_skeletons_from_boxes'`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_e2589f31dc15f8cadca6c560ff" ON "hmt"."reputation" ("chain_id", "address", "type", "job_request_type") `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "hmt"."IDX_e2589f31dc15f8cadca6c560ff"`, + ); + await queryRunner.query( + `ALTER TABLE "hmt"."reputation" DROP COLUMN "job_request_type"`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_5012dff596f037415a1370a0cb" ON "hmt"."reputation" ("chain_id", "address", "type") `, + ); + } +} 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 e63eece4b1..916f276d6a 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 @@ -28,6 +28,7 @@ import _ from 'lodash'; import { CvatJobType, FortuneJobType, MarketingJobType } from '@/common/enums'; import { ServerConfigService, Web3ConfigService } from '@/config'; import { ReputationService } from '@/modules/reputation'; +import { ReputationEntityType } from '@/modules/reputation/constants'; import { StorageService } from '@/modules/storage'; import { WalletWithProvider, Web3Service } from '@/modules/web3'; import { @@ -708,6 +709,7 @@ describe('EscrowCompletionService', () => { let mockedSigner: SignerMock; const mockedCreateBulkPayoutTransaction = jest.fn(); let mockedRawTransaction: { nonce: number }; + let jobRequestType: FortuneJobType; beforeEach(() => { mockedSigner = createSignerMock(); @@ -725,6 +727,13 @@ describe('EscrowCompletionService', () => { mockedCreateBulkPayoutTransaction.mockResolvedValueOnce( mockedRawTransaction, ); + + const manifest = generateFortuneManifest(); + jobRequestType = manifest.requestType; + mockedEscrowUtils.getEscrow.mockResolvedValue({ + manifest: faker.internet.url(), + } as unknown as IEscrow); + mockStorageService.downloadJsonLikeData.mockResolvedValue(manifest); }); it('should succesfully process payouts batch', async () => { @@ -732,6 +741,16 @@ describe('EscrowCompletionService', () => { EscrowCompletionStatus.AWAITING_PAYOUTS, ); const payoutsBatch = generateEscrowPayoutsBatch(); + payoutsBatch.payouts = [ + { + address: faker.finance.ethereumAddress(), + amount: faker.number.bigInt({ min: 1n }).toString(), + }, + { + address: faker.finance.ethereumAddress(), + amount: faker.number.bigInt({ min: 1n }).toString(), + }, + ]; await service['processPayoutsBatch'](awaitingPayoutsRecord, { ...payoutsBatch, @@ -759,6 +778,21 @@ describe('EscrowCompletionService', () => { id: payoutsBatch.id, }), ); + + expect(mockReputationService.increaseReputation).toHaveBeenCalledTimes( + payoutsBatch.payouts.length, + ); + for (const payout of payoutsBatch.payouts) { + expect(mockReputationService.increaseReputation).toHaveBeenCalledWith( + { + chainId: awaitingPayoutsRecord.chainId, + address: payout.address, + type: ReputationEntityType.WORKER, + jobRequestType, + }, + 1, + ); + } }); it('should reset nonce if expired', async () => { @@ -859,6 +893,9 @@ describe('EscrowCompletionService', () => { launcherAddress = faker.finance.ethereumAddress(); exchangeOracleAddress = faker.finance.ethereumAddress(); recordingOracleAddress = faker.finance.ethereumAddress(); + mockStorageService.downloadJsonLikeData.mockResolvedValue( + generateFortuneManifest(), + ); }); describe('handle failures', () => { @@ -1075,6 +1112,7 @@ describe('EscrowCompletionService', () => { launcherAddress, exchangeOracleAddress, recordingOracleAddress, + FortuneJobType.FORTUNE, ); const expectedWebhookData = { 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 46755dd3d1..bf6304c3b3 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 @@ -20,6 +20,7 @@ import { ServerConfigService, Web3ConfigService } from '@/config'; import { isDuplicatedError } from '@/database'; import logger from '@/logger'; import { ReputationService } from '@/modules/reputation'; +import { ReputationEntityType } from '@/modules/reputation/constants'; import { StorageService } from '@/modules/storage'; import { Web3Service } from '@/modules/web3'; /** @@ -264,11 +265,14 @@ export class EscrowCompletionService { /** * This operation can fail and lost, so it's "at most once" */ + const jobRequestType = + await this.getJobRequestTypeFromEscrowData(escrowData); await this.reputationService.assessEscrowParties( chainId, escrowData.launcher, escrowData.exchangeOracle!, escrowData.recordingOracle!, + jobRequestType, ); } @@ -474,6 +478,15 @@ export class EscrowCompletionService { ); await this.escrowPayoutsBatchRepository.deleteOne(payoutsBatch); + const jobRequestType = await this.getJobRequestTypeForEscrow( + escrowCompletionEntity.chainId, + escrowCompletionEntity.escrowAddress, + ); + await this.increasePayoutRecipientsReputation( + escrowCompletionEntity.chainId, + Array.from(recipientToAmountMap.keys()), + jobRequestType, + ); } catch (error) { if (ethers.isError(error, 'NONCE_EXPIRED')) { payoutsBatch.txNonce = null; @@ -484,6 +497,60 @@ export class EscrowCompletionService { } } + private async increasePayoutRecipientsReputation( + chainId: ChainId, + recipients: string[], + jobRequestType: JobRequestType, + ): Promise { + for (const address of recipients) { + try { + await this.reputationService.increaseReputation( + { + chainId, + address, + type: ReputationEntityType.WORKER, + jobRequestType, + }, + 1, + ); + } catch (error) { + this.logger.error('Failed to increase payout recipient reputation', { + error, + address, + chainId, + jobRequestType, + }); + } + } + } + + private async getJobRequestTypeForEscrow( + chainId: ChainId, + escrowAddress: string, + ): Promise { + const escrowData = await EscrowUtils.getEscrow(chainId, escrowAddress); + if (!escrowData) { + throw new Error('Escrow data is missing'); + } + + return this.getJobRequestTypeFromEscrowData(escrowData); + } + + private async getJobRequestTypeFromEscrowData( + escrowData: Awaited>, + ): Promise { + if (!escrowData) { + throw new Error('Escrow data is missing'); + } + + const manifest = + await this.storageService.downloadJsonLikeData( + escrowData.manifest as string, + ); + + return manifestUtils.getJobRequestType(manifest); + } + private getEscrowResultsProcessor( jobRequestType: JobRequestType, ): EscrowResultsProcessor { diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/fixtures/index.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/fixtures/index.ts index a15853c4f1..071bfe2a70 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/fixtures/index.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/fixtures/index.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; +import { CvatJobType } from '@/common/enums'; import { generateTestnetChainId } from '@/modules/web3/fixtures'; import { ReputationEntityType } from '../constants'; @@ -20,6 +21,7 @@ export function generateReputationEntity(score?: number): ReputationEntity { chainId: generateTestnetChainId(), address: faker.finance.ethereumAddress(), type: generateReputationEntityType(), + jobRequestType: CvatJobType.IMAGE_BOXES, reputationPoints: score || generateRandomScorePoints(), createdAt: faker.date.recent(), updatedAt: new Date(), diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.controller.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.controller.ts index 839230de3d..62f349965d 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.controller.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.controller.ts @@ -47,6 +47,7 @@ export class ReputationController { const { chainId, address, + jobRequestTypes, roles, orderBy = GetReputationQueryOrderBy.CREATED_AT, orderDirection = SortDirection.DESC, @@ -58,6 +59,7 @@ export class ReputationController { { chainId, address, + jobRequestTypes, types: roles, }, { diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.dto.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.dto.ts index 5132971dde..48a3d3d461 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.dto.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.dto.ts @@ -3,7 +3,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEthereumAddress, IsOptional, Max, Min } from 'class-validator'; -import { SortDirection } from '@/common/enums'; +import { + CvatJobType, + FortuneJobType, + MarketingJobType, + SortDirection, +} from '@/common/enums'; +import type { JobRequestType } from '@/common/types'; import { IsChainId, IsLowercasedEnum } from '@/common/validators'; import { @@ -47,6 +53,29 @@ export class GetReputationsQueryDto { @IsOptional() roles?: ReputationEntityType[]; + @ApiPropertyOptional({ + enum: [ + ...Object.values(FortuneJobType), + ...Object.values(MarketingJobType), + ...Object.values(CvatJobType), + ], + name: 'job_request_types', + isArray: true, + }) + /** + * NOTE: Order of decorators here matters + * + * Query param is parsed as string if single value passed + * and as array if multiple + */ + @Transform(({ value }) => (Array.isArray(value) ? value : [value])) + @IsLowercasedEnum( + { ...FortuneJobType, ...MarketingJobType, ...CvatJobType }, + { each: true }, + ) + @IsOptional() + jobRequestTypes?: JobRequestType[]; + @ApiPropertyOptional({ name: 'order_by', enum: GetReputationQueryOrderBy, @@ -94,4 +123,14 @@ export class ReputationResponseDto { @ApiProperty({ enum: ReputationEntityType }) role: ReputationEntityType; + + @ApiProperty({ + enum: [ + ...Object.values(FortuneJobType), + ...Object.values(MarketingJobType), + ...Object.values(CvatJobType), + ], + name: 'job_request_type', + }) + jobRequestType: JobRequestType; } diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.entity.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.entity.ts index 4bae3b5a18..eeb2b5e1b0 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.entity.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.entity.ts @@ -1,12 +1,14 @@ import { Column, Entity, Index } from 'typeorm'; import { DATABASE_SCHEMA_NAME } from '@/common/constants'; +import { CvatJobType } from '@/common/enums'; +import type { JobRequestType } from '@/common/types'; import { BaseEntity } from '@/database'; import { ReputationEntityType } from './constants'; @Entity({ schema: DATABASE_SCHEMA_NAME, name: 'reputation' }) -@Index(['chainId', 'address', 'type'], { unique: true }) +@Index(['chainId', 'address', 'type', 'jobRequestType'], { unique: true }) export class ReputationEntity extends BaseEntity { @Column({ type: 'int' }) chainId: number; @@ -20,6 +22,9 @@ export class ReputationEntity extends BaseEntity { }) type: ReputationEntityType; + @Column({ type: 'varchar', default: CvatJobType.IMAGE_BOXES }) + jobRequestType: JobRequestType; + @Column({ type: 'int' }) reputationPoints: number; } diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts index 8846cbca70..21aace32fc 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { DataSource, FindManyOptions, In } from 'typeorm'; import { SortDirection } from '@/common/enums'; +import { JobRequestType } from '@/common/types'; import { BaseRepository } from '@/database'; import { ReputationEntityType, ReputationOrderBy } from './constants'; @@ -19,12 +20,14 @@ export class ReputationRepository extends BaseRepository { chainId, address, type, + jobRequestType, }: ExclusiveReputationCriteria): Promise { return this.findOne({ where: { chainId, address, type, + jobRequestType, }, }); } @@ -33,6 +36,7 @@ export class ReputationRepository extends BaseRepository { filters: { address?: string; chainId?: ChainId; + jobRequestTypes?: JobRequestType[]; types?: ReputationEntityType[]; }, options?: { @@ -49,6 +53,9 @@ export class ReputationRepository extends BaseRepository { if (filters.types) { query.type = In(filters.types); } + if (filters.jobRequestTypes) { + query.jobRequestType = In(filters.jobRequestTypes); + } if (filters.address) { query.address = filters.address; } diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.spec.ts index 5028487983..a65477911d 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.spec.ts @@ -5,6 +5,7 @@ import { createMock } from '@golevelup/ts-jest'; import { EscrowClient } from '@human-protocol/sdk'; import { Test } from '@nestjs/testing'; +import { CvatJobType } from '@/common/enums'; import { ReputationConfigService, Web3ConfigService } from '@/config'; import { Web3Service } from '@/modules/web3'; import { @@ -90,18 +91,21 @@ describe('ReputationService', () => { chainId: withLowScore.chainId, address: withLowScore.address, role: withLowScore.type, + jobRequestType: withLowScore.jobRequestType, level: 'low', }); expect(reputations[1]).toEqual({ chainId: withMediumScore.chainId, address: withMediumScore.address, role: withMediumScore.type, + jobRequestType: withMediumScore.jobRequestType, level: 'medium', }); expect(reputations[2]).toEqual({ chainId: withHighScore.chainId, address: withHighScore.address, role: withHighScore.type, + jobRequestType: withHighScore.jobRequestType, level: 'high', }); }); @@ -129,6 +133,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: faker.finance.ethereumAddress(), type: generateReputationEntityType(), + jobRequestType: CvatJobType.IMAGE_BOXES, }, score, ), @@ -145,6 +150,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: faker.finance.ethereumAddress(), type: generateReputationEntityType(), + jobRequestType: CvatJobType.IMAGE_BOXES, }; const score = generateRandomScorePoints(); @@ -174,6 +180,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: mockWeb3ConfigService.operatorAddress, type: ReputationEntityType.REPUTATION_ORACLE, + jobRequestType: CvatJobType.IMAGE_BOXES, }; const score = generateRandomScorePoints(); @@ -206,6 +213,7 @@ describe('ReputationService', () => { chainId: reputationEntity.chainId, address: reputationEntity.address, type: reputationEntity.type, + jobRequestType: reputationEntity.jobRequestType, }; const score = generateRandomScorePoints(); const initialEntityScore = reputationEntity.reputationPoints; @@ -234,6 +242,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: faker.finance.ethereumAddress(), type: generateReputationEntityType(), + jobRequestType: CvatJobType.IMAGE_BOXES, }, score, ), @@ -253,6 +262,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: faker.finance.ethereumAddress(), type: generateReputationEntityType(), + jobRequestType: CvatJobType.IMAGE_BOXES, }; const score = generateRandomScorePoints(); @@ -285,6 +295,7 @@ describe('ReputationService', () => { chainId: reputationEntity.chainId, address: reputationEntity.address, type: reputationEntity.type, + jobRequestType: reputationEntity.jobRequestType, }; const score = generateRandomScorePoints(); const initialEntityScore = reputationEntity.reputationPoints; @@ -307,6 +318,7 @@ describe('ReputationService', () => { chainId: generateTestnetChainId(), address: mockWeb3ConfigService.operatorAddress, type: ReputationEntityType.REPUTATION_ORACLE, + jobRequestType: CvatJobType.IMAGE_BOXES, }; const score = generateRandomScorePoints(); @@ -356,6 +368,7 @@ describe('ReputationService', () => { jobLauncherAddress, exchangeOracleAddress, recordingOracleAddress, + CvatJobType.IMAGE_BOXES, ); expect(spyOnIncreaseReputation).toHaveBeenCalledTimes(4); @@ -364,6 +377,7 @@ describe('ReputationService', () => { chainId, address: jobLauncherAddress, type: ReputationEntityType.JOB_LAUNCHER, + jobRequestType: CvatJobType.IMAGE_BOXES, }, 1, ); @@ -372,6 +386,7 @@ describe('ReputationService', () => { chainId, address: exchangeOracleAddress, type: ReputationEntityType.EXCHANGE_ORACLE, + jobRequestType: CvatJobType.IMAGE_BOXES, }, 1, ); @@ -380,6 +395,7 @@ describe('ReputationService', () => { chainId, address: recordingOracleAddress, type: ReputationEntityType.RECORDING_ORACLE, + jobRequestType: CvatJobType.IMAGE_BOXES, }, 1, ); @@ -388,6 +404,7 @@ describe('ReputationService', () => { chainId, address: mockWeb3ConfigService.operatorAddress, type: ReputationEntityType.REPUTATION_ORACLE, + jobRequestType: CvatJobType.IMAGE_BOXES, }, 1, ); diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.ts index 5da96b30b4..b598ff4521 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.service.ts @@ -2,6 +2,7 @@ import { ChainId } from '@human-protocol/sdk'; import { Injectable } from '@nestjs/common'; import { SortDirection } from '@/common/enums'; +import { JobRequestType } from '@/common/types'; import { ReputationConfigService, Web3ConfigService } from '@/config'; import { isDuplicatedError } from '@/database'; import { Web3Service } from '@/modules/web3'; @@ -53,12 +54,12 @@ export class ReputationService { * If the entity doesn't exist in the database - creates it first. */ async increaseReputation( - { chainId, address, type }: ExclusiveReputationCriteria, + { chainId, address, type, jobRequestType }: ExclusiveReputationCriteria, points: number, ): Promise { assertAdjustableReputationPoints(points); - const searchCriteria = { chainId, address, type }; + const searchCriteria = { chainId, address, type, jobRequestType }; let existingEntity = await this.reputationRepository.findExclusive(searchCriteria); @@ -76,6 +77,7 @@ export class ReputationService { reputationEntity.chainId = chainId; reputationEntity.address = address; reputationEntity.type = type; + reputationEntity.jobRequestType = jobRequestType; reputationEntity.reputationPoints = initialReputation; try { @@ -104,7 +106,7 @@ export class ReputationService { * If the entity doesn't exist in the database - creates it first. */ async decreaseReputation( - { chainId, address, type }: ExclusiveReputationCriteria, + { chainId, address, type, jobRequestType }: ExclusiveReputationCriteria, points: number, ): Promise { assertAdjustableReputationPoints(points); @@ -116,7 +118,7 @@ export class ReputationService { return; } - const searchCriteria = { chainId, address, type }; + const searchCriteria = { chainId, address, type, jobRequestType }; let existingEntity = await this.reputationRepository.findExclusive(searchCriteria); @@ -126,6 +128,7 @@ export class ReputationService { reputationEntity.chainId = chainId; reputationEntity.address = address; reputationEntity.type = type; + reputationEntity.jobRequestType = jobRequestType; reputationEntity.reputationPoints = INITIAL_REPUTATION; try { @@ -157,6 +160,7 @@ export class ReputationService { filter: { address?: string; chainId?: ChainId; + jobRequestTypes?: JobRequestType[]; types?: ReputationEntityType[]; }, options?: { @@ -175,6 +179,7 @@ export class ReputationService { chainId: reputation.chainId, address: reputation.address, role: reputation.type, + jobRequestType: reputation.jobRequestType, level: this.getReputationLevel(reputation.reputationPoints), })); } @@ -184,6 +189,7 @@ export class ReputationService { jobLauncherAddress: string, exchangeOracleAddress: string, recordingOracleAddress: string, + jobRequestType: JobRequestType, ): Promise { const reputationTypeToAddress = new Map([ [ReputationEntityType.JOB_LAUNCHER, jobLauncherAddress], @@ -200,7 +206,7 @@ export class ReputationService { address, ] of reputationTypeToAddress.entries()) { await this.increaseReputation( - { chainId, address, type: reputationEntityType }, + { chainId, address, type: reputationEntityType, jobRequestType }, 1, ); } diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/types.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/types.ts index b40311a6e3..f8a12d7740 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/types.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/types.ts @@ -1,5 +1,7 @@ import { ChainId } from '@human-protocol/sdk'; +import { JobRequestType } from '@/common/types'; + import { ReputationEntityType, ReputationLevel } from './constants'; export type ReputationData = { @@ -7,10 +9,12 @@ export type ReputationData = { address: string; level: ReputationLevel; role: ReputationEntityType; + jobRequestType: JobRequestType; }; export type ExclusiveReputationCriteria = { chainId: number; address: string; type: ReputationEntityType; + jobRequestType: JobRequestType; }; From d6d8475ab226b93fa8ae185d7346a1345df8d26b Mon Sep 17 00:00:00 2001 From: portuu3 Date: Tue, 5 May 2026 16:05:51 +0200 Subject: [PATCH 2/2] fix case insentitive address search --- .../src/modules/reputation/reputation.repository.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts index 21aace32fc..7613ac970a 100644 --- a/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts +++ b/packages/apps/reputation-oracle/server/src/modules/reputation/reputation.repository.ts @@ -1,6 +1,6 @@ import { ChainId } from '@human-protocol/sdk'; import { Injectable } from '@nestjs/common'; -import { DataSource, FindManyOptions, In } from 'typeorm'; +import { DataSource, FindManyOptions, In, Raw } from 'typeorm'; import { SortDirection } from '@/common/enums'; import { JobRequestType } from '@/common/types'; @@ -10,6 +10,12 @@ import { ReputationEntityType, ReputationOrderBy } from './constants'; import { ReputationEntity } from './reputation.entity'; import type { ExclusiveReputationCriteria } from './types'; +function caseInsensitiveAddress(address: string) { + return Raw((addressAlias) => `LOWER(${addressAlias}) = LOWER(:address)`, { + address, + }); +} + @Injectable() export class ReputationRepository extends BaseRepository { constructor(dataSource: DataSource) { @@ -25,7 +31,7 @@ export class ReputationRepository extends BaseRepository { return this.findOne({ where: { chainId, - address, + address: caseInsensitiveAddress(address), type, jobRequestType, }, @@ -57,7 +63,7 @@ export class ReputationRepository extends BaseRepository { query.jobRequestType = In(filters.jobRequestTypes); } if (filters.address) { - query.address = filters.address; + query.address = caseInsensitiveAddress(filters.address); } return this.find({