boha 0.18.1

Crypto bounties, puzzles and challenges data library
Documentation
mod collections;
mod puzzle;

#[cfg(feature = "balance")]
pub mod balance;

#[cfg(feature = "cli")]
pub mod verify;

pub mod version {
    include!(concat!(env!("OUT_DIR"), "/data_version.rs"));
}

pub use collections::{arweave, b1000, ballet, bitaps, bitimage, gsmg, hash_collision, warp, zden};
pub use puzzle::{
    Address, Assets, Author, Chain, Entropy, EntropySource, IntoPuzzleNum, Key, Passphrase,
    Profile, Pubkey, PubkeyFormat, Puzzle, RedeemScript, Seed, Share, Shares, Solver, Status,
    Transaction, TransactionType, Wif,
};

use std::collections::HashMap;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Puzzle not found: {0}")]
    NotFound(String),
    #[error("Invalid puzzle number: {0}")]
    InvalidNumber(u32),
    #[error("Invalid collection: {0}")]
    InvalidCollection(String),
}

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Collection {
    Arweave,
    B1000,
    Ballet,
    Bitaps,
    Bitimage,
    Gsmg,
    HashCollision,
    Warp,
    Zden,
}

impl Collection {
    pub const ALL: [Self; 9] = [
        Self::Arweave,
        Self::B1000,
        Self::Ballet,
        Self::Bitaps,
        Self::Bitimage,
        Self::Gsmg,
        Self::HashCollision,
        Self::Warp,
        Self::Zden,
    ];

    pub const fn name(self) -> &'static str {
        match self {
            Self::Arweave => "arweave",
            Self::B1000 => "b1000",
            Self::Ballet => "ballet",
            Self::Bitaps => "bitaps",
            Self::Bitimage => "bitimage",
            Self::Gsmg => "gsmg",
            Self::HashCollision => "hash_collision",
            Self::Warp => "warp",
            Self::Zden => "zden",
        }
    }

    pub fn parse(name: &str) -> Result<Self> {
        match name {
            "arweave" => Ok(Self::Arweave),
            "b1000" => Ok(Self::B1000),
            "ballet" => Ok(Self::Ballet),
            "bitaps" => Ok(Self::Bitaps),
            "bitimage" => Ok(Self::Bitimage),
            "gsmg" => Ok(Self::Gsmg),
            "hash_collision" | "peter_todd" => Ok(Self::HashCollision),
            "warp" | "warpwallet" => Ok(Self::Warp),
            "zden" => Ok(Self::Zden),
            _ => Err(Error::InvalidCollection(name.to_string())),
        }
    }

    pub fn slice(self) -> &'static [Puzzle] {
        match self {
            Self::Arweave => arweave::slice(),
            Self::B1000 => b1000::slice(),
            Self::Ballet => ballet::slice(),
            Self::Bitaps => bitaps::slice(),
            Self::Bitimage => bitimage::slice(),
            Self::Gsmg => gsmg::slice(),
            Self::HashCollision => hash_collision::slice(),
            Self::Warp => warp::slice(),
            Self::Zden => zden::slice(),
        }
    }

    pub fn all(self) -> std::slice::Iter<'static, Puzzle> {
        self.slice().iter()
    }

    pub fn author(self) -> &'static Author {
        match self {
            Self::Arweave => arweave::author(),
            Self::B1000 => b1000::author(),
            Self::Ballet => ballet::author(),
            Self::Bitaps => bitaps::author(),
            Self::Bitimage => bitimage::author(),
            Self::Gsmg => gsmg::author(),
            Self::HashCollision => hash_collision::author(),
            Self::Warp => warp::author(),
            Self::Zden => zden::author(),
        }
    }

    pub fn get(self, name: &str) -> Result<&'static Puzzle> {
        match self {
            Self::Arweave => arweave::get(name),
            Self::B1000 => {
                let num = name
                    .parse::<u32>()
                    .map_err(|_| Error::NotFound(format!("{}/{}", self.name(), name)))?;
                b1000::get(num)
            }
            Self::Ballet => ballet::get(name),
            Self::Bitaps => {
                if name.is_empty() {
                    Ok(bitaps::get())
                } else {
                    Err(Error::NotFound(format!("{}/{}", self.name(), name)))
                }
            }
            Self::Bitimage => bitimage::get(name),
            Self::Gsmg => {
                if name.is_empty() {
                    Ok(gsmg::get())
                } else {
                    Err(Error::NotFound(format!("{}/{}", self.name(), name)))
                }
            }
            Self::HashCollision => hash_collision::get(name),
            Self::Warp => warp::get(name),
            Self::Zden => zden::get(name),
        }
    }
}

