boha 0.5.0

Crypto bounties, puzzles and challenges data library
Documentation
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::Path;

#[derive(Debug, Deserialize)]
struct Btc1000Metadata {
    source_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct Btc1000File {
    metadata: Option<Btc1000Metadata>,
    puzzles: Vec<Btc1000Puzzle>,
}

#[derive(Debug, Deserialize)]
struct Btc1000Puzzle {
    bits: u16,
    address: String,
    h160: Option<String>,
    prize: Option<f64>,
    status: String,
    #[allow(dead_code)]
    has_pubkey: Option<bool>,
    private_key: Option<String>,
    public_key: Option<String>,
    pubkey_format: Option<String>,
    start_date: Option<String>,
    solve_date: Option<String>,
    source_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct HashCollisionMetadata {
    source_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct HashCollisionFile {
    metadata: Option<HashCollisionMetadata>,
    puzzles: Vec<HashCollisionPuzzle>,
}

#[derive(Debug, Deserialize)]
struct HashCollisionPuzzle {
    name: String,
    address: String,
    status: String,
    redeem_script: String,
    script_hash: Option<String>,
    prize: Option<f64>,
    start_date: Option<String>,
    solve_date: Option<String>,
    source_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GsmgMetadata {
    source_url: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GsmgFile {
    metadata: Option<GsmgMetadata>,
    puzzle: GsmgPuzzle,
}

#[derive(Debug, Deserialize)]
struct GsmgPuzzle {
    address: String,
    h160: Option<String>,
    status: String,
    prize: Option<f64>,
    public_key: Option<String>,
    pubkey_format: Option<String>,
    start_date: Option<String>,
    solve_date: Option<String>,
    source_url: Option<String>,
}

fn main() {
    println!("cargo:rerun-if-changed=data/b1000.toml");
    println!("cargo:rerun-if-changed=data/hash_collision.toml");
    println!("cargo:rerun-if-changed=data/gsmg.toml");

    let out_dir = env::var("OUT_DIR").unwrap();

    generate_b1000(&out_dir);
    generate_hash_collision(&out_dir);
    generate_gsmg(&out_dir);
}

fn generate_b1000(out_dir: &str) {
    let dest_path = Path::new(out_dir).join("b1000_data.rs");

    let toml_content =
        fs::read_to_string("data/b1000.toml").expect("Failed to read data/b1000.toml");

    let data: Btc1000File = toml::from_str(&toml_content).expect("Failed to parse b1000.toml");

    let default_source_url = data.metadata.as_ref().and_then(|m| m.source_url.as_ref());

    let mut output = String::new();
    output.push_str("static PUZZLES: &[Puzzle] = &[\n");

    for puzzle in &data.puzzles {
        let status = match puzzle.status.as_str() {
            "solved" => "Status::Solved",
            "claimed" => "Status::Claimed",
            "swept" => "Status::Swept",
            _ => "Status::Unsolved",
        };

        let pubkey = match (&puzzle.public_key, &puzzle.pubkey_format) {
            (Some(pk), Some(fmt)) => {
                let format = match fmt.as_str() {
                    "compressed" => "PubkeyFormat::Compressed",
                    "uncompressed" => "PubkeyFormat::Uncompressed",
                    _ => panic!("Invalid pubkey_format '{}' for puzzle {}", fmt, puzzle.bits),
                };
                format!("Some(Pubkey {{ key: \"{}\", format: {} }})", pk, format)
            }
            (None, None) => "None".to_string(),
            (Some(_), None) => panic!("Puzzle {} has public_key but no pubkey_format", puzzle.bits),
            (None, Some(_)) => panic!("Puzzle {} has pubkey_format but no public_key", puzzle.bits),
        };

        let private_key = match &puzzle.private_key {
            Some(pk) => format!("Some(\"{}\")", pk),
            None => "None".to_string(),
        };

        let prize = match puzzle.prize {
            Some(p) => format!("Some({:.6})", p),
            None => "None".to_string(),
        };

        let start_date = match &puzzle.start_date {
            Some(d) => format!("Some(\"{}\")", d),
            None => "None".to_string(),
        };

        let solve_date = match &puzzle.solve_date {
            Some(d) => format!("Some(\"{}\")", d),
            None => "None".to_string(),
        };

        let source_url = puzzle
            .source_url
            .as_ref()
            .or(default_source_url)
            .map(|url| format!("Some(\"{}\")", url))
            .unwrap_or_else(|| "None".to_string());

        let h160 = match &puzzle.h160 {
            Some(h) => format!("Some(\"{}\")", h),
            None => "None".to_string(),
        };

        output.push_str(&format!(
            r#"    Puzzle {{
        id: "b1000/{}",
        chain: Chain::Bitcoin,
        address: "{}",
        address_type: Some(AddressType::P2PKH),
        h160: {},
        status: {},
        pubkey: {},
        private_key: {},
        redeem_script: None,
        script_hash: None,
        bits: Some({}),
        prize: {},
        start_date: {},
        solve_date: {},
        source_url: {},
    }},
"#,
            puzzle.bits,
            puzzle.address,
            h160,
            status,
            pubkey,
            private_key,
            puzzle.bits,
            prize,
            start_date,
            solve_date,
            source_url,
        ));
    }

    output.push_str("];\n");

    fs::write(&dest_path, output).expect("Failed to write b1000_data.rs");
}

fn generate_hash_collision(out_dir: &str) {
    let dest_path = Path::new(out_dir).join("hash_collision_data.rs");

    let toml_content = fs::read_to_string("data/hash_collision.toml")
        .expect("Failed to read data/hash_collision.toml");

    let data: HashCollisionFile =
        toml::from_str(&toml_content).expect("Failed to parse hash_collision.toml");

    let default_source_url = data.metadata.as_ref().and_then(|m| m.source_url.as_ref());

    let mut output = String::new();
    output.push_str("static PUZZLES: &[Puzzle] = &[\n");

    for puzzle in &data.puzzles {
        let status = match puzzle.status.as_str() {
            "solved" => "Status::Solved",
            "claimed" => "Status::Claimed",
            "swept" => "Status::Swept",
            _ => "Status::Unsolved",
        };

        let prize = match puzzle.prize {
            Some(p) => format!("Some({:.6})", p),
            None => "None".to_string(),
        };

        let start_date = match &puzzle.start_date {
            Some(d) => format!("Some(\"{}\")", d),
            None => "None".to_string(),
        };

        let solve_date = match &puzzle.solve_date {
            Some(d) => format!("Some(\"{}\")", d),
            None => "None".to_string(),
        };

        let source_url = puzzle
            .source_url
            .as_ref()
            .or(default_source_url)
            .map(|url| format!("Some(\"{}\")", url))
            .unwrap_or_else(|| "None".to_string());

        let script_hash = match &puzzle.script_hash {
            Some(h) => format!("Some(\"{}\")", h),
            None => "None".to_string(),
        };

        output.push_str(&format!(
            r#"    Puzzle {{
        id: "hash_collision/{}",
        chain: Chain::Bitcoin,
        address: "{}",
        address_type: Some(AddressType::P2SH),
        h160: None,
        status: {},
        pubkey: None,
        private_key: None,
        redeem_script: Some("{}"),
        script_hash: {},
        bits: None,
        prize: {},
        start_date: {},
        solve_date: {},
        source_url: {},
    }},
"#,
            puzzle.name,
            puzzle.address,
            status,
            puzzle.redeem_script,
            script_hash,
            prize,
            start_date,
            solve_date,
            source_url,
        ));
    }

