zinc-wallet-cli 0.4.0

Agent-first Bitcoin + Ordinals CLI wallet with account-based taproot ordinals + native segwit payment addresses (optional human mode)
use crate::error::AppError;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LockMetadata {
    pub pid: u32,
    pub created_at_unix: u64,
}

pub struct ProfileLock {
    path: PathBuf,
    _file: fs::File,
}

impl ProfileLock {
    pub fn acquire(profile_path: &Path) -> Result<Self, crate::error::AppError> {
        let lock_path = profile_path.with_extension("lock");

        if let Some(parent) = lock_path.parent() {
            crate::paths::create_secure_dir_all(parent).map_err(|e| {
                crate::error::AppError::Config(format!("failed to create lock dir: {e}"))
            })?;
        }

        let mut options = fs::OpenOptions::new();
        options.write(true).create_new(true);

        #[cfg(unix)]
        {
            use std::os::unix::fs::OpenOptionsExt;
            options.mode(0o600);
        }

        let file = options
            .open(&lock_path)
            .map_err(|_| AppError::Config("profile is locked by another instance".to_string()))?;

        let metadata = LockMetadata {
            pid: process::id(),
            created_at_unix: now_unix(),
        };
        let bytes = serde_json::to_vec(&metadata)
            .map_err(|e| AppError::Internal(format!("failed to serialize lock metadata: {e}")))?;
        (&file)
            .write_all(&bytes)
            .map_err(|e| AppError::Config(format!("failed to write lock metadata: {e}")))?;
        (&file)
            .flush()
            .map_err(|e| AppError::Config(format!("failed to flush lock metadata: {e}")))?;

        Ok(Self {
            path: lock_path,
            _file: file,
        })
    }
}

impl Drop for ProfileLock {
    fn drop(&mut self) {
        let _ = fs::remove_file(&self.path);
    }
}

#[must_use]
pub fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}