use crate::wallet;
const WALLET_FILE: &str = ".lh_wallet";
fn install_at_rest(mnemonic: &bip39::Mnemonic) {
use zeroize::Zeroize as _;
let mut entropy = mnemonic.to_entropy();
let key = wallet::at_rest_key_from_entropy(&entropy);
entropy.zeroize();
super::install_at_rest_encryption(key);
}
pub(crate) struct MasterWallet {
pub(crate) mnemonic: bip39::Mnemonic,
pub(crate) signer: k256::ecdsa::SigningKey,
pub(crate) address: [u8; 20],
}
impl MasterWallet {
pub(crate) fn address_hex(&self) -> String {
crate::encoding::bytes_to_hex_str(&self.address)
}
}
pub(crate) async fn load() -> Option<MasterWallet> {
let fs = super::shared_opfs();
let bytes = fs.read(WALLET_FILE).await.ok()?;
if bytes.is_empty() {
return None;
}
let phrase = String::from_utf8(bytes).ok()?;
let w = restore_from_phrase(&phrase).ok()?;
install_at_rest(&w.mnemonic);
Some(w)
}
pub(crate) async fn create_and_persist() -> Result<MasterWallet, String> {
super::debuglog::log("create wallet: generating mnemonic");
let fs = super::shared_opfs();
let (mnemonic, signer) = wallet::generate_with_mnemonic();
super::debuglog::log("create wallet: writing seed (opfs write)");
fs.write_atomic(WALLET_FILE, mnemonic.to_string().as_bytes())
.await
.map_err(|e| format!("wallet save: {e}"))?;
super::debuglog::log("create wallet: seed written — installing at-rest key");
install_at_rest(&mnemonic);
let address = wallet::address(&signer);
Ok(MasterWallet {
mnemonic,
signer,
address,
})
}
pub(crate) async fn generate_in_memory() -> Result<MasterWallet, String> {
super::debuglog::log("create wallet: generating mnemonic (in memory, no disk)");
let (mnemonic, signer) = wallet::generate_with_mnemonic();
let address = wallet::address(&signer);
let app_copy = MasterWallet {
mnemonic: mnemonic.clone(),
signer: signer.clone(),
address,
};
super::APP.with(|cell| cell.borrow_mut().wallet = Some(app_copy));
Ok(MasterWallet {
mnemonic,
signer,
address,
})
}
pub(crate) async fn persist_current_seed() -> Result<(), String> {
let mnemonic = super::APP
.with(|cell| cell.borrow().wallet.as_ref().map(|w| w.mnemonic.clone()))
.ok_or_else(|| "no in-memory wallet to persist".to_string())?;
super::debuglog::log("persist seed: writing seed (opfs write, post-payment)");
let fs = super::shared_opfs();
fs.write_atomic(WALLET_FILE, mnemonic.to_string().as_bytes())
.await
.map_err(|e| format!("wallet save: {e}"))?;
super::debuglog::log("persist seed: seed written — installing at-rest key");
install_at_rest(&mnemonic);
Ok(())
}
pub(crate) async fn import(phrase: &str) -> Result<MasterWallet, String> {
let mnemonic = wallet::mnemonic_from_phrase(phrase)?;
let fs = super::shared_opfs();
fs.write_atomic(WALLET_FILE, mnemonic.to_string().as_bytes())
.await
.map_err(|e| format!("wallet save: {e}"))?;
install_at_rest(&mnemonic);
let signer = wallet::signer_from_mnemonic(&mnemonic);
let address = wallet::address(&signer);
Ok(MasterWallet {
mnemonic,
signer,
address,
})
}
const DEVICE_KEY_FILE: &str = ".lh_device_key";
pub(crate) async fn persist_device_key(private_key_hex: &str) -> Result<(), String> {
let fs = super::shared_opfs();
let bytes = private_key_hex.as_bytes();
let data = super::encryption::seal(bytes)
.await
.unwrap_or_else(|| bytes.to_vec());
fs.write_atomic(DEVICE_KEY_FILE, &data)
.await
.map_err(|e| format!("device key save: {e}"))
}
pub(crate) async fn load_device_key() -> Option<k256::ecdsa::SigningKey> {
let fs = super::shared_opfs();
let bytes = fs.read(DEVICE_KEY_FILE).await.ok()?;
if bytes.is_empty() {
return None;
}
let plain = super::encryption::open(&bytes).await.unwrap_or(bytes);
let hex = String::from_utf8(plain).ok()?;
wallet::from_private_key_hex(hex.trim()).ok()
}
const LINKED_OWNER_FILE: &str = ".lh_linked_owner";
pub(crate) async fn persist_linked_owner(owner_hex: &str) -> Result<(), String> {
let fs = super::shared_opfs();
fs.write_atomic(LINKED_OWNER_FILE, owner_hex.trim().as_bytes())
.await
.map_err(|e| format!("linked owner save: {e}"))
}
pub(crate) async fn load_linked_owner() -> Option<String> {
let fs = super::shared_opfs();
let bytes = fs.read(LINKED_OWNER_FILE).await.ok()?;
let s = String::from_utf8(bytes).ok()?;
let t = s.trim();
if t.is_empty() || !t.starts_with("0x") {
None
} else {
Some(t.to_string())
}
}
pub(crate) async fn storage_is_volatile() -> bool {
use wasm_bindgen_futures::JsFuture;
let Some(win) = web_sys::window() else { return false };
let storage = win.navigator().storage();
if let Ok(promise) = storage.persist() {
if let Ok(val) = JsFuture::from(promise).await {
if val.as_bool() == Some(true) {
return false; }
}
}
if let Ok(promise) = storage.persisted() {
if let Ok(val) = JsFuture::from(promise).await {
if val.as_bool() == Some(true) {
return false;
}
}
}
if let Ok(promise) = storage.estimate() {
if let Ok(val) = JsFuture::from(promise).await {
if let Some(quota) = js_sys::Reflect::get(&val, &"quota".into())
.ok()
.and_then(|q| q.as_f64())
{
return quota > 0.0 && quota < 32_000_000.0;
}
}
}
false
}
fn restore_from_phrase(phrase: &str) -> Result<MasterWallet, String> {
let mnemonic = wallet::mnemonic_from_phrase(phrase)?;
let signer = wallet::signer_from_mnemonic(&mnemonic);
let address = wallet::address(&signer);
Ok(MasterWallet {
mnemonic,
signer,
address,
})
}