388 lines
12 KiB
TypeScript
388 lines
12 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
Hammer,
|
|
Play,
|
|
Pause,
|
|
Square,
|
|
Cpu,
|
|
Blocks,
|
|
TrendingUp,
|
|
Clock,
|
|
Zap,
|
|
} from 'lucide-react';
|
|
import { useMiningStore, formatHashrate } from '../../store/mining';
|
|
import { useNodeStore } from '../../store/node';
|
|
import { useWalletStore } from '../../store/wallet';
|
|
|
|
export default function MiningDashboard() {
|
|
const {
|
|
status,
|
|
recentBlocks,
|
|
hashrateHistory,
|
|
defaultThreads,
|
|
defaultCoinbaseAddress,
|
|
startMining,
|
|
stopMining,
|
|
pauseMining,
|
|
resumeMining,
|
|
setThreads,
|
|
refreshStatus,
|
|
refreshStats,
|
|
setupEventListeners,
|
|
cleanupEventListeners,
|
|
} = useMiningStore();
|
|
|
|
const nodeStatus = useNodeStore((state) => state.status);
|
|
const addresses = useWalletStore((state) => state.addresses);
|
|
|
|
const [threads, setThreadsLocal] = useState(defaultThreads);
|
|
const [coinbaseAddress, setCoinbaseAddress] = useState(
|
|
defaultCoinbaseAddress || addresses[0]?.address || ''
|
|
);
|
|
const [isStarting, setIsStarting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Max threads available
|
|
const maxThreads = navigator.hardwareConcurrency || 8;
|
|
|
|
// Setup event listeners on mount
|
|
useEffect(() => {
|
|
setupEventListeners();
|
|
return () => cleanupEventListeners();
|
|
}, [setupEventListeners, cleanupEventListeners]);
|
|
|
|
// Refresh stats periodically when mining
|
|
useEffect(() => {
|
|
if (!status.isMining) return;
|
|
|
|
const interval = setInterval(() => {
|
|
refreshStatus();
|
|
refreshStats();
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [status.isMining, refreshStatus, refreshStats]);
|
|
|
|
// Update coinbase address when addresses change
|
|
useEffect(() => {
|
|
if (!coinbaseAddress && addresses.length > 0) {
|
|
setCoinbaseAddress(addresses[0].address);
|
|
}
|
|
}, [addresses, coinbaseAddress]);
|
|
|
|
const handleStart = async () => {
|
|
if (!nodeStatus.isConnected) {
|
|
setError('Please connect to a node first');
|
|
return;
|
|
}
|
|
|
|
if (!coinbaseAddress) {
|
|
setError('Please select a coinbase address for rewards');
|
|
return;
|
|
}
|
|
|
|
setIsStarting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await startMining(coinbaseAddress, threads);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to start mining');
|
|
} finally {
|
|
setIsStarting(false);
|
|
}
|
|
};
|
|
|
|
const handleStop = async () => {
|
|
try {
|
|
await stopMining();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to stop mining');
|
|
}
|
|
};
|
|
|
|
const handlePauseResume = async () => {
|
|
try {
|
|
if (status.isPaused) {
|
|
await resumeMining();
|
|
} else {
|
|
await pauseMining();
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Action failed');
|
|
}
|
|
};
|
|
|
|
const handleThreadsChange = async (newThreads: number) => {
|
|
setThreadsLocal(newThreads);
|
|
if (status.isMining) {
|
|
try {
|
|
await setThreads(newThreads);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to update threads');
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
|
<Hammer size={28} />
|
|
Mining
|
|
</h1>
|
|
<p className="text-gray-400 mt-1">
|
|
Mine SYN coins using your computer's CPU
|
|
</p>
|
|
</div>
|
|
|
|
{/* Control buttons */}
|
|
<div className="flex items-center gap-2">
|
|
{status.isMining ? (
|
|
<>
|
|
<button
|
|
onClick={handlePauseResume}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
|
|
status.isPaused
|
|
? 'bg-green-600 hover:bg-green-700 text-white'
|
|
: 'bg-yellow-600 hover:bg-yellow-700 text-white'
|
|
}`}
|
|
>
|
|
{status.isPaused ? <Play size={18} /> : <Pause size={18} />}
|
|
{status.isPaused ? 'Resume' : 'Pause'}
|
|
</button>
|
|
<button
|
|
onClick={handleStop}
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white transition-colors"
|
|
>
|
|
<Square size={18} />
|
|
Stop
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={handleStart}
|
|
disabled={isStarting || !nodeStatus.isConnected}
|
|
className="flex items-center gap-2 px-6 py-2 rounded-lg bg-synor-600 hover:bg-synor-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isStarting ? (
|
|
<Cpu size={18} className="animate-spin" />
|
|
) : (
|
|
<Play size={18} />
|
|
)}
|
|
Start Mining
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error display */}
|
|
{error && (
|
|
<div className="p-4 rounded-lg bg-red-900/30 border border-red-800 text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Warning if not connected */}
|
|
{!nodeStatus.isConnected && (
|
|
<div className="p-4 rounded-lg bg-yellow-900/30 border border-yellow-800 text-yellow-400">
|
|
Please connect to a node before starting mining
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard
|
|
icon={<Zap size={20} className={status.isMining && !status.isPaused ? 'animate-pulse' : ''} />}
|
|
label="Hashrate"
|
|
value={formatHashrate(status.hashrate)}
|
|
highlight={status.isMining && !status.isPaused}
|
|
/>
|
|
<StatCard
|
|
icon={<Blocks size={20} />}
|
|
label="Blocks Found"
|
|
value={status.blocksFound.toString()}
|
|
/>
|
|
<StatCard
|
|
icon={<Cpu size={20} />}
|
|
label="Threads"
|
|
value={`${status.threads || threads}/${maxThreads}`}
|
|
/>
|
|
<StatCard
|
|
icon={<Clock size={20} />}
|
|
label="Status"
|
|
value={
|
|
status.isMining
|
|
? status.isPaused
|
|
? 'Paused'
|
|
: 'Mining'
|
|
: 'Idle'
|
|
}
|
|
highlight={status.isMining && !status.isPaused}
|
|
/>
|
|
</div>
|
|
|
|
{/* Configuration (when not mining) */}
|
|
{!status.isMining && (
|
|
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
|
|
<h3 className="text-lg font-semibold text-white mb-4">
|
|
Mining Configuration
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{/* Coinbase address */}
|
|
<div>
|
|
<label className="block text-sm text-gray-400 mb-2">
|
|
Reward Address
|
|
</label>
|
|
<select
|
|
value={coinbaseAddress}
|
|
onChange={(e) => setCoinbaseAddress(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-lg bg-gray-800 border border-gray-700 text-white focus:outline-none focus:border-synor-500"
|
|
>
|
|
<option value="">Select address...</option>
|
|
{addresses.map((addr) => (
|
|
<option key={addr.address} value={addr.address}>
|
|
{addr.label || `Address ${addr.index}`} - {addr.address.slice(0, 20)}...
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Mining rewards will be sent to this address
|
|
</p>
|
|
</div>
|
|
|
|
{/* Thread slider */}
|
|
<div>
|
|
<label className="block text-sm text-gray-400 mb-2">
|
|
Mining Threads: {threads}
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min={1}
|
|
max={maxThreads}
|
|
value={threads}
|
|
onChange={(e) => handleThreadsChange(parseInt(e.target.value))}
|
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-synor-500"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
|
<span>1 (Low Power)</span>
|
|
<span>{maxThreads} (Max Performance)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Hashrate Chart (simplified) */}
|
|
{status.isMining && hashrateHistory.length > 0 && (
|
|
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<TrendingUp size={20} />
|
|
Hashrate History
|
|
</h3>
|
|
<div className="h-32 flex items-end gap-0.5">
|
|
{hashrateHistory.slice(-60).map((point, i) => {
|
|
const maxHash = Math.max(...hashrateHistory.map((h) => h.hashrate));
|
|
const height = maxHash > 0 ? (point.hashrate / maxHash) * 100 : 0;
|
|
return (
|
|
<div
|
|
key={i}
|
|
className="flex-1 bg-synor-500 rounded-t transition-all duration-300"
|
|
style={{ height: `${height}%` }}
|
|
title={`${formatHashrate(point.hashrate)}`}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
|
<span>60s ago</span>
|
|
<span>Now</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Blocks */}
|
|
{recentBlocks.length > 0 && (
|
|
<div className="p-6 rounded-xl bg-gray-900 border border-gray-800">
|
|
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Blocks size={20} />
|
|
Blocks Found
|
|
</h3>
|
|
<div className="space-y-2">
|
|
{recentBlocks.slice(0, 5).map((block, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center justify-between p-3 rounded-lg bg-gray-800"
|
|
>
|
|
<div>
|
|
<p className="text-sm text-white font-mono">
|
|
Block #{block.height.toLocaleString()}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{block.hash.slice(0, 24)}...
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm text-synor-400 font-medium">
|
|
+{(block.reward / 100_000_000).toFixed(2)} SYN
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{new Date(block.timestamp).toLocaleTimeString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mining Tips */}
|
|
<div className="p-6 rounded-xl bg-gray-900/50 border border-gray-800">
|
|
<h3 className="text-sm font-semibold text-gray-400 mb-3">Mining Tips</h3>
|
|
<ul className="space-y-2 text-sm text-gray-500">
|
|
<li>• Use fewer threads to keep your computer responsive</li>
|
|
<li>• Mining profitability depends on network difficulty</li>
|
|
<li>• Ensure adequate cooling for sustained mining</li>
|
|
<li>• For best results, use an embedded node for lower latency</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Stat card component
|
|
function StatCard({
|
|
icon,
|
|
label,
|
|
value,
|
|
highlight = false,
|
|
}: {
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
value: string;
|
|
highlight?: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={`p-4 rounded-xl border ${
|
|
highlight
|
|
? 'bg-synor-900/30 border-synor-700'
|
|
: 'bg-gray-900 border-gray-800'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 text-gray-400 mb-2">
|
|
{icon}
|
|
<span className="text-sm">{label}</span>
|
|
</div>
|
|
<p
|
|
className={`text-xl font-bold ${
|
|
highlight ? 'text-synor-400' : 'text-white'
|
|
}`}
|
|
>
|
|
{value}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|