Skip to content

Commit 16788c1

Browse files
flopez7flopez7
andauthored
[Exchange Oracle] Add manifest to cache (#3862)
Co-authored-by: flopez7 <francisco@hmt.ai>
1 parent 33167c5 commit 16788c1

2 files changed

Lines changed: 133 additions & 7 deletions

File tree

packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.spec.ts

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { createMock } from '@golevelup/ts-jest';
22
import { HMToken__factory } from '@human-protocol/core/typechain-types';
3-
import { Encryption, EscrowClient, OperatorUtils } from '@human-protocol/sdk';
3+
import {
4+
Encryption,
5+
EncryptionUtils,
6+
EscrowClient,
7+
OperatorUtils,
8+
} from '@human-protocol/sdk';
49
import { HttpService } from '@nestjs/axios';
510
import { ConfigService } from '@nestjs/config';
611
import { Test } from '@nestjs/testing';
@@ -69,7 +74,6 @@ jest.mock('minio', () => {
6974

7075
describe('JobService', () => {
7176
let jobService: JobService;
72-
let web3Service: Web3Service;
7377
let storageService: StorageService;
7478
let jobRepository: JobRepository;
7579
let assignmentRepository: AssignmentRepository;
@@ -136,14 +140,18 @@ describe('JobService', () => {
136140
}).compile();
137141

138142
jobService = moduleRef.get<JobService>(JobService);
139-
web3Service = moduleRef.get<Web3Service>(Web3Service);
140143
storageService = moduleRef.get<StorageService>(StorageService);
141144
jobRepository = moduleRef.get<JobRepository>(JobRepository);
142145
assignmentRepository =
143146
moduleRef.get<AssignmentRepository>(AssignmentRepository);
144147
webhookRepository = moduleRef.get<WebhookRepository>(WebhookRepository);
145148
});
146149

150+
afterEach(() => {
151+
(jobService as any).manifestCache.clear();
152+
jest.clearAllMocks();
153+
});
154+
147155
describe('createJob', () => {
148156
beforeAll(async () => {
149157
jest.spyOn(jobRepository, 'createUnique');
@@ -497,7 +505,6 @@ describe('JobService', () => {
497505
.mockResolvedValue(solutionsUrl);
498506

499507
await jobService.solveJob(assignment.id, 'solution');
500-
expect(web3Service.getSigner).toHaveBeenCalledWith(chainId);
501508
expect(webhookRepository.createUnique).toHaveBeenCalledWith({
502509
escrowAddress,
503510
chainId,
@@ -515,7 +522,6 @@ describe('JobService', () => {
515522
await expect(jobService.solveJob(1, 'solution')).rejects.toThrow(
516523
new ConflictError(ErrorAssignment.InvalidStatus),
517524
);
518-
expect(web3Service.getSigner).toHaveBeenCalledWith(chainId);
519525
});
520526

521527
it('should fail if user is not assigned to the job', async () => {
@@ -556,7 +562,6 @@ describe('JobService', () => {
556562
await expect(jobService.solveJob(1, 'solution')).rejects.toThrow(
557563
'This job has already been completed',
558564
);
559-
expect(web3Service.getSigner).toHaveBeenCalledWith(chainId);
560565
});
561566

562567
it('should fail if user has already submitted a solution', async () => {
@@ -589,7 +594,100 @@ describe('JobService', () => {
589594
await expect(jobService.solveJob(1, 'solution')).rejects.toThrow(
590595
new ValidationError(ErrorJob.SolutionAlreadySubmitted),
591596
);
592-
expect(web3Service.getSigner).toHaveBeenCalledWith(chainId);
597+
});
598+
});
599+
600+
describe('getManifest', () => {
601+
const downloadFileFromUrlMock = jest.mocked(downloadFileFromUrl);
602+
603+
it('should fetch and parse a non encrypted manifest', async () => {
604+
const manifest: ManifestDto = {
605+
requesterTitle: 'Example Title',
606+
requesterDescription: 'Example Description',
607+
submissionsRequired: 5,
608+
fundAmount: 100,
609+
};
610+
611+
downloadFileFromUrlMock.mockResolvedValue(JSON.stringify(manifest));
612+
EncryptionUtils.isEncrypted = jest.fn().mockReturnValue(false);
613+
614+
const result = await jobService.getManifest(
615+
chainId,
616+
escrowAddress,
617+
MOCK_MANIFEST_URL,
618+
);
619+
620+
expect(result).toEqual(manifest);
621+
expect(Encryption.build).not.toHaveBeenCalled();
622+
});
623+
624+
it('should fetch and decrypt an encrypted manifest', async () => {
625+
const manifest: ManifestDto = {
626+
requesterTitle: 'Example Title',
627+
requesterDescription: 'Example Description',
628+
submissionsRequired: 5,
629+
fundAmount: 100,
630+
};
631+
632+
downloadFileFromUrlMock.mockResolvedValue('encrypted-content');
633+
EncryptionUtils.isEncrypted = jest.fn().mockReturnValue(true);
634+
(Encryption.build as any).mockImplementation(() => ({
635+
decrypt: jest.fn().mockResolvedValue(JSON.stringify(manifest)),
636+
}));
637+
638+
const result = await jobService.getManifest(
639+
chainId,
640+
escrowAddress,
641+
MOCK_MANIFEST_URL,
642+
);
643+
644+
expect(result).toEqual(manifest);
645+
expect(Encryption.build).toHaveBeenCalled();
646+
});
647+
648+
it('should cache the manifest in memory for repeated requests', async () => {
649+
const manifest: ManifestDto = {
650+
requesterTitle: 'Example Title',
651+
requesterDescription: 'Example Description',
652+
submissionsRequired: 5,
653+
fundAmount: 100,
654+
};
655+
656+
downloadFileFromUrlMock.mockResolvedValue(manifest);
657+
658+
const firstManifest = await jobService.getManifest(
659+
chainId,
660+
escrowAddress,
661+
MOCK_MANIFEST_URL,
662+
);
663+
const secondManifest = await jobService.getManifest(
664+
chainId,
665+
escrowAddress,
666+
MOCK_MANIFEST_URL,
667+
);
668+
669+
expect(firstManifest).toEqual(manifest);
670+
expect(secondManifest).toEqual(manifest);
671+
expect(downloadFileFromUrlMock).toHaveBeenCalledTimes(1);
672+
});
673+
674+
it('should retry downloading the manifest after a failed request', async () => {
675+
downloadFileFromUrlMock.mockRejectedValue(
676+
new Error('Storage file not found'),
677+
);
678+
jest
679+
.spyOn(webhookRepository, 'createUnique')
680+
.mockResolvedValue({} as any);
681+
682+
await expect(
683+
jobService.getManifest(chainId, escrowAddress, MOCK_MANIFEST_URL),
684+
).rejects.toThrow(ErrorJob.ManifestNotFound);
685+
await expect(
686+
jobService.getManifest(chainId, escrowAddress, MOCK_MANIFEST_URL),
687+
).rejects.toThrow(ErrorJob.ManifestNotFound);
688+
689+
expect(downloadFileFromUrlMock).toHaveBeenCalledTimes(2);
690+
expect(webhookRepository.createUnique).toHaveBeenCalledTimes(2);
593691
});
594692
});
595693

packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { JobRepository } from './job.repository';
4343

4444
@Injectable()
4545
export class JobService {
46+
private readonly manifestCache = new Map<string, Promise<ManifestDto>>();
47+
4648
constructor(
4749
private readonly pgpConfigService: PGPConfigService,
4850
public readonly jobRepository: JobRepository,
@@ -344,6 +346,32 @@ export class JobService {
344346
chainId: number,
345347
escrowAddress: string,
346348
manifestUrl: string,
349+
): Promise<ManifestDto> {
350+
const cacheKey = `${chainId}:${escrowAddress}`;
351+
352+
const cachedManifest = this.manifestCache.get(cacheKey);
353+
if (cachedManifest) {
354+
return cachedManifest;
355+
}
356+
357+
const manifestRequest = this.fetchManifest(
358+
chainId,
359+
escrowAddress,
360+
manifestUrl,
361+
).catch((error) => {
362+
this.manifestCache.delete(cacheKey);
363+
throw error;
364+
});
365+
366+
this.manifestCache.set(cacheKey, manifestRequest);
367+
368+
return manifestRequest;
369+
}
370+
371+
private async fetchManifest(
372+
chainId: number,
373+
escrowAddress: string,
374+
manifestUrl: string,
347375
): Promise<ManifestDto> {
348376
let manifest: ManifestDto | null = null;
349377

0 commit comments

Comments
 (0)