From 6dbb620e824aee7383ac70c4a1b96ca03445c72e Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 3 Feb 2026 00:02:32 -0800 Subject: [PATCH] =?UTF-8?q?feat(mining-admin):=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=96=B0=E5=A2=9E100=E4=BA=BF=E9=94=80?= =?UTF-8?q?=E6=AF=81=E6=B1=A0=E5=92=8C200=E4=B8=87=E6=8C=96=E7=9F=BF?= =?UTF-8?q?=E6=B1=A0=E8=B4=A6=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在配置管理页面新增两个池账户卡片,UI 风格对齐做市商管理中的 "现金余额(积分值)",仅支持区块链方式充值与提现: - 100亿销毁池 (wallet-22fd661f, 2-of-3 门限) 地址: 0xdE2932D2A25e1698c1354A41e2e46B414C46F5a1 - 200万挖矿池 (wallet-974e78f5, 2-of-3 门限) 地址: 0x8BC9091375ae8ef43ae011F0f9bAf10e51bC9D59 具体改动: - .env.production: 新增 BURN_POOL / MINING_POOL 钱包名和地址环境变量 - configs.api.ts: 新增 tradingClient、PoolAccountBalance 接口、 getPoolAccountBalance 和 poolAccountBlockchainWithdraw API - use-configs.ts: 新增 usePoolAccountBalance (30s 刷新) 和 usePoolAccountBlockchainWithdraw hooks - configs/page.tsx: 新增两个并排池账户 Card,包含余额展示、 QR 码充值弹窗、区块链提现弹窗(含 0x 地址校验) 后端需实现: - GET /admin/pool-accounts/:walletName/balance - POST /admin/pool-accounts/:walletName/blockchain-withdraw Co-Authored-By: Claude Opus 4.5 --- frontend/mining-admin-web/.env.production | 8 + .../src/app/(dashboard)/configs/page.tsx | 329 +++++++++++++++++- .../src/features/configs/api/configs.api.ts | 69 ++++ .../src/features/configs/hooks/use-configs.ts | 32 ++ 4 files changed, 435 insertions(+), 3 deletions(-) diff --git a/frontend/mining-admin-web/.env.production b/frontend/mining-admin-web/.env.production index 3cd5c968..751811e3 100644 --- a/frontend/mining-admin-web/.env.production +++ b/frontend/mining-admin-web/.env.production @@ -1,3 +1,11 @@ NEXT_PUBLIC_API_URL=https://mapi.szaiai.com/api/v2/mining-admin TRADING_SERVICE_URL=https://mapi.szaiai.com/api/v2/trading NEXT_PUBLIC_APP_NAME=挖矿管理后台 + +# 100亿销毁池钱包配置 +NEXT_PUBLIC_BURN_POOL_WALLET_NAME=wallet-22fd661f +NEXT_PUBLIC_BURN_POOL_WALLET_ADDRESS=0xdE2932D2A25e1698c1354A41e2e46B414C46F5a1 + +# 200万挖矿池钱包配置 +NEXT_PUBLIC_MINING_POOL_WALLET_NAME=wallet-974e78f5 +NEXT_PUBLIC_MINING_POOL_WALLET_ADDRESS=0x8BC9091375ae8ef43ae011F0f9bAf10e51bC9D59 diff --git a/frontend/mining-admin-web/src/app/(dashboard)/configs/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/configs/page.tsx index 82851658..b1c3d3b1 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/configs/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/configs/page.tsx @@ -1,20 +1,34 @@ 'use client'; 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 } from '@/features/configs/hooks/use-configs'; +import { useConfigs, useUpdateConfig, useTransferEnabled, useSetTransferEnabled, useMiningStatus, useActivateMining, useDeactivateMining, useP2pTransferFee, useSetP2pTransferFee, usePoolAccountBalance, usePoolAccountBlockchainWithdraw } 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 { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, + DialogTrigger, +} from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; -import { Pencil, Save, X, Play, Pause, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'; +import { Pencil, Save, X, Play, Pause, AlertCircle, CheckCircle2, Loader2, Wallet, PlusCircle, MinusCircle, Copy, Check, Flame, HardHat } from 'lucide-react'; import type { SystemConfig } from '@/types/config'; +const BURN_POOL_WALLET_NAME = process.env.NEXT_PUBLIC_BURN_POOL_WALLET_NAME || 'wallet-22fd661f'; +const BURN_POOL_WALLET_ADDRESS = process.env.NEXT_PUBLIC_BURN_POOL_WALLET_ADDRESS || '0xdE2932D2A25e1698c1354A41e2e46B414C46F5a1'; +const MINING_POOL_WALLET_NAME = process.env.NEXT_PUBLIC_MINING_POOL_WALLET_NAME || 'wallet-974e78f5'; +const MINING_POOL_WALLET_ADDRESS = process.env.NEXT_PUBLIC_MINING_POOL_WALLET_ADDRESS || '0x8BC9091375ae8ef43ae011F0f9bAf10e51bC9D59'; + const categoryLabels: Record = { mining: '挖矿配置', trading: '交易配置', @@ -34,11 +48,22 @@ export default function ConfigsPage() { const { data: feeConfig, isLoading: feeLoading } = useP2pTransferFee(); const setP2pTransferFee = useSetP2pTransferFee(); + const { data: burnPoolBalance, isLoading: burnPoolLoading } = usePoolAccountBalance(BURN_POOL_WALLET_NAME); + const { data: miningPoolBalance, isLoading: miningPoolLoading } = usePoolAccountBalance(MINING_POOL_WALLET_NAME); + const blockchainWithdrawMutation = usePoolAccountBlockchainWithdraw(); + const [editingConfig, setEditingConfig] = useState(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 [copiedAddress, setCopiedAddress] = useState(null); + useEffect(() => { if (feeConfig) { setFeeValue(feeConfig.fee); @@ -46,6 +71,19 @@ export default function ConfigsPage() { } }, [feeConfig]); + const handleCopyAddress = async (address: string) => { + await navigator.clipboard.writeText(address); + setCopiedAddress(address); + setTimeout(() => setCopiedAddress(null), 2000); + }; + + const formatBalance = (value: string | undefined, decimals: number = 2) => { + if (!value) return '0'; + const num = parseFloat(value); + if (isNaN(num)) return '0'; + return num.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); + }; + const handleEdit = (config: SystemConfig) => { setEditingConfig(config); setEditValue(config.configValue); @@ -194,6 +232,291 @@ export default function ConfigsPage() { + {/* 池账户管理 */} +
+ {/* 100亿销毁池 */} + + +
+ + + 100亿销毁池 + + + 2-of-3 门限 + +
+ + 钱包: {BURN_POOL_WALLET_NAME} + +
+ +
+ {burnPoolLoading ? ( + + ) : ( +
+
+

总余额

+

{formatBalance(burnPoolBalance?.balance)}

+
+
+

可用余额

+

{formatBalance(burnPoolBalance?.availableBalance)}

+
+
+

冻结中

+

{formatBalance(burnPoolBalance?.frozenBalance)}

+
+
+ )} + +
+ {/* 区块链充值 */} + + + + + + + 区块链充值 — 100亿销毁池 + 向以下地址转入 fUSDT(积分值代币) + +
+
+ +
+
+ +
+ + {BURN_POOL_WALLET_ADDRESS} + + +
+
+
+ + 转账后系统将自动检测并入账(约需12个区块确认) +
+
+
+
+ + {/* 区块链提现 */} + + + + + + + 区块链提现 — 100亿销毁池 + 转账 fUSDT(积分值代币)到指定地址 + +
+
+ + setBurnWithdrawAddress(e.target.value)} + placeholder="0x..." + /> +
+
+ + setBurnWithdrawAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ +
+ + 区块链提现将通过 2-of-3 门限签名从销毁池钱包转账到目标地址 +
+
+
+
+
+
+
+
+ + {/* 200万挖矿池 */} + + +
+ + + 200万挖矿池 + + + 2-of-3 门限 + +
+ + 钱包: {MINING_POOL_WALLET_NAME} + +
+ +
+ {miningPoolLoading ? ( + + ) : ( +
+
+

总余额

+

{formatBalance(miningPoolBalance?.balance)}

+
+
+

可用余额

+

{formatBalance(miningPoolBalance?.availableBalance)}

+
+
+

冻结中

+

{formatBalance(miningPoolBalance?.frozenBalance)}

+
+
+ )} + +
+ {/* 区块链充值 */} + + + + + + + 区块链充值 — 200万挖矿池 + 向以下地址转入 fUSDT(积分值代币) + +
+
+ +
+
+ +
+ + {MINING_POOL_WALLET_ADDRESS} + + +
+
+
+ + 转账后系统将自动检测并入账(约需12个区块确认) +
+
+
+
+ + {/* 区块链提现 */} + + + + + + + 区块链提现 — 200万挖矿池 + 转账 fUSDT(积分值代币)到指定地址 + +
+
+ + setMiningWithdrawAddress(e.target.value)} + placeholder="0x..." + /> +
+
+ + setMiningWithdrawAmount(e.target.value)} + placeholder="请输入金额" + /> +
+ +
+ + 区块链提现将通过 2-of-3 门限签名从挖矿池钱包转账到目标地址 +
+
+
+
+
+
+
+
+
+ 划转开关 diff --git a/frontend/mining-admin-web/src/features/configs/api/configs.api.ts b/frontend/mining-admin-web/src/features/configs/api/configs.api.ts index f276c7c8..b9f99e21 100644 --- a/frontend/mining-admin-web/src/features/configs/api/configs.api.ts +++ b/frontend/mining-admin-web/src/features/configs/api/configs.api.ts @@ -1,12 +1,62 @@ import { apiClient } from '@/lib/api/client'; +import axios from 'axios'; import type { SystemConfig } from '@/types/config'; +const tradingBaseURL = '/api/trading'; + +const tradingClient = axios.create({ + baseURL: tradingBaseURL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +tradingClient.interceptors.request.use( + (config) => { + const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +tradingClient.interceptors.response.use( + (response) => { + if (response.data && response.data.data !== undefined) { + response.data = response.data.data; + } + return response; + }, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('admin_token'); + if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) { + window.location.href = '/login'; + } + } + return Promise.reject(error); + } +); + export interface ContributionSyncStatus { isSynced: boolean; miningNetworkTotal: string; networkTotalContribution: string; } +export interface PoolAccountBalance { + walletName: string; + walletAddress: string; + balance: string; + availableBalance: string; + frozenBalance: string; + threshold: string; + lastUpdated?: string; +} + export interface MiningStatus { initialized: boolean; isActive: boolean; @@ -63,4 +113,23 @@ export const configsApi = { setP2pTransferFee: async (fee: string, minTransferAmount: string): Promise => { await apiClient.post('/configs/p2p-transfer-fee', { fee, minTransferAmount }); }, + + // 获取池账户余额 + getPoolAccountBalance: async (walletName: string): Promise => { + const response = await tradingClient.get(`/admin/pool-accounts/${walletName}/balance`); + return response.data; + }, + + // 区块链提现(从池账户) + poolAccountBlockchainWithdraw: async (walletName: string, toAddress: string, amount: string): Promise<{ + success: boolean; + message: string; + txHash?: string; + blockNumber?: number; + newBalance?: string; + error?: string; + }> => { + const response = await tradingClient.post(`/admin/pool-accounts/${walletName}/blockchain-withdraw`, { toAddress, amount }); + return response.data; + }, }; diff --git a/frontend/mining-admin-web/src/features/configs/hooks/use-configs.ts b/frontend/mining-admin-web/src/features/configs/hooks/use-configs.ts index 140956fe..8fae4091 100644 --- a/frontend/mining-admin-web/src/features/configs/hooks/use-configs.ts +++ b/frontend/mining-admin-web/src/features/configs/hooks/use-configs.ts @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { configsApi } from '../api/configs.api'; import { useToast } from '@/lib/hooks/use-toast'; +import { toast } from '@/lib/hooks/use-toast'; export function useConfigs() { return useQuery({ @@ -119,3 +120,34 @@ export function useSetP2pTransferFee() { }, }); } + +export function usePoolAccountBalance(walletName: string) { + return useQuery({ + queryKey: ['configs', 'pool-account', walletName], + queryFn: () => configsApi.getPoolAccountBalance(walletName), + refetchInterval: 30000, + enabled: !!walletName, + }); +} + +export function usePoolAccountBlockchainWithdraw() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ walletName, toAddress, amount }: { walletName: string; toAddress: string; amount: string }) => + configsApi.poolAccountBlockchainWithdraw(walletName, 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: ['configs', 'pool-account'] }); + }, + onError: (error: any) => { + toast({ title: '错误', description: error.response?.data?.message || '区块链提现失败', variant: 'destructive' }); + }, + }); +}