| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- 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>
- );
- }
|