272 lines
8.7 KiB
TypeScript
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
|