//! A minimal self-custodial Ergo wallet: generate a BIP39 mnemonic, derive
//! the mainnet P2PK address via ergo-lib (the reference implementation โ€” no
//! hand-rolled crypto), and keep the seed phrase in the macOS Keychain.

use ergo_lib::ergotree_ir::chain::address::{Address, AddressEncoder, NetworkPrefix};
use ergo_lib::wallet::derivation_path::{ChildIndexHardened, ChildIndexNormal, DerivationPath};
use ergo_lib::wallet::ext_secret_key::ExtSecretKey;
use ergo_lib::wallet::mnemonic::Mnemonic as ErgoMnemonic;
use std::path::PathBuf;

pub mod send;

/// A wallet: the mnemonic and the address derived from it.
pub struct Wallet {
    pub mnemonic: String,
    pub address: String,
}

impl Wallet {
    /// Create a fresh 15-word wallet.
    pub fn generate() -> Result<Wallet, String> {
        let m = bip39::Mnemonic::generate(15).map_err(|e| format!("mnemonic gen: {e}"))?;
        Wallet::from_phrase(&m.to_string())
    }

    /// Rebuild a wallet from an existing phrase.
    pub fn from_phrase(phrase: &str) -> Result<Wallet, String> {
        let address = derive_p2pk_address(phrase)?;
        Ok(Wallet { mnemonic: phrase.to_string(), address })
    }

    /// Load the stored wallet, or generate + store a new one on first run.
    ///
    /// The seed lives in a `0600` file under Application Support. A macOS
    /// Keychain item would be encrypted at rest but prompts for the login
    /// password whenever the app's code signature changes โ€” hostile to a
    /// one-click miner. The `back up your seed` flow makes the real backup
    /// the user's paper copy; this file is just so the app remembers itself.
    pub fn load_or_create() -> Result<Wallet, String> {
        if let Some(phrase) = seed_read()? {
            return Wallet::from_phrase(phrase.trim());
        }
        let w = Wallet::generate()?;
        seed_write(&w.mnemonic)?;
        Ok(w)
    }
}

/// Check an address someone pasted, and hand back the clean form.
///
/// A pool pays to a key, so a payout address has to be a mainnet P2PK. Saying
/// so here costs a second; finding out from the pool means a day of mining
/// credited to nothing. ergo-lib does the real work โ€” the encoding carries a
/// checksum and a network prefix, so a typo and a testnet address both fail.
pub fn validate_payout_address(s: &str) -> Result<String, String> {
    let s = s.trim();
    if s.is_empty() {
        return Err("paste an address".into());
    }
    let addr = AddressEncoder::new(NetworkPrefix::Mainnet)
        .parse_address_from_str(s)
        .map_err(|_| {
            if s.starts_with('3') {
                "that is a testnet address".to_string()
            } else {
                "not a valid Ergo address".to_string()
            }
        })?;
    match addr {
        Address::P2Pk(_) => Ok(s.to_string()),
        _ => Err("a pool pays to a P2PK address โ€” it starts with 9".into()),
    }
}

fn seed_path() -> Result<PathBuf, String> {
    let home = std::env::var("HOME").map_err(|_| "no HOME".to_string())?;
    Ok(PathBuf::from(home).join("Library/Application Support/ai.cyber.erga/seed"))
}

fn seed_read() -> Result<Option<String>, String> {
    let p = seed_path()?;
    match std::fs::read_to_string(&p) {
        Ok(s) if !s.trim().is_empty() => Ok(Some(s)),
        Ok(_) => Ok(None),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(format!("read seed: {e}")),
    }
}

fn seed_write(phrase: &str) -> Result<(), String> {
    let p = seed_path()?;
    if let Some(dir) = p.parent() {
        std::fs::create_dir_all(dir).map_err(|e| format!("mkdir: {e}"))?;
    }
    std::fs::write(&p, phrase).map_err(|e| format!("write seed: {e}"))?;
    // owner-only permissions
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600));
    }
    Ok(())
}

