Skip to content

Commit 4f4cad6

Browse files
Merge pull request #1 from AlexGavrilov939/refactoring
refactor: remove 'any' types and improve error handling
2 parents b7d785a + 1c7032d commit 4f4cad6

13 files changed

Lines changed: 252 additions & 31 deletions

File tree

backend/src/config/env.validation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const envSchema = z.object({
1717
'Invalid private key format (must be 0x followed by 64 hex chars)',
1818
),
1919
INK_SEPOLIA_RPC_URL: z.string().url('Invalid RPC URL'),
20+
CHAIN_ID: z.coerce.number().default(763373),
2021

2122
MINT_TOKEN_ADDRESS: z
2223
.string()
@@ -25,6 +26,12 @@ const envSchema = z.object({
2526
WEBHOOK_SECRET: z.string().min(32, 'Webhook secret must be at least 32 characters'),
2627

2728
FRONTEND_URL: z.string().url().default('http://localhost:3000'),
29+
30+
SENTRY_DSN: z.string().url().optional(),
31+
SENTRY_ENVIRONMENT: z.string().optional(),
32+
SENTRY_RELEASE: z.string().optional(),
33+
SENTRY_TRACES_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(0.1),
34+
SENTRY_PROFILES_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(0.1),
2835
});
2936

3037
export type EnvConfig = z.infer<typeof envSchema>;

backend/src/infra/database/connection.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import * as dotenv from 'dotenv';
22
import { drizzle } from 'drizzle-orm/postgres-js';
33
import postgres from 'postgres';
4+
import { validateEnv } from '../../config/env.validation';
45
import * as schema from './schema';
56

67
dotenv.config();
78

8-
const connectionString = process.env.DATABASE_URL;
9+
const env = validateEnv();
910

10-
if (!connectionString) {
11-
throw new Error('DATABASE_URL environment variable is not set');
12-
}
13-
14-
export const client = postgres(connectionString, {
11+
export const client = postgres(env.DATABASE_URL, {
1512
max: 10,
1613
idle_timeout: 20,
1714
connect_timeout: 10000,

backend/src/infra/database/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const wallets = pgTable(
1414
{
1515
id: uuid('id').primaryKey().defaultRandom(),
1616
address: varchar('address', { length: 42 }).notNull().unique(),
17-
email: varchar('email', { length: 255 }).notNull(),
17+
email: varchar('email', { length: 255 }).notNull().unique(),
1818
createdAt: timestamp('created_at').defaultNow().notNull(),
1919
updatedAt: timestamp('updated_at').defaultNow().notNull(),
2020
},

backend/src/infra/monitoring/sentry.config.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
import * as Sentry from '@sentry/node';
22
import { nodeProfilingIntegration } from '@sentry/profiling-node';
33
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
4+
import * as dotenv from 'dotenv';
5+
import { validateEnv } from '../../config/env.validation';
6+
7+
dotenv.config();
48

59
interface SentryConfig {
610
dsn?: string;
711
environment: string;
8-
release?: string;
12+
release: string;
913
tracesSampleRate: number;
1014
profilesSampleRate: number;
1115
enabled: boolean;
1216
}
1317

1418
function getSentryConfig(): SentryConfig {
15-
const dsn = process.env.SENTRY_DSN;
16-
const environment = process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'development';
17-
const release = process.env.SENTRY_RELEASE || `mint-flow-backend@${process.env.npm_package_version || '1.0.0'}`;
18-
19-
const tracesSampleRate = Number.parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.1');
20-
const profilesSampleRate = Number.parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE || '0.1');
19+
const env = validateEnv();
2120

22-
const enabled = Boolean(dsn) && environment !== 'test';
21+
const environment = env.SENTRY_ENVIRONMENT || env.NODE_ENV;
22+
const release = env.SENTRY_RELEASE || `mint-flow-backend@1.0.0`;
23+
const enabled = Boolean(env.SENTRY_DSN) && environment !== 'test';
2324

2425
return {
25-
dsn,
26+
dsn: env.SENTRY_DSN,
2627
environment,
2728
release,
28-
tracesSampleRate,
29-
profilesSampleRate,
29+
tracesSampleRate: env.SENTRY_TRACES_SAMPLE_RATE,
30+
profilesSampleRate: env.SENTRY_PROFILES_SAMPLE_RATE,
3031
enabled,
3132
};
3233
}

backend/src/main.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@ import 'reflect-metadata';
22
import { setupSwagger } from '@config/swagger.config';
33
import fastifyCors from '@fastify/cors';
44
import helmet from '@fastify/helmet';
5+
import { client } from '@infra/database/connection';
56
import { initializeSentry } from '@infra/monitoring/sentry.config';
67
import { ValidationPipe } from '@nestjs/common';
78
import { NestFactory } from '@nestjs/core';
89
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
910
import { AppModule } from './app.module';
1011

12+
let app: NestFastifyApplication;
13+
1114
async function bootstrap() {
1215
initializeSentry();
1316

14-
const app = await NestFactory.create<NestFastifyApplication>(
17+
app = await NestFactory.create<NestFastifyApplication>(
1518
AppModule,
1619
new FastifyAdapter({
1720
logger: true,
1821
trustProxy: true,
1922
}),
2023
);
2124

25+
app.enableShutdownHooks();
26+
2227
// biome-ignore lint/suspicious/noExplicitAny: Fastify plugin type compatibility with NestJS adapter
2328
await app.register(helmet as any, {
2429
contentSecurityPolicy: {
@@ -62,8 +67,35 @@ async function bootstrap() {
6267
const port = Number(process.env.PORT) || 3001;
6368

6469
await app.listen(port, '0.0.0.0');
70+
71+
console.log(`Application is running on port ${port}`);
6572
}
6673

74+
async function gracefulShutdown(signal: string) {
75+
console.log(`Received ${signal}, starting graceful shutdown...`);
76+
77+
try {
78+
if (app) {
79+
console.log('Closing application...');
80+
await app.close();
81+
console.log('Application closed successfully');
82+
}
83+
84+
console.log('Closing database connection...');
85+
await client.end();
86+
console.log('Database connection closed');
87+
88+
console.log('Graceful shutdown completed');
89+
process.exit(0);
90+
} catch (error) {
91+
console.error('Error during graceful shutdown:', error);
92+
process.exit(1);
93+
}
94+
}
95+
96+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
97+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
98+
6799
bootstrap().catch((error) => {
68100
console.error('Failed to start application:', error);
69101
process.exit(1);

backend/src/modules/minting/infrastructure/adapters/gelato-client.adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class GelatoClientAdapter implements IGelatoClientPort {
2727
const privateKey = this.configService.getOrThrow<string>('GELATO_SIGNER_PRIVATE_KEY');
2828
const rpcUrl = this.configService.getOrThrow<string>('INK_SEPOLIA_RPC_URL');
2929
this.tokenAddress = this.configService.getOrThrow<string>('MINT_TOKEN_ADDRESS');
30-
this.chainId = 763373; // Ink Sepolia
30+
this.chainId = this.configService.getOrThrow<number>('CHAIN_ID');
3131

3232
this.relay = new GelatoRelay();
3333
this.provider = new ethers.JsonRpcProvider(rpcUrl);

backend/src/shared/filters/http-exception.filter.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
2-
import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common';
2+
import { Catch, HttpException, HttpStatus, Inject, Logger } from '@nestjs/common';
3+
import { ConfigService } from '@nestjs/config';
34
import type { FastifyReply } from 'fastify';
45
import { ZodError } from 'zod';
56

67
@Catch()
78
export class HttpExceptionFilter implements ExceptionFilter {
89
private readonly logger = new Logger(HttpExceptionFilter.name);
910

11+
constructor(@Inject(ConfigService) private readonly configService: ConfigService) {}
12+
1013
catch(exception: unknown, host: ArgumentsHost): void {
1114
const ctx = host.switchToHttp();
1215
const reply = ctx.getResponse<FastifyReply>();
@@ -113,13 +116,15 @@ export class HttpExceptionFilter implements ExceptionFilter {
113116
};
114117
}
115118

119+
const nodeEnv = this.configService.get<string>('NODE_ENV', 'production');
120+
116121
return {
117122
status: HttpStatus.INTERNAL_SERVER_ERROR,
118123
response: {
119124
error: {
120125
code: 'INTERNAL_ERROR',
121126
message:
122-
process.env.NODE_ENV === 'development'
127+
nodeEnv === 'development'
123128
? exception instanceof Error
124129
? exception.message
125130
: 'Unknown error'

frontend/app/error.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
export default function Error({
4+
error,
5+
reset,
6+
}: {
7+
error: Error & { digest?: string };
8+
reset: () => void;
9+
}) {
10+
return (
11+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
12+
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 text-center">
13+
<div className="mb-4">
14+
<svg
15+
className="mx-auto h-12 w-12 text-red-500"
16+
fill="none"
17+
viewBox="0 0 24 24"
18+
stroke="currentColor"
19+
aria-hidden="true"
20+
>
21+
<path
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
strokeWidth={2}
25+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
26+
/>
27+
</svg>
28+
</div>
29+
30+
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">
31+
Something went wrong!
32+
</h2>
33+
34+
<p className="text-gray-600 dark:text-gray-400 mb-6">
35+
{error.message || 'An unexpected error occurred. Please try again.'}
36+
</p>
37+
38+
{error.digest && (
39+
<p className="text-xs text-gray-500 dark:text-gray-500 mb-6 font-mono">
40+
Error ID: {error.digest}
41+
</p>
42+
)}
43+
44+
<button
45+
onClick={reset}
46+
className="w-full px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
47+
>
48+
Try again
49+
</button>
50+
</div>
51+
</div>
52+
);
53+
}

frontend/features/auth/components/WalletInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useAuth } from '../hooks/useAuth';
44
import { useState, useEffect } from 'react';
55
import { ethers } from 'ethers';
6+
import type { AxiosError } from 'axios';
67

78
const INK_SEPOLIA_RPC_URL = process.env.NEXT_PUBLIC_INK_SEPOLIA_RPC_URL || 'https://rpc-gel-sepolia.inkonchain.com';
89
const EXPLORER_URL = process.env.NEXT_PUBLIC_EXPLORER_URL || 'https://explorer-sepolia.inkonchain.com';
@@ -148,7 +149,7 @@ export function WalletInfo() {
148149

149150
{registrationError &&
150151
!isRegistered &&
151-
(registrationError as any)?.response?.status !== 409 && (
152+
(registrationError as AxiosError)?.response?.status !== 409 && (
152153
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-3">
153154
<p className="text-xs text-red-800 dark:text-red-200">
154155
Registration failed. Please try reconnecting.

frontend/features/auth/hooks/useAuth.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
44
import { useMutation } from '@tanstack/react-query';
55
import { useEffect, useState } from 'react';
6+
import type { AxiosError } from 'axios';
67
import { apiClient } from '@/lib/api';
78
import type { RegisterWalletRequest, RegisterWalletResponse } from '../types';
89

@@ -25,9 +26,10 @@ export function useAuth(): UseAuthReturn {
2526
try {
2627
const response = await apiClient.post<RegisterWalletResponse>('/wallets', data);
2728
return response.data;
28-
} catch (error: any) {
29+
} catch (error) {
30+
const axiosError = error as AxiosError<{ error?: { code?: string } }>;
2931
// 409 Conflict means wallet already registered - treat as success
30-
if (error?.response?.status === 409 || error?.response?.data?.error?.code === 'CONFLICT') {
32+
if (axiosError?.response?.status === 409 || axiosError?.response?.data?.error?.code === 'CONFLICT') {
3133
return { success: true, alreadyRegistered: true };
3234
}
3335
throw error;
@@ -37,7 +39,7 @@ export function useAuth(): UseAuthReturn {
3739
console.log('✅ Wallet registered successfully:', data);
3840
setIsRegistered(true);
3941
},
40-
onError: (error: any) => {
42+
onError: (error) => {
4143
console.error('❌ Wallet registration failed:', error);
4244
},
4345
});
@@ -69,7 +71,7 @@ export function useAuth(): UseAuthReturn {
6971
};
7072

7173
registerWallet();
72-
}, [user, primaryWallet, isRegistered]);
74+
}, [user, primaryWallet, isRegistered, registerWalletMutation]);
7375

7476
const logout = async () => {
7577
setIsRegistered(false);

0 commit comments

Comments
 (0)