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 // 省级公司账户
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 手续费配置
|
// 手续费配置
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 { 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,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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