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:
hailin 2026-01-28 17:56:15 -08:00
parent 94f9e7d5b5
commit 58feec255d
11 changed files with 886 additions and 95 deletions

View File

@ -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',
};
}
}

View File

@ -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,

View File

@ -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',
};
}
}
/**
*
*/

View File

@ -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)

View File

@ -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,
};
}

View File

@ -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;
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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`);

View File

@ -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({