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:
parent
025cc6871b
commit
8e30438433
|
|
@ -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
|
||||
|
|
@ -23,8 +23,6 @@ enum SystemAccountType {
|
|||
PROVINCE // 省级公司账户
|
||||
CITY // 市级公司账户
|
||||
FEE // 手续费账户
|
||||
HOT_WALLET // 热钱包(KAVA链上)
|
||||
COLD_WALLET // 冷钱包(离线存储)
|
||||
}
|
||||
|
||||
// 池账户类型
|
||||
|
|
@ -93,20 +91,9 @@ enum CounterpartyType {
|
|||
USER // 用户
|
||||
SYSTEM_ACCOUNT // 系统账户
|
||||
POOL // 池账户
|
||||
BLOCKCHAIN // 区块链地址
|
||||
EXTERNAL // 外部
|
||||
}
|
||||
|
||||
// 提现状态
|
||||
enum WithdrawStatus {
|
||||
PENDING // 待处理
|
||||
PROCESSING // 处理中
|
||||
CONFIRMING // 链上确认中
|
||||
COMPLETED // 已完成
|
||||
FAILED // 失败
|
||||
CANCELLED // 已取消
|
||||
}
|
||||
|
||||
// Outbox 状态
|
||||
enum OutboxStatus {
|
||||
PENDING
|
||||
|
|
@ -402,193 +389,6 @@ model UserWalletTransaction {
|
|||
@@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")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 手续费配置
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { SystemAccountController } from './controllers/system-account.controller
|
|||
import { PoolAccountController } from './controllers/pool-account.controller';
|
||||
import { UserWalletController } from './controllers/user-wallet.controller';
|
||||
import { RegionController } from './controllers/region.controller';
|
||||
import { BlockchainController } from './controllers/blockchain.controller';
|
||||
import { ApplicationModule } from '../application/application.module';
|
||||
|
||||
@Module({
|
||||
|
|
@ -15,7 +14,6 @@ import { ApplicationModule } from '../application/application.module';
|
|||
PoolAccountController,
|
||||
UserWalletController,
|
||||
RegionController,
|
||||
BlockchainController,
|
||||
],
|
||||
})
|
||||
export class ApiModule {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
|||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||
import { RedisService } from '../../infrastructure/redis/redis.service';
|
||||
import { KavaBlockchainService } from '../../infrastructure/blockchain/kava-blockchain.service';
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('health')
|
||||
|
|
@ -11,7 +10,6 @@ export class HealthController {
|
|||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly kava: KavaBlockchainService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
|
|
@ -26,7 +24,6 @@ export class HealthController {
|
|||
checks: {
|
||||
database: 'unknown',
|
||||
redis: 'unknown',
|
||||
blockchain: 'unknown',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -48,9 +45,6 @@ export class HealthController {
|
|||
checks.status = 'degraded';
|
||||
}
|
||||
|
||||
// Blockchain check
|
||||
checks.checks.blockchain = this.kava.isReady() ? 'healthy' : 'degraded';
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule';
|
|||
import { SystemAccountService } from './services/system-account.service';
|
||||
import { PoolAccountService } from './services/pool-account.service';
|
||||
import { UserWalletService } from './services/user-wallet.service';
|
||||
import { BlockchainIntegrationService } from './services/blockchain.service';
|
||||
import { ContributionWalletService } from './services/contribution-wallet.service';
|
||||
|
||||
// Schedulers
|
||||
|
|
@ -23,7 +22,6 @@ import { UserRegisteredConsumer } from '../infrastructure/kafka/consumers/user-r
|
|||
SystemAccountService,
|
||||
PoolAccountService,
|
||||
UserWalletService,
|
||||
BlockchainIntegrationService,
|
||||
ContributionWalletService,
|
||||
// Schedulers
|
||||
OutboxScheduler,
|
||||
|
|
@ -36,7 +34,6 @@ import { UserRegisteredConsumer } from '../infrastructure/kafka/consumers/user-r
|
|||
SystemAccountService,
|
||||
PoolAccountService,
|
||||
UserWalletService,
|
||||
BlockchainIntegrationService,
|
||||
ContributionWalletService,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,10 @@ import { SystemAccountRepository } from './persistence/repositories/system-accou
|
|||
import { PoolAccountRepository } from './persistence/repositories/pool-account.repository';
|
||||
import { UserWalletRepository } from './persistence/repositories/user-wallet.repository';
|
||||
import { RegionRepository } from './persistence/repositories/region.repository';
|
||||
import { BlockchainRepository } from './persistence/repositories/blockchain.repository';
|
||||
import { OutboxRepository } from './persistence/repositories/outbox.repository';
|
||||
import { ProcessedEventRepository } from './persistence/repositories/processed-event.repository';
|
||||
import { RedisService } from './redis/redis.service';
|
||||
import { KafkaProducerService } from './kafka/kafka-producer.service';
|
||||
import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
|
||||
// 注意: Consumers 移到 ApplicationModule 中,因为它们依赖应用服务
|
||||
|
||||
@Global()
|
||||
|
|
@ -49,12 +47,10 @@ import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
|
|||
PoolAccountRepository,
|
||||
UserWalletRepository,
|
||||
RegionRepository,
|
||||
BlockchainRepository,
|
||||
OutboxRepository,
|
||||
ProcessedEventRepository,
|
||||
// Services
|
||||
KafkaProducerService,
|
||||
KavaBlockchainService,
|
||||
// Consumers 已移到 ApplicationModule
|
||||
{
|
||||
provide: 'REDIS_OPTIONS',
|
||||
|
|
@ -74,12 +70,10 @@ import { KavaBlockchainService } from './blockchain/kava-blockchain.service';
|
|||
PoolAccountRepository,
|
||||
UserWalletRepository,
|
||||
RegionRepository,
|
||||
BlockchainRepository,
|
||||
OutboxRepository,
|
||||
ProcessedEventRepository,
|
||||
// Services
|
||||
KafkaProducerService,
|
||||
KavaBlockchainService,
|
||||
RedisService,
|
||||
ClientsModule,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue