From c91e39f2ca2b4428f09dbfb0ef9ab565375dafe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Mon, 3 Nov 2025 14:04:29 +0100 Subject: [PATCH 1/2] Refactor abuse flow to avoid pausing escrows when abuse is not confirmed yet --- .../server/src/common/enums/job.ts | 1 - .../assignment/assignment.service.spec.ts | 2 +- .../src/modules/job/job.service.spec.ts | 50 ------ .../server/src/modules/job/job.service.ts | 30 ---- .../modules/webhook/webhook.service.spec.ts | 12 +- .../src/modules/webhook/webhook.service.ts | 7 +- .../src/modules/abuse/abuse.service.spec.ts | 116 +++----------- .../server/src/modules/abuse/abuse.service.ts | 143 +++++------------- 8 files changed, 65 insertions(+), 296 deletions(-) diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts b/packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts index 052a3754ad..68659c8162 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts @@ -1,6 +1,5 @@ export enum JobStatus { ACTIVE = 'active', - PAUSED = 'paused', COMPLETED = 'completed', CANCELED = 'canceled', } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/assignment/assignment.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/assignment/assignment.service.spec.ts index 76fec45ecf..6bd57ac270 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/assignment/assignment.service.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/assignment/assignment.service.spec.ts @@ -229,7 +229,7 @@ describe('AssignmentService', () => { id: 1, manifestUrl: MOCK_MANIFEST_URL, reputationNetwork: reputationNetwork, - status: JobStatus.PAUSED, + status: JobStatus.CANCELED, } as any); await expect( 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 9073407ca7..5eea7efe29 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 @@ -667,54 +667,4 @@ describe('JobService', () => { ).rejects.toThrow(`Solution not found in Escrow: ${escrowAddress}`); }); }); - - describe('pauseJob', () => { - const webhook: WebhookDto = { - chainId, - escrowAddress, - eventType: EventType.ABUSE_DETECTED, - }; - - it('should create a new job in the database', async () => { - jest - .spyOn(jobRepository, 'findOneByChainIdAndEscrowAddress') - .mockResolvedValue({ - chainId: chainId, - escrowAddress: escrowAddress, - status: JobStatus.ACTIVE, - } as JobEntity); - const result = await jobService.pauseJob(webhook); - - expect(result).toEqual(undefined); - expect(jobRepository.updateOne).toHaveBeenCalledWith({ - chainId: chainId, - escrowAddress: escrowAddress, - status: JobStatus.PAUSED, - }); - }); - - it('should fail if job not exists', async () => { - jest - .spyOn(jobRepository, 'findOneByChainIdAndEscrowAddress') - .mockResolvedValue(null); - - await expect(jobService.pauseJob(webhook)).rejects.toThrow( - ErrorJob.NotFound, - ); - }); - - it('should fail if job is not in Active status', async () => { - jest - .spyOn(jobRepository, 'findOneByChainIdAndEscrowAddress') - .mockResolvedValue({ - chainId: chainId, - escrowAddress: escrowAddress, - status: JobStatus.CANCELED, - } as JobEntity); - - await expect(jobService.pauseJob(webhook)).rejects.toThrow( - ErrorJob.InvalidStatus, - ); - }); - }); }); 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 7089fed2a2..72f8129444 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 @@ -382,34 +382,4 @@ export class JobService { return manifest; } - - public async pauseJob(webhook: WebhookDto): Promise { - const jobEntity = await this.jobRepository.findOneByChainIdAndEscrowAddress( - webhook.chainId, - webhook.escrowAddress, - ); - if (!jobEntity) { - throw new ServerError(ErrorJob.NotFound); - } - if (jobEntity.status !== JobStatus.ACTIVE) { - throw new ConflictError(ErrorJob.InvalidStatus); - } - jobEntity.status = JobStatus.PAUSED; - await this.jobRepository.updateOne(jobEntity); - } - - public async resumeJob(webhook: WebhookDto): Promise { - const jobEntity = await this.jobRepository.findOneByChainIdAndEscrowAddress( - webhook.chainId, - webhook.escrowAddress, - ); - if (!jobEntity) { - throw new ServerError(ErrorJob.NotFound); - } - if (jobEntity.status !== JobStatus.PAUSED) { - throw new ConflictError(ErrorJob.InvalidStatus); - } - jobEntity.status = JobStatus.ACTIVE; - await this.jobRepository.updateOne(jobEntity); - } } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.spec.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.spec.ts index 148596b5ae..dc445214b6 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.spec.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.spec.ts @@ -169,7 +169,7 @@ describe('WebhookService', () => { }); it('should handle an incoming escrow abuse webhook', async () => { - jest.spyOn(jobService, 'pauseJob').mockResolvedValue(); + jest.spyOn(jobService, 'cancelJob').mockResolvedValue(); const webhook: WebhookDto = { chainId, escrowAddress, @@ -178,16 +178,6 @@ describe('WebhookService', () => { expect(await webhookService.handleWebhook(webhook)).toBe(undefined); }); - it('should handle an incoming escrow resume webhook', async () => { - jest.spyOn(jobService, 'resumeJob').mockResolvedValue(); - const webhook: WebhookDto = { - chainId, - escrowAddress, - eventType: EventType.ABUSE_DISMISSED, - }; - expect(await webhookService.handleWebhook(webhook)).toBe(undefined); - }); - it('should return an error when the event type is invalid', async () => { const webhook: WebhookDto = { chainId, diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts index fac0c3820a..c751ffd89e 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/webhook/webhook.service.ts @@ -54,13 +54,8 @@ export class WebhookService { break; case EventType.ABUSE_DETECTED: - await this.jobService.pauseJob(webhook); - break; - - case EventType.ABUSE_DISMISSED: - await this.jobService.resumeJob(webhook); + await this.jobService.cancelJob(webhook); break; - default: throw new ValidationError( `Invalid webhook event type: ${webhook.eventType}`, diff --git a/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.spec.ts index b9e1e730e7..d428f930fa 100644 --- a/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.spec.ts @@ -294,13 +294,6 @@ describe('AbuseService', () => { .mockResolvedValueOnce({ exchangeOracle: fakeAddress, } as unknown as IEscrow); - mockedOperatorUtils.getOperator - .mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator) - .mockResolvedValueOnce({ - webhookUrl: webhookUrl2, - } as IOperator); mockAbuseSlackBot.sendAbuseNotification .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined); @@ -325,9 +318,6 @@ describe('AbuseService', () => { mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ exchangeOracle: fakeAddress, } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator); mockAbuseRepository.findToClassify.mockResolvedValueOnce( mockAbuseEntities, ); @@ -345,32 +335,6 @@ describe('AbuseService', () => { }); }); - it('should handle errors when createOutgoingWebhook fails', async () => { - const mockAbuseEntities = [generateAbuseEntity({ retriesCount: 0 })]; - - mockAbuseRepository.findToClassify.mockResolvedValueOnce( - mockAbuseEntities, - ); - mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ - exchangeOracle: fakeAddress, - } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator); - - mockOutgoingWebhookService.createOutgoingWebhook.mockRejectedValueOnce( - new DatabaseError('Failed to create webhook'), - ); - - await abuseService.processAbuseRequests(); - - expect(mockAbuseRepository.findToClassify).toHaveBeenCalledTimes(1); - expect(mockAbuseRepository.updateOne).toHaveBeenCalledWith({ - ...mockAbuseEntities[0], - retriesCount: 1, - }); - }); - it('should continue if createOutgoingWebhook throws a duplicated error', async () => { const mockAbuseEntities = [generateAbuseEntity(), generateAbuseEntity()]; @@ -384,13 +348,6 @@ describe('AbuseService', () => { .mockResolvedValueOnce({ exchangeOracle: fakeAddress, } as unknown as IEscrow); - mockedOperatorUtils.getOperator - .mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator) - .mockResolvedValueOnce({ - webhookUrl: webhookUrl2, - } as IOperator); mockOutgoingWebhookService.createOutgoingWebhook.mockRejectedValueOnce( new DatabaseError(DatabaseErrorMessages.DUPLICATED), @@ -450,24 +407,6 @@ describe('AbuseService', () => { retriesCount: 1, }); }); - - it('should increment retries when operator data is missing', async () => { - const abuseEntity = generateAbuseEntity({ retriesCount: 0 }); - mockAbuseRepository.findToClassify.mockResolvedValueOnce([abuseEntity]); - - mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ - exchangeOracle: fakeAddress, - } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce(null); - - await abuseService.processAbuseRequests(); - - expect(mockAbuseRepository.findToClassify).toHaveBeenCalledTimes(1); - expect(mockAbuseRepository.updateOne).toHaveBeenCalledWith({ - ...abuseEntity, - retriesCount: 1, - }); - }); }); describe('processClassifiedAbuses', () => { @@ -485,10 +424,15 @@ describe('AbuseService', () => { } as unknown as StakingClient); mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ launcher: fakeAddress, + exchangeOracle: fakeAddress, } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator); + mockedOperatorUtils.getOperator + .mockResolvedValueOnce({ + webhookUrl: webhookUrl1, + } as IOperator) + .mockResolvedValueOnce({ + webhookUrl: webhookUrl2, + } as IOperator); mockOutgoingWebhookService.createOutgoingWebhook.mockResolvedValueOnce( undefined, ); @@ -511,7 +455,17 @@ describe('AbuseService', () => { chainId: mockAbuseEntities[0].chainId, eventType: OutgoingWebhookEventType.ABUSE_DETECTED, }, - expect.any(String), + webhookUrl1, + ); + expect( + mockOutgoingWebhookService.createOutgoingWebhook, + ).toHaveBeenCalledWith( + { + escrowAddress: mockAbuseEntities[0].escrowAddress, + chainId: mockAbuseEntities[0].chainId, + eventType: OutgoingWebhookEventType.ABUSE_DETECTED, + }, + webhookUrl2, ); expect(mockAbuseRepository.updateOne).toHaveBeenCalledWith({ ...mockAbuseEntities[0], @@ -527,12 +481,6 @@ describe('AbuseService', () => { mockAbuseRepository.findClassified.mockResolvedValueOnce( mockAbuseEntities, ); - mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ - exchangeOracle: fakeAddress, - } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce({ - webhookUrl: webhookUrl1, - } as IOperator); await abuseService.processClassifiedAbuses(); @@ -541,6 +489,11 @@ describe('AbuseService', () => { ...mockAbuseEntities[0], status: AbuseStatus.COMPLETED, }); + expect(mockedEscrowUtils.getEscrow).not.toHaveBeenCalled(); + expect(mockedOperatorUtils.getOperator).not.toHaveBeenCalled(); + expect( + mockOutgoingWebhookService.createOutgoingWebhook, + ).not.toHaveBeenCalled(); }); it('should handle empty results from findClassified', async () => { @@ -572,26 +525,5 @@ describe('AbuseService', () => { retriesCount: 1, }); }); - - it('should increment retries when operator is missing (REJECTED)', async () => { - const abuseEntity = generateAbuseEntity({ - retriesCount: 0, - decision: AbuseDecision.REJECTED, - }); - - mockAbuseRepository.findClassified.mockResolvedValueOnce([abuseEntity]); - mockedEscrowUtils.getEscrow.mockResolvedValueOnce({ - exchangeOracle: fakeAddress, - } as unknown as IEscrow); - mockedOperatorUtils.getOperator.mockResolvedValueOnce(null); - - await abuseService.processClassifiedAbuses(); - - expect(mockAbuseRepository.findClassified).toHaveBeenCalledTimes(1); - expect(mockAbuseRepository.updateOne).toHaveBeenCalledWith({ - ...abuseEntity, - retriesCount: 1, - }); - }); }); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.ts b/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.ts index b87eff134c..20ee094117 100644 --- a/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/abuse/abuse.service.ts @@ -179,39 +179,6 @@ export class AbuseService { if (!escrow) { throw new Error('Escrow data is missing'); } - const operator = await OperatorUtils.getOperator( - abuseEntity.chainId, - escrow.exchangeOracle as string, - ); - if (!operator) { - throw new Error('Operator data is missing'); - } - if (!operator.webhookUrl) { - throw new Error('Operator webhook URL is missing'); - } - - const webhookPayload = { - chainId: abuseEntity.chainId, - escrowAddress: abuseEntity.escrowAddress, - eventType: OutgoingWebhookEventType.ABUSE_DETECTED, - }; - - try { - await this.outgoingWebhookService.createOutgoingWebhook( - webhookPayload, - operator.webhookUrl, - ); - } catch (error) { - if (!isDuplicatedError(error)) { - this.logger.error('Failed to create outgoing webhook for oracle', { - error, - abuseEntityId: abuseEntity.id, - }); - - await this.handleAbuseError(abuseEntity); - continue; - } - } await this.abuseSlackBot.sendAbuseNotification({ abuseId: abuseEntity.id, @@ -239,15 +206,16 @@ export class AbuseService { for (const abuseEntity of abuseEntities) { try { const { chainId, escrowAddress } = abuseEntity; - const escrow = await EscrowUtils.getEscrow( - abuseEntity.chainId, - abuseEntity.escrowAddress, - ); - if (!escrow) { - throw new Error('Escrow data is missing'); - } if (abuseEntity.decision === AbuseDecision.ACCEPTED) { + const escrow = await EscrowUtils.getEscrow( + abuseEntity.chainId, + abuseEntity.escrowAddress, + ); + if (!escrow) { + throw new Error('Escrow data is missing'); + } + await this.slashAccount({ slasher: abuseEntity?.user?.evmAddress as string, staker: escrow.launcher, @@ -255,76 +223,41 @@ export class AbuseService { escrowAddress: escrowAddress, amount: Number(abuseEntity.amount), }); - const operator = await OperatorUtils.getOperator( - chainId, + for (const address of [ escrow.launcher, - ); - if (!operator) { - throw new Error('Operator data is missing'); - } - if (!operator.webhookUrl) { - throw new Error('Operator webhook URL is missing'); - } - - const webhookPayload = { - chainId, - escrowAddress, - eventType: OutgoingWebhookEventType.ABUSE_DETECTED, - }; - - try { - await this.outgoingWebhookService.createOutgoingWebhook( - webhookPayload, - operator.webhookUrl, - ); - } catch (error) { - if (!isDuplicatedError(error)) { - this.logger.error( - 'Failed to create outgoing webhook for oracle', - { - error, - abuseEntityId: abuseEntity.id, - }, - ); - - await this.handleAbuseError(abuseEntity); - continue; - } - } - } else { - const webhookPayload = { - chainId: chainId, - escrowAddress: escrowAddress, - eventType: OutgoingWebhookEventType.ABUSE_DISMISSED, - }; - const operator = await OperatorUtils.getOperator( - chainId, escrow.exchangeOracle as string, - ); - if (!operator) { - throw new Error('Operator data is missing'); - } - if (!operator.webhookUrl) { - throw new Error('Operator webhook URL is missing'); - } + ]) { + const operator = await OperatorUtils.getOperator(chainId, address); + if (!operator) { + throw new Error('Operator data is missing'); + } + if (!operator.webhookUrl) { + throw new Error('Operator webhook URL is missing'); + } + const webhookPayload = { + chainId, + escrowAddress, + eventType: OutgoingWebhookEventType.ABUSE_DETECTED, + }; - try { - await this.outgoingWebhookService.createOutgoingWebhook( - webhookPayload, - operator.webhookUrl, - ); - } catch (error) { - if (!isDuplicatedError(error)) { - this.logger.error( - 'Failed to create outgoing webhook for oracle', - { - error, - abuseEntityId: abuseEntity.id, - }, + try { + await this.outgoingWebhookService.createOutgoingWebhook( + webhookPayload, + operator.webhookUrl, ); + } catch (error) { + if (!isDuplicatedError(error)) { + this.logger.error( + 'Failed to create outgoing webhook for oracle', + { + error, + abuseEntityId: abuseEntity.id, + }, + ); - await this.handleAbuseError(abuseEntity); - continue; + await this.handleAbuseError(abuseEntity); + continue; + } } } } From 69a43936b39f2600177eef1f2e284667050a6a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= Date: Mon, 3 Nov 2025 14:05:57 +0100 Subject: [PATCH 2/2] Enable HMT for local ussage and move subgraph's postgres instance to v16 --- docker-setup/docker-compose.dev.yml | 2 +- .../apps/job-launcher/server/src/modules/job/job.service.ts | 1 + .../server/src/modules/payment/payment.controller.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-setup/docker-compose.dev.yml b/docker-setup/docker-compose.dev.yml index b329016fb9..3a20df61dc 100644 --- a/docker-setup/docker-compose.dev.yml +++ b/docker-setup/docker-compose.dev.yml @@ -73,7 +73,7 @@ services: graph-node-db: container_name: hp-dev-graph-node-db - image: postgres:latest + image: postgres:16 restart: *default-restart logging: <<: *default-logging diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index dfc2ef0346..fabd5f7e74 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -147,6 +147,7 @@ export class JobService { // DISABLE HMT if ( requestType !== HCaptchaJobType.HCAPTCHA && + dto.chainId !== ChainId.LOCALHOST && (dto.escrowFundToken === EscrowFundToken.HMT || dto.paymentCurrency === PaymentCurrency.HMT) ) { diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts index 1914564209..cedade9fad 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts @@ -452,6 +452,10 @@ export class PaymentController { throw new ValidationError(ErrorPayment.InvalidChainId); } + if (chainId === ChainId.LOCALHOST) { + return tokens; + } + // Disable HMT const { [EscrowFundToken.HMT]: _omit, ...withoutHMT } = tokens;