Skip to content

Commit 7c971c8

Browse files
authored
Merge pull request #3338 from humanprotocol/develop
Release 2025-05-16
2 parents 7822119 + 882bd8f commit 7c971c8

File tree

198 files changed

+2813
-2615
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

198 files changed

+2813
-2615
lines changed

.github/workflows/ci-test-subgraph.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ jobs:
2222
- name: Build core package
2323
run: yarn build:core
2424
- name: Generate manifest for Polygon for tests
25-
- run: NETWORK=polygon yarn workspace @human-protocol/subgraph generate
25+
run: NETWORK=polygon yarn workspace @human-protocol/subgraph generate
2626
- name: Run subgraph test
2727
run: yarn workspace @human-protocol/subgraph test

packages/apps/fortune/exchange-oracle/server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"axios": "^1.3.1",
3939
"class-transformer": "^0.5.1",
4040
"class-validator": "0.14.1",
41+
"ethers": "~6.13.5",
4142
"joi": "^17.13.3",
43+
"pg": "8.13.1",
4244
"reflect-metadata": "^0.2.2",
4345
"rxjs": "^7.2.0",
4446
"typeorm": "^0.3.23",
@@ -60,7 +62,6 @@
6062
"eslint-config-prettier": "^9.1.0",
6163
"eslint-plugin-prettier": "^5.2.1",
6264
"jest": "29.7.0",
63-
"pg": "8.13.1",
6465
"prettier": "^3.4.2",
6566
"source-map-support": "^0.5.20",
6667
"supertest": "^7.0.0",

packages/apps/fortune/exchange-oracle/server/src/app.module.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import { Module } from '@nestjs/common';
2-
import { AppController } from './app.controller';
3-
import { JobModule } from './modules/job/job.module';
42
import { ConfigModule } from '@nestjs/config';
3+
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
4+
import { ScheduleModule } from '@nestjs/schedule';
5+
import { AppController } from './app.controller';
56
import { envValidator } from './common/config';
6-
import { APP_INTERCEPTOR } from '@nestjs/core';
7+
import { EnvConfigModule } from './common/config/config.module';
8+
import { ExceptionFilter } from './common/exceptions/exception.filter';
9+
import { JwtHttpStrategy } from './common/guards/strategy';
710
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
11+
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
812
import { DatabaseModule } from './database/database.module';
9-
import { WebhookModule } from './modules/webhook/webhook.module';
10-
import { JwtHttpStrategy } from './common/guards/strategy';
11-
import { Web3Module } from './modules/web3/web3.module';
12-
import { UserModule } from './modules/user/user.module';
13-
import { StatsModule } from './modules/stats/stats.module';
1413
import { AssignmentModule } from './modules/assignment/assignment.module';
1514
import { CronJobModule } from './modules/cron-job/cron-job.module';
1615
import { HealthModule } from './modules/health/health.module';
17-
import { EnvConfigModule } from './common/config/config.module';
18-
import { ScheduleModule } from '@nestjs/schedule';
19-
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
16+
import { JobModule } from './modules/job/job.module';
17+
import { StatsModule } from './modules/stats/stats.module';
18+
import { UserModule } from './modules/user/user.module';
19+
import { Web3Module } from './modules/web3/web3.module';
20+
import { WebhookModule } from './modules/webhook/webhook.module';
21+
import { HttpValidationPipe } from './common/pipes';
2022

