hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use crate::error::{WalletError, WalletResult};

pub(crate) const HARDENED: u32 = 0x8000_0000;

pub fn parse_bip32_path(path: &str) -> WalletResult<Vec<u32>> {
    let trimmed = path.trim();
    if trimmed.is_empty() {
        return Err(WalletError::InvalidBip32Path("path is empty".to_string()));
    }

    let body = if trimmed == "m" {
        return Ok(Vec::new());
    } else if let Some(rest) = trimmed.strip_prefix("m/") {
        rest
    } else {
        trimmed
    };

    let mut result = Vec::new();
    for segment in body.split('/') {
        if segment.is_empty() {
            return Err(WalletError::InvalidBip32Path(format!(
                "empty segment in '{path}'"
            )));
        }

        let hardened = segment.ends_with('\'') || segment.ends_with('h') || segment.ends_with('H');
        let number_str = if hardened {
            &segment[..segment.len() - 1]
        } else {
            segment
        };

        let value = number_str.parse::<u32>().map_err(|_| {
            WalletError::InvalidBip32Path(format!("invalid segment '{segment}' in '{path}'"))
        })?;
        if value >= HARDENED {
            return Err(WalletError::InvalidBip32Path(format!(
                "segment '{segment}' is out of range (must be < 2^31)"
            )));
        }

        result.push(if hardened { value | HARDENED } else { value });
    }

    Ok(result)
}

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

    #[test]
    fn parses_hardened_and_non_hardened_segments() {
        let path = parse_bip32_path("m/44'/60'/0'/0/0").unwrap();
        assert_eq!(path, vec![44 | HARDENED, 60 | HARDENED, HARDENED, 0, 0]);
    }

    #[test]
    fn supports_h_suffix_and_path_without_m_prefix() {
        let path = parse_bip32_path("44h/1/2H").unwrap();
        assert_eq!(path, vec![44 | HARDENED, 1, 2 | HARDENED]);
    }
}