/// m/44'/429'/0'/0/0 โ†’ P2PK mainnet address (EIP-3).
fn derive_p2pk_address(phrase: &str) -> Result<String, String> {
    let seed = ErgoMnemonic::to_seed(phrase, "");
    let master = ExtSecretKey::derive_master(seed).map_err(|e| format!("master: {e:?}"))?;
    let path = DerivationPath::new(
        ChildIndexHardened::from_31_bit(0).map_err(|e| format!("{e:?}"))?,
        vec![
            ChildIndexNormal::normal(0).map_err(|e| format!("{e:?}"))?,
            ChildIndexNormal::normal(0).map_err(|e| format!("{e:?}"))?,
        ],
    );
    let key = master.derive(path).map_err(|e| format!("derive: {e:?}"))?;
    let ext_pub = key.public_key().map_err(|e| format!("pubkey: {e:?}"))?;
    let address: Address = ext_pub.into();
    Ok(AddressEncoder::new(NetworkPrefix::Mainnet).address_to_str(&address))
}

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

    #[test]
    fn payout_address_accepts_a_real_one_and_refuses_the_rest() {
        // a live mainnet P2PK
        let good = "9gm4b1ZU3bSdnxFpXTQtpAqbDGB8FcwVS3vn43nxnwdj1H6mN8r";
        assert_eq!(validate_payout_address(&format!("  {good}  ")).unwrap(), good);

        // one character changed: the checksum catches it
        let typo = "9gm4b1ZU3bSdnxFpXTQtpAqbDGB8FcwVS3vn43nxnwdj1H6mN8q";
        assert!(validate_payout_address(typo).is_err(), "a typo must not pass");

        for bad in ["", "   ", "not an address", "0x1234abcd"] {
            assert!(validate_payout_address(bad).is_err(), "accepted {bad:?}");
        }
    }

    #[test]
    fn a_generated_wallet_validates_as_a_payout_address() {
        let w = Wallet::generate().unwrap();
        assert_eq!(validate_payout_address(&w.address).unwrap(), w.address);
    }

    #[test]
    fn generated_address_is_mainnet_p2pk() {
        let w = Wallet::generate().unwrap();
        assert_eq!(w.mnemonic.split_whitespace().count(), 15);
        // mainnet P2PK addresses begin with '9'
        assert!(w.address.starts_with('9'), "got {}", w.address);
        // deterministic: same phrase โ†’ same address
        let again = Wallet::from_phrase(&w.mnemonic).unwrap();
        assert_eq!(w.address, again.address);
    }
}

Homonyms

