Initial commit: USDT transfer worker (RabbitMQ + Postgres + Vault)

This commit is contained in:
ZOMBIIIIIII
2026-04-30 20:00:21 +03:00
commit f250b99288
24 changed files with 3283 additions and 0 deletions

91
src/index.ts Normal file
View 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);
});