refactor(mining-wallet-service): remove KAVA blockchain integration

- Remove KavaBlockchainService and blockchain.repository
- Remove BlockchainIntegrationService and BlockchainController
- Update health controller to remove blockchain check
- Clean up Prisma schema (remove blockchain models and enums)
- Add migration to drop blockchain-related tables

This functionality will be re-implemented when needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-12 06:31:30 -08:00
parent 025cc6871b
commit 8e30438433
10 changed files with 27 additions and 1369 deletions

View File

@ -0,0 +1,27 @@
-- Remove KAVA blockchain related tables and enums
-- These features are being removed from mining-wallet-service
-- Drop tables in correct order (respecting foreign key constraints)
DROP TABLE IF EXISTS "burn_to_black_hole_records";
DROP TABLE IF EXISTS "black_hole_contracts";
DROP TABLE IF EXISTS "blockchain_address_bindings";
DROP TABLE IF EXISTS "dex_swap_records";
DROP TABLE IF EXISTS "deposit_records";
DROP TABLE IF EXISTS "withdraw_requests";
-- Remove WithdrawStatus enum (check if used elsewhere first)
-- Note: PostgreSQL doesn't support DROP TYPE IF EXISTS in older versions
-- So we use a DO block to handle the case safely
DO $$
BEGIN
DROP TYPE IF EXISTS "WithdrawStatus";
EXCEPTION
WHEN OTHERS THEN NULL;
END $$;
-- Update SystemAccountType enum to remove HOT_WALLET and COLD_WALLET
-- This requires recreating the enum, but existing data may use these values
-- For safety, we'll just leave the enum as is if there's data
-- Remove BLOCKCHAIN from CounterpartyType enum
-- Same consideration - leave as is if data exists

View File