2123
@Module({
2224
providers: [
25+
{
26+
provide: APP_PIPE,
27+
useClass: HttpValidationPipe,
28+
},
2329
{
2430
provide: APP_INTERCEPTOR,
2531
useClass: SnakeCaseInterceptor,
@@ -28,6 +34,10 @@ import { TransformEnumInterceptor } from './common/interceptors/transform-enum.i
2834
provide: APP_INTERCEPTOR,
2935
useClass: TransformEnumInterceptor,
3036
},
37+
{
38+
provide: APP_FILTER,
39+
useClass: ExceptionFilter,
40+
},
3141
JwtHttpStrategy,
3242
],
3343
imports: [

packages/apps/fortune/exchange-oracle/server/src/database/database.error.ts renamed to packages/apps/fortune/exchange-oracle/server/src/common/errors/database.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { QueryFailedError } from 'typeorm';
2-
import { PostgresErrorCodes } from './database.enum';
3-
4-
export class DatabaseError extends Error {
5-
constructor(message: string, stack: string) {
6-
super(message);
7-
this.stack = stack;
8-
}
9-
}
2+
import { DatabaseError } from '.';
3+
import { PostgresErrorCodes } from '../enums/database';
104

115
export function handleQueryFailedError(error: QueryFailedError): DatabaseError {
126
const stack = error.stack || '';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
export * from './database';
2+
3+
export class BaseError extends Error {
4+
constructor(message: string, stack?: string) {
5+
super(message);
6+
this.name = this.constructor.name;
7+
if (stack) {
8+
this.stack = stack;
9+
}
10+
}
11+
}
12+
13+
export class ValidationError extends BaseError {
14+
constructor(message: string, stack?: string) {
15+
super(message, stack);
16+
}
17+
}
18+
19+
export class AuthError extends BaseError {
20+
constructor(message: string, stack?: string) {
21+
super(message, stack);
22+
}
23+
}
24+
25+
export class ForbiddenError extends BaseError {
26+
constructor(message: string, stack?: string) {
27+
super(message, stack);
28+
}
29+
}
30+
31+
export class NotFoundError extends BaseError {
32+
constructor(message: string, stack?: string) {
33+
super(message, stack);
34+
}
35+
}
36+
37+
export class ConflictError extends BaseError {
38+
constructor(message: string, stack?: string) {
39+
super(message, stack);
40+
}
41+
}
42+
43+
export class ServerError extends BaseError {
44+
constructor(message: string, stack?: string) {
45+
super(message, stack);
46+
}
47+
}
48+
49+
export class DatabaseError extends BaseError {
50+
constructor(message: string, stack?: string) {
51+
super(message, stack);
52+
}
53+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
ArgumentsHost,
3+
Catch,
4+
ExceptionFilter as IExceptionFilter,
5+
HttpStatus,
6+
Logger,
7+
} from '@nestjs/common';
8+
import { Request, Response } from 'express';
9+
import {
10+
ValidationError,
11+
AuthError,
12+
ForbiddenError,
13+
NotFoundError,
14+
ConflictError,
15+
ServerError,
16+
DatabaseError,
17+
} from '../errors';
18+
19+
@Catch()
20+
export class ExceptionFilter implements IExceptionFilter {
21+
private logger = new Logger(ExceptionFilter.name);
22+
23+
private getStatus(exception: any): number {
24+
if (exception instanceof ValidationError) {
25+
return HttpStatus.BAD_REQUEST;
26+
} else if (exception instanceof AuthError) {
27+
return HttpStatus.UNAUTHORIZED;
28+
} else if (exception instanceof ForbiddenError) {
29+
return HttpStatus.FORBIDDEN;
30+
} else if (exception instanceof NotFoundError) {
31+
return HttpStatus.NOT_FOUND;
32+
} else if (exception instanceof ConflictError) {
33+
return HttpStatus.CONFLICT;
34+
} else if (exception instanceof ServerError) {
35+
return HttpStatus.UNPROCESSABLE_ENTITY;
36+
} else if (exception instanceof DatabaseError) {
37+
return HttpStatus.UNPROCESSABLE_ENTITY;
38+
} else if (exception.statusCode) {
39+
return exception.statusCode;
40+
}
41+
return HttpStatus.INTERNAL_SERVER_ERROR;
42+
}
43+
44+
catch(exception: any, host: ArgumentsHost) {
45+
const ctx = host.switchToHttp();
46+
const response = ctx.getResponse<Response>();
47+
const request = ctx.getRequest<Request>();
48+
49+
const status = this.getStatus(exception);
50+
const message = exception.message || 'Internal server error';
51+
52+
this.logger.error(
53+
`Exception caught: ${message}`,
54+
exception.stack || 'No stack trace available',
55+
);
56+
57+
response.status(status).json({
58+
status_code: status,
59+
timestamp: new Date().toISOString(),
60+
message: message,
61+
path: request.url,
62+
});
63+
}
64+
}

packages/apps/fortune/exchange-oracle/server/src/common/guards/jwt.auth.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import {
2-
CanActivate,
3-
ExecutionContext,
4-
Injectable,
5-
UnauthorizedException,
6-
} from '@nestjs/common';
1+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
72
import { Reflector } from '@nestjs/core';
83
import { AuthGuard } from '@nestjs/passport';
94
import { Role } from '../enums/role';
5+
import { AuthError } from '../errors';
106
import { JwtUser } from '../types/jwt';
117

128
@Injectable()
@@ -27,10 +23,7 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate {
2723
}
2824

2925
// Try to authenticate with JWT
30-
const canActivate = (await super.canActivate(context)) as boolean;
31-
if (!canActivate) {
32-
throw new UnauthorizedException('JWT authentication failed');
33-
}
26+
await super.canActivate(context);
3427

3528
// Roles verification
3629
let roles = this.reflector.get<Role[]>('roles', context.getHandler());
@@ -39,11 +32,11 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate {
3932
const request = context.switchToHttp().getRequest();
4033
const user = request.user as JwtUser;
4134
if (!user) {
42-
throw new UnauthorizedException('User not found in request');
35+
throw new AuthError('User not found in request');
4336
}
4437

4538
if (!roles.includes(user.role)) {
46-
throw new UnauthorizedException('Invalid role');
39+
throw new AuthError('Invalid role');
4740
}
4841

4942
return true;

packages/apps/fortune/exchange-oracle/server/src/common/guards/signature.auth.spec.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Test, TestingModule } from '@nestjs/testing';
2-
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
3-
import { SignatureAuthGuard } from './signature.auth';
4-
import { verifySignature } from '../utils/signature';
51
import { ChainId, EscrowUtils } from '@human-protocol/sdk';
6-
import { AuthSignatureRole } from '../enums/role';
7-
import { HEADER_SIGNATURE_KEY } from '../constant';
8-
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
2+
import { ExecutionContext } from '@nestjs/common';
93
import { Reflector } from '@nestjs/core';
4+
import { Test, TestingModule } from '@nestjs/testing';
5+
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
6+
import { HEADER_SIGNATURE_KEY } from '../constant';
7+
import { AuthSignatureRole } from '../enums/role';
8+
import { AuthError, NotFoundError } from '../errors';
9+
import { verifySignature } from '../utils/signature';
10+
import { SignatureAuthGuard } from './signature.auth';
1011

1112
jest.mock('../utils/signature');
1213
jest.mock('@human-protocol/sdk', () => ({
@@ -84,7 +85,7 @@ describe('SignatureAuthGuard', () => {
8485
);
8586
});
8687

87-
it('should throw UnauthorizedException if signature is not verified', async () => {
88+
it('should throw AuthError if signature is not verified', async () => {
8889
reflector.get = jest
8990
.fn()
9091
.mockReturnValue([AuthSignatureRole.JobLauncher]);
@@ -96,9 +97,7 @@ describe('SignatureAuthGuard', () => {
9697
};
9798
(verifySignature as jest.Mock).mockReturnValue(false);
9899

99-
await expect(guard.canActivate(context)).rejects.toThrow(
100-
UnauthorizedException,
101-
);
100+
await expect(guard.canActivate(context)).rejects.toThrow(AuthError);
102101
});
103102

104103
it('should handle Worker role and verify signature', async () => {
@@ -118,7 +117,7 @@ describe('SignatureAuthGuard', () => {
118117
expect(assignmentRepository.findOneById).toHaveBeenCalledWith('1');
119118
});
120119

121-
it('should throw UnauthorizedException if assignment is not found for Worker role', async () => {
120+
it('should throw AuthError if assignment is not found for Worker role', async () => {
122121
reflector.get = jest.fn().mockReturnValue([AuthSignatureRole.Worker]);
123122

124123
mockRequest.headers[HEADER_SIGNATURE_KEY] = 'validSignature';
@@ -128,9 +127,7 @@ describe('SignatureAuthGuard', () => {
128127
(verifySignature as jest.Mock).mockReturnValue(true);
129128
assignmentRepository.findOneById.mockResolvedValue(null);
130129

131-
await expect(guard.canActivate(context)).rejects.toThrow(
132-
UnauthorizedException,
133-
);
130+
await expect(guard.canActivate(context)).rejects.toThrow(NotFoundError);
134131
});
135132

136133
it('should handle multiple roles and verify signature', async () => {

packages/apps/fortune/exchange-oracle/server/src/common/guards/signature.auth.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1+
import { EscrowUtils } from '@human-protocol/sdk';
12
import {
23
CanActivate,
34
ExecutionContext,
45
Injectable,
5-
NotImplementedException,
6-
UnauthorizedException,
6+
Logger,
77
} from '@nestjs/common';
8-
import { verifySignature } from '../utils/signature';
9-
import { HEADER_SIGNATURE_KEY } from '../constant';
10-
import { EscrowUtils } from '@human-protocol/sdk';
11-
import { AuthSignatureRole } from '../enums/role';
128
import { Reflector } from '@nestjs/core';
139
import { AssignmentRepository } from '../../modules/assignment/assignment.repository';
10+
import { HEADER_SIGNATURE_KEY } from '../constant';
1411
import { ErrorAssignment, ErrorSignature } from '../constant/errors';
12+
import { AuthSignatureRole } from '../enums/role';
13+
import { AuthError, NotFoundError } from '../errors';
14+
import { verifySignature } from '../utils/signature';
1515

1616
@Injectable()
1717
export class SignatureAuthGuard implements CanActivate {
18+
private readonly logger = new Logger(SignatureAuthGuard.name);
1819
constructor(
1920
private reflector: Reflector,
2021
private readonly assignmentRepository: AssignmentRepository,
@@ -25,7 +26,7 @@ export class SignatureAuthGuard implements CanActivate {
2526
'roles',
2627
context.getHandler(),
2728
);
28-
if (!roles) throw new NotImplementedException(ErrorSignature.MissingRoles);
29+
if (!roles) throw new Error(ErrorSignature.MissingRoles);
2930
const request = context.switchToHttp().getRequest();
3031
const data = request.body;
3132
const signature = request.headers[HEADER_SIGNATURE_KEY];
@@ -38,7 +39,7 @@ export class SignatureAuthGuard implements CanActivate {
3839
if (assignment) {
3940
oracleAdresses.push(assignment.workerAddress);
4041
} else {
41-
throw new UnauthorizedException(ErrorAssignment.NotFound);
42+
throw new NotFoundError(ErrorAssignment.NotFound);
4243
}
4344
} else {
4445
const escrowData = await EscrowUtils.getEscrow(
@@ -64,9 +65,9 @@ export class SignatureAuthGuard implements CanActivate {
6465
return true;
6566
}
6667
} catch (error) {
67-
console.error(error);
68+
this.logger.error(error);
6869
}
6970

70-
throw new UnauthorizedException('Unauthorized');
71+
throw new AuthError('Unauthorized');
7172
}
7273
}

0 commit comments

Comments
 (0)