From 58feec255d70c942669e4756e6e8a6cc94b1fbc2 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 28 Jan 2026 17:56:15 -0800 Subject: [PATCH] =?UTF-8?q?feat(market-maker):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=81=9A=E5=B8=82=E5=95=86=E5=8C=BA=E5=9D=97=E9=93=BE=E5=85=85?= =?UTF-8?q?=E6=8F=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 mining-blockchain-service 支持 eUSDT/fUSDT 转账 - 添加 trading-service 区块链提现 API(自动回滚失败交易) - 前端支持中心化和区块链充提两种模式(Tab切换) - 区块链充值显示钱包地址和二维码 - 区块链提现支持输入目标地址直接转账 Co-Authored-By: Claude Opus 4.5 --- .../api/controllers/transfer.controller.ts | 82 +++- .../domain/services/chain-config.service.ts | 15 + .../domain/services/erc20-transfer.service.ts | 236 +++++++++++ .../controllers/market-maker.controller.ts | 85 +++- .../services/market-maker.service.ts | 2 + .../blockchain/blockchain.client.ts | 90 ++++ frontend/mining-admin-web/package-lock.json | 11 + frontend/mining-admin-web/package.json | 1 + .../src/app/(dashboard)/market-maker/page.tsx | 387 ++++++++++++++---- .../market-maker/api/market-maker.api.ts | 27 ++ .../market-maker/hooks/use-market-maker.ts | 45 ++ 11 files changed, 886 insertions(+), 95 deletions(-) diff --git a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts index 605f35a2..6d85126f 100644 --- a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts +++ b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts @@ -1,7 +1,7 @@ -import { Controller, Post, Body, Get } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiProperty } from '@nestjs/swagger'; +import { Controller, Post, Body, Get, Param } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiProperty, ApiParam } from '@nestjs/swagger'; import { IsString, IsNotEmpty, Matches, IsNumberString } from 'class-validator'; -import { Erc20TransferService, TransferResult } from '@/domain/services/erc20-transfer.service'; +import { Erc20TransferService, TransferResult, TokenType } from '@/domain/services/erc20-transfer.service'; import { ChainTypeEnum } from '@/domain/enums'; /** @@ -111,4 +111,80 @@ export class TransferController { hotWalletAddress, }; } + + // ============ eUSDT (积分股) 转账接口 ============ + + @Post('eusdt') + @ApiOperation({ summary: '转账 eUSDT(积分股)到指定地址' }) + @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) + @ApiResponse({ status: 400, description: '参数错误' }) + @ApiResponse({ status: 500, description: '转账失败' }) + async transferEusdt(@Body() dto: TransferDusdtDto): Promise { + const result: TransferResult = await this.erc20TransferService.transferToken( + ChainTypeEnum.KAVA, + 'EUSDT', + dto.toAddress, + dto.amount, + ); + + return { + success: result.success, + txHash: result.txHash, + error: result.error, + gasUsed: result.gasUsed, + blockNumber: result.blockNumber, + }; + } + + @Get('eusdt/balance') + @ApiOperation({ summary: '查询热钱包 eUSDT(积分股)余额' }) + @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) + async getEusdtBalance(): Promise { + const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); + const balance = await this.erc20TransferService.getTokenBalance(ChainTypeEnum.KAVA, 'EUSDT'); + + return { + address: address || '', + balance, + chain: 'KAVA', + }; + } + + // ============ fUSDT (积分值) 转账接口 ============ + + @Post('fusdt') + @ApiOperation({ summary: '转账 fUSDT(积分值)到指定地址' }) + @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) + @ApiResponse({ status: 400, description: '参数错误' }) + @ApiResponse({ status: 500, description: '转账失败' }) + async transferFusdt(@Body() dto: TransferDusdtDto): Promise { + const result: TransferResult = await this.erc20TransferService.transferToken( + ChainTypeEnum.KAVA, + 'FUSDT', + dto.toAddress, + dto.amount, + ); + + return { + success: result.success, + txHash: result.txHash, + error: result.error, + gasUsed: result.gasUsed, + blockNumber: result.blockNumber, + }; + } + + @Get('fusdt/balance') + @ApiOperation({ summary: '查询热钱包 fUSDT(积分值)余额' }) + @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) + async getFusdtBalance(): Promise { + const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); + const balance = await this.erc20TransferService.getTokenBalance(ChainTypeEnum.KAVA, 'FUSDT'); + + return { + address: address || '', + balance, + chain: 'KAVA', + }; + } } diff --git a/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts b/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts index 3c303a81..04444354 100644 --- a/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts +++ b/backend/services/mining-blockchain-service/src/domain/services/chain-config.service.ts @@ -8,6 +8,8 @@ export interface ChainConfig { chainId: number; rpcUrl: string; usdtContract: string; + eUsdtContract: string; // 积分股代币 (Energy USDT) + fUsdtContract: string; // 积分值代币 (Future USDT) nativeSymbol: string; blockTime: number; // 平均出块时间(秒) isTestnet: boolean; @@ -47,6 +49,16 @@ export class ChainConfigService { 'blockchain.kava.usdtContract', this.isTestnet ? '0x0000000000000000000000000000000000000000' : '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3', ), + // eUSDT (积分股) 合约地址 - Energy USDT + eUsdtContract: this.configService.get( + 'blockchain.kava.eUsdtContract', + '0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931', + ), + // fUSDT (积分值) 合约地址 - Future USDT + fUsdtContract: this.configService.get( + 'blockchain.kava.fUsdtContract', + '0x14dc4f7d3E4197438d058C3D156dd9826A161134', + ), nativeSymbol: 'KAVA', blockTime: 6, isTestnet: this.isTestnet, @@ -65,6 +77,9 @@ export class ChainConfigService { 'blockchain.bsc.usdtContract', this.isTestnet ? '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd' : '0x55d398326f99059fF775485246999027B3197955', ), + // BSC 不支持 eUSDT/fUSDT,使用空地址占位 + eUsdtContract: '', + fUsdtContract: '', nativeSymbol: 'BNB', blockTime: 3, isTestnet: this.isTestnet, diff --git a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts index 51cd37ba..955d186c 100644 --- a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts +++ b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts @@ -29,6 +29,9 @@ export interface TransferResult { blockNumber?: number; } +// 支持的代币类型 +export type TokenType = 'DUSDT' | 'EUSDT' | 'FUSDT'; + // MPC 签名客户端接口(避免循环依赖) export interface IMpcSigningClient { isConfigured(): boolean; @@ -316,6 +319,239 @@ export class Erc20TransferService { } } + /** + * 获取指定代币的合约地址 + */ + private getTokenContract(chainType: ChainTypeEnum, tokenType: TokenType): string { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + switch (tokenType) { + case 'DUSDT': + return config.usdtContract; + case 'EUSDT': + return config.eUsdtContract; + case 'FUSDT': + return config.fUsdtContract; + default: + throw new Error(`Unsupported token type: ${tokenType}`); + } + } + + /** + * 获取热钱包指定代币余额 + */ + async getTokenBalance(chainType: ChainTypeEnum, tokenType: TokenType): Promise { + const provider = this.providers.get(chainType); + if (!provider) { + throw new Error(`Provider not configured for chain: ${chainType}`); + } + + if (!this.hotWalletAddress) { + throw new Error('Hot wallet address not configured'); + } + + const contractAddress = this.getTokenContract(chainType, tokenType); + if (!contractAddress) { + throw new Error(`Token ${tokenType} not configured for chain ${chainType}`); + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + const balance = await contract.balanceOf(this.hotWalletAddress); + const decimals = await contract.decimals(); + + return formatUnits(balance, decimals); + } + + /** + * 通用 ERC20 代币转账(使用 MPC 签名) + * + * @param chainType 链类型 (KAVA, BSC) + * @param tokenType 代币类型 (DUSDT, EUSDT, FUSDT) + * @param toAddress 接收地址 + * @param amount 转账金额 (人类可读格式,如 "100.5") + * @returns 转账结果 + */ + async transferToken( + chainType: ChainTypeEnum, + tokenType: TokenType, + toAddress: string, + amount: string, + ): Promise { + const tokenName = tokenType === 'EUSDT' ? '积分股' : tokenType === 'FUSDT' ? '积分值' : 'dUSDT'; + this.logger.log(`[TRANSFER] Starting ${tokenType} (${tokenName}) transfer with MPC signing`); + this.logger.log(`[TRANSFER] Chain: ${chainType}`); + this.logger.log(`[TRANSFER] To: ${toAddress}`); + this.logger.log(`[TRANSFER] Amount: ${amount} ${tokenType}`); + + const provider = this.providers.get(chainType); + if (!provider) { + const error = `Provider not configured for chain: ${chainType}`; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.mpcSigningClient || !this.mpcSigningClient.isConfigured()) { + const error = 'MPC signing client not configured'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.hotWalletAddress) { + const error = 'Hot wallet address not configured'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + try { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const contractAddress = this.getTokenContract(chainType, tokenType); + + if (!contractAddress) { + const error = `Token ${tokenType} not configured for chain ${chainType}`; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + + // 获取代币精度 + const decimals = await contract.decimals(); + this.logger.log(`[TRANSFER] Token decimals: ${decimals}`); + + // 转换金额 + const amountInWei = parseUnits(amount, decimals); + this.logger.log(`[TRANSFER] Amount in wei: ${amountInWei.toString()}`); + + // 检查余额 + const balance = await contract.balanceOf(this.hotWalletAddress); + this.logger.log(`[TRANSFER] Hot wallet balance: ${formatUnits(balance, decimals)} ${tokenType}`); + + if (balance < amountInWei) { + const error = `Insufficient ${tokenType} balance in hot wallet`; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, error }; + } + + // 构建交易 + this.logger.log(`[TRANSFER] Building transaction...`); + const nonce = await provider.getTransactionCount(this.hotWalletAddress); + const feeData = await provider.getFeeData(); + + // ERC20 transfer 的 calldata + const transferData = contract.interface.encodeFunctionData('transfer', [toAddress, amountInWei]); + + // 估算 gas + const gasEstimate = await provider.estimateGas({ + from: this.hotWalletAddress, + to: contractAddress, + data: transferData, + }); + + const gasLimit = gasEstimate * BigInt(120) / BigInt(100); // 增加 20% buffer + + // 检测链是否支持 EIP-1559 + const supportsEip1559 = feeData.maxFeePerGas && feeData.maxFeePerGas > BigInt(0); + + let tx: Transaction; + if (supportsEip1559) { + tx = Transaction.from({ + type: 2, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + }); + } else { + const gasPrice = feeData.gasPrice || BigInt(1000000000); + tx = Transaction.from({ + type: 0, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + gasPrice, + }); + } + + this.logger.log(`[TRANSFER] Transaction built: nonce=${nonce}, gasLimit=${tx.gasLimit}`); + + // 获取交易哈希用于签名 + const unsignedTxHash = tx.unsignedHash; + this.logger.log(`[TRANSFER] Unsigned tx hash: ${unsignedTxHash}`); + + // 使用 MPC 签名 + this.logger.log(`[TRANSFER] Requesting MPC signature...`); + const signatureHex = await this.mpcSigningClient.signMessage(unsignedTxHash); + this.logger.log(`[TRANSFER] MPC signature obtained: ${signatureHex.slice(0, 20)}...`); + + // 解析签名 + const normalizedSig = signatureHex.startsWith('0x') ? signatureHex : `0x${signatureHex}`; + const sigBytes = normalizedSig.slice(2); + const r = `0x${sigBytes.slice(0, 64)}`; + const s = `0x${sigBytes.slice(64, 128)}`; + + // 尝试 yParity 0 和 1 来找到正确的 recovery id + let signature: Signature | null = null; + for (const yParity of [0, 1] as const) { + try { + const testSig = Signature.from({ r, s, yParity }); + const recoveredAddress = recoverAddress(unsignedTxHash, testSig); + + if (recoveredAddress.toLowerCase() === this.hotWalletAddress.toLowerCase()) { + this.logger.log(`[TRANSFER] Found correct yParity: ${yParity}`); + signature = testSig; + break; + } + } catch (e) { + this.logger.debug(`[TRANSFER] yParity=${yParity} failed: ${e}`); + } + } + + if (!signature) { + throw new Error('Failed to recover correct signature - address mismatch'); + } + + // 创建已签名交易 + const signedTx = tx.clone(); + signedTx.signature = signature; + + // 广播交易 + this.logger.log(`[TRANSFER] Broadcasting transaction...`); + const txResponse = await provider.broadcastTransaction(signedTx.serialized); + this.logger.log(`[TRANSFER] Transaction sent: ${txResponse.hash}`); + + // 等待确认 + this.logger.log(`[TRANSFER] Waiting for confirmation...`); + const receipt = await txResponse.wait(); + + if (receipt && receipt.status === 1) { + this.logger.log(`[TRANSFER] Transaction confirmed!`); + this.logger.log(`[TRANSFER] Block: ${receipt.blockNumber}`); + this.logger.log(`[TRANSFER] Gas used: ${receipt.gasUsed.toString()}`); + + return { + success: true, + txHash: txResponse.hash, + gasUsed: receipt.gasUsed.toString(), + blockNumber: receipt.blockNumber, + }; + } else { + const error = 'Transaction failed (reverted)'; + this.logger.error(`[TRANSFER] ${error}`); + return { success: false, txHash: txResponse.hash, error }; + } + } catch (error: any) { + this.logger.error(`[TRANSFER] Transfer failed:`, error); + return { + success: false, + error: error.message || 'Unknown error during transfer', + }; + } + } + /** * 检查热钱包是否已配置 */ diff --git a/backend/services/trading-service/src/api/controllers/market-maker.controller.ts b/backend/services/trading-service/src/api/controllers/market-maker.controller.ts index 42682da6..f394cbc9 100644 --- a/backend/services/trading-service/src/api/controllers/market-maker.controller.ts +++ b/backend/services/trading-service/src/api/controllers/market-maker.controller.ts @@ -9,8 +9,9 @@ import { HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; -import { IsString, IsOptional, IsNumber } from 'class-validator'; +import { IsString, IsOptional, IsNumber, Matches } from 'class-validator'; import { MarketMakerService, LedgerType, AssetType } from '../../application/services/market-maker.service'; +import { BlockchainClient } from '../../infrastructure/blockchain/blockchain.client'; import { Public } from '../../shared/guards/jwt-auth.guard'; // DTO 定义 @@ -123,10 +124,23 @@ class UpdateMakerConfigDto { refreshIntervalMs?: number; } +// 区块链提现 DTO +class BlockchainWithdrawDto { + @IsString() + @Matches(/^0x[a-fA-F0-9]{40}$/, { message: 'Invalid EVM address format' }) + toAddress: string; + + @IsString() + amount: string; +} + @ApiTags('Market Maker') @Controller('admin/market-maker') export class MarketMakerController { - constructor(private readonly marketMakerService: MarketMakerService) {} + constructor( + private readonly marketMakerService: MarketMakerService, + private readonly blockchainClient: BlockchainClient, + ) {} @Post('initialize') @Public() // TODO: 生产环境应添加管理员权限验证 @@ -195,6 +209,7 @@ export class MarketMakerController { priceStrategy: config.priceStrategy, discountRate: config.discountRate.toString(), isActive: config.isActive, + kavaWalletAddress: config.kavaWalletAddress, }, runningStatus, }; @@ -308,6 +323,72 @@ export class MarketMakerController { }; } + // ============ 区块链提现接口 ============ + + @Post(':name/blockchain-withdraw-cash') + @Public() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '区块链提现积分值(fUSDT)' }) + @ApiResponse({ status: 200, description: '区块链提现结果' }) + async blockchainWithdrawCash(@Param('name') name: string, @Body() dto: BlockchainWithdrawDto) { + // 1. 先从做市商账户扣款 + await this.marketMakerService.withdraw(name, dto.amount, `区块链提现到 ${dto.toAddress.slice(0, 10)}...`); + + // 2. 调用区块链服务执行转账 + const result = await this.blockchainClient.transferFusdt(dto.toAddress, dto.amount); + + if (!result.success) { + // 如果链上转账失败,需要回滚(充值回去) + await this.marketMakerService.deposit(name, dto.amount, `区块链提现失败回滚: ${result.error}`); + return { + success: false, + error: result.error, + message: '区块链转账失败,已回滚', + }; + } + + const config = await this.marketMakerService.getConfig(name); + return { + success: true, + message: `区块链提现成功: ${dto.amount} fUSDT`, + txHash: result.txHash, + blockNumber: result.blockNumber, + newBalance: config?.cashBalance.toString(), + }; + } + + @Post(':name/blockchain-withdraw-shares') + @Public() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '区块链提现积分股(eUSDT)' }) + @ApiResponse({ status: 200, description: '区块链提现结果' }) + async blockchainWithdrawShares(@Param('name') name: string, @Body() dto: BlockchainWithdrawDto) { + // 1. 先从做市商账户扣款 + await this.marketMakerService.withdrawShares(name, dto.amount, `区块链提现到 ${dto.toAddress.slice(0, 10)}...`); + + // 2. 调用区块链服务执行转账 + const result = await this.blockchainClient.transferEusdt(dto.toAddress, dto.amount); + + if (!result.success) { + // 如果链上转账失败,需要回滚(充值回去) + await this.marketMakerService.depositShares(name, dto.amount, `区块链提现失败回滚: ${result.error}`); + return { + success: false, + error: result.error, + message: '区块链转账失败,已回滚', + }; + } + + const config = await this.marketMakerService.getConfig(name); + return { + success: true, + message: `区块链提现成功: ${dto.amount} eUSDT`, + txHash: result.txHash, + blockNumber: result.blockNumber, + newBalance: config?.shareBalance.toString(), + }; + } + @Post(':name/start') @Public() @HttpCode(HttpStatus.OK) diff --git a/backend/services/trading-service/src/application/services/market-maker.service.ts b/backend/services/trading-service/src/application/services/market-maker.service.ts index 29336331..359ce859 100644 --- a/backend/services/trading-service/src/application/services/market-maker.service.ts +++ b/backend/services/trading-service/src/application/services/market-maker.service.ts @@ -36,6 +36,7 @@ export interface MarketMakerConfig { priceStrategy: string; discountRate: Decimal; isActive: boolean; + kavaWalletAddress: string | null; } export enum LedgerType { @@ -102,6 +103,7 @@ export class MarketMakerService { priceStrategy: config.priceStrategy, discountRate: new Decimal(config.discountRate.toString()), isActive: config.isActive, + kavaWalletAddress: config.kavaWalletAddress, }; } diff --git a/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts b/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts index 3e84886b..7b87f790 100644 --- a/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts +++ b/backend/services/trading-service/src/infrastructure/blockchain/blockchain.client.ts @@ -108,4 +108,94 @@ export class BlockchainClient { const status = await this.getStatus(); return status?.configured ?? false; } + + // ============ eUSDT (积分股) 接口 ============ + + /** + * 转账 eUSDT(积分股)到指定地址 + * @param toAddress 接收地址 + * @param amount 金额(人类可读格式) + */ + async transferEusdt(toAddress: string, amount: string): Promise { + this.logger.log(`[TRANSFER-EUSDT] Calling mining-blockchain-service: to=${toAddress}, amount=${amount}`); + + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.post(`${this.baseUrl}/api/v1/transfer/eusdt`, { + toAddress, + amount, + }), + ); + + this.logger.log(`[TRANSFER-EUSDT] Response: ${JSON.stringify(response.data)}`); + return response.data; + } catch (error: any) { + const errorMessage = error.response?.data?.message || error.message || 'Unknown error'; + this.logger.error(`[TRANSFER-EUSDT] Failed: ${errorMessage}`); + return { + success: false, + error: errorMessage, + }; + } + } + + /** + * 查询热钱包 eUSDT(积分股)余额 + */ + async getEusdtBalance(): Promise { + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/api/v1/transfer/eusdt/balance`), + ); + return response.data; + } catch (error: any) { + this.logger.error(`[BALANCE-EUSDT] Failed to get balance: ${error.message}`); + return null; + } + } + + // ============ fUSDT (积分值) 接口 ============ + + /** + * 转账 fUSDT(积分值)到指定地址 + * @param toAddress 接收地址 + * @param amount 金额(人类可读格式) + */ + async transferFusdt(toAddress: string, amount: string): Promise { + this.logger.log(`[TRANSFER-FUSDT] Calling mining-blockchain-service: to=${toAddress}, amount=${amount}`); + + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.post(`${this.baseUrl}/api/v1/transfer/fusdt`, { + toAddress, + amount, + }), + ); + + this.logger.log(`[TRANSFER-FUSDT] Response: ${JSON.stringify(response.data)}`); + return response.data; + } catch (error: any) { + const errorMessage = error.response?.data?.message || error.message || 'Unknown error'; + this.logger.error(`[TRANSFER-FUSDT] Failed: ${errorMessage}`); + return { + success: false, + error: errorMessage, + }; + } + } + + /** + * 查询热钱包 fUSDT(积分值)余额 + */ + async getFusdtBalance(): Promise { + try { + const response: AxiosResponse = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/api/v1/transfer/fusdt/balance`), + ); + return response.data; + } catch (error: any) { + this.logger.error(`[BALANCE-FUSDT] Failed to get balance: ${error.message}`); + return null; + } + } } diff --git a/frontend/mining-admin-web/package-lock.json b/frontend/mining-admin-web/package-lock.json index 030ec15b..62c6c05d 100644 --- a/frontend/mining-admin-web/package-lock.json +++ b/frontend/mining-admin-web/package-lock.json @@ -32,6 +32,7 @@ "echarts-for-react": "^3.0.2", "lucide-react": "^0.344.0", "next": "14.1.0", + "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.50.1", @@ -4221,6 +4222,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6591,6 +6593,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/frontend/mining-admin-web/package.json b/frontend/mining-admin-web/package.json index c829a8b0..fd2c5901 100644 --- a/frontend/mining-admin-web/package.json +++ b/frontend/mining-admin-web/package.json @@ -34,6 +34,7 @@ "echarts-for-react": "^3.0.2", "lucide-react": "^0.344.0", "next": "14.1.0", + "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.50.1", diff --git a/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx index f963486e..dfc715a9 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/market-maker/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { QRCodeSVG } from 'qrcode.react'; import { PageHeader } from '@/components/layout/page-header'; import { useMarketMakerConfig, @@ -9,6 +10,8 @@ import { useWithdrawCash, useDepositShares, useWithdrawShares, + useBlockchainWithdrawCash, + useBlockchainWithdrawShares, useStartTaker, useStopTaker, useTakeOrder, @@ -51,6 +54,8 @@ import { Zap, PlusCircle, MinusCircle, + Copy, + Check, } from 'lucide-react'; export default function MarketMakerPage() { @@ -65,6 +70,8 @@ export default function MarketMakerPage() { const withdrawCashMutation = useWithdrawCash(); const depositSharesMutation = useDepositShares(); const withdrawSharesMutation = useWithdrawShares(); + const blockchainWithdrawCashMutation = useBlockchainWithdrawCash(); + const blockchainWithdrawSharesMutation = useBlockchainWithdrawShares(); const startTakerMutation = useStartTaker(); const stopTakerMutation = useStopTaker(); const takeOrderMutation = useTakeOrder(); @@ -79,6 +86,18 @@ export default function MarketMakerPage() { const [withdrawCashAmount, setWithdrawCashAmount] = useState(''); const [depositSharesAmount, setDepositSharesAmount] = useState(''); const [withdrawSharesAmount, setWithdrawSharesAmount] = useState(''); + const [copiedAddress, setCopiedAddress] = useState(false); + // 区块链提现 + const [blockchainWithdrawCashAddress, setBlockchainWithdrawCashAddress] = useState(''); + const [blockchainWithdrawCashAmount, setBlockchainWithdrawCashAmount] = useState(''); + const [blockchainWithdrawSharesAddress, setBlockchainWithdrawSharesAddress] = useState(''); + const [blockchainWithdrawSharesAmount, setBlockchainWithdrawSharesAmount] = useState(''); + + const handleCopyAddress = async (address: string) => { + await navigator.clipboard.writeText(address); + setCopiedAddress(true); + setTimeout(() => setCopiedAddress(false), 2000); + }; const config = configData?.config; @@ -173,31 +192,74 @@ export default function MarketMakerPage() { 充值 - + - 充值现金 - 向做市商账户充值积分值 + 充值现金(积分值) + 选择充值方式向做市商账户充值 -
- - setDepositCashAmount(e.target.value)} - placeholder="请输入金额" - /> -
- - - + + + 中心化充值 + 区块链充值 + + +
+ + setDepositCashAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ +
+ +
+ 向以下地址转入 fUSDT (积分值代币) +
+ {config.kavaWalletAddress ? ( +
+
+ +
+
+ +
+ + {config.kavaWalletAddress} + + +
+
+
+ + 转账后系统将自动检测并入账(约需12个区块确认) +
+
+ ) : ( +
+ +

做市商钱包地址未配置

+
+ )} +
+
@@ -208,31 +270,82 @@ export default function MarketMakerPage() { 提现 - + - 提现现金 - 从做市商账户提取积分值 + 提现现金(积分值) + 选择提现方式从做市商账户提取 -
- - setWithdrawCashAmount(e.target.value)} - placeholder="请输入金额" - /> -
- - - + + + 中心化提现 + 区块链提现 + + +
+ + setWithdrawCashAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ +
+ +
+ 转账 fUSDT(积分值代币)到指定地址 +
+
+ + setBlockchainWithdrawCashAddress(e.target.value)} + placeholder="0x..." + /> +
+
+ + setBlockchainWithdrawCashAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ +
+ + 区块链提现将从做市商钱包直接转账到目标地址 +
+
+
@@ -275,31 +388,74 @@ export default function MarketMakerPage() { 充值 - + 充值积分股 - 向做市商账户充值积分股(用于挂卖单) + 选择充值方式向做市商账户充值积分股 -
- - setDepositSharesAmount(e.target.value)} - placeholder="请输入数量" - /> -
- - - + + + 中心化充值 + 区块链充值 + + +
+ + setDepositSharesAmount(e.target.value)} + placeholder="请输入数量" + /> +
+ +
+ +
+ 向以下地址转入 eUSDT (积分股代币) +
+ {config.kavaWalletAddress ? ( +
+
+ +
+
+ +
+ + {config.kavaWalletAddress} + + +
+
+
+ + 转账后系统将自动检测并入账(约需12个区块确认) +
+
+ ) : ( +
+ +

做市商钱包地址未配置

+
+ )} +
+
@@ -310,31 +466,82 @@ export default function MarketMakerPage() { 提取 - + 提取积分股 - 从做市商账户提取积分股 + 选择提现方式从做市商账户提取 -
- - setWithdrawSharesAmount(e.target.value)} - placeholder="请输入数量" - /> -
- - - + + + 中心化提取 + 区块链提现 + + +
+ + setWithdrawSharesAmount(e.target.value)} + placeholder="请输入数量" + /> +
+ +
+ +
+ 转账 eUSDT(积分股代币)到指定地址 +
+
+ + setBlockchainWithdrawSharesAddress(e.target.value)} + placeholder="0x..." + /> +
+
+ + setBlockchainWithdrawSharesAmount(e.target.value)} + placeholder="请输入数量" + /> +
+ +
+ + 区块链提现将从做市商钱包直接转账到目标地址 +
+
+
diff --git a/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts b/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts index 94c8aef2..22cb2b65 100644 --- a/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts +++ b/frontend/mining-admin-web/src/features/market-maker/api/market-maker.api.ts @@ -78,6 +78,7 @@ export interface MarketMakerConfig { askQuantityPerLevel?: string; refreshIntervalMs?: number; lastRefreshAt?: string; + kavaWalletAddress?: string; } export interface MarketMakerOrder { @@ -170,6 +171,32 @@ export const marketMakerApi = { return response.data; }, + // 区块链提现积分值(fUSDT) + blockchainWithdrawCash: async (name: string, toAddress: string, amount: string): Promise<{ + success: boolean; + message: string; + txHash?: string; + blockNumber?: number; + newBalance?: string; + error?: string; + }> => { + const response = await tradingClient.post(`/admin/market-maker/${name}/blockchain-withdraw-cash`, { toAddress, amount }); + return response.data; + }, + + // 区块链提现积分股(eUSDT) + blockchainWithdrawShares: async (name: string, toAddress: string, amount: string): Promise<{ + success: boolean; + message: string; + txHash?: string; + blockNumber?: number; + newBalance?: string; + error?: string; + }> => { + const response = await tradingClient.post(`/admin/market-maker/${name}/blockchain-withdraw-shares`, { toAddress, amount }); + return response.data; + }, + // 启动吃单模式 start: async (name: string): Promise<{ success: boolean; message: string }> => { const response = await tradingClient.post(`/admin/market-maker/${name}/start`); diff --git a/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts b/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts index 098ab734..d564c095 100644 --- a/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts +++ b/frontend/mining-admin-web/src/features/market-maker/hooks/use-market-maker.ts @@ -110,6 +110,51 @@ export function useWithdrawShares() { }); } +// 区块链提现 +export function useBlockchainWithdrawCash() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ toAddress, amount }: { toAddress: string; amount: string }) => + marketMakerApi.blockchainWithdrawCash(MARKET_MAKER_NAME, toAddress, amount), + onSuccess: (data) => { + if (data.success) { + toast({ + title: '区块链提现成功', + description: `交易哈希: ${data.txHash?.slice(0, 20)}...`, + }); + } else { + toast({ title: '提现失败', description: data.error || data.message, variant: 'destructive' }); + } + queryClient.invalidateQueries({ queryKey: ['marketMaker'] }); + }, + onError: (error: any) => { + toast({ title: '错误', description: error.response?.data?.message || '区块链提现失败', variant: 'destructive' }); + }, + }); +} + +export function useBlockchainWithdrawShares() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ toAddress, amount }: { toAddress: string; amount: string }) => + marketMakerApi.blockchainWithdrawShares(MARKET_MAKER_NAME, toAddress, amount), + onSuccess: (data) => { + if (data.success) { + toast({ + title: '区块链提现成功', + description: `交易哈希: ${data.txHash?.slice(0, 20)}...`, + }); + } else { + toast({ title: '提现失败', description: data.error || data.message, variant: 'destructive' }); + } + queryClient.invalidateQueries({ queryKey: ['marketMaker'] }); + }, + onError: (error: any) => { + toast({ title: '错误', description: error.response?.data?.message || '区块链提现失败', variant: 'destructive' }); + }, + }); +} + export function useStartTaker() { const queryClient = useQueryClient(); return useMutation({