    output.push_str("];\n");

    fs::write(&dest_path, output).expect("Failed to write hash_collision_data.rs");
}

fn generate_gsmg(out_dir: &str) {
    let dest_path = Path::new(out_dir).join("gsmg_data.rs");

    let toml_content = fs::read_to_string("data/gsmg.toml").expect("Failed to read data/gsmg.toml");

    let data: GsmgFile = toml::from_str(&toml_content).expect("Failed to parse gsmg.toml");

    let puzzle = &data.puzzle;
    let default_source_url = data.metadata.as_ref().and_then(|m| m.source_url.as_ref());

    let status = match puzzle.status.as_str() {
        "solved" => "Status::Solved",
        "claimed" => "Status::Claimed",
        "swept" => "Status::Swept",
        _ => "Status::Unsolved",
    };

    let prize = match puzzle.prize {
        Some(p) => format!("Some({:.8})", p),
        None => "None".to_string(),
    };

    let start_date = match &puzzle.start_date {
        Some(d) => format!("Some(\"{}\")", d),
        None => "None".to_string(),
    };

    let solve_date = match &puzzle.solve_date {
        Some(d) => format!("Some(\"{}\")", d),
        None => "None".to_string(),
    };

    let source_url = puzzle
        .source_url
        .as_ref()
        .or(default_source_url)
        .map(|url| format!("Some(\"{}\")", url))
        .unwrap_or_else(|| "None".to_string());

    let pubkey = match (&puzzle.public_key, &puzzle.pubkey_format) {
        (Some(pk), Some(fmt)) => {
            let format = match fmt.as_str() {
                "compressed" => "PubkeyFormat::Compressed",
                "uncompressed" => "PubkeyFormat::Uncompressed",
                _ => panic!("Invalid pubkey_format '{}' for gsmg", fmt),
            };
            format!("Some(Pubkey {{ key: \"{}\", format: {} }})", pk, format)
        }
        (None, None) => "None".to_string(),
        (Some(_), None) => panic!("gsmg has public_key but no pubkey_format"),
        (None, Some(_)) => panic!("gsmg has pubkey_format but no public_key"),
    };

    let h160 = match &puzzle.h160 {
        Some(h) => format!("Some(\"{}\")", h),
        None => "None".to_string(),
    };

    let output = format!(
        r#"static PUZZLE: Puzzle = Puzzle {{
    id: "gsmg",
    chain: Chain::Bitcoin,
    address: "{}",
    address_type: Some(AddressType::P2PKH),
    h160: {},
    status: {},
    pubkey: {},
    private_key: None,
    redeem_script: None,
    script_hash: None,
    bits: None,
    prize: {},
    start_date: {},
    solve_date: {},
    source_url: {},
}};
"#,
        puzzle.address, h160, status, pubkey, prize, start_date, solve_date, source_url,
    );

    fs::write(&dest_path, output).expect("Failed to write gsmg_data.rs");
}