feat(market-maker): 实现做市商区块链充提功能
- 扩展 mining-blockchain-service 支持 eUSDT/fUSDT 转账 - 添加 trading-service 区块链提现 API(自动回滚失败交易) - 前端支持中心化和区块链充提两种模式(Tab切换) - 区块链充值显示钱包地址和二维码 - 区块链提现支持输入目标地址直接转账 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
94f9e7d5b5
commit
58feec255d
|
|
@ -1,7 +1,7 @@
|
||||||
import { Controller, Post, Body, Get } from '@nestjs/common';
|
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiProperty } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiProperty, ApiParam } from '@nestjs/swagger';
|
||||||
import { IsString, IsNotEmpty, Matches, IsNumberString } from 'class-validator';
|
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';
|
import { ChainTypeEnum } from '@/domain/enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,4 +111,80 @@ export class TransferController {
|
||||||
hotWalletAddress,
|
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<TransferResponseDto> {
|
||||||
|
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<BalanceResponseDto> {
|
||||||
|
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<TransferResponseDto> {
|
||||||
|
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<BalanceResponseDto> {
|
||||||
|
const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA);
|
||||||
|
const balance = await this.erc20TransferService.getTokenBalance(ChainTypeEnum.KAVA, 'FUSDT');
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: address || '',
|
||||||
|
balance,
|
||||||
|
chain: 'KAVA',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export interface ChainConfig {
|
||||||
chainId: number;
|
chainId: number;
|
||||||
rpcUrl: string;
|
rpcUrl: string;
|
||||||
usdtContract: string;
|
usdtContract: string;
|
||||||
|
eUsdtContract: string; // 积分股代币 (Energy USDT)
|
||||||
|
fUsdtContract: string; // 积分值代币 (Future USDT)
|
||||||
nativeSymbol: string;
|
nativeSymbol: string;
|
||||||
blockTime: number; // 平均出块时间(秒)
|
blockTime: number; // 平均出块时间(秒)
|
||||||
isTestnet: boolean;
|
isTestnet: boolean;
|
||||||
|
|
@ -47,6 +49,16 @@ export class ChainConfigService {
|
||||||
'blockchain.kava.usdtContract',
|
'blockchain.kava.usdtContract',
|
||||||
this.isTestnet ? '0x0000000000000000000000000000000000000000' : '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3',
|
this.isTestnet ? '0x0000000000000000000000000000000000000000' : '0xA9F3A35dBa8699c8C681D8db03F0c1A8CEB9D7c3',
|
||||||
),
|
),
|
||||||
|
// eUSDT (积分股) 合约地址 - Energy USDT
|
||||||
|
eUsdtContract: this.configService.get<string>(
|
||||||
|
'blockchain.kava.eUsdtContract',
|
||||||
|
'0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931',
|
||||||
|
),
|
||||||
|
// fUSDT (积分值) 合约地址 - Future USDT
|
||||||
|
fUsdtContract: this.configService.get<string>(
|
||||||
|
'blockchain.kava.fUsdtContract',
|
||||||
|
'0x14dc4f7d3E4197438d058C3D156dd9826A161134',
|
||||||
|
),
|
||||||
nativeSymbol: 'KAVA',
|
nativeSymbol: 'KAVA',
|
||||||
blockTime: 6,
|
blockTime: 6,
|
||||||
isTestnet: this.isTestnet,
|
isTestnet: this.isTestnet,
|
||||||
|
|
@ -65,6 +77,9 @@ export class ChainConfigService {
|
||||||
'blockchain.bsc.usdtContract',
|
'blockchain.bsc.usdtContract',
|
||||||
this.isTestnet ? '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd' : '0x55d398326f99059fF775485246999027B3197955',
|
this.isTestnet ? '0x337610d27c682E347C9cD60BD4b3b107C9d34dDd' : '0x55d398326f99059fF775485246999027B3197955',
|
||||||
),
|
),
|
||||||
|
// BSC 不支持 eUSDT/fUSDT,使用空地址占位
|
||||||
|
eUsdtContract: '',
|
||||||
|
fUsdtContract: '',
|
||||||
nativeSymbol: 'BNB',
|
nativeSymbol: 'BNB',
|
||||||
blockTime: 3,
|
blockTime: 3,
|
||||||
isTestnet: this.isTestnet,
|
isTestnet: this.isTestnet,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ export interface TransferResult {
|
||||||
blockNumber?: number;
|
blockNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支持的代币类型
|
||||||
|
export type TokenType = 'DUSDT' | 'EUSDT' | 'FUSDT';
|
||||||
|
|
||||||
// MPC 签名客户端接口(避免循环依赖)
|
// MPC 签名客户端接口(避免循环依赖)
|
||||||
export interface IMpcSigningClient {
|
export interface IMpcSigningClient {
|
||||||
isConfigured(): boolean;
|
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<string> {
|
||||||
|
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<TransferResult> {
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查热钱包是否已配置
|
* 检查热钱包是否已配置
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ import {
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
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 { MarketMakerService, LedgerType, AssetType } from '../../application/services/market-maker.service';
|
||||||
|
import { BlockchainClient } from '../../infrastructure/blockchain/blockchain.client';
|
||||||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||||
|
|
||||||
// DTO 定义
|
// DTO 定义
|
||||||
|
|
@ -123,10 +124,23 @@ class UpdateMakerConfigDto {
|
||||||
refreshIntervalMs?: number;
|
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')
|
@ApiTags('Market Maker')
|
||||||
@Controller('admin/market-maker')
|
@Controller('admin/market-maker')
|
||||||
export class MarketMakerController {
|
export class MarketMakerController {
|
||||||
constructor(private readonly marketMakerService: MarketMakerService) {}
|
constructor(
|
||||||
|
private readonly marketMakerService: MarketMakerService,
|
||||||
|
private readonly blockchainClient: BlockchainClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('initialize')
|
@Post('initialize')
|
||||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||||
|
|
@ -195,6 +209,7 @@ export class MarketMakerController {
|
||||||
priceStrategy: config.priceStrategy,
|
priceStrategy: config.priceStrategy,
|
||||||
discountRate: config.discountRate.toString(),
|
discountRate: config.discountRate.toString(),
|
||||||
isActive: config.isActive,
|
isActive: config.isActive,
|
||||||
|
kavaWalletAddress: config.kavaWalletAddress,
|
||||||
},
|
},
|
||||||
runningStatus,
|
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')
|
@Post(':name/start')
|
||||||
@Public()
|
@Public()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export interface MarketMakerConfig {
|
||||||
priceStrategy: string;
|
priceStrategy: string;
|
||||||
discountRate: Decimal;
|
discountRate: Decimal;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
kavaWalletAddress: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LedgerType {
|
export enum LedgerType {
|
||||||
|
|
@ -102,6 +103,7 @@ export class MarketMakerService {
|
||||||
priceStrategy: config.priceStrategy,
|
priceStrategy: config.priceStrategy,
|
||||||
discountRate: new Decimal(config.discountRate.toString()),
|
discountRate: new Decimal(config.discountRate.toString()),
|
||||||
isActive: config.isActive,
|
isActive: config.isActive,
|
||||||
|
kavaWalletAddress: config.kavaWalletAddress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,4 +108,94 @@ export class BlockchainClient {
|
||||||
const status = await this.getStatus();
|
const status = await this.getStatus();
|
||||||
return status?.configured ?? false;
|
return status?.configured ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ eUSDT (积分股) 接口 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转账 eUSDT(积分股)到指定地址
|
||||||
|
* @param toAddress 接收地址
|
||||||
|
* @param amount 金额(人类可读格式)
|
||||||
|
*/
|
||||||
|
async transferEusdt(toAddress: string, amount: string): Promise<TransferResult> {
|
||||||
|
this.logger.log(`[TRANSFER-EUSDT] Calling mining-blockchain-service: to=${toAddress}, amount=${amount}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<TransferResult> = await firstValueFrom(
|
||||||
|
this.httpService.post<TransferResult>(`${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<BalanceResult | null> {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<BalanceResult> = await firstValueFrom(
|
||||||
|
this.httpService.get<BalanceResult>(`${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<TransferResult> {
|
||||||
|
this.logger.log(`[TRANSFER-FUSDT] Calling mining-blockchain-service: to=${toAddress}, amount=${amount}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<TransferResult> = await firstValueFrom(
|
||||||
|
this.httpService.post<TransferResult>(`${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<BalanceResult | null> {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<BalanceResult> = await firstValueFrom(
|
||||||
|
this.httpService.get<BalanceResult>(`${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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
|
|
@ -4221,6 +4222,7 @@
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|
@ -6591,6 +6593,15 @@
|
||||||
"node": ">=6"
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import {
|
import {
|
||||||
useMarketMakerConfig,
|
useMarketMakerConfig,
|
||||||
|
|
@ -9,6 +10,8 @@ import {
|
||||||
useWithdrawCash,
|
useWithdrawCash,
|
||||||
useDepositShares,
|
useDepositShares,
|
||||||
useWithdrawShares,
|
useWithdrawShares,
|
||||||
|
useBlockchainWithdrawCash,
|
||||||
|
useBlockchainWithdrawShares,
|
||||||
useStartTaker,
|
useStartTaker,
|
||||||
useStopTaker,
|
useStopTaker,
|
||||||
useTakeOrder,
|
useTakeOrder,
|
||||||
|
|
@ -51,6 +54,8 @@ import {
|
||||||
Zap,
|
Zap,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
MinusCircle,
|
MinusCircle,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function MarketMakerPage() {
|
export default function MarketMakerPage() {
|
||||||
|
|
@ -65,6 +70,8 @@ export default function MarketMakerPage() {
|
||||||
const withdrawCashMutation = useWithdrawCash();
|
const withdrawCashMutation = useWithdrawCash();
|
||||||
const depositSharesMutation = useDepositShares();
|
const depositSharesMutation = useDepositShares();
|
||||||
const withdrawSharesMutation = useWithdrawShares();
|
const withdrawSharesMutation = useWithdrawShares();
|
||||||
|
const blockchainWithdrawCashMutation = useBlockchainWithdrawCash();
|
||||||
|
const blockchainWithdrawSharesMutation = useBlockchainWithdrawShares();
|
||||||
const startTakerMutation = useStartTaker();
|
const startTakerMutation = useStartTaker();
|
||||||
const stopTakerMutation = useStopTaker();
|
const stopTakerMutation = useStopTaker();
|
||||||
const takeOrderMutation = useTakeOrder();
|
const takeOrderMutation = useTakeOrder();
|
||||||
|
|
@ -79,6 +86,18 @@ export default function MarketMakerPage() {
|
||||||
const [withdrawCashAmount, setWithdrawCashAmount] = useState('');
|
const [withdrawCashAmount, setWithdrawCashAmount] = useState('');
|
||||||
const [depositSharesAmount, setDepositSharesAmount] = useState('');
|
const [depositSharesAmount, setDepositSharesAmount] = useState('');
|
||||||
const [withdrawSharesAmount, setWithdrawSharesAmount] = 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;
|
const config = configData?.config;
|
||||||
|
|
||||||
|
|
@ -173,31 +192,74 @@ export default function MarketMakerPage() {
|
||||||
充值
|
充值
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>充值现金</DialogTitle>
|
<DialogTitle>充值现金(积分值)</DialogTitle>
|
||||||
<DialogDescription>向做市商账户充值积分值</DialogDescription>
|
<DialogDescription>选择充值方式向做市商账户充值</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<Tabs defaultValue="centralized" className="w-full">
|
||||||
<Label>充值金额</Label>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Input
|
<TabsTrigger value="centralized">中心化充值</TabsTrigger>
|
||||||
type="number"
|
<TabsTrigger value="blockchain">区块链充值</TabsTrigger>
|
||||||
value={depositCashAmount}
|
</TabsList>
|
||||||
onChange={(e) => setDepositCashAmount(e.target.value)}
|
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||||
placeholder="请输入金额"
|
<div>
|
||||||
/>
|
<Label>充值金额</Label>
|
||||||
</div>
|
<Input
|
||||||
<DialogFooter>
|
type="number"
|
||||||
<Button
|
value={depositCashAmount}
|
||||||
onClick={() => {
|
onChange={(e) => setDepositCashAmount(e.target.value)}
|
||||||
depositCashMutation.mutate({ amount: depositCashAmount });
|
placeholder="请输入金额"
|
||||||
setDepositCashAmount('');
|
/>
|
||||||
}}
|
</div>
|
||||||
disabled={depositCashMutation.isPending || !depositCashAmount}
|
<Button
|
||||||
>
|
className="w-full"
|
||||||
{depositCashMutation.isPending ? '处理中...' : '确认充值'}
|
onClick={() => {
|
||||||
</Button>
|
depositCashMutation.mutate({ amount: depositCashAmount });
|
||||||
</DialogFooter>
|
setDepositCashAmount('');
|
||||||
|
}}
|
||||||
|
disabled={depositCashMutation.isPending || !depositCashAmount}
|
||||||
|
>
|
||||||
|
{depositCashMutation.isPending ? '处理中...' : '确认充值'}
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="blockchain" className="space-y-4 pt-4">
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
向以下地址转入 <strong>fUSDT</strong> (积分值代币)
|
||||||
|
</div>
|
||||||
|
{config.kavaWalletAddress ? (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<div className="p-4 bg-white rounded-lg">
|
||||||
|
<QRCodeSVG value={config.kavaWalletAddress} size={180} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Label className="text-xs text-muted-foreground">钱包地址 (Kava EVM)</Label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<code className="flex-1 text-xs bg-muted p-2 rounded break-all">
|
||||||
|
{config.kavaWalletAddress}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCopyAddress(config.kavaWalletAddress!)}
|
||||||
|
>
|
||||||
|
{copiedAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded w-full">
|
||||||
|
<AlertCircle className="h-3 w-3 inline mr-1" />
|
||||||
|
转账后系统将自动检测并入账(约需12个区块确认)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-4">
|
||||||
|
<AlertCircle className="h-8 w-8 mx-auto mb-2 text-yellow-500" />
|
||||||
|
<p>做市商钱包地址未配置</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -208,31 +270,82 @@ export default function MarketMakerPage() {
|
||||||
提现
|
提现
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>提现现金</DialogTitle>
|
<DialogTitle>提现现金(积分值)</DialogTitle>
|
||||||
<DialogDescription>从做市商账户提取积分值</DialogDescription>
|
<DialogDescription>选择提现方式从做市商账户提取</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<Tabs defaultValue="centralized" className="w-full">
|
||||||
<Label>提现金额</Label>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Input
|
<TabsTrigger value="centralized">中心化提现</TabsTrigger>
|
||||||
type="number"
|
<TabsTrigger value="blockchain">区块链提现</TabsTrigger>
|
||||||
value={withdrawCashAmount}
|
</TabsList>
|
||||||
onChange={(e) => setWithdrawCashAmount(e.target.value)}
|
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||||
placeholder="请输入金额"
|
<div>
|
||||||
/>
|
<Label>提现金额</Label>
|
||||||
</div>
|
<Input
|
||||||
<DialogFooter>
|
type="number"
|
||||||
<Button
|
value={withdrawCashAmount}
|
||||||
onClick={() => {
|
onChange={(e) => setWithdrawCashAmount(e.target.value)}
|
||||||
withdrawCashMutation.mutate({ amount: withdrawCashAmount });
|
placeholder="请输入金额"
|
||||||
setWithdrawCashAmount('');
|
/>
|
||||||
}}
|
</div>
|
||||||
disabled={withdrawCashMutation.isPending || !withdrawCashAmount}
|
<Button
|
||||||
>
|
className="w-full"
|
||||||
{withdrawCashMutation.isPending ? '处理中...' : '确认提现'}
|
onClick={() => {
|
||||||
</Button>
|
withdrawCashMutation.mutate({ amount: withdrawCashAmount });
|
||||||
</DialogFooter>
|
setWithdrawCashAmount('');
|
||||||
|
}}
|
||||||
|
disabled={withdrawCashMutation.isPending || !withdrawCashAmount}
|
||||||
|
>
|
||||||
|
{withdrawCashMutation.isPending ? '处理中...' : '确认提现'}
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="blockchain" className="space-y-4 pt-4">
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
转账 <strong>fUSDT</strong>(积分值代币)到指定地址
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>目标地址 (Kava EVM)</Label>
|
||||||
|
<Input
|
||||||
|
value={blockchainWithdrawCashAddress}
|
||||||
|
onChange={(e) => setBlockchainWithdrawCashAddress(e.target.value)}
|
||||||
|
placeholder="0x..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>提现金额</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={blockchainWithdrawCashAmount}
|
||||||
|
onChange={(e) => setBlockchainWithdrawCashAmount(e.target.value)}
|
||||||
|
placeholder="请输入金额"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
blockchainWithdrawCashMutation.mutate({
|
||||||
|
toAddress: blockchainWithdrawCashAddress,
|
||||||
|
amount: blockchainWithdrawCashAmount,
|
||||||
|
});
|
||||||
|
setBlockchainWithdrawCashAddress('');
|
||||||
|
setBlockchainWithdrawCashAmount('');
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
blockchainWithdrawCashMutation.isPending ||
|
||||||
|
!blockchainWithdrawCashAddress ||
|
||||||
|
!blockchainWithdrawCashAmount
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{blockchainWithdrawCashMutation.isPending ? '链上转账中...' : '确认区块链提现'}
|
||||||
|
</Button>
|
||||||
|
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded">
|
||||||
|
<AlertCircle className="h-3 w-3 inline mr-1" />
|
||||||
|
区块链提现将从做市商钱包直接转账到目标地址
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -275,31 +388,74 @@ export default function MarketMakerPage() {
|
||||||
充值
|
充值
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>充值积分股</DialogTitle>
|
<DialogTitle>充值积分股</DialogTitle>
|
||||||
<DialogDescription>向做市商账户充值积分股(用于挂卖单)</DialogDescription>
|
<DialogDescription>选择充值方式向做市商账户充值积分股</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<Tabs defaultValue="centralized" className="w-full">
|
||||||
<Label>充值数量</Label>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Input
|
<TabsTrigger value="centralized">中心化充值</TabsTrigger>
|
||||||
type="number"
|
<TabsTrigger value="blockchain">区块链充值</TabsTrigger>
|
||||||
value={depositSharesAmount}
|
</TabsList>
|
||||||
onChange={(e) => setDepositSharesAmount(e.target.value)}
|
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||||
placeholder="请输入数量"
|
<div>
|
||||||
/>
|
<Label>充值数量</Label>
|
||||||
</div>
|
<Input
|
||||||
<DialogFooter>
|
type="number"
|
||||||
<Button
|
value={depositSharesAmount}
|
||||||
onClick={() => {
|
onChange={(e) => setDepositSharesAmount(e.target.value)}
|
||||||
depositSharesMutation.mutate({ amount: depositSharesAmount });
|
placeholder="请输入数量"
|
||||||
setDepositSharesAmount('');
|
/>
|
||||||
}}
|
</div>
|
||||||
disabled={depositSharesMutation.isPending || !depositSharesAmount}
|
<Button
|
||||||
>
|
className="w-full"
|
||||||
{depositSharesMutation.isPending ? '处理中...' : '确认充值'}
|
onClick={() => {
|
||||||
</Button>
|
depositSharesMutation.mutate({ amount: depositSharesAmount });
|
||||||
</DialogFooter>
|
setDepositSharesAmount('');
|
||||||
|
}}
|
||||||
|
disabled={depositSharesMutation.isPending || !depositSharesAmount}
|
||||||
|
>
|
||||||
|
{depositSharesMutation.isPending ? '处理中...' : '确认充值'}
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="blockchain" className="space-y-4 pt-4">
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
向以下地址转入 <strong>eUSDT</strong> (积分股代币)
|
||||||
|
</div>
|
||||||
|
{config.kavaWalletAddress ? (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<div className="p-4 bg-white rounded-lg">
|
||||||
|
<QRCodeSVG value={config.kavaWalletAddress} size={180} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Label className="text-xs text-muted-foreground">钱包地址 (Kava EVM)</Label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<code className="flex-1 text-xs bg-muted p-2 rounded break-all">
|
||||||
|
{config.kavaWalletAddress}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCopyAddress(config.kavaWalletAddress!)}
|
||||||
|
>
|
||||||
|
{copiedAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded w-full">
|
||||||
|
<AlertCircle className="h-3 w-3 inline mr-1" />
|
||||||
|
转账后系统将自动检测并入账(约需12个区块确认)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-4">
|
||||||
|
<AlertCircle className="h-8 w-8 mx-auto mb-2 text-yellow-500" />
|
||||||
|
<p>做市商钱包地址未配置</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -310,31 +466,82 @@ export default function MarketMakerPage() {
|
||||||
提取
|
提取
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>提取积分股</DialogTitle>
|
<DialogTitle>提取积分股</DialogTitle>
|
||||||
<DialogDescription>从做市商账户提取积分股</DialogDescription>
|
<DialogDescription>选择提现方式从做市商账户提取</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<Tabs defaultValue="centralized" className="w-full">
|
||||||
<Label>提取数量</Label>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Input
|
<TabsTrigger value="centralized">中心化提取</TabsTrigger>
|
||||||
type="number"
|
<TabsTrigger value="blockchain">区块链提现</TabsTrigger>
|
||||||
value={withdrawSharesAmount}
|
</TabsList>
|
||||||
onChange={(e) => setWithdrawSharesAmount(e.target.value)}
|
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||||
placeholder="请输入数量"
|
<div>
|
||||||
/>
|
<Label>提取数量</Label>
|
||||||
</div>
|
<Input
|
||||||
<DialogFooter>
|
type="number"
|
||||||
<Button
|
value={withdrawSharesAmount}
|
||||||
onClick={() => {
|
onChange={(e) => setWithdrawSharesAmount(e.target.value)}
|
||||||
withdrawSharesMutation.mutate({ amount: withdrawSharesAmount });
|
placeholder="请输入数量"
|
||||||
setWithdrawSharesAmount('');
|
/>
|
||||||
}}
|
</div>
|
||||||
disabled={withdrawSharesMutation.isPending || !withdrawSharesAmount}
|
<Button
|
||||||
>
|
className="w-full"
|
||||||
{withdrawSharesMutation.isPending ? '处理中...' : '确认提取'}
|
onClick={() => {
|
||||||
</Button>
|
withdrawSharesMutation.mutate({ amount: withdrawSharesAmount });
|
||||||
</DialogFooter>
|
setWithdrawSharesAmount('');
|
||||||
|
}}
|
||||||
|
disabled={withdrawSharesMutation.isPending || !withdrawSharesAmount}
|
||||||
|
>
|
||||||
|
{withdrawSharesMutation.isPending ? '处理中...' : '确认提取'}
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="blockchain" className="space-y-4 pt-4">
|
||||||
|
<div className="text-sm text-muted-foreground text-center">
|
||||||
|
转账 <strong>eUSDT</strong>(积分股代币)到指定地址
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>目标地址 (Kava EVM)</Label>
|
||||||
|
<Input
|
||||||
|
value={blockchainWithdrawSharesAddress}
|
||||||
|
onChange={(e) => setBlockchainWithdrawSharesAddress(e.target.value)}
|
||||||
|
placeholder="0x..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>提取数量</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={blockchainWithdrawSharesAmount}
|
||||||
|
onChange={(e) => setBlockchainWithdrawSharesAmount(e.target.value)}
|
||||||
|
placeholder="请输入数量"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
blockchainWithdrawSharesMutation.mutate({
|
||||||
|
toAddress: blockchainWithdrawSharesAddress,
|
||||||
|
amount: blockchainWithdrawSharesAmount,
|
||||||
|
});
|
||||||
|
setBlockchainWithdrawSharesAddress('');
|
||||||
|
setBlockchainWithdrawSharesAmount('');
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
blockchainWithdrawSharesMutation.isPending ||
|
||||||
|
!blockchainWithdrawSharesAddress ||
|
||||||
|
!blockchainWithdrawSharesAmount
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{blockchainWithdrawSharesMutation.isPending ? '链上转账中...' : '确认区块链提现'}
|
||||||
|
</Button>
|
||||||
|
<div className="text-xs text-yellow-600 bg-yellow-50 p-2 rounded">
|
||||||
|
<AlertCircle className="h-3 w-3 inline mr-1" />
|
||||||
|
区块链提现将从做市商钱包直接转账到目标地址
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ export interface MarketMakerConfig {
|
||||||
askQuantityPerLevel?: string;
|
askQuantityPerLevel?: string;
|
||||||
refreshIntervalMs?: number;
|
refreshIntervalMs?: number;
|
||||||
lastRefreshAt?: string;
|
lastRefreshAt?: string;
|
||||||
|
kavaWalletAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketMakerOrder {
|
export interface MarketMakerOrder {
|
||||||
|
|
@ -170,6 +171,32 @@ export const marketMakerApi = {
|
||||||
return response.data;
|
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 }> => {
|
start: async (name: string): Promise<{ success: boolean; message: string }> => {
|
||||||
const response = await tradingClient.post(`/admin/market-maker/${name}/start`);
|
const response = await tradingClient.post(`/admin/market-maker/${name}/start`);
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
export function useStartTaker() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue