import 'dotenv/config'; import type {Server} from 'node:http'; import {loadConfig,resolveEthereumConfig} from './config.js'; import {PostgresClient} from './db/PostgresClient.js'; import {EthersEthereumGateway} from './ethereum/EthersEthereumGateway.js'; import {createApp} from './http/app.js'; import {createLogger} from './logger.js'; import {AmqpClient} from './queue/AmqpClient.js'; import {VaultClient} from './secrets/VaultClient.js'; import {TransferOrchestrator} from './services/TransferOrchestrator.js'; async function main(): Promise { const config = loadConfig(); const logger = createLogger(config.logLevel); logger.info({ event: 'bootstrap.start' }, 'starting usdt-transfer service'); const vault = new VaultClient({ addr: config.vaultAddr, mountPoint: config.vaultMountPoint, roleId: config.vaultRoleId, secretId: config.vaultSecretId }); const secrets = await vault.bootstrap(); logger.info({ event: 'bootstrap.vault_loaded' }, 'vault secrets loaded'); const ethereumConfig = resolveEthereumConfig(config, secrets.ethereum); const db = new PostgresClient(secrets.database); await db.ping(); logger.info({ event: 'bootstrap.pg_ready' }, 'postgres pool ready'); const ethereum = new EthersEthereumGateway({ rpcUrl: ethereumConfig.ethRpcUrl, privateKey: ethereumConfig.hotWalletPrivateKey, usdtContractAddress: ethereumConfig.usdtContractAddress }); let triggerShutdown: (reason: string) => void = () => {}; const amqp = new AmqpClient({ secret: secrets.rabbitmq, logger, onConnectionLost: () => triggerShutdown('amqp_connection_lost') }); await amqp.connect(); const orchestrator = new TransferOrchestrator(db, ethereum, amqp, { maxTransferAmountUnits: config.maxTransferAmountUnits, minEthBalanceWei: config.minEthBalanceWei, balanceCheckAttempts: config.balanceCheckAttempts, balanceCheckIntervalMs: config.balanceCheckIntervalMs, staleWeb3ProcessingMs: config.staleWeb3ProcessingMs }); await amqp.startConsumer((message, log) => orchestrator.handle(message, log)); const app = createApp({ ethereum, db, amqp, logger }); const server: Server = await new Promise((resolve) => { const s = app.listen(config.port, () => resolve(s)); }); logger.info({ event: 'bootstrap.http_listening', port: config.port }, 'http listening'); let shuttingDown = false; triggerShutdown = (reason: string) => { if (shuttingDown) { return; } shuttingDown = true; logger.warn({ event: 'shutdown.start', reason }, 'shutting down'); void (async () => { try { await new Promise((resolve) => server.close(() => resolve())); await amqp.close(); await db.close(); logger.info({ event: 'shutdown.done' }, 'shutdown complete'); process.exit(reason === 'amqp_connection_lost' ? 1 : 0); } catch (error) { logger.error({ event: 'shutdown.error', err: error }, 'shutdown error'); process.exit(1); } })(); }; process.on('SIGINT', () => triggerShutdown('SIGINT')); process.on('SIGTERM', () => triggerShutdown('SIGTERM')); } main().catch((error) => { console.error('fatal bootstrap error:', error); process.exit(1); });