rwadurian/backend/services/mining-blockchain-service/src/application/schedulers/hot-wallet-balance.schedule...

123 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { Erc20TransferService } from '@/domain/services/erc20-transfer.service';
import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter';
import { ChainType } from '@/domain/value-objects';
import { ChainTypeEnum } from '@/domain/enums';
import Redis from 'ioredis';
/**
* 热钱包余额定时更新调度器
* [2026-01-07] 更新:添加原生代币 (KAVA) 余额缓存
*
* 每 5 秒查询热钱包在各链上的余额,并更新到 Redis 缓存:
* - dUSDT (绿积分) 余额
* - 原生代币 (KAVA/BNB) 余额
*
* wallet-service 在用户发起转账时读取此缓存,预检查热钱包余额是否足够。
* reporting-service 读取此缓存用于仪表板显示。
*
* 注意:使用 Redis DB 0公共数据库以便所有服务都能读取。
*
* Redis Key 格式:
* - hot_wallet:dusdt_balance:{chainType} - dUSDT 余额
* - hot_wallet:native_balance:{chainType} - 原生代币余额 (KAVA/BNB)
* Redis Value: 余额字符串(如 "10000.00"
* TTL: 30 秒(防止服务故障时缓存过期)
*
* 回滚方式:恢复此文件到之前的版本(移除原生代币缓存逻辑)
*/
@Injectable()
export class HotWalletBalanceScheduler implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(HotWalletBalanceScheduler.name);
// Redis key 前缀
private readonly REDIS_KEY_PREFIX_DUSDT = 'hot_wallet:dusdt_balance:';
private readonly REDIS_KEY_PREFIX_NATIVE = 'hot_wallet:native_balance:';
// 缓存过期时间(秒)
private readonly CACHE_TTL_SECONDS = 30;
// 支持的链类型
private readonly SUPPORTED_CHAINS = [ChainTypeEnum.KAVA, ChainTypeEnum.BSC];
// 使用独立的 Redis 连接,连接到 DB 0公共数据库
private readonly sharedRedis: Redis;
constructor(
private readonly configService: ConfigService,
private readonly transferService: Erc20TransferService,
private readonly evmProvider: EvmProviderAdapter,
) {
// 创建连接到 DB 0 的 Redis 客户端(公共数据库,所有服务可读取)
this.sharedRedis = new Redis({
host: this.configService.get<string>('redis.host') || 'localhost',
port: this.configService.get<number>('redis.port') || 6379,
password: this.configService.get<string>('redis.password') || undefined,
db: 0, // 使用 DB 0 作为公共数据库
});
this.sharedRedis.on('connect', () => {
this.logger.log('[REDIS] Connected to shared Redis DB 0 for hot wallet balance');
});
this.sharedRedis.on('error', (err) => {
this.logger.error('[REDIS] Shared Redis connection error', err);
});
}
onModuleDestroy() {
this.sharedRedis.disconnect();
}
async onModuleInit() {
this.logger.log('[INIT] HotWalletBalanceScheduler initialized');
// 启动时立即执行一次
await this.updateHotWalletBalances();
}
/**
* 每 5 秒更新热钱包余额到 Redis
*/
@Cron('*/5 * * * * *') // 每 5 秒执行
async updateHotWalletBalances(): Promise<void> {
for (const chainType of this.SUPPORTED_CHAINS) {
try {
// 检查该链是否已配置
if (!this.transferService.isConfigured(chainType)) {
this.logger.debug(`[SKIP] Chain ${chainType} not configured, skipping balance update`);
continue;
}
// 获取热钱包地址
const hotWalletAddress = this.transferService.getHotWalletAddress(chainType);
if (!hotWalletAddress) {
this.logger.debug(`[SKIP] Hot wallet address not configured for ${chainType}`);
continue;
}
// 查询 dUSDT 余额
const dusdtBalance = await this.transferService.getHotWalletBalance(chainType);
const dusdtKey = `${this.REDIS_KEY_PREFIX_DUSDT}${chainType}`;
await this.sharedRedis.setex(dusdtKey, this.CACHE_TTL_SECONDS, dusdtBalance);
// [2026-01-07] 新增:查询原生代币余额 (KAVA/BNB)
const nativeBalance = await this.evmProvider.getNativeBalance(
ChainType.fromEnum(chainType),
hotWalletAddress,
);
const nativeKey = `${this.REDIS_KEY_PREFIX_NATIVE}${chainType}`;
await this.sharedRedis.setex(nativeKey, this.CACHE_TTL_SECONDS, nativeBalance.formatted);
this.logger.debug(
`[UPDATE] ${chainType} hot wallet: dUSDT=${dusdtBalance}, native=${nativeBalance.formatted}`,
);
} catch (error) {
this.logger.error(`[ERROR] Failed to update ${chainType} hot wallet balance`, error);
// 单链失败不影响其他链的更新
}
}
}
}