mod commands;
use uhash::wallet;
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use serde::Serialize;
#[derive(Serialize)]
struct JsonError {
error: String,
}
#[derive(Parser)]
#[command(name = "uhash")]
#[command(author = "Cyberia")]
#[command(version)]
#[command(about = "UniversalHash local compute CLI")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
wallet: Option<PathBuf>,
}
#[derive(Subcommand)]
enum Commands {
Prove {
#[arg(long)]
challenge: String,
#[arg(long)]
difficulty: u32,
#[arg(long, default_value = "0")]
start_nonce: u64,
#[arg(long, default_value = "1000000")]
max_attempts: u64,
},
Mine {
#[arg(long)]
challenge: String,
#[arg(short, long)]
difficulty: u32,
#[arg(long, value_enum, default_value_t = MiningBackend::Auto)]
backend: MiningBackend,
#[arg(short, long)]
threads: Option<usize>,
#[arg(long, default_value = "4096")]
batch_size: usize,
#[arg(long, default_value = "0")]
max_batches: u64,
#[arg(long, default_value_t = false)]
stop_on_proof: bool,
#[arg(long, default_value = "0")]
start_nonce: u64,
},
Verify {
#[arg(long)]
challenge: String,
#[arg(long)]
nonce: u64,
#[arg(long)]
hash: String,
#[arg(long)]
difficulty: Option<u32>,
},
Inspect {
#[arg(long)]
challenge: Option<String>,
#[arg(long)]
hash: Option<String>,
},
Bench {
#[arg(short, long, default_value = "100")]
count: u32,
#[arg(long, value_enum, default_value_t = MiningBackend::Auto)]
backend: MiningBackend,
#[arg(short, long)]
threads: Option<usize>,
},
Tune {
#[arg(long)]
reset: bool,
#[arg(long)]
retune: bool,
},
Wallet {
#[command(subcommand)]
command: commands::wallet::WalletCommand,
},
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
enum MiningBackend {
Auto,
Cpu,
Cuda,
Metal,
Opencl,
Wgpu,
}
impl MiningBackend {
fn auto_fallback_chain() -> &'static [MiningBackend] {
if cfg!(target_os = "macos") {
&[Self::Metal, Self::Cuda, Self::Opencl, Self::Wgpu, Self::Cpu]
} else {
&[Self::Cuda, Self::Opencl, Self::Wgpu, Self::Cpu]
}
}
fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Cpu => "cpu",
Self::Cuda => "cuda",
Self::Metal => "metal",
Self::Opencl => "opencl",
Self::Wgpu => "wgpu",
}
}
}
fn try_init_cuda_backend() -> anyhow::Result<()> {
#[cfg(feature = "gpu-cuda")]
{
let _ = cudarc::driver::CudaContext::new(0)
.map_err(|e| anyhow::anyhow!("CUDA init failed: {}", e))?;
Ok(())
}
#[cfg(not(feature = "gpu-cuda"))]
{
anyhow::bail!("CUDA backend not enabled in this build (rebuild with --features gpu-cuda)")
}
}
fn try_init_metal_backend() -> anyhow::Result<()> {
#[cfg(all(feature = "gpu-metal", target_os = "macos"))]
{
let _ = metal::Device::system_default()
.ok_or_else(|| anyhow::anyhow!("No Metal device found"))?;
Ok(())
}
#[cfg(not(all(feature = "gpu-metal", target_os = "macos")))]
{
anyhow::bail!(
"Metal backend not available in this build/platform (macOS + --features gpu-metal required)"
)
}
}
fn try_init_opencl_backend() -> anyhow::Result<()> {
#[cfg(feature = "gpu-opencl")]
{
let _ = uhash::opencl_solver::OpenClSolver::new()?;
Ok(())
}
#[cfg(not(feature = "gpu-opencl"))]
{
anyhow::bail!(
"OpenCL backend not enabled in this build (rebuild with --features gpu-opencl)"
)
}
}
fn try_init_wgpu_backend() -> anyhow::Result<()> {
#[cfg(feature = "gpu-wgpu")]
{
let _ = uhash::wgpu_solver::WgpuSolver::new()?;
Ok(())
}
#[cfg(not(feature = "gpu-wgpu"))]
{
anyhow::bail!("WGPU backend not enabled in this build (rebuild with --features gpu-wgpu)")
}
}
fn try_init_backend(backend: MiningBackend) -> anyhow::Result<()> {
match backend {
MiningBackend::Cpu => Ok(()),
MiningBackend::Cuda => try_init_cuda_backend(),
MiningBackend::Metal => try_init_metal_backend(),
MiningBackend::Opencl => try_init_opencl_backend(),
MiningBackend::Wgpu => try_init_wgpu_backend(),
MiningBackend::Auto => Ok(()),
}
}
fn prepare_backend(
requested: MiningBackend,
json: bool,
) -> anyhow::Result<(MiningBackend, Option<String>)> {
if requested == MiningBackend::Auto {
for &candidate in MiningBackend::auto_fallback_chain() {
match try_init_backend(candidate) {
Ok(()) => return Ok((candidate, None)),
Err(e) => {
if !json {
eprintln!(
"Auto: {} unavailable ({}), trying next...",
candidate.as_str(),
e
);
}
}
}
}
let note = "Auto backend fallback: no GPU backends available, using cpu".to_string();
if !json {
eprintln!("Warning: {}", note);
}
return Ok((MiningBackend::Cpu, Some(note)));
}
if requested == MiningBackend::Cpu {
return Ok((MiningBackend::Cpu, None));
}
try_init_backend(requested)?;
Ok((requested, None))
}
fn cmd_bench(
count: u32,
backend: MiningBackend,
threads: Option<usize>,
json: bool,
) -> anyhow::Result<()> {
let (effective_backend, backend_note) = prepare_backend(backend, json)?;
commands::bench::cmd_benchmark(count, effective_backend, backend_note, threads, json)
}
#[allow(clippy::too_many_arguments)]
fn cmd_mine(
challenge_hex: &str,
difficulty: u32,
backend: MiningBackend,
threads: Option<usize>,
batch_size: usize,
max_batches: u64,
stop_on_proof: bool,
start_nonce: u64,
json: bool,
) -> anyhow::Result<()> {
let (effective_backend, backend_note) = prepare_backend(backend, json)?;
commands::mine::cmd_mine(
challenge_hex,
difficulty,
effective_backend,
backend_note,
threads,
batch_size,
max_batches,
stop_on_proof,
start_nonce,
json,
)
}
fn cmd_tune(reset: bool, retune: bool, json: bool) -> anyhow::Result<()> {
commands::tune::cmd_tune(reset, retune, json)
}
fn cmd_prove(
challenge_hex: &str,
difficulty: u32,
start_nonce: u64,
max_attempts: u64,
json: bool,
) -> anyhow::Result<()> {
commands::prove::cmd_prove(challenge_hex, difficulty, start_nonce, max_attempts, json)
}
fn cmd_verify(
challenge_hex: &str,
nonce: u64,
hash_hex: &str,
difficulty: Option<u32>,
json: bool,
) -> anyhow::Result<()> {
commands::prove::cmd_verify(challenge_hex, nonce, hash_hex, difficulty, json)
}
fn cmd_inspect(
challenge_hex: Option<&str>,
hash_hex: Option<&str>,
json: bool,
) -> anyhow::Result<()> {
commands::prove::cmd_inspect(challenge_hex, hash_hex, json)
}
fn main() {
let cli = Cli::parse();
let json = cli.json;
let result = match cli.command {
Commands::Prove {
challenge,
difficulty,
start_nonce,
max_attempts,
} => cmd_prove(&challenge, difficulty, start_nonce, max_attempts, json),
Commands::Mine {
challenge,
difficulty,
backend,
threads,
batch_size,
max_batches,
stop_on_proof,
start_nonce,
} => cmd_mine(
&challenge,
difficulty,
backend,
threads,
batch_size,
max_batches,
stop_on_proof,
start_nonce,
json,
),
Commands::Verify {
challenge,
nonce,
hash,
difficulty,
} => cmd_verify(&challenge, nonce, &hash, difficulty, json),
Commands::Inspect { challenge, hash } => {
cmd_inspect(challenge.as_deref(), hash.as_deref(), json)
}
Commands::Bench {
count,
backend,
threads,
} => cmd_bench(count, backend, threads, json),
Commands::Tune { reset, retune } => cmd_tune(reset, retune, json),
Commands::Wallet { command } => commands::wallet::cmd_wallet(command, cli.wallet, json),
};
if let Err(e) = result {
if json {
let err = JsonError {
error: e.to_string(),
};
println!(
"{}",
serde_json::to_string(&err).unwrap_or_else(|_| "{}".to_string())
);
} else {
eprintln!("Error: {}", e);
}
std::process::exit(1);
}
}