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 { 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<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;
|
||||
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<string>(
|
||||
'blockchain.kava.eUsdtContract',
|
||||
'0x7C3275D808eFbAE90C06C7E3A9AfDdcAa8563931',
|
||||
),
|
||||
// fUSDT (积分值) 合约地址 - Future USDT
|
||||
fUsdtContract: this.configService.get<string>(
|
||||
'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,
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
} 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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
充值
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>充值现金</DialogTitle>
|
||||
<DialogDescription>向做市商账户充值积分值</DialogDescription>
|
||||
<DialogTitle>充值现金(积分值)</DialogTitle>
|
||||
<DialogDescription>选择充值方式向做市商账户充值</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label>充值金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={depositCashAmount}
|
||||
onChange={(e) => setDepositCashAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
depositCashMutation.mutate({ amount: depositCashAmount });
|
||||
setDepositCashAmount('');
|
||||
}}
|
||||
disabled={depositCashMutation.isPending || !depositCashAmount}
|
||||
>
|
||||
{depositCashMutation.isPending ? '处理中...' : '确认充值'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Tabs defaultValue="centralized" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="centralized">中心化充值</TabsTrigger>
|
||||
<TabsTrigger value="blockchain">区块链充值</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label>充值金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={depositCashAmount}
|
||||
onChange={(e) => setDepositCashAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
depositCashMutation.mutate({ amount: depositCashAmount });
|
||||
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>
|
||||
</Dialog>
|
||||
|
||||
|
|
@ -208,31 +270,82 @@ export default function MarketMakerPage() {
|
|||
提现
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>提现现金</DialogTitle>
|
||||
<DialogDescription>从做市商账户提取积分值</DialogDescription>
|
||||
<DialogTitle>提现现金(积分值)</DialogTitle>
|
||||
<DialogDescription>选择提现方式从做市商账户提取</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label>提现金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={withdrawCashAmount}
|
||||
onChange={(e) => setWithdrawCashAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
withdrawCashMutation.mutate({ amount: withdrawCashAmount });
|
||||
setWithdrawCashAmount('');
|
||||
}}
|
||||
disabled={withdrawCashMutation.isPending || !withdrawCashAmount}
|
||||
>
|
||||
{withdrawCashMutation.isPending ? '处理中...' : '确认提现'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Tabs defaultValue="centralized" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="centralized">中心化提现</TabsTrigger>
|
||||
<TabsTrigger value="blockchain">区块链提现</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label>提现金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={withdrawCashAmount}
|
||||
onChange={(e) => setWithdrawCashAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
withdrawCashMutation.mutate({ amount: withdrawCashAmount });
|
||||
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>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
@ -275,31 +388,74 @@ export default function MarketMakerPage() {
|
|||
充值
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>充值积分股</DialogTitle>
|
||||
<DialogDescription>向做市商账户充值积分股(用于挂卖单)</DialogDescription>
|
||||
<DialogDescription>选择充值方式向做市商账户充值积分股</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label>充值数量</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={depositSharesAmount}
|
||||
onChange={(e) => setDepositSharesAmount(e.target.value)}
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
depositSharesMutation.mutate({ amount: depositSharesAmount });
|
||||
setDepositSharesAmount('');
|
||||
}}
|
||||
disabled={depositSharesMutation.isPending || !depositSharesAmount}
|
||||
>
|
||||
{depositSharesMutation.isPending ? '处理中...' : '确认充值'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Tabs defaultValue="centralized" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="centralized">中心化充值</TabsTrigger>
|
||||
<TabsTrigger value="blockchain">区块链充值</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label>充值数量</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={depositSharesAmount}
|
||||
onChange={(e) => setDepositSharesAmount(e.target.value)}
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
depositSharesMutation.mutate({ amount: depositSharesAmount });
|
||||
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>
|
||||
</Dialog>
|
||||
|
||||
|
|
@ -310,31 +466,82 @@ export default function MarketMakerPage() {
|
|||
提取
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>提取积分股</DialogTitle>
|
||||
<DialogDescription>从做市商账户提取积分股</DialogDescription>
|
||||
<DialogDescription>选择提现方式从做市商账户提取</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label>提取数量</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={withdrawSharesAmount}
|
||||
onChange={(e) => setWithdrawSharesAmount(e.target.value)}
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
withdrawSharesMutation.mutate({ amount: withdrawSharesAmount });
|
||||
setWithdrawSharesAmount('');
|
||||
}}
|
||||
disabled={withdrawSharesMutation.isPending || !withdrawSharesAmount}
|
||||
>
|
||||
{withdrawSharesMutation.isPending ? '处理中...' : '确认提取'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Tabs defaultValue="centralized" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="centralized">中心化提取</TabsTrigger>
|
||||
<TabsTrigger value="blockchain">区块链提现</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="centralized" className="space-y-4 pt-4">
|
||||
<div>
|
||||
<Label>提取数量</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={withdrawSharesAmount}
|
||||
onChange={(e) => setWithdrawSharesAmount(e.target.value)}
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
withdrawSharesMutation.mutate({ amount: withdrawSharesAmount });
|
||||
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>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in New Issue