@ -23,8 +23,6 @@ enum SystemAccountType {
PROVINCE // 省级公司账户 PROVINCE // 省级公司账户
CITY // 市级公司账户 CITY // 市级公司账户
FEE // 手续费账户 FEE // 手续费账户
HOT_WALLET // 热钱包KAVA链上
COLD_WALLET // 冷钱包(离线存储)
} }
// 池账户类型 // 池账户类型
@ -93,20 +91,9 @@ enum CounterpartyType {
USER // 用户 USER // 用户
SYSTEM_ACCOUNT // 系统账户 SYSTEM_ACCOUNT // 系统账户
POOL // 池账户 POOL // 池账户
BLOCKCHAIN // 区块链地址
EXTERNAL // 外部 EXTERNAL // 外部
} }
// 提现状态
enum WithdrawStatus {
PENDING // 待处理
PROCESSING // 处理中
CONFIRMING // 链上确认中
COMPLETED // 已完成
FAILED // 失败
CANCELLED // 已取消
}
// Outbox 状态 // Outbox 状态
enum OutboxStatus { enum OutboxStatus {
PENDING PENDING
@ -402,193 +389,6 @@ model UserWalletTransaction {
@@map("user_wallet_transactions") @@map("user_wallet_transactions")
} }
// =============================================================================
// KAVA 区块链集成
// =============================================================================
// 提现请求
model WithdrawRequest {
id String @id @default(uuid())
requestNo String @unique @map("request_no")
accountSequence String @map("account_sequence")
// 提现信息
assetType AssetType @map("asset_type")
amount Decimal @db.Decimal(30, 8)
fee Decimal @default(0) @db.Decimal(30, 8)
netAmount Decimal @map("net_amount") @db.Decimal(30, 8)
// 目标地址
toAddress String @map("to_address")
// 状态
status WithdrawStatus @default(PENDING)
// 链上信息
txHash String? @map("tx_hash")
blockNumber BigInt? @map("block_number")
confirmations Int @default(0)
// 错误信息
errorMessage String? @map("error_message")
// 审核信息
approvedBy String? @map("approved_by")
approvedAt DateTime? @map("approved_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
completedAt DateTime? @map("completed_at")
@@index([accountSequence])
@@index([status])
@@index([txHash])
@@index([createdAt(sort: Desc)])
@@map("withdraw_requests")
}
// 充值记录(链上检测到的充值)
model DepositRecord {
id String @id @default(uuid())
txHash String @unique @map("tx_hash")
// 来源信息
fromAddress String @map("from_address")
toAddress String @map("to_address")
// 充值信息
assetType AssetType @map("asset_type")
amount Decimal @db.Decimal(30, 8)
// 链上信息
blockNumber BigInt @map("block_number")
confirmations Int @default(0)
// 匹配的用户(如果能匹配到)
matchedAccountSeq String? @map("matched_account_seq")
isProcessed Boolean @default(false) @map("is_processed")
processedAt DateTime? @map("processed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([fromAddress])
@@index([toAddress])
@@index([matchedAccountSeq])
@@index([isProcessed])
@@index([createdAt(sort: Desc)])
@@map("deposit_records")
}
// DEX Swap 记录
model DexSwapRecord {
id String @id @default(uuid())
swapNo String @unique @map("swap_no")
accountSequence String @map("account_sequence")
// Swap 信息
fromAsset AssetType @map("from_asset")
toAsset AssetType @map("to_asset")
fromAmount Decimal @map("from_amount") @db.Decimal(30, 8)
toAmount Decimal @map("to_amount") @db.Decimal(30, 8)
exchangeRate Decimal @map("exchange_rate") @db.Decimal(30, 18)
// 滑点/手续费
slippage Decimal @default(0) @db.Decimal(10, 4)
fee Decimal @default(0) @db.Decimal(30, 8)
// 状态
status WithdrawStatus @default(PENDING)
// 链上信息
txHash String? @map("tx_hash")
blockNumber BigInt? @map("block_number")
errorMessage String? @map("error_message")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
completedAt DateTime? @map("completed_at")
@@index([accountSequence])
@@index([status])
@@index([txHash])
@@index([createdAt(sort: Desc)])
@@map("dex_swap_records")
}
// 链上地址绑定
model BlockchainAddressBinding {
id String @id @default(uuid())
accountSequence String @unique @map("account_sequence")
// KAVA 地址
kavaAddress String @unique @map("kava_address")
// 验证信息
isVerified Boolean @default(false) @map("is_verified")
verifiedAt DateTime? @map("verified_at")
verificationTxHash String? @map("verification_tx_hash")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([kavaAddress])
@@map("blockchain_address_bindings")
}
// 黑洞合约KAVA 链上销毁地址)
model BlackHoleContract {
id String @id @default(uuid())
contractAddress String @unique @map("contract_address")
name String
// 累计销毁
totalBurned Decimal @default(0) @map("total_burned") @db.Decimal(30, 8)
targetBurn Decimal @map("target_burn") @db.Decimal(30, 8)
remainingBurn Decimal @map("remaining_burn") @db.Decimal(30, 8)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
burnRecords BurnToBlackHoleRecord[]
@@map("black_hole_contracts")
}
// 销毁到黑洞的记录
model BurnToBlackHoleRecord {
id String @id @default(uuid())
blackHoleId String @map("black_hole_id")
// 销毁信息
amount Decimal @db.Decimal(30, 8)
// 来源
sourceType CounterpartyType @map("source_type")
sourceAccountSeq String? @map("source_account_seq")
sourceUserId String? @map("source_user_id")
sourcePoolType PoolAccountType? @map("source_pool_type")
// 链上信息
txHash String? @map("tx_hash")
blockNumber BigInt? @map("block_number")
// 备注
memo String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
blackHole BlackHoleContract @relation(fields: [blackHoleId], references: [id])
@@index([blackHoleId])
@@index([sourceAccountSeq])
@@index([txHash])
@@index([createdAt(sort: Desc)])
@@map("burn_to_black_hole_records")
}
// ============================================================================= // =============================================================================
// 手续费配置 // 手续费配置
// ============================================================================= // =============================================================================

View File

@ -4,7 +4,6 @@ import { SystemAccountController } from './controllers/system-account.controller
import { PoolAccountController } from './controllers/pool-account.controller'; import { PoolAccountController } from './controllers/pool-account.controller';
import { UserWalletController } from './controllers/user-wallet.controller'; import { UserWalletController } from './controllers/user-wallet.controller';
import { RegionController } from './controllers/region.controller'; import { RegionController } from './controllers/region.controller';
import { BlockchainController } from './controllers/blockchain.controller';
import { ApplicationModule } from '../application/application.module'; import { ApplicationModule } from '../application/application.module';
@Module({ @Module({
@ -15,7 +14,6 @@ import { ApplicationModule } from '../application/application.module';
PoolAccountController, PoolAccountController,
UserWalletController, UserWalletController,
RegionController, RegionController,
BlockchainController,
], ],
}) })
export class ApiModule {} export class ApiModule {}

View File

@ -1,129 +0,0 @@
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { BlockchainIntegrationService } from '../../application/services/blockchain.service';
import { CurrentUser, CurrentUserPayload } from '../../shared/decorators/current-user.decorator';
import { AdminOnly } from '../../shared/guards/jwt-auth.guard';
import { AssetType, WithdrawStatus } from '@prisma/client';
import Decimal from 'decimal.js';
class CreateWithdrawRequestDto {
assetType: AssetType;
amount: string;
toAddress: string;
}
class BindAddressDto {
kavaAddress: string;
}
class CreateSwapRequestDto {
fromAsset: AssetType;
toAsset: AssetType;
fromAmount: string;
minToAmount: string;
}
@ApiTags('Blockchain')
@Controller('blockchain')
@ApiBearerAuth()
export class BlockchainController {
constructor(private readonly blockchainService: BlockchainIntegrationService) {}
// ==================== User Operations ====================
@Get('my/address')
@ApiOperation({ summary: '获取我的绑定地址' })
async getMyAddress(@CurrentUser() user: CurrentUserPayload) {
return this.blockchainService.getUserAddressBinding(user.accountSequence);
}
@Post('my/address')
@ApiOperation({ summary: '绑定KAVA地址' })
async bindAddress(
@CurrentUser() user: CurrentUserPayload,
@Body() dto: BindAddressDto,
) {
await this.blockchainService.bindUserAddress(user.accountSequence, dto.kavaAddress);
return { success: true, kavaAddress: dto.kavaAddress };
}
@Get('my/withdrawals')
@ApiOperation({ summary: '获取我的提现记录' })
@ApiQuery({ name: 'status', required: false })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'offset', required: false, type: Number })
async getMyWithdrawals(
@CurrentUser() user: CurrentUserPayload,
@Query('status') status?: WithdrawStatus,
@Query('limit') limit?: number,
@Query('offset') offset?: number,
) {
return this.blockchainService.getUserWithdrawRequests(user.accountSequence, {
status,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
});
}
@Post('my/withdraw')
@ApiOperation({ summary: '创建提现请求' })
async createWithdrawRequest(
@CurrentUser() user: CurrentUserPayload,
@Body() dto: CreateWithdrawRequestDto,
) {
return this.blockchainService.createWithdrawRequest(
user.accountSequence,
dto.assetType,
new Decimal(dto.amount),
dto.toAddress,
);
}
@Post('my/swap')
@ApiOperation({ summary: '创建DEX Swap请求' })
async createSwapRequest(
@CurrentUser() user: CurrentUserPayload,
@Body() dto: CreateSwapRequestDto,
) {
return this.blockchainService.createSwapRequest(
user.accountSequence,
dto.fromAsset,
dto.toAsset,
new Decimal(dto.fromAmount),
new Decimal(dto.minToAmount),
);
}
// ==================== Admin Operations ====================
@Get('withdrawals/:id')
@AdminOnly()
@ApiOperation({ summary: '获取提现请求详情' })
async getWithdrawRequest(@Param('id') id: string) {
return this.blockchainService.getWithdrawRequest(id);
}
@Post('withdrawals/:id/approve')
@AdminOnly()
@ApiOperation({ summary: '审批提现请求' })
async approveWithdraw(
@Param('id') id: string,
@CurrentUser() user: CurrentUserPayload,
) {
return this.blockchainService.approveWithdrawRequest(id, user.userId);
}
@Post('withdrawals/:id/execute')
@AdminOnly()
@ApiOperation({ summary: '执行链上提现' })
async executeWithdraw(@Param('id') id: string) {
return this.blockchainService.executeWithdraw(id);
}
@Post('withdrawals/:id/confirm')
@AdminOnly()
@ApiOperation({ summary: '确认提现完成' })
async confirmWithdraw(@Param('id') id: string) {
return this.blockchainService.confirmWithdrawComplete(id);
}
}

View File

@ -3,7 +3,6 @@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Public } from '../../shared/guards/jwt-auth.guard'; import { Public } from '../../shared/guards/jwt-auth.guard';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
import { RedisService } from '../../infrastructure/redis/redis.service'; import { RedisService } from '../../infrastructure/redis/redis.service';
import { KavaBlockchainService } from '../../infrastructure/blockchain/kava-blockchain.service';
@ApiTags('Health') @ApiTags('Health')
@Controller('health') @Controller('health')
@ -11,7 +10,6 @@ export class HealthController {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly redis: RedisService, private readonly redis: RedisService,
private readonly kava: KavaBlockchainService,
) {} ) {}
@Get() @Get()
@ -26,7 +24,6 @@ export class HealthController {
checks: { checks: {
database: 'unknown', database: 'unknown',
redis: 'unknown', redis: 'unknown',
blockchain: 'unknown',
}, },
}; };
@ -48,9 +45,6 @@ export class HealthController {
checks.status = 'degraded'; checks.status = 'degraded';
} }
// Blockchain check
checks.checks.blockchain = this.kava.isReady() ? 'healthy' : 'degraded';
return checks; return checks;
} }

View File

@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule';
import { SystemAccountService } from './services/system-account.service'; import { SystemAccountService } from './services/system-account.service';
import { PoolAccountService } from './services/pool-account.service'; import { PoolAccountService } from './services/pool-account.service';
import { UserWalletService } from './services/user-wallet.service'; import { UserWalletService } from './services/user-wallet.service';
import { BlockchainIntegrationService } from './services/blockchain.service';
import { ContributionWalletService } from './services/contribution-wallet.service'; import { ContributionWalletService } from './services/contribution-wallet.service';
// Schedulers // Schedulers
@ -23,7 +22,6 @@ import { UserRegisteredConsumer } from '../infrastructure/kafka/consumers/user-r
SystemAccountService, SystemAccountService,
PoolAccountService, PoolAccountService,
UserWalletService, UserWalletService,
BlockchainIntegrationService,
ContributionWalletService, ContributionWalletService,
// Schedulers // Schedulers
OutboxScheduler, OutboxScheduler,
@ -36,7 +34,6 @@ import { UserRegisteredConsumer } from '../infrastructure/kafka/consumers/user-r
SystemAccountService, SystemAccountService,
PoolAccountService, PoolAccountService,
UserWalletService, UserWalletService,
BlockchainIntegrationService,
ContributionWalletService, ContributionWalletService,
], ],
}) })

View File

