diff --git a/backend/services/mining-admin-service/.env.example b/backend/services/mining-admin-service/.env.example index f09b68e5..d4da39b9 100644 --- a/backend/services/mining-admin-service/.env.example +++ b/backend/services/mining-admin-service/.env.example @@ -20,6 +20,12 @@ JWT_EXPIRES_IN=24h CONTRIBUTION_SERVICE_URL=http://localhost:3020 MINING_SERVICE_URL=http://localhost:3021 TRADING_SERVICE_URL=http://localhost:3022 +MINING_WALLET_SERVICE_URL=http://localhost:3025 +MINING_BLOCKCHAIN_SERVICE_URL=http://localhost:3020 + +# Pool Account Wallet Names (MPC 用户名,用于 walletName → poolType 映射) +BURN_POOL_WALLET_USERNAME=wallet-22fd661f +MINING_POOL_WALLET_USERNAME=wallet-974e78f5 # Default Admin DEFAULT_ADMIN_USERNAME=admin diff --git a/backend/services/mining-admin-service/src/api/api.module.ts b/backend/services/mining-admin-service/src/api/api.module.ts index 9ea32dcd..969cb3b7 100644 --- a/backend/services/mining-admin-service/src/api/api.module.ts +++ b/backend/services/mining-admin-service/src/api/api.module.ts @@ -15,6 +15,7 @@ import { BatchMiningController } from './controllers/batch-mining.controller'; import { VersionController } from './controllers/version.controller'; import { UpgradeVersionController } from './controllers/upgrade-version.controller'; import { MobileVersionController } from './controllers/mobile-version.controller'; +import { PoolAccountController } from './controllers/pool-account.controller'; @Module({ imports: [ @@ -40,6 +41,7 @@ import { MobileVersionController } from './controllers/mobile-version.controller VersionController, UpgradeVersionController, MobileVersionController, + PoolAccountController, ], }) export class ApiModule {} diff --git a/backend/services/mining-admin-service/src/api/controllers/pool-account.controller.ts b/backend/services/mining-admin-service/src/api/controllers/pool-account.controller.ts new file mode 100644 index 00000000..23cac4fb --- /dev/null +++ b/backend/services/mining-admin-service/src/api/controllers/pool-account.controller.ts @@ -0,0 +1,203 @@ +import { Controller, Get, Post, Body, Param, Logger, BadRequestException, HttpException, HttpStatus } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger'; +import { ConfigService } from '@nestjs/config'; + +// walletName → poolType 映射 +interface PoolMapping { + blockchainPoolType: string; // blockchain-service 使用的类型 (BURN_POOL / MINING_POOL) + walletPoolType: string; // wallet-service 使用的类型 (SHARE_POOL_A / SHARE_POOL_B) + name: string; +} + +@ApiTags('Pool Accounts') +@ApiBearerAuth() +@Controller('admin/pool-accounts') +export class PoolAccountController { + private readonly logger = new Logger(PoolAccountController.name); + private readonly walletServiceUrl: string; + private readonly blockchainServiceUrl: string; + private readonly walletNameMap: Record; + + constructor(private readonly configService: ConfigService) { + this.walletServiceUrl = this.configService.get( + 'MINING_WALLET_SERVICE_URL', + 'http://localhost:3025', + ); + this.blockchainServiceUrl = this.configService.get( + 'MINING_BLOCKCHAIN_SERVICE_URL', + 'http://localhost:3020', + ); + + // 从环境变量构建 walletName → poolType 映射 + const burnPoolUsername = this.configService.get('BURN_POOL_WALLET_USERNAME', ''); + const miningPoolUsername = this.configService.get('MINING_POOL_WALLET_USERNAME', ''); + + this.walletNameMap = {}; + if (burnPoolUsername) { + this.walletNameMap[burnPoolUsername] = { + blockchainPoolType: 'BURN_POOL', + walletPoolType: 'SHARE_POOL_A', + name: '100亿销毁池', + }; + } + if (miningPoolUsername) { + this.walletNameMap[miningPoolUsername] = { + blockchainPoolType: 'MINING_POOL', + walletPoolType: 'SHARE_POOL_B', + name: '200万挖矿池', + }; + } + + this.logger.log( + `Pool account proxy initialized: walletService=${this.walletServiceUrl}, blockchainService=${this.blockchainServiceUrl}, pools=${Object.keys(this.walletNameMap).join(',')}`, + ); + } + + @Get(':walletName/balance') + @ApiOperation({ summary: '获取池账户余额(代理到 wallet-service + blockchain-service)' }) + @ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' }) + async getPoolAccountBalance(@Param('walletName') walletName: string) { + const poolInfo = this.walletNameMap[walletName]; + if (!poolInfo) { + throw new BadRequestException(`Unknown wallet name: ${walletName}`); + } + + // 并行调用 wallet-service 和 blockchain-service + const [walletResponse, blockchainResponse] = await Promise.all([ + fetch(`${this.walletServiceUrl}/api/v2/pool-accounts/${poolInfo.walletPoolType}`).catch((err) => { + this.logger.error(`Failed to fetch wallet balance: ${err.message}`); + return null; + }), + fetch(`${this.blockchainServiceUrl}/api/v1/transfer/pool-accounts/${poolInfo.blockchainPoolType}/wallet-info`).catch((err) => { + this.logger.error(`Failed to fetch blockchain wallet info: ${err.message}`); + return null; + }), + ]); + + // 从 wallet-service 获取链下余额 + let balance = '0'; + let lastUpdated: string | undefined; + if (walletResponse && walletResponse.ok) { + const walletResult = await walletResponse.json(); + const data = walletResult.data || walletResult; + balance = data.balance?.toString() || '0'; + lastUpdated = data.updatedAt; + } + + // 从 blockchain-service 获取钱包地址和链上余额 + let walletAddress = ''; + let onChainBalance = '0'; + if (blockchainResponse && blockchainResponse.ok) { + const blockchainResult = await blockchainResponse.json(); + const data = blockchainResult.data || blockchainResult; + walletAddress = data.address || ''; + onChainBalance = data.balance || '0'; + } + + return { + walletName, + walletAddress, + balance, + availableBalance: balance, + frozenBalance: '0', + onChainBalance, + threshold: '0', + lastUpdated, + }; + } + + @Post(':walletName/blockchain-withdraw') + @ApiOperation({ summary: '池账户区块链提现(代理到 blockchain-service + wallet-service)' }) + @ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' }) + async poolAccountBlockchainWithdraw( + @Param('walletName') walletName: string, + @Body() body: { toAddress: string; amount: string }, + ) { + const poolInfo = this.walletNameMap[walletName]; + if (!poolInfo) { + throw new BadRequestException(`Unknown wallet name: ${walletName}`); + } + + if (!body.toAddress || !body.amount) { + throw new BadRequestException('toAddress and amount are required'); + } + + this.logger.log( + `[withdraw] ${poolInfo.name}: ${body.amount} fUSDT to ${body.toAddress}`, + ); + + // Step 1: 通过 blockchain-service 执行区块链转账 + let blockchainData: any; + try { + const blockchainResponse = await fetch( + `${this.blockchainServiceUrl}/api/v1/transfer/pool-account`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + poolType: poolInfo.blockchainPoolType, + toAddress: body.toAddress, + amount: body.amount, + }), + }, + ); + + const blockchainResult = await blockchainResponse.json(); + blockchainData = blockchainResult.data || blockchainResult; + + if (!blockchainResponse.ok || !blockchainData.success) { + this.logger.error(`[withdraw] Blockchain transfer failed: ${blockchainData.error}`); + throw new HttpException( + blockchainData.error || '区块链转账失败', + blockchainResponse.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } catch (error) { + if (error instanceof HttpException) throw error; + this.logger.error(`[withdraw] Failed to call blockchain service: ${error}`); + throw new HttpException( + `区块链服务调用失败: ${error instanceof Error ? error.message : error}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + // Step 2: 在 wallet-service 记录提现(扣减余额 + 分类账) + try { + const walletResponse = await fetch( + `${this.walletServiceUrl}/api/v2/pool-accounts/blockchain-withdraw`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + poolType: poolInfo.walletPoolType, + amount: body.amount, + txHash: blockchainData.txHash, + toAddress: body.toAddress, + blockNumber: blockchainData.blockNumber?.toString(), + }), + }, + ); + + if (!walletResponse.ok) { + const errResult = await walletResponse.json().catch(() => ({})); + this.logger.error( + `[withdraw] Failed to record withdrawal in wallet-service: ${JSON.stringify(errResult)}`, + ); + } + } catch (error) { + // 区块链转账已成功,wallet-service 记录失败不影响最终结果 + this.logger.error(`[withdraw] Failed to record withdrawal in wallet-service: ${error}`); + } + + this.logger.log( + `[withdraw] Success: ${poolInfo.name}, txHash=${blockchainData.txHash}`, + ); + + return { + success: true, + message: '提现成功', + txHash: blockchainData.txHash, + blockNumber: blockchainData.blockNumber, + }; + } +} diff --git a/backend/services/mining-blockchain-service/prisma/schema.prisma b/backend/services/mining-blockchain-service/prisma/schema.prisma index 9b01d520..706cd374 100644 --- a/backend/services/mining-blockchain-service/prisma/schema.prisma +++ b/backend/services/mining-blockchain-service/prisma/schema.prisma @@ -327,3 +327,67 @@ model MarketMakerBlockCheckpoint { @@unique([chainType, assetType], name: "uk_mm_chain_asset") @@map("market_maker_block_checkpoints") } + +// ============================================ +// 池账户充值交易表 +// 记录检测到的100亿销毁池和200万挖矿池的 fUSDT 充值 +// ============================================ +model PoolAccountDeposit { + id BigInt @id @default(autoincrement()) @map("deposit_id") + + chainType String @map("chain_type") @db.VarChar(20) // KAVA + txHash String @map("tx_hash") @db.VarChar(66) + + fromAddress String @map("from_address") @db.VarChar(42) + toAddress String @map("to_address") @db.VarChar(42) // 池钱包地址 + + // 池类型: BURN_POOL (100亿销毁池) 或 MINING_POOL (200万挖矿池) + poolType String @map("pool_type") @db.VarChar(20) + tokenContract String @map("token_contract") @db.VarChar(42) + amount Decimal @db.Decimal(78, 0) // 原始金额 (wei单位) + amountFormatted Decimal @map("amount_formatted") @db.Decimal(36, 8) // 格式化金额 + + blockNumber BigInt @map("block_number") + blockTimestamp DateTime @map("block_timestamp") + logIndex Int @map("log_index") + + // 确认状态 + confirmations Int @default(0) + status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, CREDITED, FAILED + + // 记账状态 + creditedAt DateTime? @map("credited_at") + creditReference String? @map("credit_reference") @db.VarChar(100) + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([chainType, txHash, logIndex], name: "uk_pool_chain_tx_log") + @@index([chainType, status], name: "idx_pool_chain_status") + @@index([poolType], name: "idx_pool_type") + @@index([blockNumber], name: "idx_pool_block") + @@map("pool_account_deposits") +} + +// ============================================ +// 池账户区块扫描检查点 +// 独立于做市商和用户的扫描进度 +// ============================================ +model PoolAccountBlockCheckpoint { + id BigInt @id @default(autoincrement()) @map("checkpoint_id") + + chainType String @map("chain_type") @db.VarChar(20) + poolType String @map("pool_type") @db.VarChar(20) // BURN_POOL 或 MINING_POOL + + lastScannedBlock BigInt @map("last_scanned_block") + lastScannedAt DateTime @map("last_scanned_at") + + isHealthy Boolean @default(true) @map("is_healthy") + lastError String? @map("last_error") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([chainType, poolType], name: "uk_pool_chain_pool") + @@map("pool_account_block_checkpoints") +} diff --git a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts index 555f8c21..6ad99495 100644 --- a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts +++ b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts @@ -1,7 +1,7 @@ import { Controller, Post, Body, Get, Param } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiProperty, ApiParam } from '@nestjs/swagger'; import { IsString, IsNotEmpty, Matches, IsNumberString } from 'class-validator'; -import { Erc20TransferService, TransferResult, TokenType } from '@/domain/services/erc20-transfer.service'; +import { Erc20TransferService, TransferResult, TokenType, PoolWalletType } from '@/domain/services/erc20-transfer.service'; import { ChainTypeEnum } from '@/domain/enums'; /** @@ -210,4 +210,54 @@ export class TransferController { }, }; } + + // ============ 池账户接口 ============ + + @Post('pool-account') + @ApiOperation({ summary: '池账户 fUSDT 提现' }) + @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) + async transferFromPoolAccount( + @Body() body: { poolType: PoolWalletType; toAddress: string; amount: string }, + ): Promise { + const result: TransferResult = await this.erc20TransferService.transferTokenAsPoolAccount( + ChainTypeEnum.KAVA, + body.poolType, + body.toAddress, + body.amount, + ); + + return { + success: result.success, + txHash: result.txHash, + error: result.error, + gasUsed: result.gasUsed, + blockNumber: result.blockNumber, + }; + } + + @Get('pool-accounts/:poolType/wallet-info') + @ApiOperation({ summary: '查询池钱包信息' }) + @ApiParam({ name: 'poolType', description: '池类型: BURN_POOL 或 MINING_POOL' }) + async getPoolWalletInfo( + @Param('poolType') poolType: PoolWalletType, + ): Promise<{ address: string; balance: string; chain: string; configured: boolean }> { + const address = this.erc20TransferService.getPoolWalletAddress(poolType); + const configured = this.erc20TransferService.isPoolWalletConfigured(poolType, ChainTypeEnum.KAVA); + + let balance = '0'; + if (address) { + try { + balance = await this.erc20TransferService.getPoolWalletTokenBalance(ChainTypeEnum.KAVA, poolType); + } catch { + balance = '0'; + } + } + + return { + address: address || '', + balance, + chain: 'KAVA', + configured, + }; + } } diff --git a/backend/services/mining-blockchain-service/src/application/application.module.ts b/backend/services/mining-blockchain-service/src/application/application.module.ts index 4ea6b85a..c1e0980f 100644 --- a/backend/services/mining-blockchain-service/src/application/application.module.ts +++ b/backend/services/mining-blockchain-service/src/application/application.module.ts @@ -3,6 +3,7 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module'; import { DomainModule } from '@/domain/domain.module'; import { MpcTransferInitializerService } from './services/mpc-transfer-initializer.service'; import { MarketMakerDepositDetectionService } from './services/market-maker-deposit-detection.service'; +import { PoolAccountDepositDetectionService } from './services/pool-account-deposit-detection.service'; import { OutboxPublisherService } from './services/outbox-publisher.service'; import { DepositAckConsumerService } from '@/infrastructure/kafka/deposit-ack-consumer.service'; @@ -13,6 +14,8 @@ import { DepositAckConsumerService } from '@/infrastructure/kafka/deposit-ack-co MpcTransferInitializerService, // 做市商充值检测服务 MarketMakerDepositDetectionService, + // 池账户充值检测服务 + PoolAccountDepositDetectionService, // Outbox 事件发布服务(将确认的充值事件发布到 Kafka) OutboxPublisherService, // 充值 ACK 消费者(接收 wallet-service 的确认回执) diff --git a/backend/services/mining-blockchain-service/src/application/services/pool-account-deposit-detection.service.ts b/backend/services/mining-blockchain-service/src/application/services/pool-account-deposit-detection.service.ts new file mode 100644 index 00000000..c28a6caa --- /dev/null +++ b/backend/services/mining-blockchain-service/src/application/services/pool-account-deposit-detection.service.ts @@ -0,0 +1,285 @@ +import { Injectable, Logger, Inject, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { EvmProviderAdapter, TransferEvent } from '@/infrastructure/blockchain/evm-provider.adapter'; +import { ConfirmationPolicyService } from '@/domain/services/confirmation-policy.service'; +import { ChainConfigService } from '@/domain/services/chain-config.service'; +import { + POOL_ACCOUNT_DEPOSIT_REPOSITORY, + IPoolAccountDepositRepository, + PoolAccountDepositDto, + POOL_ACCOUNT_CHECKPOINT_REPOSITORY, + IPoolAccountCheckpointRepository, +} from '@/domain/repositories/pool-account-deposit.repository.interface'; +import { + OUTBOX_EVENT_REPOSITORY, + IOutboxEventRepository, +} from '@/domain/repositories/outbox-event.repository.interface'; +import { ChainType, BlockNumber } from '@/domain/value-objects'; +import { ChainTypeEnum, PoolAccountType, PoolAccountDepositStatus } from '@/domain/enums'; +import { PoolAccountDepositConfirmedEvent } from '@/domain/events'; + +interface PoolWalletConfig { + poolType: PoolAccountType; + walletAddress: string; + name: string; +} + +/** + * 池账户充值检测服务 + * + * 负责扫描区块链、检测100亿销毁池和200万挖矿池的 fUSDT 充值、更新确认状态 + * 当充值达到确认数后,通过 Kafka 通知 mining-wallet-service 进行入账 + */ +@Injectable() +export class PoolAccountDepositDetectionService implements OnModuleInit { + private readonly logger = new Logger(PoolAccountDepositDetectionService.name); + private readonly scanBatchSize: number; + private readonly fUsdtContract: string; + private readonly poolWalletConfigs: PoolWalletConfig[]; + private isEnabled: boolean = false; + + constructor( + private readonly configService: ConfigService, + private readonly evmProvider: EvmProviderAdapter, + private readonly confirmationPolicy: ConfirmationPolicyService, + private readonly chainConfig: ChainConfigService, + @Inject(POOL_ACCOUNT_DEPOSIT_REPOSITORY) + private readonly depositRepo: IPoolAccountDepositRepository, + @Inject(POOL_ACCOUNT_CHECKPOINT_REPOSITORY) + private readonly checkpointRepo: IPoolAccountCheckpointRepository, + @Inject(OUTBOX_EVENT_REPOSITORY) + private readonly outboxRepo: IOutboxEventRepository, + ) { + this.scanBatchSize = this.configService.get('blockchain.scanBatchSize', 100); + this.fUsdtContract = this.configService.get('blockchain.kava.fUsdtContract', ''); + + const burnPoolAddress = this.configService.get('blockchain.burnPool.walletAddress', ''); + const miningPoolAddress = this.configService.get('blockchain.miningPool.walletAddress', ''); + + this.poolWalletConfigs = [ + { + poolType: PoolAccountType.BURN_POOL, + walletAddress: burnPoolAddress, + name: '100亿销毁池', + }, + { + poolType: PoolAccountType.MINING_POOL, + walletAddress: miningPoolAddress, + name: '200万挖矿池', + }, + ]; + } + + async onModuleInit() { + const validConfigs = this.poolWalletConfigs.filter((c) => c.walletAddress); + + if (validConfigs.length === 0 || !this.fUsdtContract) { + this.logger.warn('[INIT] 没有有效的池钱包配置或 fUSDT 合约未配置,池账户充值监控功能已禁用'); + this.isEnabled = false; + return; + } + + this.poolWalletConfigs.forEach((c) => { + if (!c.walletAddress) { + this.logger.warn(`[INIT] ${c.name} 钱包地址未配置,该池充值监控已禁用`); + } + }); + + this.isEnabled = true; + this.logger.log(`[INIT] PoolAccountDepositDetectionService initialized`); + this.logger.log(`[INIT] fUSDT 合约: ${this.fUsdtContract}`); + validConfigs.forEach((c) => { + this.logger.log(`[INIT] ${c.name}: 钱包=${c.walletAddress}`); + }); + } + + /** + * 定时扫描区块(每5秒) + */ + @Cron(CronExpression.EVERY_5_SECONDS) + async scanBlocks(): Promise { + if (!this.isEnabled) return; + + const chainType = ChainType.fromEnum(ChainTypeEnum.KAVA); + + for (const walletConfig of this.poolWalletConfigs) { + if (!walletConfig.walletAddress) continue; + + try { + await this.scanPoolDeposits(chainType, walletConfig); + } catch (error) { + this.logger.error(`Error scanning ${walletConfig.name} deposits:`, error); + await this.checkpointRepo.recordError( + chainType, + walletConfig.poolType, + error instanceof Error ? error.message : 'Unknown error', + ); + } + } + } + + /** + * 扫描单个池钱包的充值 + */ + private async scanPoolDeposits(chainType: ChainType, walletConfig: PoolWalletConfig): Promise { + let lastBlock = await this.checkpointRepo.getLastScannedBlock(chainType, walletConfig.poolType); + + if (!lastBlock) { + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + lastBlock = currentBlock.subtract(10); + await this.checkpointRepo.initializeIfNotExists(chainType, walletConfig.poolType, lastBlock); + } + + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + + const fromBlock = lastBlock.add(1); + let toBlock = fromBlock.add(this.scanBatchSize - 1); + + if (toBlock.isGreaterThan(currentBlock)) { + toBlock = currentBlock; + } + + if (fromBlock.isGreaterThan(currentBlock)) { + return; + } + + this.logger.debug( + `Scanning ${walletConfig.name}: blocks ${fromBlock} to ${toBlock}`, + ); + + // 扫描 fUSDT Transfer 事件 + const events = await this.evmProvider.scanTransferEvents( + chainType, + fromBlock, + toBlock, + this.fUsdtContract, + ); + + // 过滤出充值到池钱包的交易 + const deposits = events.filter( + (e) => e.to.toLowerCase() === walletConfig.walletAddress.toLowerCase(), + ); + + for (const deposit of deposits) { + await this.processDeposit(chainType, walletConfig, deposit); + } + + if (toBlock.isGreaterThan(lastBlock)) { + await this.checkpointRepo.updateCheckpoint(chainType, walletConfig.poolType, toBlock); + } + } + + /** + * 处理检测到的充值 + */ + private async processDeposit( + chainType: ChainType, + walletConfig: PoolWalletConfig, + event: TransferEvent, + ): Promise { + if (await this.depositRepo.existsByTxHashAndLogIndex(event.txHash, event.logIndex)) { + this.logger.debug(`Pool deposit already processed: ${event.txHash}:${event.logIndex}`); + return; + } + + const tokenDecimals = await this.evmProvider.getTokenDecimals( + chainType, + this.fUsdtContract, + ); + + const divisor = BigInt(10 ** tokenDecimals); + const integerPart = event.value / divisor; + const fractionalPart = event.value % divisor; + const amountFormatted = `${integerPart}.${fractionalPart.toString().padStart(tokenDecimals, '0')}`; + + const deposit: PoolAccountDepositDto = { + chainType: chainType.toString(), + txHash: event.txHash, + fromAddress: event.from, + toAddress: event.to, + poolType: walletConfig.poolType, + tokenContract: this.fUsdtContract, + amount: event.value, + amountFormatted, + blockNumber: event.blockNumber, + blockTimestamp: event.blockTimestamp, + logIndex: event.logIndex, + confirmations: 0, + status: PoolAccountDepositStatus.DETECTED, + }; + + await this.depositRepo.save(deposit); + + this.logger.log( + `[DEPOSIT] ${walletConfig.name} fUSDT deposit detected: ${event.txHash.slice(0, 10)}... -> ${walletConfig.walletAddress.slice(0, 10)}... (${amountFormatted})`, + ); + } + + /** + * 定时更新确认状态(每30秒) + */ + @Cron(CronExpression.EVERY_30_SECONDS) + async updateConfirmations(): Promise { + if (!this.isEnabled) return; + + const chainType = ChainType.fromEnum(ChainTypeEnum.KAVA); + + try { + await this.updateChainConfirmations(chainType); + } catch (error) { + this.logger.error(`Error updating pool account confirmations:`, error); + } + } + + /** + * 更新确认状态 + */ + private async updateChainConfirmations(chainType: ChainType): Promise { + const pendingDeposits = await this.depositRepo.findPendingConfirmation(chainType); + if (pendingDeposits.length === 0) return; + + const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); + const requiredConfirmations = this.confirmationPolicy.getRequiredConfirmations(chainType); + + for (const deposit of pendingDeposits) { + const confirmations = Number(currentBlock.value - deposit.blockNumber); + const isConfirmed = confirmations >= requiredConfirmations; + + const newStatus = isConfirmed + ? PoolAccountDepositStatus.CONFIRMED + : PoolAccountDepositStatus.CONFIRMING; + + await this.depositRepo.updateConfirmations(deposit.id!, confirmations, newStatus); + + if (isConfirmed && deposit.status !== PoolAccountDepositStatus.CONFIRMED) { + const event = new PoolAccountDepositConfirmedEvent({ + depositId: deposit.id!.toString(), + chainType: deposit.chainType, + txHash: deposit.txHash, + fromAddress: deposit.fromAddress, + toAddress: deposit.toAddress, + poolType: deposit.poolType as 'BURN_POOL' | 'MINING_POOL', + tokenContract: deposit.tokenContract, + amount: deposit.amount.toString(), + amountFormatted: deposit.amountFormatted, + confirmations, + blockNumber: deposit.blockNumber.toString(), + blockTimestamp: deposit.blockTimestamp.toISOString(), + }); + + // 写入 outbox,保证可靠投递 + await this.outboxRepo.create({ + eventType: event.eventType, + aggregateId: deposit.id!.toString(), + aggregateType: 'PoolAccountDeposit', + payload: event.toPayload(), + }); + + this.logger.log( + `[CONFIRMED] Pool deposit confirmed: ${deposit.txHash.slice(0, 10)}... (${confirmations} confirmations, ${deposit.poolType})`, + ); + } + } + } +} diff --git a/backend/services/mining-blockchain-service/src/config/blockchain.config.ts b/backend/services/mining-blockchain-service/src/config/blockchain.config.ts index b7179cbc..d243d031 100644 --- a/backend/services/mining-blockchain-service/src/config/blockchain.config.ts +++ b/backend/services/mining-blockchain-service/src/config/blockchain.config.ts @@ -92,5 +92,17 @@ export default registerAs('blockchain', () => { walletAddress: process.env.FUSDT_MARKET_MAKER_ADDRESS || '', mpcUsername: process.env.FUSDT_MARKET_MAKER_USERNAME || '', }, + + // 100亿销毁池 MPC 钱包配置 (2-of-3 门限) + burnPool: { + walletAddress: process.env.BURN_POOL_WALLET_ADDRESS || '', + mpcUsername: process.env.BURN_POOL_WALLET_USERNAME || '', + }, + + // 200万挖矿池 MPC 钱包配置 (2-of-3 门限) + miningPool: { + walletAddress: process.env.MINING_POOL_WALLET_ADDRESS || '', + mpcUsername: process.env.MINING_POOL_WALLET_USERNAME || '', + }, }; }); diff --git a/backend/services/mining-blockchain-service/src/domain/enums/index.ts b/backend/services/mining-blockchain-service/src/domain/enums/index.ts index c1c9d6e3..2efdabd3 100644 --- a/backend/services/mining-blockchain-service/src/domain/enums/index.ts +++ b/backend/services/mining-blockchain-service/src/domain/enums/index.ts @@ -3,3 +3,5 @@ export * from './deposit-status.enum'; export * from './transaction-status.enum'; export * from './market-maker-asset-type.enum'; export * from './market-maker-deposit-status.enum'; +export * from './pool-account-type.enum'; +export * from './pool-account-deposit-status.enum'; diff --git a/backend/services/mining-blockchain-service/src/domain/enums/pool-account-deposit-status.enum.ts b/backend/services/mining-blockchain-service/src/domain/enums/pool-account-deposit-status.enum.ts new file mode 100644 index 00000000..30030cf4 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/pool-account-deposit-status.enum.ts @@ -0,0 +1,15 @@ +/** + * 池账户充值状态 + */ +export enum PoolAccountDepositStatus { + /** 已检测到 */ + DETECTED = 'DETECTED', + /** 确认中 */ + CONFIRMING = 'CONFIRMING', + /** 已确认 (达到确认数) */ + CONFIRMED = 'CONFIRMED', + /** 已入账 (wallet-service 确认入账) */ + CREDITED = 'CREDITED', + /** 失败 */ + FAILED = 'FAILED', +} diff --git a/backend/services/mining-blockchain-service/src/domain/enums/pool-account-type.enum.ts b/backend/services/mining-blockchain-service/src/domain/enums/pool-account-type.enum.ts new file mode 100644 index 00000000..150d3308 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/enums/pool-account-type.enum.ts @@ -0,0 +1,9 @@ +/** + * 池账户类型 + */ +export enum PoolAccountType { + /** 100亿销毁池 */ + BURN_POOL = 'BURN_POOL', + /** 200万挖矿池 */ + MINING_POOL = 'MINING_POOL', +} diff --git a/backend/services/mining-blockchain-service/src/domain/events/index.ts b/backend/services/mining-blockchain-service/src/domain/events/index.ts index 96dbe253..863bd7a5 100644 --- a/backend/services/mining-blockchain-service/src/domain/events/index.ts +++ b/backend/services/mining-blockchain-service/src/domain/events/index.ts @@ -4,3 +4,4 @@ export * from './deposit-confirmed.event'; export * from './wallet-address-created.event'; export * from './transaction-broadcasted.event'; export * from './market-maker-deposit-confirmed.event'; +export * from './pool-account-deposit-confirmed.event'; diff --git a/backend/services/mining-blockchain-service/src/domain/events/pool-account-deposit-confirmed.event.ts b/backend/services/mining-blockchain-service/src/domain/events/pool-account-deposit-confirmed.event.ts new file mode 100644 index 00000000..422308fa --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/events/pool-account-deposit-confirmed.event.ts @@ -0,0 +1,34 @@ +import { DomainEvent } from './domain-event.base'; + +export interface PoolAccountDepositConfirmedPayload { + depositId: string; + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + poolType: 'BURN_POOL' | 'MINING_POOL'; + tokenContract: string; + amount: string; + amountFormatted: string; + confirmations: number; + blockNumber: string; + blockTimestamp: string; + [key: string]: unknown; +} + +/** + * 池账户充值确认事件 + * 当池钱包的 fUSDT 充值交易达到确认数时触发 + * 由 mining-wallet-service 消费,用于池账户入账 + */ +export class PoolAccountDepositConfirmedEvent extends DomainEvent { + readonly eventType = 'mining_blockchain.pool_account.deposit.confirmed'; + + constructor(private readonly payload: PoolAccountDepositConfirmedPayload) { + super(); + } + + toPayload(): PoolAccountDepositConfirmedPayload { + return this.payload; + } +} diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/index.ts b/backend/services/mining-blockchain-service/src/domain/repositories/index.ts index d11aeec9..8fc16459 100644 --- a/backend/services/mining-blockchain-service/src/domain/repositories/index.ts +++ b/backend/services/mining-blockchain-service/src/domain/repositories/index.ts @@ -4,3 +4,4 @@ export * from './block-checkpoint.repository.interface'; export * from './transaction-request.repository.interface'; export * from './outbox-event.repository.interface'; export * from './market-maker-deposit.repository.interface'; +export * from './pool-account-deposit.repository.interface'; diff --git a/backend/services/mining-blockchain-service/src/domain/repositories/pool-account-deposit.repository.interface.ts b/backend/services/mining-blockchain-service/src/domain/repositories/pool-account-deposit.repository.interface.ts new file mode 100644 index 00000000..6e968adf --- /dev/null +++ b/backend/services/mining-blockchain-service/src/domain/repositories/pool-account-deposit.repository.interface.ts @@ -0,0 +1,97 @@ +import { ChainType, BlockNumber } from '@/domain/value-objects'; +import { PoolAccountType, PoolAccountDepositStatus } from '@/domain/enums'; + +export const POOL_ACCOUNT_DEPOSIT_REPOSITORY = Symbol('POOL_ACCOUNT_DEPOSIT_REPOSITORY'); + +/** + * 池账户充值记录 DTO + */ +export interface PoolAccountDepositDto { + id?: bigint; + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + poolType: PoolAccountType; + tokenContract: string; + amount: bigint; + amountFormatted: string; + blockNumber: bigint; + blockTimestamp: Date; + logIndex: number; + confirmations: number; + status: PoolAccountDepositStatus; + creditedAt?: Date | null; + creditReference?: string | null; + createdAt?: Date; + updatedAt?: Date; +} + +export interface IPoolAccountDepositRepository { + /** + * 保存充值记录 + */ + save(deposit: PoolAccountDepositDto): Promise; + + /** + * 根据ID查找 + */ + findById(id: bigint): Promise; + + /** + * 检查交易是否存在(按txHash+logIndex) + */ + existsByTxHashAndLogIndex(txHash: string, logIndex: number): Promise; + + /** + * 查找待确认的充值 + */ + findPendingConfirmation(chainType: ChainType): Promise; + + /** + * 更新确认状态 + */ + updateConfirmations( + id: bigint, + confirmations: number, + status: PoolAccountDepositStatus, + ): Promise; + + /** + * 更新入账状态 + */ + updateCreditedStatus( + id: bigint, + creditedAt: Date, + creditReference: string, + ): Promise; +} + +export const POOL_ACCOUNT_CHECKPOINT_REPOSITORY = Symbol('POOL_ACCOUNT_CHECKPOINT_REPOSITORY'); + +export interface IPoolAccountCheckpointRepository { + /** + * 获取上次扫描的区块 + */ + getLastScannedBlock(chainType: ChainType, poolType: PoolAccountType): Promise; + + /** + * 更新检查点 + */ + updateCheckpoint(chainType: ChainType, poolType: PoolAccountType, blockNumber: BlockNumber): Promise; + + /** + * 初始化检查点(如果不存在) + */ + initializeIfNotExists(chainType: ChainType, poolType: PoolAccountType, blockNumber: BlockNumber): Promise; + + /** + * 记录错误 + */ + recordError(chainType: ChainType, poolType: PoolAccountType, error: string): Promise; + + /** + * 标记健康 + */ + markHealthy(chainType: ChainType, poolType: PoolAccountType): Promise; +} diff --git a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts index 7bb39d49..80caf1dc 100644 --- a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts +++ b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts @@ -47,8 +47,19 @@ export interface IMpcSigningClient { isFusdtMarketMakerConfigured(): boolean; getFusdtMarketMakerAddress(): string; signMessageAsFusdtMarketMaker(messageHash: string): Promise; + // 100亿销毁池钱包 + isBurnPoolConfigured(): boolean; + getBurnPoolAddress(): string; + signMessageAsBurnPool(messageHash: string): Promise; + // 200万挖矿池钱包 + isMiningPoolConfigured(): boolean; + getMiningPoolAddress(): string; + signMessageAsMiningPool(messageHash: string): Promise; } +// 池账户类型(用于 transferTokenAsPoolAccount) +export type PoolWalletType = 'BURN_POOL' | 'MINING_POOL'; + export const MPC_SIGNING_CLIENT = Symbol('MPC_SIGNING_CLIENT'); /** @@ -66,6 +77,10 @@ export class Erc20TransferService { private readonly eusdtMarketMakerAddress: string; // fUSDT (积分值) 做市商钱包地址 private readonly fusdtMarketMakerAddress: string; + // 100亿销毁池钱包地址 + private readonly burnPoolAddress: string; + // 200万挖矿池钱包地址 + private readonly miningPoolAddress: string; private mpcSigningClient: IMpcSigningClient | null = null; constructor( @@ -76,6 +91,8 @@ export class Erc20TransferService { this.hotWalletAddress = this.configService.get('C2C_BOT_WALLET_ADDRESS', ''); this.eusdtMarketMakerAddress = this.configService.get('EUSDT_MARKET_MAKER_ADDRESS', ''); this.fusdtMarketMakerAddress = this.configService.get('FUSDT_MARKET_MAKER_ADDRESS', ''); + this.burnPoolAddress = this.configService.get('BURN_POOL_WALLET_ADDRESS', ''); + this.miningPoolAddress = this.configService.get('MINING_POOL_WALLET_ADDRESS', ''); this.initializeWalletConfig(); } @@ -895,4 +912,224 @@ export class Erc20TransferService { return formatUnits(balance, decimals); } + + // ============ 池账户钱包方法 ============ + + /** + * 获取池钱包地址 + */ + getPoolWalletAddress(poolType: PoolWalletType): string | null { + const address = poolType === 'BURN_POOL' ? this.burnPoolAddress : this.miningPoolAddress; + return address || null; + } + + /** + * 检查池钱包是否已配置 + */ + isPoolWalletConfigured(poolType: PoolWalletType, chainType: ChainTypeEnum): boolean { + try { + this.rpcProviderManager.getProvider(chainType); + if (poolType === 'BURN_POOL') { + return !!this.burnPoolAddress && !!this.mpcSigningClient?.isBurnPoolConfigured(); + } else { + return !!this.miningPoolAddress && !!this.mpcSigningClient?.isMiningPoolConfigured(); + } + } catch { + return false; + } + } + + /** + * 获取池钱包的 fUSDT 链上余额 + */ + async getPoolWalletTokenBalance(chainType: ChainTypeEnum, poolType: PoolWalletType): Promise { + const provider = this.getProvider(chainType); + const walletAddress = poolType === 'BURN_POOL' ? this.burnPoolAddress : this.miningPoolAddress; + const poolName = poolType === 'BURN_POOL' ? '100亿销毁池' : '200万挖矿池'; + + if (!walletAddress) { + throw new Error(`${poolName} wallet address not configured`); + } + + const contractAddress = this.getTokenContract(chainType, 'FUSDT'); + if (!contractAddress) { + throw new Error(`fUSDT not configured for chain ${chainType}`); + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + const balance = await contract.balanceOf(walletAddress); + const decimals = await contract.decimals(); + + return formatUnits(balance, decimals); + } + + /** + * 池账户 fUSDT 转账(使用池钱包 MPC 签名) + */ + async transferTokenAsPoolAccount( + chainType: ChainTypeEnum, + poolType: PoolWalletType, + toAddress: string, + amount: string, + ): Promise { + const isBurnPool = poolType === 'BURN_POOL'; + const walletAddress = isBurnPool ? this.burnPoolAddress : this.miningPoolAddress; + const poolName = isBurnPool ? '100亿销毁池' : '200万挖矿池'; + + this.logger.log(`[POOL-TRANSFER] Starting fUSDT ${poolName} transfer`); + this.logger.log(`[POOL-TRANSFER] From: ${walletAddress}`); + this.logger.log(`[POOL-TRANSFER] To: ${toAddress}`); + this.logger.log(`[POOL-TRANSFER] Amount: ${amount} fUSDT`); + + const provider = this.getProvider(chainType); + + // 检查钱包是否配置 + if (isBurnPool) { + if (!this.mpcSigningClient || !this.mpcSigningClient.isBurnPoolConfigured()) { + const error = 'Burn Pool MPC signing not configured'; + this.logger.error(`[POOL-TRANSFER] ${error}`); + return { success: false, error }; + } + } else { + if (!this.mpcSigningClient || !this.mpcSigningClient.isMiningPoolConfigured()) { + const error = 'Mining Pool MPC signing not configured'; + this.logger.error(`[POOL-TRANSFER] ${error}`); + return { success: false, error }; + } + } + + if (!walletAddress) { + const error = `${poolName} wallet address not configured`; + this.logger.error(`[POOL-TRANSFER] ${error}`); + return { success: false, error }; + } + + try { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const contractAddress = this.getTokenContract(chainType, 'FUSDT'); + + if (!contractAddress) { + const error = `fUSDT not configured for chain ${chainType}`; + this.logger.error(`[POOL-TRANSFER] ${error}`); + return { success: false, error }; + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + + const decimals = await contract.decimals(); + const amountInWei = parseUnits(amount, decimals); + + // 检查余额 + const balance = await contract.balanceOf(walletAddress); + this.logger.log(`[POOL-TRANSFER] ${poolName} balance: ${formatUnits(balance, decimals)} fUSDT`); + + if (balance < amountInWei) { + const error = `Insufficient fUSDT balance in ${poolName} wallet`; + this.logger.error(`[POOL-TRANSFER] ${error}`); + return { success: false, error }; + } + + // 构建交易 + const nonce = await provider.getTransactionCount(walletAddress); + const feeData = await provider.getFeeData(); + const transferData = contract.interface.encodeFunctionData('transfer', [toAddress, amountInWei]); + + const gasEstimate = await provider.estimateGas({ + from: walletAddress, + to: contractAddress, + data: transferData, + }); + const gasLimit = gasEstimate * BigInt(120) / BigInt(100); + + const supportsEip1559 = feeData.maxFeePerGas && feeData.maxFeePerGas > BigInt(0); + + let tx: Transaction; + if (supportsEip1559) { + tx = Transaction.from({ + type: 2, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + }); + } else { + const gasPrice = feeData.gasPrice || BigInt(1000000000); + tx = Transaction.from({ + type: 0, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + gasPrice, + }); + } + + const unsignedTxHash = tx.unsignedHash; + + // 使用对应的池钱包 MPC 签名 + this.logger.log(`[POOL-TRANSFER] Requesting ${poolName} MPC signature...`); + const signatureHex = isBurnPool + ? await this.mpcSigningClient!.signMessageAsBurnPool(unsignedTxHash) + : await this.mpcSigningClient!.signMessageAsMiningPool(unsignedTxHash); + + // 解析签名 + const normalizedSig = signatureHex.startsWith('0x') ? signatureHex : `0x${signatureHex}`; + const sigBytes = normalizedSig.slice(2); + const r = `0x${sigBytes.slice(0, 64)}`; + const s = `0x${sigBytes.slice(64, 128)}`; + + let signature: Signature | null = null; + for (const yParity of [0, 1] as const) { + try { + const testSig = Signature.from({ r, s, yParity }); + const recoveredAddress = recoverAddress(unsignedTxHash, testSig); + if (recoveredAddress.toLowerCase() === walletAddress.toLowerCase()) { + signature = testSig; + break; + } + } catch (e) { + this.logger.debug(`[POOL-TRANSFER] yParity=${yParity} failed: ${e}`); + } + } + + if (!signature) { + throw new Error('Failed to recover correct signature - address mismatch'); + } + + const signedTx = tx.clone(); + signedTx.signature = signature; + + this.logger.log(`[POOL-TRANSFER] Broadcasting transaction...`); + const txResponse = await provider.broadcastTransaction(signedTx.serialized); + this.logger.log(`[POOL-TRANSFER] Transaction sent: ${txResponse.hash}`); + + const receipt = await txResponse.wait(); + + if (receipt && receipt.status === 1) { + this.logger.log(`[POOL-TRANSFER] Transaction confirmed! Block: ${receipt.blockNumber}`); + this.rpcProviderManager.reportSuccess(chainType); + return { + success: true, + txHash: txResponse.hash, + gasUsed: receipt.gasUsed.toString(), + blockNumber: receipt.blockNumber, + }; + } else { + return { success: false, txHash: txResponse.hash, error: 'Transaction failed (reverted)' }; + } + } catch (error: any) { + if (this.isRpcConnectionError(error)) { + this.rpcProviderManager.reportFailure(chainType, error); + } + this.logger.error(`[POOL-TRANSFER] Transfer failed:`, error); + return { + success: false, + error: error.message || 'Unknown error during transfer', + }; + } + } } diff --git a/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts b/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts index 6112e503..8c31e918 100644 --- a/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/mining-blockchain-service/src/infrastructure/infrastructure.module.ts @@ -15,6 +15,8 @@ import { OUTBOX_EVENT_REPOSITORY, MARKET_MAKER_DEPOSIT_REPOSITORY, MARKET_MAKER_CHECKPOINT_REPOSITORY, + POOL_ACCOUNT_DEPOSIT_REPOSITORY, + POOL_ACCOUNT_CHECKPOINT_REPOSITORY, } from '@/domain/repositories'; import { DepositTransactionRepositoryImpl, @@ -24,6 +26,8 @@ import { OutboxEventRepositoryImpl, MarketMakerDepositRepositoryImpl, MarketMakerCheckpointRepositoryImpl, + PoolAccountDepositRepositoryImpl, + PoolAccountCheckpointRepositoryImpl, } from './persistence/repositories'; @Global() @@ -76,6 +80,14 @@ import { provide: MARKET_MAKER_CHECKPOINT_REPOSITORY, useClass: MarketMakerCheckpointRepositoryImpl, }, + { + provide: POOL_ACCOUNT_DEPOSIT_REPOSITORY, + useClass: PoolAccountDepositRepositoryImpl, + }, + { + provide: POOL_ACCOUNT_CHECKPOINT_REPOSITORY, + useClass: PoolAccountCheckpointRepositoryImpl, + }, ], exports: [ PrismaService, @@ -96,6 +108,8 @@ import { OUTBOX_EVENT_REPOSITORY, MARKET_MAKER_DEPOSIT_REPOSITORY, MARKET_MAKER_CHECKPOINT_REPOSITORY, + POOL_ACCOUNT_DEPOSIT_REPOSITORY, + POOL_ACCOUNT_CHECKPOINT_REPOSITORY, ], }) export class InfrastructureModule {} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts index 2468d3c6..4c0ada23 100644 --- a/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts +++ b/backend/services/mining-blockchain-service/src/infrastructure/kafka/event-publisher.service.ts @@ -107,6 +107,8 @@ export class EventPublisherService implements OnModuleInit, OnModuleDestroy { 'mining_blockchain.mpc.signing.requested': 'mining_mpc.SigningRequested', // 做市商充值事件 - 发送到 trading-service 消费的 topic 'mining_blockchain.market_maker.deposit.confirmed': 'mining_blockchain.market_maker.deposits', + // 池账户充值事件 - 发送到 mining-wallet-service 消费的 topic + 'mining_blockchain.pool_account.deposit.confirmed': 'mining_blockchain.pool_account.deposits', }; return topicMap[eventType] || 'mining_blockchain.events'; } diff --git a/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts index 17a9d12d..08203535 100644 --- a/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts +++ b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts @@ -42,6 +42,12 @@ export class MpcSigningClient { // fUSDT (积分值) 做市商钱包 private readonly fusdtMarketMakerUsername: string; private readonly fusdtMarketMakerAddress: string; + // 100亿销毁池钱包 + private readonly burnPoolUsername: string; + private readonly burnPoolAddress: string; + // 200万挖矿池钱包 + private readonly miningPoolUsername: string; + private readonly miningPoolAddress: string; // MPC system 配置 private readonly mpcAccountServiceUrl: string; private readonly mpcJwtSecret: string; @@ -62,6 +68,12 @@ export class MpcSigningClient { // fUSDT (积分值) 做市商钱包配置 this.fusdtMarketMakerUsername = this.configService.get('FUSDT_MARKET_MAKER_USERNAME', ''); this.fusdtMarketMakerAddress = this.configService.get('FUSDT_MARKET_MAKER_ADDRESS', ''); + // 100亿销毁池钱包配置 + this.burnPoolUsername = this.configService.get('BURN_POOL_WALLET_USERNAME', ''); + this.burnPoolAddress = this.configService.get('BURN_POOL_WALLET_ADDRESS', ''); + // 200万挖矿池钱包配置 + this.miningPoolUsername = this.configService.get('MINING_POOL_WALLET_USERNAME', ''); + this.miningPoolAddress = this.configService.get('MINING_POOL_WALLET_ADDRESS', ''); // MPC system 配置 this.mpcAccountServiceUrl = this.configService.get('MPC_ACCOUNT_SERVICE_URL', 'http://localhost:4000'); this.mpcJwtSecret = this.configService.get('MPC_JWT_SECRET', ''); @@ -78,6 +90,12 @@ export class MpcSigningClient { if (!this.fusdtMarketMakerUsername || !this.fusdtMarketMakerAddress) { this.logger.warn('[INIT] fUSDT Market Maker not configured'); } + if (!this.burnPoolUsername || !this.burnPoolAddress) { + this.logger.warn('[INIT] Burn Pool wallet not configured'); + } + if (!this.miningPoolUsername || !this.miningPoolAddress) { + this.logger.warn('[INIT] Mining Pool wallet not configured'); + } if (!this.mpcJwtSecret) { this.logger.warn('[INIT] MPC_JWT_SECRET not configured - signing will fail'); } @@ -85,6 +103,8 @@ export class MpcSigningClient { this.logger.log(`[INIT] C2C Bot Wallet: ${this.hotWalletAddress || '(not configured)'}`); this.logger.log(`[INIT] eUSDT Market Maker: ${this.eusdtMarketMakerAddress || '(not configured)'}`); this.logger.log(`[INIT] fUSDT Market Maker: ${this.fusdtMarketMakerAddress || '(not configured)'}`); + this.logger.log(`[INIT] Burn Pool: ${this.burnPoolAddress || '(not configured)'}`); + this.logger.log(`[INIT] Mining Pool: ${this.miningPoolAddress || '(not configured)'}`); this.logger.log(`[INIT] MPC Account Service: ${this.mpcAccountServiceUrl}`); this.logger.log(`[INIT] Using HTTP direct call to mpc-system`); } @@ -152,6 +172,48 @@ export class MpcSigningClient { return this.fusdtMarketMakerUsername; } + // ============ 100亿销毁池钱包 ============ + + isBurnPoolConfigured(): boolean { + return !!this.burnPoolUsername && !!this.burnPoolAddress; + } + + getBurnPoolAddress(): string { + return this.burnPoolAddress; + } + + getBurnPoolUsername(): string { + return this.burnPoolUsername; + } + + async signMessageAsBurnPool(messageHash: string): Promise { + if (!this.burnPoolUsername) { + throw new Error('Burn Pool MPC username not configured'); + } + return this.signMessageWithUsername(this.burnPoolUsername, messageHash); + } + + // ============ 200万挖矿池钱包 ============ + + isMiningPoolConfigured(): boolean { + return !!this.miningPoolUsername && !!this.miningPoolAddress; + } + + getMiningPoolAddress(): string { + return this.miningPoolAddress; + } + + getMiningPoolUsername(): string { + return this.miningPoolUsername; + } + + async signMessageAsMiningPool(messageHash: string): Promise { + if (!this.miningPoolUsername) { + throw new Error('Mining Pool MPC username not configured'); + } + return this.signMessageWithUsername(this.miningPoolUsername, messageHash); + } + /** * 签名消息(使用 C2C Bot 热钱包) * diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts index a0fe72fa..6493c741 100644 --- a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/index.ts @@ -5,3 +5,5 @@ export * from './transaction-request.repository.impl'; export * from './outbox-event.repository.impl'; export * from './market-maker-deposit.repository.impl'; export * from './market-maker-checkpoint.repository.impl'; +export * from './pool-account-deposit.repository.impl'; +export * from './pool-account-checkpoint.repository.impl'; diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-checkpoint.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-checkpoint.repository.impl.ts new file mode 100644 index 00000000..ce1fadc7 --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-checkpoint.repository.impl.ts @@ -0,0 +1,123 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { IPoolAccountCheckpointRepository } from '@/domain/repositories/pool-account-deposit.repository.interface'; +import { ChainType, BlockNumber } from '@/domain/value-objects'; +import { PoolAccountType } from '@/domain/enums'; + +@Injectable() +export class PoolAccountCheckpointRepositoryImpl implements IPoolAccountCheckpointRepository { + constructor(private readonly prisma: PrismaService) {} + + async getLastScannedBlock( + chainType: ChainType, + poolType: PoolAccountType, + ): Promise { + const record = await this.prisma.poolAccountBlockCheckpoint.findUnique({ + where: { + uk_pool_chain_pool: { + chainType: chainType.toString(), + poolType, + }, + }, + }); + return record ? BlockNumber.create(record.lastScannedBlock) : null; + } + + async updateCheckpoint( + chainType: ChainType, + poolType: PoolAccountType, + blockNumber: BlockNumber, + ): Promise { + await this.prisma.poolAccountBlockCheckpoint.upsert({ + where: { + uk_pool_chain_pool: { + chainType: chainType.toString(), + poolType, + }, + }, + update: { + lastScannedBlock: blockNumber.value, + lastScannedAt: new Date(), + isHealthy: true, + lastError: null, + }, + create: { + chainType: chainType.toString(), + poolType, + lastScannedBlock: blockNumber.value, + lastScannedAt: new Date(), + isHealthy: true, + }, + }); + } + + async initializeIfNotExists( + chainType: ChainType, + poolType: PoolAccountType, + blockNumber: BlockNumber, + ): Promise { + const existing = await this.prisma.poolAccountBlockCheckpoint.findUnique({ + where: { + uk_pool_chain_pool: { + chainType: chainType.toString(), + poolType, + }, + }, + }); + + if (!existing) { + await this.prisma.poolAccountBlockCheckpoint.create({ + data: { + chainType: chainType.toString(), + poolType, + lastScannedBlock: blockNumber.value, + lastScannedAt: new Date(), + isHealthy: true, + }, + }); + } + } + + async recordError( + chainType: ChainType, + poolType: PoolAccountType, + error: string, + ): Promise { + await this.prisma.poolAccountBlockCheckpoint.upsert({ + where: { + uk_pool_chain_pool: { + chainType: chainType.toString(), + poolType, + }, + }, + update: { + isHealthy: false, + lastError: error, + }, + create: { + chainType: chainType.toString(), + poolType, + lastScannedBlock: BigInt(0), + lastScannedAt: new Date(), + isHealthy: false, + lastError: error, + }, + }); + } + + async markHealthy( + chainType: ChainType, + poolType: PoolAccountType, + ): Promise { + await this.prisma.poolAccountBlockCheckpoint.updateMany({ + where: { + chainType: chainType.toString(), + poolType, + }, + data: { + isHealthy: true, + lastError: null, + }, + }); + } +} diff --git a/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-deposit.repository.impl.ts b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-deposit.repository.impl.ts new file mode 100644 index 00000000..1e53467e --- /dev/null +++ b/backend/services/mining-blockchain-service/src/infrastructure/persistence/repositories/pool-account-deposit.repository.impl.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + IPoolAccountDepositRepository, + PoolAccountDepositDto, +} from '@/domain/repositories/pool-account-deposit.repository.interface'; +import { ChainType } from '@/domain/value-objects'; +import { PoolAccountType, PoolAccountDepositStatus } from '@/domain/enums'; + +@Injectable() +export class PoolAccountDepositRepositoryImpl implements IPoolAccountDepositRepository { + constructor(private readonly prisma: PrismaService) {} + + async save(deposit: PoolAccountDepositDto): Promise { + const data = { + chainType: deposit.chainType, + txHash: deposit.txHash, + fromAddress: deposit.fromAddress, + toAddress: deposit.toAddress, + poolType: deposit.poolType, + tokenContract: deposit.tokenContract, + amount: new Prisma.Decimal(deposit.amount.toString()), + amountFormatted: new Prisma.Decimal(deposit.amountFormatted), + blockNumber: deposit.blockNumber, + blockTimestamp: deposit.blockTimestamp, + logIndex: deposit.logIndex, + confirmations: deposit.confirmations, + status: deposit.status, + creditedAt: deposit.creditedAt, + creditReference: deposit.creditReference, + }; + + if (deposit.id) { + const updated = await this.prisma.poolAccountDeposit.update({ + where: { id: deposit.id }, + data, + }); + return this.mapToDto(updated); + } else { + const created = await this.prisma.poolAccountDeposit.create({ + data, + }); + return this.mapToDto(created); + } + } + + async findById(id: bigint): Promise { + const record = await this.prisma.poolAccountDeposit.findUnique({ + where: { id }, + }); + return record ? this.mapToDto(record) : null; + } + + async existsByTxHashAndLogIndex(txHash: string, logIndex: number): Promise { + const count = await this.prisma.poolAccountDeposit.count({ + where: { + txHash, + logIndex, + }, + }); + return count > 0; + } + + async findPendingConfirmation(chainType: ChainType): Promise { + const records = await this.prisma.poolAccountDeposit.findMany({ + where: { + chainType: chainType.toString(), + status: { + in: [PoolAccountDepositStatus.DETECTED, PoolAccountDepositStatus.CONFIRMING], + }, + }, + orderBy: { blockNumber: 'asc' }, + }); + return records.map(this.mapToDto); + } + + async updateConfirmations( + id: bigint, + confirmations: number, + status: PoolAccountDepositStatus, + ): Promise { + await this.prisma.poolAccountDeposit.update({ + where: { id }, + data: { confirmations, status }, + }); + } + + async updateCreditedStatus( + id: bigint, + creditedAt: Date, + creditReference: string, + ): Promise { + await this.prisma.poolAccountDeposit.update({ + where: { id }, + data: { + creditedAt, + creditReference, + status: PoolAccountDepositStatus.CREDITED, + }, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private mapToDto(record: any): PoolAccountDepositDto { + return { + id: record.id, + chainType: record.chainType, + txHash: record.txHash, + fromAddress: record.fromAddress, + toAddress: record.toAddress, + poolType: record.poolType as PoolAccountType, + tokenContract: record.tokenContract, + amount: BigInt(record.amount.toString()), + amountFormatted: record.amountFormatted.toString(), + blockNumber: record.blockNumber, + blockTimestamp: record.blockTimestamp, + logIndex: record.logIndex, + confirmations: record.confirmations, + status: record.status as PoolAccountDepositStatus, + creditedAt: record.creditedAt, + creditReference: record.creditReference, + createdAt: record.createdAt, + updatedAt: record.updatedAt, + }; + } +} diff --git a/backend/services/mining-wallet-service/prisma/schema.prisma b/backend/services/mining-wallet-service/prisma/schema.prisma index 7686816d..f93e6333 100644 --- a/backend/services/mining-wallet-service/prisma/schema.prisma +++ b/backend/services/mining-wallet-service/prisma/schema.prisma @@ -86,6 +86,10 @@ enum TransactionType { // 系统调整 ADJUSTMENT // 系统调整 INITIAL_INJECT // 初始注入 + + // 区块链充值/提现 + BLOCKCHAIN_DEPOSIT // 区块链充值 + BLOCKCHAIN_WITHDRAW // 区块链提现 } // 交易对手方类型 diff --git a/backend/services/mining-wallet-service/prisma/seed.ts b/backend/services/mining-wallet-service/prisma/seed.ts index d5204a7c..9f0f5c12 100644 --- a/backend/services/mining-wallet-service/prisma/seed.ts +++ b/backend/services/mining-wallet-service/prisma/seed.ts @@ -71,14 +71,14 @@ async function main() { { poolType: 'SHARE_POOL_A', name: '积分股池A', - balance: new Decimal('10000000000'), // 100亿初始发行量 - description: '销毁池来源,初始100亿', + balance: new Decimal('0'), // 初始余额为0,通过区块链充值 + description: '销毁池来源(100亿),通过区块链充值', }, { poolType: 'SHARE_POOL_B', name: '积分股池B', - balance: new Decimal('2000000'), // 200万初始发行量 - description: '挖矿分配池,初始200万', + balance: new Decimal('0'), // 初始余额为0,通过区块链充值 + description: '挖矿分配池(200万),通过区块链充值', }, { poolType: 'BLACK_HOLE_POOL', diff --git a/backend/services/mining-wallet-service/src/api/controllers/pool-account.controller.ts b/backend/services/mining-wallet-service/src/api/controllers/pool-account.controller.ts index 08f45c9c..51bb050c 100644 --- a/backend/services/mining-wallet-service/src/api/controllers/pool-account.controller.ts +++ b/backend/services/mining-wallet-service/src/api/controllers/pool-account.controller.ts @@ -20,6 +20,14 @@ class BurnToBlackHoleDto { referenceId?: string; } +class BlockchainWithdrawDto { + poolType: PoolAccountType; + amount: string; + txHash: string; + toAddress: string; + blockNumber?: string; +} + @ApiTags('Pool Accounts') @Controller('pool-accounts') @ApiBearerAuth() @@ -123,4 +131,21 @@ export class PoolAccountController { referenceId: dto.referenceId, }); } + + @Post('blockchain-withdraw') + @Public() + @ApiOperation({ summary: '记录区块链提现(仅限内网调用)' }) + @ApiResponse({ status: 200, description: '提现记录成功' }) + async recordBlockchainWithdraw(@Body() dto: BlockchainWithdrawDto) { + await this.poolAccountService.blockchainWithdraw( + dto.poolType, + new Decimal(dto.amount), + { + txHash: dto.txHash, + toAddress: dto.toAddress, + blockNumber: dto.blockNumber, + }, + ); + return { success: true }; + } } diff --git a/backend/services/mining-wallet-service/src/application/application.module.ts b/backend/services/mining-wallet-service/src/application/application.module.ts index 8ed4d781..febc094f 100644 --- a/backend/services/mining-wallet-service/src/application/application.module.ts +++ b/backend/services/mining-wallet-service/src/application/application.module.ts @@ -17,6 +17,7 @@ import { UserRegisteredConsumer } from '../infrastructure/kafka/consumers/user-r import { MiningDistributionConsumer } from '../infrastructure/kafka/consumers/mining-distribution.consumer'; import { BurnConsumer } from '../infrastructure/kafka/consumers/burn.consumer'; import { ManualMiningConsumer } from '../infrastructure/kafka/consumers/manual-mining.consumer'; +import { PoolAccountDepositConsumer } from '../infrastructure/kafka/consumers/pool-account-deposit.consumer'; @Module({ imports: [ScheduleModule.forRoot()], @@ -27,6 +28,7 @@ import { ManualMiningConsumer } from '../infrastructure/kafka/consumers/manual-m MiningDistributionConsumer, BurnConsumer, ManualMiningConsumer, + PoolAccountDepositConsumer, ], providers: [ // Services diff --git a/backend/services/mining-wallet-service/src/application/services/pool-account.service.ts b/backend/services/mining-wallet-service/src/application/services/pool-account.service.ts index 7409d0a4..d85e06a2 100644 --- a/backend/services/mining-wallet-service/src/application/services/pool-account.service.ts +++ b/backend/services/mining-wallet-service/src/application/services/pool-account.service.ts @@ -488,4 +488,91 @@ export class PoolAccountService { ) { return this.poolAccountRepo.getTransactions(poolType, options); } + + // ============ 区块链充值/提现 ============ + + /** + * 区块链充值入账 + * 由 Kafka 消费者调用,处理 mining-blockchain-service 发布的充值确认事件 + */ + async blockchainDeposit( + poolType: PoolAccountType, + amount: Decimal, + metadata: { + txHash: string; + fromAddress: string; + blockNumber: string; + depositId: string; + }, + ): Promise { + const poolName = poolType === 'SHARE_POOL_A' ? '100亿销毁池' : '200万挖矿池'; + + const memo = `区块链充值入账, ${poolName}, 金额${amount.toFixed(8)} fUSDT, 交易哈希${metadata.txHash.slice(0, 10)}..., 来源${metadata.fromAddress.slice(0, 10)}...`; + + await this.poolAccountRepo.updateBalanceWithTransaction( + poolType, + amount, + { + transactionType: 'BLOCKCHAIN_DEPOSIT', + counterpartyType: 'EXTERNAL', + counterpartyAddress: metadata.fromAddress, + referenceId: metadata.depositId, + referenceType: 'BLOCKCHAIN_DEPOSIT', + txHash: metadata.txHash, + memo, + metadata: { + txHash: metadata.txHash, + fromAddress: metadata.fromAddress, + blockNumber: metadata.blockNumber, + depositId: metadata.depositId, + amount: amount.toString(), + }, + }, + ); + + this.logger.log( + `Blockchain deposit: ${amount.toFixed(8)} fUSDT to ${poolType} (tx: ${metadata.txHash.slice(0, 10)}...)`, + ); + } + + /** + * 区块链提现扣减 + * 由 Admin API 调用,管理员发起提现操作 + */ + async blockchainWithdraw( + poolType: PoolAccountType, + amount: Decimal, + metadata: { + txHash: string; + toAddress: string; + blockNumber?: string; + }, + ): Promise { + const poolName = poolType === 'SHARE_POOL_A' ? '100亿销毁池' : '200万挖矿池'; + + const memo = `区块链提现, ${poolName}, 金额${amount.toFixed(8)} fUSDT, 目标${metadata.toAddress.slice(0, 10)}..., 交易哈希${metadata.txHash.slice(0, 10)}...`; + + await this.poolAccountRepo.updateBalanceWithTransaction( + poolType, + amount.negated(), + { + transactionType: 'BLOCKCHAIN_WITHDRAW', + counterpartyType: 'EXTERNAL', + counterpartyAddress: metadata.toAddress, + referenceType: 'BLOCKCHAIN_WITHDRAW', + txHash: metadata.txHash, + memo, + metadata: { + txHash: metadata.txHash, + toAddress: metadata.toAddress, + blockNumber: metadata.blockNumber, + amount: amount.toString(), + }, + }, + ); + + this.logger.log( + `Blockchain withdraw: ${amount.toFixed(8)} fUSDT from ${poolType} (tx: ${metadata.txHash.slice(0, 10)}...)`, + ); + } } diff --git a/backend/services/mining-wallet-service/src/infrastructure/kafka/consumers/pool-account-deposit.consumer.ts b/backend/services/mining-wallet-service/src/infrastructure/kafka/consumers/pool-account-deposit.consumer.ts new file mode 100644 index 00000000..5f425e28 --- /dev/null +++ b/backend/services/mining-wallet-service/src/infrastructure/kafka/consumers/pool-account-deposit.consumer.ts @@ -0,0 +1,156 @@ +import { Controller, Logger, OnModuleInit } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import Decimal from 'decimal.js'; +import { RedisService } from '../../redis/redis.service'; +import { ProcessedEventRepository } from '../../persistence/repositories/processed-event.repository'; +import { PoolAccountService } from '../../../application/services/pool-account.service'; +import { PoolAccountType } from '@prisma/client'; + +// 4小时 TTL(秒) +const IDEMPOTENCY_TTL_SECONDS = 4 * 60 * 60; + +// 池类型到 PoolAccountType 的映射 +const POOL_TYPE_MAP: Record = { + 'BURN_POOL': 'SHARE_POOL_A', + 'MINING_POOL': 'SHARE_POOL_B', +}; + +interface PoolAccountDepositConfirmedPayload { + depositId: string; + chainType: string; + txHash: string; + fromAddress: string; + toAddress: string; + poolType: 'BURN_POOL' | 'MINING_POOL'; + tokenContract: string; + amount: string; + amountFormatted: string; + confirmations: number; + blockNumber: string; + blockTimestamp: string; +} + +/** + * 池账户充值事件消费者 + * 监听 mining-blockchain-service 发布的池账户充值确认事件 + */ +@Controller() +export class PoolAccountDepositConsumer implements OnModuleInit { + private readonly logger = new Logger(PoolAccountDepositConsumer.name); + + constructor( + private readonly redis: RedisService, + private readonly processedEventRepo: ProcessedEventRepository, + private readonly poolAccountService: PoolAccountService, + ) {} + + async onModuleInit() { + this.logger.log('PoolAccountDepositConsumer initialized'); + } + + /** + * 处理池账户充值确认事件 + * Topic: mining_blockchain.pool_account.deposits + */ + @EventPattern('mining_blockchain.pool_account.deposits') + async handlePoolAccountDeposit(@Payload() message: any): Promise { + const eventData = message.value || message; + const eventId = eventData.eventId || message.eventId; + + if (!eventId) { + this.logger.warn('Received pool deposit event without eventId, skipping'); + return; + } + + await this.processDeposit(eventId, eventData.payload || eventData); + } + + /** + * 处理充值入账 + */ + private async processDeposit( + eventId: string, + payload: PoolAccountDepositConfirmedPayload, + ): Promise { + this.logger.debug(`Processing pool deposit event: ${eventId}`); + + // 幂等性检查 + if (await this.isEventProcessed(eventId)) { + this.logger.debug(`Event ${eventId} already processed, skipping`); + return; + } + + try { + const amount = new Decimal(payload.amountFormatted); + + if (amount.isZero() || amount.isNegative()) { + this.logger.warn(`Invalid deposit amount: ${payload.amountFormatted}, skipping`); + return; + } + + // 映射池类型 + const poolType = POOL_TYPE_MAP[payload.poolType]; + if (!poolType) { + this.logger.error(`Unknown pool type: ${payload.poolType}, skipping`); + return; + } + + // 调用 pool account service 进行入账 + await this.poolAccountService.blockchainDeposit(poolType, amount, { + txHash: payload.txHash, + fromAddress: payload.fromAddress, + blockNumber: payload.blockNumber, + depositId: payload.depositId, + }); + + // 标记为已处理 + await this.markEventProcessed(eventId, 'pool_account.deposit.confirmed'); + + const poolName = payload.poolType === 'BURN_POOL' ? '100亿销毁池' : '200万挖矿池'; + this.logger.log( + `Pool deposit processed: ${amount.toFixed(8)} fUSDT to ${poolName} (tx: ${payload.txHash.slice(0, 10)}...)`, + ); + } catch (error) { + this.logger.error( + `Failed to process pool deposit event ${eventId}`, + error instanceof Error ? error.stack : error, + ); + throw error; // 让 Kafka 重试 + } + } + + /** + * 幂等性检查 - Redis + DB 双重检查 + */ + private async isEventProcessed(eventId: string): Promise { + const redisKey = `processed-event:pool-deposit:${eventId}`; + + const cached = await this.redis.get(redisKey); + if (cached) return true; + + const dbRecord = await this.processedEventRepo.findByEventId(eventId); + if (dbRecord) { + await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS); + return true; + } + + return false; + } + + /** + * 标记事件为已处理 + */ + private async markEventProcessed( + eventId: string, + eventType: string, + ): Promise { + await this.processedEventRepo.create({ + eventId, + eventType, + sourceService: 'mining-blockchain-service', + }); + + const redisKey = `processed-event:pool-deposit:${eventId}`; + await this.redis.set(redisKey, '1', IDEMPOTENCY_TTL_SECONDS); + } +} diff --git a/frontend/mining-admin-web/src/features/configs/api/configs.api.ts b/frontend/mining-admin-web/src/features/configs/api/configs.api.ts index b9f99e21..95634e96 100644 --- a/frontend/mining-admin-web/src/features/configs/api/configs.api.ts +++ b/frontend/mining-admin-web/src/features/configs/api/configs.api.ts @@ -1,46 +1,6 @@ import { apiClient } from '@/lib/api/client'; -import axios from 'axios'; import type { SystemConfig } from '@/types/config'; -const tradingBaseURL = '/api/trading'; - -const tradingClient = axios.create({ - baseURL: tradingBaseURL, - timeout: 30000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -tradingClient.interceptors.request.use( - (config) => { - const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error) -); - -tradingClient.interceptors.response.use( - (response) => { - if (response.data && response.data.data !== undefined) { - response.data = response.data.data; - } - return response; - }, - (error) => { - if (error.response?.status === 401) { - localStorage.removeItem('admin_token'); - if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) { - window.location.href = '/login'; - } - } - return Promise.reject(error); - } -); - export interface ContributionSyncStatus { isSynced: boolean; miningNetworkTotal: string; @@ -114,13 +74,13 @@ export const configsApi = { await apiClient.post('/configs/p2p-transfer-fee', { fee, minTransferAmount }); }, - // 获取池账户余额 + // 获取池账户余额(通过 mining-admin-service 代理) getPoolAccountBalance: async (walletName: string): Promise => { - const response = await tradingClient.get(`/admin/pool-accounts/${walletName}/balance`); - return response.data; + const response = await apiClient.get(`/admin/pool-accounts/${walletName}/balance`); + return response.data.data; }, - // 区块链提现(从池账户) + // 区块链提现(从池账户,通过 mining-admin-service 代理) poolAccountBlockchainWithdraw: async (walletName: string, toAddress: string, amount: string): Promise<{ success: boolean; message: string; @@ -129,7 +89,7 @@ export const configsApi = { newBalance?: string; error?: string; }> => { - const response = await tradingClient.post(`/admin/pool-accounts/${walletName}/blockchain-withdraw`, { toAddress, amount }); - return response.data; + const response = await apiClient.post(`/admin/pool-accounts/${walletName}/blockchain-withdraw`, { toAddress, amount }); + return response.data.data; }, };