warriors/trisha/wgpu/lib.rs
soft3/glia/run/lib.rs
soft3/mir/src/lib.rs
soft3/foculus/src/lib.rs
cyb/optica/src/lib.rs
soft3/strata/src/lib.rs
warriors/trisha/rs/lib.rs
cyb/shell/src/lib.rs
cyb/core/src/lib.rs
soft3/glia/import/lib.rs
warriors/trisha/honeycrisp/lib.rs
neural/trident/src/lib.rs
soft3/crate/src/lib.rs
cyb/honeycrisp/src/lib.rs
cyb/prysm/rs/lib.rs
soft3/lens/src/lib.rs
soft3/tru/rs/lib.rs
soft3/cybergraph/src/lib.rs
soft3/nox/rs/lib.rs
soft3/mudra/src/lib.rs
soft3/radio/cyber-bao/src/lib.rs
neural/rs/dialect/src/lib.rs
soft3/lens/assayer/src/lib.rs
cyb/honeycrisp/aruminium/src/lib.rs
soft3/lens/brakedown/src/lib.rs
soft3/bbg/rs/src/lib.rs
neural/rs/sigil/src/lib.rs
soft3/radio/iroh-willow/src/lib.rs
cyb/crates/cyb/src/lib.rs
neural/rs/link/src/lib.rs
cyb/honeycrisp/unimem/src/lib.rs
soft3/lens/porphyry/src/lib.rs
soft3/radio/iroh-docs/src/lib.rs
soft3/radio/iroh-car/src/lib.rs
soft3/radio/iroh-dns-server/src/lib.rs
soft3/radio/iroh/src/lib.rs
cyb/crates/cyb-reserve/src/lib.rs
soft3/strata/ext/src/lib.rs
soft3/radio/iroh-relay/src/lib.rs
neural/rune/rs/parse/lib.rs
neural/rs/codegen/src/lib.rs
soft3/radio/iroh-blobs/src/lib.rs
soft3/zheng/rs/src/lib.rs
soft3/strata/kuro/rs/lib.rs
soft3/tok/rs/src/lib.rs
soft3/conformance/rs/src/lib.rs
soft3/radio/iroh-base/src/lib.rs
neural/rs/macros/src/lib.rs
neural/rs/darwin-sys/src/lib.rs
soft3/radio/iroh-ffi/src/lib.rs
cyb/honeycrisp/rane/src/lib.rs
neural/rune/rs/prysm/lib.rs
soft3/lens/core/src/lib.rs
neural/rs/mir-format/src/lib.rs
neural/rs/core/src/lib.rs
soft3/strata/nebu/rs/lib.rs
neural/rune/rs/compile/lib.rs
neural/rune/rs/interp/lib.rs
soft3/strata/compute/src/lib.rs
soft3/lens/binius/src/lib.rs
soft3/strata/proof/src/lib.rs
neural/eidos/rs/src/lib.rs
neural/rune/rs/mold/lib.rs
soft3/strata/core/src/lib.rs
soft3/hemera/wgsl/src/lib.rs
neural/rune/rs/subject/lib.rs
soft3/soma/kernel/src/lib.rs
neural/rune/rs/lower/lib.rs
cyb/honeycrisp/acpu/src/lib.rs
neural/rune/rs/parse-pure/lib.rs
soft3/radio/iroh-gossip/src/lib.rs
neural/rune/rs/lex/lib.rs
soft3/hemera/rs/src/lib.rs
neural/rune/rs/ast/lib.rs
soft3/lens/ikat/src/lib.rs
soft3/tape/impl/rust/src/lib.rs
cyb/evy/crates/evy_radio/src/lib.rs
neural/inf/rs/lex/src/lib.rs
cyb/evy/forks/bevy_animation/src/lib.rs
neural/inf/rs/oracle/src/lib.rs
neural/trident/editor/zed/src/lib.rs
warriors/trisha/.vendor/twenty-first/src/lib.rs
warriors/erga/rs/pool/src/lib.rs
cyb/wysm/crates/c_api/src/lib.rs
cyb/evy/forks/bevy_anti_alias/src/lib.rs
soft3/strata/trop/rs/src/lib.rs
cyb/evy/forks/bevy_pbr/src/lib.rs
soft3/radio/quinn/quinn-udp/src/lib.rs
warriors/trisha/.vendor/triton-vm/src/lib.rs
soft3/strata/genies/rs/src/lib.rs
soft3/radio/nettools/portmapper/src/lib.rs
soft3/lytics/rs/core/src/lib.rs
cyb/wysm/crates/c_api/macro/lib.rs
soft3/strata/kuro/wgsl/src/lib.rs
cyb/evy/forks/bevy_tasks/src/lib.rs
neural/inf/rs/plan/src/lib.rs
cyb/wysm/crates/ir/src/lib.rs
cyb/evy/forks/bevy_gizmos/src/lib.rs
soft3/strata/genies/wgsl/src/lib.rs
warriors/erga/rs/autolykos/src/lib.rs
cyb/evy/forks/bevy_sprite/src/lib.rs
cyb/evy/crates/evy_prysm_core/src/lib.rs
warriors/trisha/.vendor/triton-constraint-circuit/src/lib.rs
cyb/evy/forks/bevy_transform/src/lib.rs
cyb/evy/crates/evy_engine_dispatch/src/lib.rs
warriors/trisha/.vendor/triton-air/src/lib.rs
cyb/evy/forks/bevy_diagnostic/src/lib.rs
cyb/wysm/crates/fuzz/src/lib.rs
neural/inf/rs/source/src/lib.rs
cyb/wysm/crates/wasmi/src/lib.rs
cyb/evy/forks/bevy_ecs/src/lib.rs
soft3/strata/jali/rs/src/lib.rs
warriors/trisha/.vendor/triton-isa/src/lib.rs
soft3/lytics/rs/event/src/lib.rs
soft3/strata/trop/wgsl/src/lib.rs
cyb/evy/crates/evy_dialect/src/lib.rs
warriors/trisha/.vendor/triton-constraint-builder/src/lib.rs
cyb/evy/crates/evy_platform_caps/src/lib.rs
cyb/evy/forks/bevy_sprite_render/src/lib.rs
cyb/evy/forks/bevy_gizmos_render/src/lib.rs
cyb/wysm/crates/collections/src/lib.rs
neural/inf/rs/ast/src/lib.rs
cyb/evy/forks/bevy_mesh/src/lib.rs
soft3/radio/quinn/bench/src/lib.rs
cyb/wysm/crates/c_api/artifact/lib.rs
warriors/erga/rs/rtable-bench/src/lib.rs
cyb/wysm/crates/core/src/lib.rs
soft3/strata/jali/wgsl/src/lib.rs
soft3/radio/iroh/bench/src/lib.rs
warriors/erga/rs/blake-bench/src/lib.rs
neural/inf/rs/parse/src/lib.rs
cyb/evy/forks/bevy_post_process/src/lib.rs
soft3/radio/quinn/quinn/src/lib.rs
cyb/evy/crates/evy_ecs_storage/src/lib.rs
warriors/erga/rs/app/src/lib.rs
neural/inf/rs/lower/src/lib.rs
cyb/evy/forks/bevy_core_pipeline/src/lib.rs
cyb/evy/forks/bevy_render/src/lib.rs
neural/rs/tests/macro-integration/src/lib.rs
soft3/radio/iroh-ffi/iroh-js/src/lib.rs
cyb/evy/forks/bevy_image/src/lib.rs
cyb/evy/crates/evy_engine_tasks/src/lib.rs
warriors/erga/rs/mine-bench/src/lib.rs
soft3/strata/nebu/wgsl/src/lib.rs
soft3/radio/nettools/netwatch/src/lib.rs
warriors/erga/rs/miner/src/lib.rs
neural/inf/rs/value/src/lib.rs
cyb/wysm/crates/wast/src/lib.rs
soft3/radio/tests/integration/src/lib.rs
cyb/wysm/crates/wasi/src/lib.rs
cyb/evy/crates/evy_diagnostic/src/lib.rs
soft3/radio/quinn/perf/src/lib.rs
soft3/radio/quinn/quinn-proto/src/lib.rs
cyb/evy/forks/naga/src/lib.rs
neural/inf/rs/eval/src/lib.rs
cyb/evy/crates/evy_engine_core/src/lib.rs
neural/inf/rs/cozo/cozo-lib-python/src/lib.rs
neural/inf/rs/cozo/cozo-lib-swift/src/lib.rs
neural/inf/rs/cozo/cozorocks/src/lib.rs
neural/inf/rs/cozo/cozo-lib-java/src/lib.rs
neural/inf/rs/cozo/cozo-lib-c/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/src/lib.rs
neural/inf/rs/cozo/cozo-lib-nodejs/src/lib.rs
neural/inf/rs/cozo/cozo-core/src/lib.rs
neural/inf/rs/cozo/cozo-lib-wasm/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/unimem/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/rane/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/unimem/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/acpu/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-ad6c77c38e86bc291/aruminium/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/aruminium/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/rane/src/lib.rs
cyb/honeycrisp/.claude/worktrees/agent-aa1259cb10112b22a/acpu/src/lib.rs

Graph