@ -1,353 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { BlockchainRepository } from '../../infrastructure/persistence/repositories/blockchain.repository';
import { UserWalletRepository } from '../../infrastructure/persistence/repositories/user-wallet.repository';
import { OutboxRepository } from '../../infrastructure/persistence/repositories/outbox.repository';
import { KavaBlockchainService } from '../../infrastructure/blockchain/kava-blockchain.service';
import { WithdrawRequest, DepositRecord, DexSwapRecord, AssetType, WithdrawStatus } from '@prisma/client';
import Decimal from 'decimal.js';
import { DomainException } from '../../shared/filters/domain-exception.filter';
@Injectable()
export class BlockchainIntegrationService {
private readonly logger = new Logger(BlockchainIntegrationService.name);
constructor(
private readonly blockchainRepo: BlockchainRepository,
private readonly userWalletRepo: UserWalletRepository,
private readonly outboxRepo: OutboxRepository,
private readonly kavaService: KavaBlockchainService,
) {}
/**
*
*/
async createWithdrawRequest(
accountSequence: string,
assetType: AssetType,
amount: Decimal,
toAddress: string,
feeRate: Decimal = new Decimal('0.001'),
): Promise<WithdrawRequest> {
// 验证地址格式
if (!this.kavaService.isValidAddress(toAddress)) {
throw new DomainException('Invalid blockchain address', 'INVALID_ADDRESS');
}
// 检查用户余额
const wallet = await this.userWalletRepo.findByAccountAndType(accountSequence, 'TOKEN_STORAGE');
if (!wallet) {
throw new DomainException('User wallet not found', 'WALLET_NOT_FOUND');
}
const balance = new Decimal(wallet.balance.toString());
if (balance.lessThan(amount)) {
throw new DomainException(`Insufficient balance: ${balance} < ${amount}`, 'INSUFFICIENT_BALANCE');
}
// 计算手续费
const fee = amount.mul(feeRate);
if (fee.greaterThan(amount)) {
throw new DomainException('Fee exceeds amount', 'FEE_TOO_HIGH');
}
// 生成请求号
const requestNo = `WD${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
// 创建提现请求
const request = await this.blockchainRepo.createWithdrawRequest({
requestNo,
accountSequence,
assetType,
amount,
fee,
toAddress,
});
// 冻结用户余额
await this.userWalletRepo.freezeBalance(
accountSequence,
'TOKEN_STORAGE',
'SHARE',
amount,
true,
{
referenceId: request.id,
referenceType: 'WITHDRAW_REQUEST',
memo: `提现冻结, 目标地址${toAddress}, 数量${amount.toFixed(8)}`,
},
);
await this.outboxRepo.create({
aggregateType: 'WithdrawRequest',
aggregateId: request.id,
eventType: 'WITHDRAW_REQUEST_CREATED',
payload: {
requestId: request.id,
requestNo,
accountSequence,
assetType,
amount: amount.toString(),
fee: fee.toString(),
toAddress,
},
});
this.logger.log(`Withdraw request created: ${requestNo}`);
return request;
}
/**
*
*/
async approveWithdrawRequest(
requestId: string,
approvedBy: string,
): Promise<WithdrawRequest> {
const request = await this.blockchainRepo.findWithdrawById(requestId);
if (!request) {
throw new DomainException('Withdraw request not found', 'REQUEST_NOT_FOUND');
}
if (request.status !== 'PENDING') {
throw new DomainException(`Cannot approve request in status: ${request.status}`, 'INVALID_STATUS');
}
const updated = await this.blockchainRepo.updateWithdrawStatus(requestId, 'PROCESSING', {
approvedBy,
});
await this.outboxRepo.create({
aggregateType: 'WithdrawRequest',
aggregateId: requestId,
eventType: 'WITHDRAW_REQUEST_APPROVED',
payload: {
requestId,
requestNo: request.requestNo,
approvedBy,
approvedAt: updated.approvedAt?.toISOString(),
},
});
return updated;
}
/**
*
*/
async executeWithdraw(requestId: string): Promise<WithdrawRequest> {
const request = await this.blockchainRepo.findWithdrawById(requestId);
if (!request) {
throw new DomainException('Withdraw request not found', 'REQUEST_NOT_FOUND');
}
if (request.status !== 'PROCESSING') {
throw new DomainException(`Cannot execute request in status: ${request.status}`, 'INVALID_STATUS');
}
if (!this.kavaService.isReady()) {
throw new DomainException('Blockchain service not ready', 'BLOCKCHAIN_NOT_READY');
}
try {
// 执行链上转账
const netAmount = new Decimal(request.netAmount.toString());
const result = await this.kavaService.sendNative(request.toAddress, netAmount);
// 更新状态为确认中
const updated = await this.blockchainRepo.updateWithdrawStatus(requestId, 'CONFIRMING', {
txHash: result.txHash,
blockNumber: BigInt(result.blockNumber),
});
await this.outboxRepo.create({
aggregateType: 'WithdrawRequest',
aggregateId: requestId,
eventType: 'WITHDRAW_TX_SENT',
payload: {
requestId,
txHash: result.txHash,
blockNumber: result.blockNumber,
},
});
return updated;
} catch (error) {
// 标记失败
await this.blockchainRepo.updateWithdrawStatus(requestId, 'FAILED', {
errorMessage: error instanceof Error ? error.message : 'Unknown error',
});
// 解冻用户余额
await this.userWalletRepo.freezeBalance(
request.accountSequence,
'TOKEN_STORAGE',
'SHARE',
new Decimal(request.amount.toString()),
false,
{
referenceId: requestId,
referenceType: 'WITHDRAW_REQUEST',
memo: `提现失败解冻, 原因: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
);
throw error;
}
}
/**
*
*/
async confirmWithdrawComplete(requestId: string): Promise<WithdrawRequest> {
const request = await this.blockchainRepo.findWithdrawById(requestId);
if (!request) {
throw new DomainException('Withdraw request not found', 'REQUEST_NOT_FOUND');
}
if (request.status !== 'CONFIRMING' || !request.txHash) {
throw new DomainException(`Cannot confirm request in status: ${request.status}`, 'INVALID_STATUS');
}
// 检查链上确认
const txStatus = await this.kavaService.getTransactionStatus(request.txHash);
if (!txStatus.confirmed || txStatus.status !== 'success') {
throw new DomainException('Transaction not confirmed yet', 'TX_NOT_CONFIRMED');
}
// 更新为完成
const updated = await this.blockchainRepo.updateWithdrawStatus(requestId, 'COMPLETED', {
confirmations: txStatus.confirmations,
});
// 从冻结余额扣除(实际扣款)
await this.userWalletRepo.updateBalanceWithTransaction(
request.accountSequence,
'TOKEN_STORAGE',
'SHARE',
new Decimal(request.amount.toString()).negated(),
{
transactionType: 'WITHDRAW',
counterpartyType: 'BLOCKCHAIN',
counterpartyAddress: request.toAddress,
referenceId: requestId,
referenceType: 'WITHDRAW_REQUEST',
txHash: request.txHash,
memo: `提现成功, 目标地址${request.toAddress}, 数量${request.netAmount}, 手续费${request.fee}`,
},
);
await this.outboxRepo.create({
aggregateType: 'WithdrawRequest',
aggregateId: requestId,
eventType: 'WITHDRAW_COMPLETED',
payload: {
requestId,
txHash: request.txHash,
confirmations: txStatus.confirmations,
completedAt: updated.completedAt?.toISOString(),
},
});
this.logger.log(`Withdraw completed: ${request.requestNo}`);
return updated;
}
/**
*
*/
async bindUserAddress(
accountSequence: string,
kavaAddress: string,
): Promise<void> {
if (!this.kavaService.isValidAddress(kavaAddress)) {
throw new DomainException('Invalid KAVA address', 'INVALID_ADDRESS');
}
await this.blockchainRepo.bindAddress({
accountSequence,
kavaAddress,
});
await this.outboxRepo.create({
aggregateType: 'BlockchainAddress',
aggregateId: accountSequence,
eventType: 'ADDRESS_BOUND',
payload: {
accountSequence,
kavaAddress,
},
});
this.logger.log(`Address bound for user ${accountSequence}: ${kavaAddress}`);
}
/**
* DEX Swap
*/
async createSwapRequest(
accountSequence: string,
fromAsset: AssetType,
toAsset: AssetType,
fromAmount: Decimal,
minToAmount: Decimal,
): Promise<DexSwapRecord> {
const swapNo = `SW${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
// 这里应该调用 DEX 获取实际汇率
// 简化处理:假设 1:1 汇率
const exchangeRate = new Decimal(1);
const toAmount = fromAmount.mul(exchangeRate);
if (toAmount.lessThan(minToAmount)) {
throw new DomainException('Slippage too high', 'SLIPPAGE_EXCEEDED');
}
const slippage = toAmount.minus(minToAmount).div(minToAmount).mul(100);
const fee = fromAmount.mul(new Decimal('0.003')); // 0.3% 手续费
const swap = await this.blockchainRepo.createSwapRecord({
swapNo,
accountSequence,
fromAsset,
toAsset,
fromAmount,
toAmount,
exchangeRate,
slippage,
fee,
});
await this.outboxRepo.create({
aggregateType: 'DexSwap',
aggregateId: swap.id,
eventType: 'SWAP_REQUEST_CREATED',
payload: {
swapId: swap.id,
swapNo,
accountSequence,
fromAsset,
toAsset,
fromAmount: fromAmount.toString(),
toAmount: toAmount.toString(),
exchangeRate: exchangeRate.toString(),
},
});
return swap;
}
async getWithdrawRequest(requestId: string): Promise<WithdrawRequest | null> {
return this.blockchainRepo.findWithdrawById(requestId);
}
async getUserWithdrawRequests(
accountSequence: string,
options?: { status?: WithdrawStatus; limit?: number; offset?: number },
) {
return this.blockchainRepo.findWithdrawsByAccount(accountSequence, options);
}
async getUserAddressBinding(accountSequence: string) {
return this.blockchainRepo.findAddressByAccount(accountSequence);
}
}

View File

@ -1,271 +0,0 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ethers } from 'ethers';
import Decimal from 'decimal.js';
export interface TransactionResult {
txHash: string;
blockNumber: number;
gasUsed: bigint;
status: 'success' | 'failed';
}
export interface TokenBalance {
balance: Decimal;
decimals: number;
}
@Injectable()
export class KavaBlockchainService implements OnModuleInit {
private readonly logger = new Logger(KavaBlockchainService.name);
private provider: ethers.JsonRpcProvider;
private hotWallet: ethers.Wallet | null = null;
private blackHoleAddress: string;
private isConnected = false;
constructor(private readonly configService: ConfigService) {
this.blackHoleAddress = this.configService.get<string>(
'KAVA_BLACK_HOLE_ADDRESS',
'0x000000000000000000000000000000000000dEaD',
);
}
async onModuleInit() {
await this.connect();
}
private async connect(): Promise<void> {
try {
const rpcUrl = this.configService.get<string>('KAVA_RPC_URL', 'https://evm.kava.io');
const chainId = this.configService.get<number>('KAVA_CHAIN_ID', 2222);
this.provider = new ethers.JsonRpcProvider(rpcUrl, chainId);
// Test connection
const network = await this.provider.getNetwork();
this.logger.log(`Connected to KAVA network: ${network.chainId}`);
// Initialize hot wallet if private key is provided
const privateKey = this.configService.get<string>('KAVA_HOT_WALLET_PRIVATE_KEY');
if (privateKey) {
this.hotWallet = new ethers.Wallet(privateKey, this.provider);
this.logger.log(`Hot wallet initialized: ${this.hotWallet.address}`);
} else {
this.logger.warn('No hot wallet private key provided - blockchain operations limited');
}
this.isConnected = true;
} catch (error) {
this.logger.error('Failed to connect to KAVA blockchain', error);
this.isConnected = false;
}
}
isReady(): boolean {
return this.isConnected && this.hotWallet !== null;
}
getHotWalletAddress(): string | null {
return this.hotWallet?.address || null;
}
getBlackHoleAddress(): string {
return this.blackHoleAddress;
}
/**
*
*/
async getBalance(address: string): Promise<Decimal> {
const balance = await this.provider.getBalance(address);
return new Decimal(ethers.formatEther(balance));
}
/**
*
*/
async getCurrentBlockNumber(): Promise<number> {
return this.provider.getBlockNumber();
}
/**
*
*/
async getTransactionStatus(txHash: string): Promise<{
confirmed: boolean;
blockNumber: number | null;
confirmations: number;
status: 'success' | 'failed' | 'pending';
}> {
const receipt = await this.provider.getTransactionReceipt(txHash);
if (!receipt) {
return {
confirmed: false,
blockNumber: null,
confirmations: 0,
status: 'pending',
};
}
const currentBlock = await this.getCurrentBlockNumber();
const confirmations = currentBlock - receipt.blockNumber;
return {
confirmed: confirmations >= 1,
blockNumber: receipt.blockNumber,
confirmations,
status: receipt.status === 1 ? 'success' : 'failed',
};
}
/**
* KAVA
*/
async sendNative(
toAddress: string,
amount: Decimal,
): Promise<TransactionResult> {
if (!this.hotWallet) {
throw new Error('Hot wallet not initialized');
}
const tx = await this.hotWallet.sendTransaction({
to: toAddress,
value: ethers.parseEther(amount.toString()),
});
const receipt = await tx.wait();
if (!receipt) {
throw new Error('Transaction failed - no receipt');
}
return {
txHash: receipt.hash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed,
status: receipt.status === 1 ? 'success' : 'failed',
};
}
/**
* ERC20
*/
async sendToken(
tokenAddress: string,
toAddress: string,
amount: Decimal,
decimals: number = 18,
): Promise<TransactionResult> {
if (!this.hotWallet) {
throw new Error('Hot wallet not initialized');
}
const erc20Abi = [
'function transfer(address to, uint256 amount) returns (bool)',
'function balanceOf(address account) view returns (uint256)',
'function decimals() view returns (uint8)',
];
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, this.hotWallet);
const amountWei = ethers.parseUnits(amount.toString(), decimals);
const tx = await tokenContract.transfer(toAddress, amountWei);
const receipt = await tx.wait();
if (!receipt) {
throw new Error('Transaction failed - no receipt');
}
return {
txHash: receipt.hash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed,
status: receipt.status === 1 ? 'success' : 'failed',
};
}
/**
*
*/
async burnToBlackHole(
tokenAddress: string,
amount: Decimal,
decimals: number = 18,
): Promise<TransactionResult> {
return this.sendToken(tokenAddress, this.blackHoleAddress, amount, decimals);
}
/**
* ERC20
*/
async getTokenBalance(
tokenAddress: string,
walletAddress: string,
): Promise<TokenBalance> {
const erc20Abi = [
'function balanceOf(address account) view returns (uint256)',
'function decimals() view returns (uint8)',
];
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, this.provider);
const [balance, decimals] = await Promise.all([
tokenContract.balanceOf(walletAddress),
tokenContract.decimals(),
]);
return {
balance: new Decimal(ethers.formatUnits(balance, decimals)),
decimals,
};
}
/**
* Gas
*/
async estimateGas(
toAddress: string,
value: Decimal,
data?: string,
): Promise<{ gasLimit: bigint; gasPrice: bigint; estimatedFee: Decimal }> {
const gasPrice = (await this.provider.getFeeData()).gasPrice || 0n;
const gasLimit = await this.provider.estimateGas({
to: toAddress,
value: ethers.parseEther(value.toString()),
data: data || '0x',
});
const estimatedFee = new Decimal(ethers.formatEther(gasLimit * gasPrice));
return {
gasLimit,
gasPrice,
estimatedFee,
};
}
/**
*
*/
isValidAddress(address: string): boolean {
return ethers.isAddress(address);
}
/**
*
*/
onNewBlock(callback: (blockNumber: number) => void): void {
this.provider.on('block', callback);
}
/**
*
*/
removeAllListeners(): void {
this.provider.removeAllListeners();
}
}

View File

@ -6,12 +6,10 @@ import { SystemAccountRepository } from './persistence/repositories/system-accou
import { PoolAccountRepository } from './persistence/repositories/pool-account.repository'; import { PoolAccountRepository } from './persistence/repositories/pool-account.repository';
import { UserWalletRepository } from './persistence/repositories/user-wallet.repository'; import { UserWalletRepository } from './persistence/repositories/user-wallet.repository';
import { RegionRepository } from './persistence/repositories/region.repository'; import { RegionRepository } from './persistence/repositories/region.repository';
import { BlockchainRepository } from './persistence/repositories/blockchain.repository';
import { OutboxRepository } from './persistence/repositories/outbox.repository'; import { OutboxRepository } from './persistence/repositories/outbox.repository';
import { ProcessedEventRepository } from './persistence/repositories/processed-event.repository'; import { ProcessedEventRepository } from './persistence/repositories/processed-event.repository';
import { RedisService } from './redis/redis.service'; import { RedisService } from './redis/redis.service';
import { KafkaProducerService } from './kafka/kafka-producer.service'; import { KafkaProducerService } from './kafka/kafka-producer.service';
import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
// 注意: Consumers 移到 ApplicationModule 中,因为它们依赖应用服务 // 注意: Consumers 移到 ApplicationModule 中,因为它们依赖应用服务
@Global() @Global()
@ -49,12 +47,10 @@ import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
PoolAccountRepository, PoolAccountRepository,
UserWalletRepository, UserWalletRepository,
RegionRepository, RegionRepository,
BlockchainRepository,
OutboxRepository, OutboxRepository,
ProcessedEventRepository, ProcessedEventRepository,
// Services // Services
KafkaProducerService, KafkaProducerService,
KavaBlockchainService,
// Consumers 已移到 ApplicationModule // Consumers 已移到 ApplicationModule
{ {
provide: 'REDIS_OPTIONS', provide: 'REDIS_OPTIONS',
@ -74,12 +70,10 @@ import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
PoolAccountRepository, PoolAccountRepository,
UserWalletRepository, UserWalletRepository,
RegionRepository, RegionRepository,
BlockchainRepository,
OutboxRepository, OutboxRepository,
ProcessedEventRepository, ProcessedEventRepository,
// Services // Services
KafkaProducerService, KafkaProducerService,
KavaBlockchainService,
RedisService, RedisService,
ClientsModule, ClientsModule,
], ],

View File

@ -1,399 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
WithdrawRequest,
DepositRecord,
DexSwapRecord,
BlockchainAddressBinding,
BlackHoleContract,
BurnToBlackHoleRecord,
WithdrawStatus,
AssetType,
CounterpartyType,
PoolAccountType,
Prisma,
} from '@prisma/client';
import Decimal from 'decimal.js';
@Injectable()
export class BlockchainRepository {
private readonly logger = new Logger(BlockchainRepository.name);
constructor(private readonly prisma: PrismaService) {}
// ==================== Withdraw Requests ====================
async createWithdrawRequest(data: {
requestNo: string;
accountSequence: string;
assetType: AssetType;
amount: Decimal;
fee: Decimal;
toAddress: string;
}): Promise<WithdrawRequest> {
const netAmount = data.amount.minus(data.fee);
return this.prisma.withdrawRequest.create({
data: {
requestNo: data.requestNo,
accountSequence: data.accountSequence,
assetType: data.assetType,
amount: data.amount.toFixed(8),
fee: data.fee.toFixed(8),
netAmount: netAmount.toFixed(8),
toAddress: data.toAddress,
status: 'PENDING',
},
});
}
async findWithdrawById(id: string): Promise<WithdrawRequest | null> {
return this.prisma.withdrawRequest.findUnique({
where: { id },
});
}
async findWithdrawByRequestNo(requestNo: string): Promise<WithdrawRequest | null> {
return this.prisma.withdrawRequest.findUnique({
where: { requestNo },
});
}
async findWithdrawsByAccount(
accountSequence: string,
options?: {
status?: WithdrawStatus;
limit?: number;
offset?: number;
},
): Promise<{ requests: WithdrawRequest[]; total: number }> {
const where: Prisma.WithdrawRequestWhereInput = {
accountSequence,
};
if (options?.status) {
where.status = options.status;
}
const [requests, total] = await Promise.all([
this.prisma.withdrawRequest.findMany({
where,
orderBy: { createdAt: 'desc' },
take: options?.limit || 50,
skip: options?.offset || 0,
}),
this.prisma.withdrawRequest.count({ where }),
]);
return { requests, total };
}
async updateWithdrawStatus(
id: string,
status: WithdrawStatus,
data?: {
txHash?: string;
blockNumber?: bigint;
confirmations?: number;
errorMessage?: string;
approvedBy?: string;
},
): Promise<WithdrawRequest> {
const updateData: Prisma.WithdrawRequestUpdateInput = {
status,
};
if (data?.txHash) updateData.txHash = data.txHash;
if (data?.blockNumber) updateData.blockNumber = data.blockNumber;
if (data?.confirmations) updateData.confirmations = data.confirmations;
if (data?.errorMessage) updateData.errorMessage = data.errorMessage;
if (data?.approvedBy) {
updateData.approvedBy = data.approvedBy;
updateData.approvedAt = new Date();
}
if (status === 'COMPLETED') {
updateData.completedAt = new Date();
}
return this.prisma.withdrawRequest.update({
where: { id },
data: updateData,
});
}
// ==================== Deposit Records ====================
async createDepositRecord(data: {
txHash: string;
fromAddress: string;
toAddress: string;
assetType: AssetType;
amount: Decimal;
blockNumber: bigint;
confirmations?: number;
matchedAccountSeq?: string;
}): Promise<DepositRecord> {
return this.prisma.depositRecord.create({
data: {
txHash: data.txHash,
fromAddress: data.fromAddress,
toAddress: data.toAddress,
assetType: data.assetType,
amount: data.amount.toFixed(8),
blockNumber: data.blockNumber,
confirmations: data.confirmations || 0,
matchedAccountSeq: data.matchedAccountSeq,
},
});
}
async findDepositByTxHash(txHash: string): Promise<DepositRecord | null> {
return this.prisma.depositRecord.findUnique({
where: { txHash },
});
}
async findUnprocessedDeposits(limit: number = 100): Promise<DepositRecord[]> {
return this.prisma.depositRecord.findMany({
where: {
isProcessed: false,
matchedAccountSeq: { not: null },
},
orderBy: { createdAt: 'asc' },
take: limit,
});
}
async markDepositProcessed(id: string): Promise<DepositRecord> {
return this.prisma.depositRecord.update({
where: { id },
data: {
isProcessed: true,
processedAt: new Date(),
},
});
}
async updateDepositConfirmations(txHash: string, confirmations: number): Promise<DepositRecord> {
return this.prisma.depositRecord.update({
where: { txHash },
data: { confirmations },
});
}
// ==================== DEX Swap Records ====================
async createSwapRecord(data: {
swapNo: string;
accountSequence: string;
fromAsset: AssetType;
toAsset: AssetType;
fromAmount: Decimal;
toAmount: Decimal;
exchangeRate: Decimal;
slippage?: Decimal;
fee?: Decimal;
}): Promise<DexSwapRecord> {
return this.prisma.dexSwapRecord.create({
data: {
swapNo: data.swapNo,
accountSequence: data.accountSequence,
fromAsset: data.fromAsset,
toAsset: data.toAsset,
fromAmount: data.fromAmount.toFixed(8),
toAmount: data.toAmount.toFixed(8),
exchangeRate: data.exchangeRate.toFixed(18),
slippage: data.slippage?.toFixed(4) || '0',
fee: data.fee?.toFixed(8) || '0',
status: 'PENDING',
},
});
}
async findSwapById(id: string): Promise<DexSwapRecord | null> {
return this.prisma.dexSwapRecord.findUnique({
where: { id },
});
}
async findSwapBySwapNo(swapNo: string): Promise<DexSwapRecord | null> {
return this.prisma.dexSwapRecord.findUnique({
where: { swapNo },
});
}
async updateSwapStatus(
id: string,
status: WithdrawStatus,
data?: {
txHash?: string;
blockNumber?: bigint;
errorMessage?: string;
},
): Promise<DexSwapRecord> {
const updateData: Prisma.DexSwapRecordUpdateInput = {
status,
};
if (data?.txHash) updateData.txHash = data.txHash;
if (data?.blockNumber) updateData.blockNumber = data.blockNumber;
if (data?.errorMessage) updateData.errorMessage = data.errorMessage;
if (status === 'COMPLETED') {
updateData.completedAt = new Date();
}
return this.prisma.dexSwapRecord.update({
where: { id },
data: updateData,
});
}
// ==================== Address Binding ====================
async bindAddress(data: {
accountSequence: string;
kavaAddress: string;
}): Promise<BlockchainAddressBinding> {
return this.prisma.blockchainAddressBinding.upsert({
where: { accountSequence: data.accountSequence },
create: {
accountSequence: data.accountSequence,
kavaAddress: data.kavaAddress,
},
update: {
kavaAddress: data.kavaAddress,
isVerified: false,
verifiedAt: null,
verificationTxHash: null,
},
});
}
async findAddressByAccount(accountSequence: string): Promise<BlockchainAddressBinding | null> {
return this.prisma.blockchainAddressBinding.findUnique({
where: { accountSequence },
});
}
async findAccountByAddress(kavaAddress: string): Promise<BlockchainAddressBinding | null> {
return this.prisma.blockchainAddressBinding.findUnique({
where: { kavaAddress },
});
}
async verifyAddress(
accountSequence: string,
verificationTxHash: string,
): Promise<BlockchainAddressBinding> {
return this.prisma.blockchainAddressBinding.update({
where: { accountSequence },
data: {
isVerified: true,
verifiedAt: new Date(),
verificationTxHash,
},
});
}
// ==================== Black Hole Contract ====================
async createBlackHoleContract(data: {
contractAddress: string;
name: string;
targetBurn: Decimal;
}): Promise<BlackHoleContract> {
return this.prisma.blackHoleContract.create({
data: {
contractAddress: data.contractAddress,
name: data.name,
targetBurn: data.targetBurn.toFixed(8),
remainingBurn: data.targetBurn.toFixed(8),
},
});
}
async findBlackHoleContract(contractAddress: string): Promise<BlackHoleContract | null> {
return this.prisma.blackHoleContract.findUnique({
where: { contractAddress },
});
}
async getActiveBlackHoleContract(): Promise<BlackHoleContract | null> {
return this.prisma.blackHoleContract.findFirst({
where: { isActive: true },
});
}
async recordBurnToBlackHole(data: {
blackHoleId: string;
amount: Decimal;
sourceType: CounterpartyType;
sourceAccountSeq?: string;
sourceUserId?: string;
sourcePoolType?: PoolAccountType;
txHash?: string;
blockNumber?: bigint;
memo?: string;
}): Promise<BurnToBlackHoleRecord> {
return this.prisma.$transaction(async (tx) => {
// 更新黑洞合约统计
const blackHole = await tx.blackHoleContract.findUnique({
where: { id: data.blackHoleId },
});
if (!blackHole) {
throw new Error(`Black hole contract not found: ${data.blackHoleId}`);
}
const newTotalBurned = new Decimal(blackHole.totalBurned.toString()).plus(data.amount);
const newRemainingBurn = new Decimal(blackHole.remainingBurn.toString()).minus(data.amount);
await tx.blackHoleContract.update({
where: { id: data.blackHoleId },
data: {
totalBurned: newTotalBurned.toFixed(8),
remainingBurn: newRemainingBurn.greaterThan(0) ? newRemainingBurn.toFixed(8) : '0',
},
});
// 创建销毁记录
return tx.burnToBlackHoleRecord.create({
data: {
blackHoleId: data.blackHoleId,
amount: data.amount.toFixed(8),
sourceType: data.sourceType,
sourceAccountSeq: data.sourceAccountSeq,
sourceUserId: data.sourceUserId,
sourcePoolType: data.sourcePoolType,
txHash: data.txHash,
blockNumber: data.blockNumber,
memo: data.memo,
},
});
});
}
async getBurnRecords(
blackHoleId: string,
options?: {
limit?: number;
offset?: number;
},
): Promise<{ records: BurnToBlackHoleRecord[]; total: number }> {
const where: Prisma.BurnToBlackHoleRecordWhereInput = {
blackHoleId,
};
const [records, total] = await Promise.all([
this.prisma.burnToBlackHoleRecord.findMany({
where,
orderBy: { createdAt: 'desc' },
take: options?.limit || 50,
skip: options?.offset || 0,
}),
this.prisma.burnToBlackHoleRecord.count({ where }),
]);
return { records, total };
}
}