rwadurian/backend/services/trading-service/src/infrastructure/kafka/market-maker-deposit-consum...

296 lines
9.1 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 { Kafka, Consumer, EachMessagePayload } from 'kafkajs';
import { PrismaService } from '../persistence/prisma/prisma.service';
import { ProcessedEventRepository } from '../persistence/repositories/processed-event.repository';
import { RedisService } from '../redis/redis.service';
import { Prisma, MarketMakerAssetType, MarketMakerDepositStatus } from '@prisma/client';
// 4小时 TTL
const IDEMPOTENCY_TTL_SECONDS = 4 * 60 * 60;
interface MarketMakerDepositConfirmedPayload {
depositId: string;
chainType: string;
txHash: string;
fromAddress: string;
toAddress: string;
assetType: 'EUSDT' | 'FUSDT';
tokenContract: string;
amount: string;
amountFormatted: string;
confirmations: number;
blockNumber: string;
blockTimestamp: string;
}
/**
* Market Maker Deposit Consumer Service
* 监听 mining-blockchain-service 的做市商充值确认事件
* 当充值确认后,为做市商账户入账
*/
@Injectable()
export class MarketMakerDepositConsumerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(MarketMakerDepositConsumerService.name);
private kafka: Kafka;
private consumer: Consumer;
private isRunning = false;
constructor(
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
private readonly processedEventRepository: ProcessedEventRepository,
private readonly redis: RedisService,
) {
const brokers = this.configService
.get<string>('KAFKA_BROKERS', 'localhost:9092')
.split(',');
this.kafka = new Kafka({
clientId: 'trading-service-mm-deposit',
brokers,
});
this.consumer = this.kafka.consumer({
groupId: this.configService.get<string>(
'MM_DEPOSIT_CONSUMER_GROUP',
'trading-service-mm-deposit-group',
),
});
}
async onModuleInit() {
await this.start();
}
async onModuleDestroy() {
await this.stop();
}
async start(): Promise<void> {
if (this.isRunning) {
this.logger.warn('Market maker deposit consumer is already running');
return;
}
const topic = 'blockchain.market_maker.deposits';
try {
await this.consumer.connect();
this.logger.log('Market maker deposit consumer connected');
await this.consumer.subscribe({
topics: [topic],
fromBeginning: false,
});
this.logger.log(`Subscribed to topic: ${topic}`);
await this.consumer.run({
eachMessage: async (payload: EachMessagePayload) => {
await this.handleMessage(payload);
},
});
this.isRunning = true;
this.logger.log('Market maker deposit consumer started - listening for deposit confirmed events');
} catch (error) {
this.logger.error('Failed to start market maker deposit consumer', error);
}
}
async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
try {
await this.consumer.disconnect();
this.isRunning = false;
this.logger.log('Market maker deposit consumer stopped');
} catch (error) {
this.logger.error('Failed to stop market maker deposit consumer', error);
}
}
private async handleMessage(payload: EachMessagePayload): Promise<void> {
const { topic, message } = payload;
try {
if (!message.value) {
return;
}
const eventData = JSON.parse(message.value.toString());
// 检查事件类型
if (eventData.eventType !== 'blockchain.market_maker.deposit.confirmed') {
return;
}
const depositPayload = eventData.payload as MarketMakerDepositConfirmedPayload;
await this.handleDepositConfirmed(eventData.eventId, depositPayload);
} catch (error) {
this.logger.error(`Error processing message from topic ${topic}`, error);
}
}
/**
* 处理做市商充值确认事件
*/
private async handleDepositConfirmed(
eventId: string,
payload: MarketMakerDepositConfirmedPayload,
): Promise<void> {
this.logger.log(
`Processing MarketMakerDepositConfirmed: ${payload.txHash}, amount: ${payload.amountFormatted} ${payload.assetType}`,
);
// 幂等性检查
if (await this.isEventProcessed(eventId)) {
this.logger.debug(`Event ${eventId} already processed, skipping`);
return;
}
try {
// 查找做市商配置(通过钱包地址)
const marketMaker = await this.prisma.marketMakerConfig.findFirst({
where: {
kavaWalletAddress: {
equals: payload.toAddress,
mode: 'insensitive',
},
},
});
if (!marketMaker) {
this.logger.error(
`Market maker not found for wallet address: ${payload.toAddress}`,
);
await this.markEventProcessed(eventId);
return;
}
// 使用事务处理入账
await this.prisma.$transaction(async (tx) => {
// 1. 创建或更新充值记录
const depositRecord = await tx.marketMakerDeposit.upsert({
where: { txHash: payload.txHash },
create: {
marketMakerId: marketMaker.id,
chainType: payload.chainType,
txHash: payload.txHash,
blockNumber: BigInt(payload.blockNumber),
blockTimestamp: new Date(payload.blockTimestamp),
assetType: payload.assetType as MarketMakerAssetType,
tokenContract: payload.tokenContract,
fromAddress: payload.fromAddress,
toAddress: payload.toAddress,
amount: new Prisma.Decimal(payload.amountFormatted),
amountRaw: new Prisma.Decimal(payload.amount),
confirmations: payload.confirmations,
requiredConfirms: 12,
status: MarketMakerDepositStatus.CONFIRMED,
},
update: {
confirmations: payload.confirmations,
status: MarketMakerDepositStatus.CONFIRMED,
},
});
// 检查是否已入账
if (depositRecord.status === MarketMakerDepositStatus.CREDITED) {
this.logger.debug(`Deposit ${payload.txHash} already credited, skipping`);
return;
}
// 2. 根据资产类型更新余额
const assetField = payload.assetType === 'EUSDT' ? 'shareBalance' : 'cashBalance';
const currentBalance = payload.assetType === 'EUSDT'
? marketMaker.shareBalance
: marketMaker.cashBalance;
const newBalance = currentBalance.add(new Prisma.Decimal(payload.amountFormatted));
// 更新做市商余额
await tx.marketMakerConfig.update({
where: { id: marketMaker.id },
data: {
[assetField]: newBalance,
},
});
// 3. 创建流水记录
const ledger = await tx.marketMakerLedger.create({
data: {
marketMakerId: marketMaker.id,
type: 'DEPOSIT',
assetType: payload.assetType === 'EUSDT' ? 'SHARE' : 'CASH',
amount: new Prisma.Decimal(payload.amountFormatted),
balanceBefore: currentBalance,
balanceAfter: newBalance,
memo: `区块链充值: ${payload.txHash.slice(0, 10)}... (${payload.fromAddress.slice(0, 10)}...)`,
},
});
// 4. 更新充值记录状态为已入账
await tx.marketMakerDeposit.update({
where: { id: depositRecord.id },
data: {
status: MarketMakerDepositStatus.CREDITED,
creditedAt: new Date(),
creditedAmount: new Prisma.Decimal(payload.amountFormatted),
ledgerId: ledger.id,
},
});
this.logger.log(
`[CREDITED] Market maker deposit: ${payload.txHash.slice(0, 10)}... | ` +
`${payload.assetType}: ${payload.amountFormatted} | ` +
`Balance: ${currentBalance} -> ${newBalance}`,
);
});
// 标记为已处理
await this.markEventProcessed(eventId);
} catch (error) {
this.logger.error(
`Failed to process deposit for ${payload.txHash}`,
error instanceof Error ? error.stack : error,
);
throw error;
}
}
private async isEventProcessed(eventId: string): Promise<boolean> {
const redisKey = `trading:processed-event:mm-deposit:${eventId}`;
const cached = await this.redis.get(redisKey);
if (cached) return true;
const dbRecord = await this.processedEventRepository.findByEventId(eventId);
if (dbRecord) {
await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS);
return true;
}
return false;
}
private async markEventProcessed(eventId: string): Promise<void> {
const redisKey = `trading:processed-event:mm-deposit:${eventId}`;
try {
await this.processedEventRepository.create({
eventId,
eventType: 'MarketMakerDepositConfirmed',
sourceService: 'mining-blockchain-service',
});
} catch (error) {
if (!(error instanceof Error && error.message.includes('Unique constraint'))) {
throw error;
}
}
await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS);
}
}