Files
managing-apps/src/Managing.Web3Proxy/src/routes/home.ts
2025-10-06 02:46:08 +07:00

272 lines
8.7 KiB
TypeScript

import {FastifyPluginAsyncTypebox, Type} from '@fastify/type-provider-typebox'
import {handleError} from '../utils/errorHandler.js'
import {getClientForAddress} from '../plugins/custom/gmx.js'
import {getPrivyClient} from '../plugins/custom/privy.js'
import {createClient} from 'redis'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
'/',
{
schema: {
tags: ['Home'],
description: 'Welcome endpoint that confirms the API is running',
response: {
200: Type.Object({
message: Type.String()
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
},
async function (request, reply) {
try {
return { message: 'Welcome to the official Web3 Proxy API!' }
} catch (error) {
return handleError(request, reply, error, 'home-root');
}
}
)
// Enhanced health check endpoint
fastify.get('/health', {
schema: {
tags: ['Health'],
description: 'Enhanced health check endpoint that verifies API, Privy, and GMX connectivity',
response: {
200: Type.Object({
status: Type.String(),
timestamp: Type.String(),
version: Type.String(),
checks: Type.Object({
privy: Type.Object({
status: Type.String(),
message: Type.String()
}),
gmx: Type.Object({
status: Type.String(),
message: Type.String(),
data: Type.Optional(Type.Any())
}),
redis: Type.Object({
status: Type.String(),
message: Type.String(),
data: Type.Optional(Type.Any())
})
})
}),
500: Type.Object({
success: Type.Boolean(),
error: Type.String()
})
}
}
}, async function (request, reply) {
try {
console.log('Checking health...')
const checks = {
privy: await checkPrivy(fastify),
gmx: await checkGmx(),
redis: await checkRedis()
}
// If any check failed, set status to degraded
const overallStatus = Object.values(checks).some(check => check.status !== 'healthy')
? 'degraded'
: 'healthy';
return {
status: overallStatus,
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0',
checks
}
} catch (error) {
return handleError(request, reply, error, 'health');
}
})
// Helper function to check Privy connectivity and configuration
async function checkPrivy(fastify) {
try {
// Try to initialize the Privy client - this will validate env vars and connectivity
const privy = getPrivyClient(fastify);
// Check if the necessary configuration is available
const hasPrivyAppId = !!process.env.PRIVY_APP_ID;
const hasPrivySecret = !!process.env.PRIVY_APP_SECRET;
const hasPrivyAuthKey = !!process.env.PRIVY_AUTHORIZATION_KEY;
// Get the client status
const allConfigPresent = hasPrivyAppId && hasPrivySecret && hasPrivyAuthKey;
if (!allConfigPresent) {
return {
status: 'degraded',
message: 'Privy configuration incomplete: ' +
(!hasPrivyAppId ? 'PRIVY_APP_ID ' : '') +
(!hasPrivySecret ? 'PRIVY_APP_SECRET ' : '') +
(!hasPrivyAuthKey ? 'PRIVY_AUTHORIZATION_KEY ' : '')
};
}
// If we got this far, the Privy client was successfully initialized
return {
status: 'healthy',
message: 'Privy client successfully initialized'
};
} catch (error) {
return {
status: 'unhealthy',
message: `Privy client initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
// Helper function to check GMX connectivity using the GMX SDK
async function checkGmx() {
try {
// Use a dummy zero address for the health check
const account = "0x0000000000000000000000000000000000000000";
// Use the existing getClientForAddress function to get a proper GMX SDK instance
const sdk = await getClientForAddress(account);
// Get the uiFeeFactor - this is a lightweight call
const startTime = Date.now();
const uiFeeFactor = await sdk.utils.getUiFeeFactor();
const responseTime = Date.now() - startTime;
return {
status: 'healthy',
message: 'GMX SDK successfully initialized',
data: {
responseTimeMs: responseTime,
uiFeeFactor: uiFeeFactor.toString(),
uiFeeReceiverAccount: sdk.config.settings?.uiFeeReceiverAccount
}
};
} catch (error) {
return {
status: 'unhealthy',
message: `GMX SDK check failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
data: {
errorType: error instanceof Error ? error.constructor.name : 'Unknown'
}
};
}
}
// Helper function to check Redis connectivity for idempotency
async function checkRedis() {
let redisClient = null;
let isConnected = false;
try {
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
const redisPassword = process.env.REDIS_PASSWORD;
// Create Redis client configuration with timeout
const redisConfig: any = {
url: redisUrl,
socket: {
connectTimeout: 2000, // 2 second connection timeout
reconnectStrategy: false // Don't retry on health check
}
};
if (redisPassword) {
redisConfig.password = redisPassword;
}
redisClient = createClient(redisConfig);
// Suppress error logs for health checks to avoid spam
redisClient.on('error', () => {
// Silently ignore errors in health check
});
// Connect to Redis with timeout
const startTime = Date.now();
await Promise.race([
redisClient.connect().then(() => { isConnected = true; }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Connection timeout after 2s')), 2000)
)
]);
const connectTime = Date.now() - startTime;
// Test basic operations
const testKey = 'health-check-test';
const testValue = JSON.stringify({ timestamp: Date.now(), test: true });
// Test SET operation
await redisClient.set(testKey, testValue, { EX: 10 }); // 10 second expiry
// Test GET operation
const retrievedValue = await redisClient.get(testKey);
const getTime = Date.now() - startTime;
// Test JSON parsing
const parsedValue = JSON.parse(retrievedValue as string);
// Clean up test key
await redisClient.del(testKey);
// Get Redis info
const info = await redisClient.info('server');
const serverInfo = info.split('\r\n').reduce((acc, line) => {
const [key, value] = line.split(':');
if (key && value) {
acc[key] = value;
}
return acc;
}, {} as Record<string, string>);
return {
status: 'healthy',
message: 'Redis connection successful',
data: {
connectTimeMs: connectTime,
getTimeMs: getTime,
redisVersion: serverInfo.redis_version,
uptimeSeconds: serverInfo.uptime_in_seconds,
connectedClients: serverInfo.connected_clients,
usedMemory: serverInfo.used_memory_human,
hasPassword: !!redisPassword
}
};
} catch (error) {
return {
status: 'degraded',
message: 'Redis unavailable, using in-memory fallback',
data: {
errorType: error instanceof Error ? error.constructor.name : 'Unknown',
redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
hasPassword: !!process.env.REDIS_PASSWORD,
note: 'Service will function normally with in-memory idempotency storage'
}
};
} finally {
// Always close the Redis connection gracefully
if (redisClient) {
try {
// Only try to quit if we successfully connected
if (isConnected && redisClient.isOpen) {
// Use disconnect instead of quit for faster cleanup
await Promise.race([
redisClient.disconnect(),
new Promise((resolve) => setTimeout(resolve, 500)) // 500ms timeout for disconnect
]);
}
} catch (closeError) {
// Silently ignore close errors in health check
}
}
}
}
}
export default plugin