opt-in-miner 0.4.1

Opt-in Monero/Wownero mining library for transparent application monetization
Documentation
use crate::{ConsentStatus, Miner, Persistence, Source};

/// High-level wrapper around [`Miner`] that handles the common case of
/// compile-time wallet/sources configuration via environment variables and
/// `Option<Miner>` management.
///
/// Use the [`mining_state!`](crate::mining_state) macro to construct from
/// compile-time environment variables, or [`MiningState::new`] for explicit parameters.
///
/// When no wallet is configured (or the `mining` feature is disabled in the consuming app),
/// all methods are safe no-ops.
pub struct MiningState {
    miner: Option<Miner>,
}

/// Result of [`MiningState::toggle`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToggleResult {
    /// Mining was started.
    Started,
    /// Mining was stopped.
    Stopped,
    /// Consent is required before mining can start.
    NeedsConsent,
    /// No miner configured (wallet/sources missing).
    Unavailable,
}

impl MiningState {
    /// Creates a new `MiningState` from explicit wallet and sources strings.
    ///
    /// `wallet`: Monero wallet address, or `None` if not configured.
    /// `sources_raw`: Comma-separated list of sources (`node:host:port` or `pool:host:port`),
    /// or `None` if not configured.
    /// `application_name`: Used for consent storage path.
    pub fn new(wallet: Option<&str>, sources_raw: Option<&str>, application_name: &str) -> Self {
        let Some(wallet) = wallet else {
            return Self { miner: None };
        };

        let sources = parse_sources(sources_raw);
        if sources.is_empty() {
            return Self { miner: None };
        }

        let miner = Miner::builder()
            .wallet(wallet)
            .sources(&sources)
            .application_name(application_name)
            .build();

        Self { miner: Some(miner) }
    }

    /// Returns whether a miner is configured (wallet and sources present).
    pub fn available(&self) -> bool {
        self.miner.is_some()
    }

    /// Returns whether consent has been granted.
    pub fn is_enabled(&self) -> bool {
        self.miner
            .as_ref()
            .is_some_and(|m| m.consent_status() == ConsentStatus::Granted)
    }

    /// Returns whether the miner is currently running.
    pub fn is_running(&self) -> bool {
        self.miner.as_ref().is_some_and(Miner::is_running)
    }

    /// Starts mining if consent has been granted. No-op otherwise.
    pub fn start(&mut self) {
        if let Some(miner) = &mut self.miner {
            miner.start();
        }
    }

    /// Stops mining.
    pub fn stop(&mut self) {
        if let Some(miner) = &mut self.miner {
            miner.stop();
        }
    }

    /// Toggles mining: stops if running, starts if consent is granted,
    /// or returns [`ToggleResult::NeedsConsent`] if no consent decision exists.
    pub fn toggle(&mut self) -> ToggleResult {
        let Some(miner) = &mut self.miner else {
            return ToggleResult::Unavailable;
        };
        if miner.is_running() {
            miner.stop();
            ToggleResult::Stopped
        } else if miner.consent_status() == ConsentStatus::Granted {
            miner.start();
            ToggleResult::Started
        } else {
            ToggleResult::NeedsConsent
        }
    }

    /// Returns the current persistence mode.
    pub fn persistence(&self) -> Persistence {
        self.miner
            .as_ref()
            .map_or(Persistence::Ask, Miner::persistence)
    }

    /// Changes the persistence mode.
    pub fn set_persistence(&mut self, persistence: Persistence) {
        if let Some(miner) = &mut self.miner {
            miner.set_persistence(persistence);
        }
    }

    /// Returns the current consent status. Defaults to [`ConsentStatus::Denied`].
    pub fn consent_status(&self) -> ConsentStatus {
        self.miner
            .as_ref()
            .map_or(ConsentStatus::Denied, Miner::consent_status)
    }

    /// Overrides the consent status.
    pub fn set_consent(&mut self, status: ConsentStatus) {
        if let Some(miner) = &mut self.miner {
            miner.set_consent(status);
        }
    }

    /// Returns the current CPU fraction (0.01–1.0).
    pub fn cpu_fraction(&self) -> f32 {
        self.miner.as_ref().map_or(0.25, Miner::cpu_fraction)
    }

    /// Changes the CPU fraction at runtime.
    pub fn set_cpu_fraction(&mut self, fraction: f32) {
        if let Some(miner) = &mut self.miner {
            miner.set_cpu_fraction(fraction);
        }
    }

    /// Returns the current number of mining threads.
    pub fn threads(&self) -> usize {
        self.miner.as_ref().map_or(1, Miner::threads)
    }

    /// Changes the number of mining threads. Restarts mining if running.
    pub fn set_threads(&mut self, count: usize) {
        if let Some(miner) = &mut self.miner {
            miner.set_threads(count);
        }
    }

    /// Returns the total number of hashes since the last start.
    pub fn hash_count(&self) -> u64 {
        self.miner.as_ref().map_or(0, Miner::hash_count)
    }
}

fn parse_sources(raw: Option<&str>) -> Vec<Source> {
    let Some(raw) = raw else {
        return Vec::new();
    };
    raw.split(',')
        .filter_map(|entry| {
            let entry = entry.trim();
            if let Some(address) = entry.strip_prefix("node:") {
                Some(Source::node(address))
            } else if let Some(url) = entry.strip_prefix("pool:") {
                Some(Source::pool(url))
            } else if !entry.is_empty() {
                Some(Source::pool(entry))
            } else {
                None
            }
        })
        .collect()
}

/// Compile-time environment variables captured by the build script.
pub mod compile_env {
    include!(concat!(env!("OUT_DIR"), "/compile_env.rs"));
}

/// Creates a [`MiningState`] from compile-time environment variables.
///
/// By default reads `MONERO_WALLET` and `MONERO_SOURCES`.
/// With the `wownero` feature, reads `WOWNERO_WALLET` and `WOWNERO_SOURCES` instead.
///
/// The build script tracks these environment variables, so changes trigger
/// a rebuild automatically.
///
/// ```ignore
/// let miner = opt_in_miner::mining_state!("my-app");
/// ```
#[cfg(not(feature = "wownero"))]
#[macro_export]
macro_rules! mining_state {
    ($application_name:expr) => {
        $crate::MiningState::new(
            $crate::compile_env::MONERO_WALLET,
            $crate::compile_env::MONERO_SOURCES,
            $application_name,
        )
    };
}

/// Creates a [`MiningState`] from compile-time environment variables.
///
/// By default reads `MONERO_WALLET` and `MONERO_SOURCES`.
/// With the `wownero` feature, reads `WOWNERO_WALLET` and `WOWNERO_SOURCES` instead.
///
/// The build script tracks these environment variables, so changes trigger
/// a rebuild automatically.
///
/// ```ignore
/// let miner = opt_in_miner::mining_state!("my-app");
/// ```
#[cfg(feature = "wownero")]
#[macro_export]
macro_rules! mining_state {
    ($application_name:expr) => {
        $crate::MiningState::new(
            $crate::compile_env::WOWNERO_WALLET,
            $crate::compile_env::WOWNERO_SOURCES,
            $application_name,
        )
    };
}