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;
pub struct Wallet {
pub mnemonic: String,
pub address: String,
}
impl 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())
}
pub fn from_phrase(phrase: &str) -> Result<Wallet, String> {
let address = derive_p2pk_address(phrase)?;
Ok(Wallet { mnemonic: phrase.to_string(), address })
}
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)
}
}
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}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600));
}
Ok(())
}
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() {
let good = "9gm4b1ZU3bSdnxFpXTQtpAqbDGB8FcwVS3vn43nxnwdj1H6mN8r";
assert_eq!(validate_payout_address(&format!(" {good} ")).unwrap(), good);
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);
assert!(w.address.starts_with('9'), "got {}", w.address);
let again = Wallet::from_phrase(&w.mnemonic).unwrap();
assert_eq!(w.address, again.address);
}
}