mirror of
https://github.com/TecharoHQ/anubis.git
synced 2025-10-04 22:01:53 +08:00
* docs(blog): add post about the odd CPU core count bug Signed-off-by: Xe Iaso <me@xeiaso.net> * chore: spelling Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
214 lines
7.7 KiB
JavaScript
214 lines
7.7 KiB
JavaScript
import React, { useState, useEffect, useMemo } from 'react';
|
|
import styles from './styles.module.css';
|
|
|
|
// A helper function to perform SHA-256 hashing.
|
|
// It takes a string, encodes it, hashes it, and returns a hex string.
|
|
async function sha256(message) {
|
|
try {
|
|
const msgBuffer = new TextEncoder().encode(message);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
return hashHex;
|
|
} catch (error) {
|
|
console.error("Hashing failed:", error);
|
|
return "Error hashing data";
|
|
}
|
|
}
|
|
|
|
// Generates a random hex string of a given byte length
|
|
const generateRandomHex = (bytes = 16) => {
|
|
const buffer = new Uint8Array(bytes);
|
|
crypto.getRandomValues(buffer);
|
|
return Array.from(buffer)
|
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
};
|
|
|
|
|
|
// Icon components for better visual feedback
|
|
const CheckIcon = () => (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className={styles.iconGreen} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
);
|
|
|
|
const XCircleIcon = () => (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className={styles.iconRed} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
);
|
|
|
|
// Main Application Component
|
|
export default function App() {
|
|
// State for the challenge, initialized with a random 16-byte hex string.
|
|
const [challenge, setChallenge] = useState(() => generateRandomHex(16));
|
|
// State for the nonce, which is the variable we can change
|
|
const [nonce, setNonce] = useState(0);
|
|
// State to store the resulting hash
|
|
const [hash, setHash] = useState('');
|
|
// A flag to indicate if the current hash is the "winning" one
|
|
const [isMining, setIsMining] = useState(false);
|
|
const [isFound, setIsFound] = useState(false);
|
|
|
|
// The mining difficulty, i.e., the required number of leading zeros
|
|
const difficulty = "00";
|
|
|
|
// Memoize the combined data to avoid recalculating on every render
|
|
const combinedData = useMemo(() => `${challenge}${nonce}`, [challenge, nonce]);
|
|
|
|
// This effect hook recalculates the hash whenever the combinedData changes.
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
const calculateHash = async () => {
|
|
const calculatedHash = await sha256(combinedData);
|
|
if (isMounted) {
|
|
setHash(calculatedHash);
|
|
setIsFound(calculatedHash.startsWith(difficulty));
|
|
}
|
|
};
|
|
calculateHash();
|
|
return () => { isMounted = false; };
|
|
}, [combinedData, difficulty]);
|
|
|
|
// This effect handles the automatic mining process
|
|
useEffect(() => {
|
|
if (!isMining) return;
|
|
|
|
let miningNonce = nonce;
|
|
let continueMining = true;
|
|
|
|
const mine = async () => {
|
|
while (continueMining) {
|
|
const currentData = `${challenge}${miningNonce}`;
|
|
const currentHash = await sha256(currentData);
|
|
|
|
if (currentHash.startsWith(difficulty)) {
|
|
setNonce(miningNonce);
|
|
setIsMining(false);
|
|
break;
|
|
}
|
|
|
|
miningNonce++;
|
|
// Update the UI periodically to avoid freezing the browser
|
|
if (miningNonce % 100 === 0) {
|
|
setNonce(miningNonce);
|
|
await new Promise(resolve => setTimeout(resolve, 0)); // Yield to the browser
|
|
}
|
|
}
|
|
};
|
|
|
|
mine();
|
|
|
|
return () => {
|
|
continueMining = false;
|
|
}
|
|
}, [isMining, challenge, nonce, difficulty]);
|
|
|
|
|
|
const handleMineClick = () => {
|
|
setIsMining(true);
|
|
}
|
|
|
|
const handleStopClick = () => {
|
|
setIsMining(false);
|
|
}
|
|
|
|
const handleResetClick = () => {
|
|
setIsMining(false);
|
|
setNonce(0);
|
|
}
|
|
|
|
const handleNewChallengeClick = () => {
|
|
setIsMining(false);
|
|
setChallenge(generateRandomHex(16));
|
|
setNonce(0);
|
|
}
|
|
|
|
// Helper to render the hash with colored leading characters
|
|
const renderHash = () => {
|
|
if (!hash) return <span>...</span>;
|
|
const prefix = hash.substring(0, difficulty.length);
|
|
const suffix = hash.substring(difficulty.length);
|
|
const prefixColor = isFound ? styles.hashPrefixGreen : styles.hashPrefixRed;
|
|
return (
|
|
<>
|
|
<span className={`${prefixColor} ${styles.hashPrefix}`}>{prefix}</span>
|
|
<span className={styles.hashSuffix}>{suffix}</span>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<div className={styles.innerContainer}>
|
|
<div className={styles.grid}>
|
|
{/* Challenge Block */}
|
|
<div className={styles.block}>
|
|
<h2 className={styles.blockTitle}>1. Challenge</h2>
|
|
<p className={styles.challengeText}>{challenge}</p>
|
|
</div>
|
|
|
|
{/* Nonce Control Block */}
|
|
<div className={styles.block}>
|
|
<h2 className={styles.blockTitle}>2. Nonce</h2>
|
|
<div className={styles.nonceControls}>
|
|
<button onClick={() => setNonce(n => n - 1)} disabled={isMining} className={styles.nonceButton}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className={styles.iconSmall} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" /></svg>
|
|
</button>
|
|
<span className={styles.nonceValue}>{nonce}</span>
|
|
<button onClick={() => setNonce(n => n + 1)} disabled={isMining} className={styles.nonceButton}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className={styles.iconSmall} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Combined Data Block */}
|
|
<div className={styles.block}>
|
|
<h2 className={styles.blockTitle}>3. Combined Data</h2>
|
|
<p className={styles.combinedDataText}>{combinedData}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Arrow pointing down */}
|
|
<div className={styles.arrowContainer}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className={styles.iconGray} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
|
</svg>
|
|
</div>
|
|
|
|
{/* Hash Output Block */}
|
|
<div className={`${styles.hashContainer} ${isFound ? styles.hashContainerSuccess : styles.hashContainerError}`}>
|
|
<div className={styles.hashContent}>
|
|
<div className={styles.hashText}>
|
|
<h2 className={styles.blockTitle}>4. Resulting Hash (SHA-256)</h2>
|
|
<p className={styles.hashValue}>{renderHash()}</p>
|
|
</div>
|
|
<div className={styles.hashIcon}>
|
|
{isFound ? <CheckIcon /> : <XCircleIcon />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mining Controls */}
|
|
<div className={styles.buttonContainer}>
|
|
{!isMining ? (
|
|
<button onClick={handleMineClick} className={`${styles.button} ${styles.buttonCyan}`}>
|
|
Auto-Mine
|
|
</button>
|
|
) : (
|
|
<button onClick={handleStopClick} className={`${styles.button} ${styles.buttonYellow}`}>
|
|
Stop Mining
|
|
</button>
|
|
)}
|
|
<button onClick={handleNewChallengeClick} className={`${styles.button} ${styles.buttonIndigo}`}>
|
|
New Challenge
|
|
</button>
|
|
<button onClick={handleResetClick} className={`${styles.button} ${styles.buttonGray}`}>
|
|
Reset Nonce
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|