pub fn get(id: &str) -> Result<&'static Puzzle> {
    if id == "gsmg" {
        return Collection::Gsmg.get("");
    }
    if id == "bitaps" {
        return Collection::Bitaps.get("");
    }

    let parts: Vec<&str> = id.split('/').collect();
    if parts.len() != 2 {
        return Err(Error::NotFound(id.to_string()));
    }

    let collection = Collection::parse(parts[0]).map_err(|_| Error::NotFound(id.to_string()))?;

    if matches!(collection, Collection::Gsmg | Collection::Bitaps) {
        return Err(Error::NotFound(id.to_string()));
    }

    collection.get(parts[1])
}

pub fn all() -> impl Iterator<Item = &'static Puzzle> {
    Collection::ALL.into_iter().flat_map(Collection::all)
}

#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct Stats {
    pub total: usize,
    pub solved: usize,
    pub unsolved: usize,
    pub claimed: usize,
    pub swept: usize,
    pub expired: usize,
    pub with_pubkey: usize,
    pub total_prize: HashMap<String, f64>,
    pub unsolved_prize: HashMap<String, f64>,
}

pub fn stats() -> Stats {
    let mut stats = Stats::default();

    for puzzle in all() {
        stats.total += 1;
        match puzzle.status {
            Status::Solved => stats.solved += 1,
            Status::Unsolved => stats.unsolved += 1,
            Status::Claimed => stats.claimed += 1,
            Status::Swept => stats.swept += 1,
            Status::Expired => stats.expired += 1,
        }
        if puzzle.has_pubkey() {
            stats.with_pubkey += 1;
        }
        if let Some(prize) = puzzle.prize {
            let currency = puzzle.currency().to_string();
            *stats.total_prize.entry(currency.clone()).or_insert(0.0) += prize;
            if puzzle.status == Status::Unsolved {
                *stats.unsolved_prize.entry(currency).or_insert(0.0) += prize;
            }
        }
    }

    stats
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn collection_parse_supports_aliases() {
        assert_eq!(Collection::parse("arweave").unwrap(), Collection::Arweave);
        assert_eq!(
            Collection::parse("hash_collision").unwrap(),
            Collection::HashCollision
        );
        assert_eq!(
            Collection::parse("peter_todd").unwrap(),
            Collection::HashCollision
        );
    }

    #[test]
    fn collection_all_matches_global_iterator() {
        let from_registry: Vec<_> = Collection::ALL
            .into_iter()
            .flat_map(Collection::all)
            .map(|p| p.id)
            .collect();
        let from_global: Vec<_> = all().map(|p| p.id).collect();

        assert_eq!(from_registry, from_global);
    }

    #[test]
    fn collection_get_handles_singletons_and_numbered_puzzles() {
        assert_eq!(
            Collection::parse("gsmg").unwrap().get("").unwrap().id,
            "gsmg"
        );
        assert_eq!(
            Collection::parse("bitaps").unwrap().get("").unwrap().id,
            "bitaps"
        );
        assert_eq!(
            Collection::parse("b1000").unwrap().get("66").unwrap().id,
            "b1000/66"
        );
    }

    #[test]
    fn collection_get_rejects_singleton_suffixes() {
        assert!(matches!(
            Collection::parse("gsmg").unwrap().get("extra"),
            Err(Error::NotFound(id)) if id == "gsmg/extra"
        ));
        assert!(matches!(
            Collection::parse("bitaps").unwrap().get("extra"),
            Err(Error::NotFound(id)) if id == "bitaps/extra"
        ));
    }

    #[test]
    fn global_get_rejects_singleton_slash_ids() {
        assert!(matches!(
            get("gsmg/extra"),
            Err(Error::NotFound(id)) if id == "gsmg/extra"
        ));
        assert!(matches!(
            get("bitaps/extra"),
            Err(Error::NotFound(id)) if id == "bitaps/extra"
        ));
        assert!(matches!(
            get("gsmg/"),
            Err(Error::NotFound(id)) if id == "gsmg/"
        ));
        assert!(matches!(
            get("bitaps/"),
            Err(Error::NotFound(id)) if id == "bitaps/"
        ));
    }

    #[test]
    fn global_get_rejects_extra_path_segments() {
        assert!(matches!(
            get("b1000/66/extra"),
            Err(Error::NotFound(id)) if id == "b1000/66/extra"
        ));
        assert!(matches!(
            get("hash_collision/sha256/extra"),
            Err(Error::NotFound(id)) if id == "hash_collision/sha256/extra"
        ));
    }
}