feat(pool-account): 池账户充值弹窗新增中心化充值 tab
与做市商管理一致,充值弹窗包含"中心化充值"和"区块链充值"两个 tab。 中心化充值直接调整池账户余额(ADJUSTMENT 类型分类账),无需区块链交易。 变更: - wallet-service: pool-account.service 新增 centralizedDeposit 方法 - wallet-service: pool-account.controller 新增 POST /centralized-deposit - admin-service: pool-account.controller 新增 POST /:walletName/centralized-deposit 代理 - frontend: configs.api + use-configs hook + configs page 充值弹窗 Tabs UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c51539e494
commit
a5f6b23a95
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Post, Body, Param, Logger, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Param, Req, Logger, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
|
|
@ -200,4 +200,57 @@ export class PoolAccountController {
|
|||
blockNumber: blockchainData.blockNumber,
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':walletName/centralized-deposit')
|
||||
@ApiOperation({ summary: '池账户中心化充值(管理员手动调整余额)' })
|
||||
@ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' })
|
||||
async centralizedDeposit(
|
||||
@Param('walletName') walletName: string,
|
||||
@Body() body: { amount: string },
|
||||
@Req() req: any,
|
||||
) {
|
||||
const poolInfo = this.walletNameMap[walletName];
|
||||
if (!poolInfo) {
|
||||
throw new BadRequestException(`Unknown wallet name: ${walletName}`);
|
||||
}
|
||||
|
||||
if (!body.amount || parseFloat(body.amount) <= 0) {
|
||||
throw new BadRequestException('amount must be greater than 0');
|
||||
}
|
||||
|
||||
const adminId = req.admin?.id || 'admin';
|
||||
this.logger.log(`[centralized-deposit] ${poolInfo.name}: ${body.amount} by admin ${adminId}`);
|
||||
|
||||
try {
|
||||
const walletResponse = await fetch(
|
||||
`${this.walletServiceUrl}/api/v2/pool-accounts/centralized-deposit`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
poolType: poolInfo.walletPoolType,
|
||||
amount: body.amount,
|
||||
adminId: String(adminId),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!walletResponse.ok) {
|
||||
const errResult = await walletResponse.json().catch(() => ({}));
|
||||
throw new HttpException(
|
||||
errResult.message || '中心化充值失败',
|
||||
walletResponse.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true, message: `${poolInfo.name}充值成功` };
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
this.logger.error(`[centralized-deposit] Failed: ${error}`);
|
||||
throw new HttpException(
|
||||
`中心化充值失败: ${error instanceof Error ? error.message : error}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,4 +148,17 @@ export class PoolAccountController {
|
|||
);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post('centralized-deposit')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '中心化充值(仅限内网调用)' })
|
||||
@ApiResponse({ status: 200, description: '充值成功' })
|
||||
async centralizedDeposit(@Body() dto: { poolType: PoolAccountType; amount: string; adminId?: string }) {
|
||||
await this.poolAccountService.centralizedDeposit(
|
||||
dto.poolType,
|
||||
new Decimal(dto.amount),
|
||||
dto.adminId,
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -575,4 +575,38 @@ export class PoolAccountService {
|
|||
`Blockchain withdraw: ${amount.toFixed(8)} fUSDT from ${poolType} (tx: ${metadata.txHash.slice(0, 10)}...)`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中心化充值(管理员手动调整余额)
|
||||
*/
|
||||
async centralizedDeposit(
|
||||
poolType: PoolAccountType,
|
||||
amount: Decimal,
|
||||
adminId?: string,
|
||||
): Promise<void> {
|
||||
if (amount.isZero() || amount.isNegative()) {
|
||||
throw new DomainException('充值金额必须大于0', 'INVALID_AMOUNT');
|
||||
}
|
||||
|
||||
const poolName = poolType === 'SHARE_POOL_A' ? '100亿销毁池' : '200万挖矿池';
|
||||
const memo = `中心化充值, ${poolName}, 金额${amount.toFixed(8)}, 操作员${adminId || 'admin'}`;
|
||||
|
||||
await this.poolAccountRepo.updateBalanceWithTransaction(
|
||||
poolType,
|
||||
amount,
|
||||
{
|
||||
transactionType: 'ADJUSTMENT',
|
||||
counterpartyType: 'SYSTEM_ACCOUNT',
|
||||
referenceType: 'ADMIN_DEPOSIT',
|
||||
memo,
|
||||
metadata: {
|
||||
type: 'centralized_deposit',
|
||||
amount: amount.toString(),
|
||||
adminId: adminId || 'admin',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`Centralized deposit: ${amount.toFixed(8)} to ${poolType} by ${adminId || 'admin'}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining, useP2pTransferFee, useSetP2pTransferFee, usePoolAccountBalance, usePoolAccountBlockchainWithdraw } from '@/features/configs/hooks/use-configs';
|
||||
import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining, useP2pTransferFee, useSetP2pTransferFee, usePoolAccountBalance, usePoolAccountBlockchainWithdraw, usePoolAccountCentralizedDeposit } from '@/features/configs/hooks/use-configs';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
|
@ -50,17 +51,20 @@ export default function ConfigsPage() {
|
|||
const { data: burnPoolBalance, isLoading: burnPoolLoading } = usePoolAccountBalance(BURN_POOL_WALLET_NAME);
|
||||
const { data: miningPoolBalance, isLoading: miningPoolLoading } = usePoolAccountBalance(MINING_POOL_WALLET_NAME);
|
||||
const blockchainWithdrawMutation = usePoolAccountBlockchainWithdraw();
|
||||
const centralizedDepositMutation = usePoolAccountCentralizedDeposit();
|
||||
|
||||
const [editingConfig, setEditingConfig] = useState<SystemConfig | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [feeValue, setFeeValue] = useState('');
|
||||
const [minAmountValue, setMinAmountValue] = useState('');
|
||||
|
||||
// 池账户区块链提现状态
|
||||
// 池账户状态
|
||||
const [burnWithdrawAddress, setBurnWithdrawAddress] = useState('');
|
||||
const [burnWithdrawAmount, setBurnWithdrawAmount] = useState('');
|
||||
const [miningWithdrawAddress, setMiningWithdrawAddress] = useState('');
|
||||
const [miningWithdrawAmount, setMiningWithdrawAmount] = useState('');
|
||||
const [burnDepositAmount, setBurnDepositAmount] = useState('');
|
||||
const [miningDepositAmount, setMiningDepositAmount] = useState('');
|
||||
const [copiedAddress, setCopiedAddress] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -271,7 +275,7 @@ export default function ConfigsPage() {
|
|||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
{/* 区块链充值 */}
|
||||
{/* 充值(中心化 + 区块链) */}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
|
|
@ -281,40 +285,75 @@ export default function ConfigsPage() {
|
|||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>区块链充值 — 100亿销毁池</DialogTitle>
|
||||
<DialogDescription>向以下地址转入 fUSDT(积分值代币)</DialogDescription>
|
||||
<DialogTitle>充值 — 100亿销毁池</DialogTitle>
|
||||
<DialogDescription>选择充值方式向销毁池充值</DialogDescription>
|
||||
</DialogHeader>
|
||||
{burnPoolBalance?.walletAddress ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={burnPoolBalance.walletAddress} size={180} />
|
||||
<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={burnDepositAmount}
|
||||
onChange={(e) => setBurnDepositAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</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">
|
||||
{burnPoolBalance.walletAddress}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopyAddress(burnPoolBalance.walletAddress)}
|
||||
>
|
||||
{copiedAddress === burnPoolBalance.walletAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
centralizedDepositMutation.mutate({
|
||||
walletName: BURN_POOL_WALLET_NAME,
|
||||
amount: burnDepositAmount,
|
||||
});
|
||||
setBurnDepositAmount('');
|
||||
}}
|
||||
disabled={centralizedDepositMutation.isPending || !burnDepositAmount}
|
||||
>
|
||||
{centralizedDepositMutation.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>
|
||||
{burnPoolBalance?.walletAddress ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={burnPoolBalance.walletAddress} 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">
|
||||
{burnPoolBalance.walletAddress}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopyAddress(burnPoolBalance.walletAddress)}
|
||||
>
|
||||
{copiedAddress === burnPoolBalance.walletAddress ? <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>
|
||||
<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-6 w-6 mx-auto mb-2 text-yellow-500" />
|
||||
<p className="text-sm">钱包地址未配置,请在后端 .env 中配置 BURN_POOL_WALLET_ADDRESS</p>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<AlertCircle className="h-6 w-6 mx-auto mb-2 text-yellow-500" />
|
||||
<p className="text-sm">钱包地址未配置</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
|
@ -419,7 +458,7 @@ export default function ConfigsPage() {
|
|||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
{/* 区块链充值 */}
|
||||
{/* 充值(中心化 + 区块链) */}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
|
|
@ -429,40 +468,75 @@ export default function ConfigsPage() {
|
|||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>区块链充值 — 200万挖矿池</DialogTitle>
|
||||
<DialogDescription>向以下地址转入 fUSDT(积分值代币)</DialogDescription>
|
||||
<DialogTitle>充值 — 200万挖矿池</DialogTitle>
|
||||
<DialogDescription>选择充值方式向挖矿池充值</DialogDescription>
|
||||
</DialogHeader>
|
||||
{miningPoolBalance?.walletAddress ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={miningPoolBalance.walletAddress} size={180} />
|
||||
<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={miningDepositAmount}
|
||||
onChange={(e) => setMiningDepositAmount(e.target.value)}
|
||||
placeholder="请输入金额"
|
||||
/>
|
||||
</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">
|
||||
{miningPoolBalance.walletAddress}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopyAddress(miningPoolBalance.walletAddress)}
|
||||
>
|
||||
{copiedAddress === miningPoolBalance.walletAddress ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
centralizedDepositMutation.mutate({
|
||||
walletName: MINING_POOL_WALLET_NAME,
|
||||
amount: miningDepositAmount,
|
||||
});
|
||||
setMiningDepositAmount('');
|
||||
}}
|
||||
disabled={centralizedDepositMutation.isPending || !miningDepositAmount}
|
||||
>
|
||||
{centralizedDepositMutation.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>
|
||||
{miningPoolBalance?.walletAddress ? (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={miningPoolBalance.walletAddress} 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">
|
||||
{miningPoolBalance.walletAddress}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopyAddress(miningPoolBalance.walletAddress)}
|
||||
>
|
||||
{copiedAddress === miningPoolBalance.walletAddress ? <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>
|
||||
<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-6 w-6 mx-auto mb-2 text-yellow-500" />
|
||||
<p className="text-sm">钱包地址未配置,请在后端 .env 中配置 MINING_POOL_WALLET_ADDRESS</p>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
<AlertCircle className="h-6 w-6 mx-auto mb-2 text-yellow-500" />
|
||||
<p className="text-sm">钱包地址未配置</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
|
|
|||
|
|
@ -92,4 +92,13 @@ export const configsApi = {
|
|||
const response = await apiClient.post(`/admin/pool-accounts/${walletName}/blockchain-withdraw`, { toAddress, amount });
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
// 中心化充值(管理员手动调整余额)
|
||||
poolAccountCentralizedDeposit: async (walletName: string, amount: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
}> => {
|
||||
const response = await apiClient.post(`/admin/pool-accounts/${walletName}/centralized-deposit`, { amount });
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -151,3 +151,18 @@ export function usePoolAccountBlockchainWithdraw() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function usePoolAccountCentralizedDeposit() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ walletName, amount }: { walletName: string; amount: string }) =>
|
||||
configsApi.poolAccountCentralizedDeposit(walletName, amount),
|
||||
onSuccess: (data) => {
|
||||
toast({ title: data.message || '充值成功' });
|
||||
queryClient.invalidateQueries({ queryKey: ['configs', 'pool-account'] });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({ title: '充值失败', description: error.response?.data?.message || '中心化充值失败', variant: 'destructive' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue