Initial commit: USDT transfer worker (RabbitMQ + Postgres + Vault)
This commit is contained in:
91
src/index.ts
Normal file
91
src/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import "dotenv/config";
|
||||
import type { Server } from "node:http";
|
||||
import { loadConfig } 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<void> {
|
||||
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 db = new PostgresClient(secrets.database);
|
||||
await db.ping();
|
||||
logger.info({ event: "bootstrap.pg_ready" }, "postgres pool ready");
|
||||
|
||||
const ethereum = new EthersEthereumGateway({
|
||||
rpcUrl: config.ethRpcUrl,
|
||||
privateKey: config.hotWalletPrivateKey,
|
||||
usdtContractAddress: config.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
|
||||
});
|
||||
|
||||
await amqp.startConsumer((message, log) => orchestrator.handle(message, log));
|
||||
|
||||
const app = createApp({ ethereum, 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<void>((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) => {
|
||||
// Logger may not be ready yet; fall back to stderr.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("fatal bootstrap error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user