rustywallet_export/
csv.rs

1//! CSV export functions.
2
3use crate::error::{ExportError, Result};
4use crate::types::{CsvColumn, CsvOptions, Network};
5use crate::{export_wif, export_hex, HexOptions};
6use rustywallet_keys::prelude::PrivateKey;
7use rustywallet_keys::public_key::PublicKeyFormat;
8use rustywallet_address::{P2PKHAddress, Network as AddrNetwork};
9
10/// Export multiple private keys to CSV format.
11///
12/// # Example
13///
14/// ```rust
15/// use rustywallet_export::{export_csv, CsvOptions, CsvColumn, Network};
16/// use rustywallet_keys::prelude::PrivateKey;
17///
18/// let keys: Vec<PrivateKey> = (0..3).map(|_| PrivateKey::random()).collect();
19/// let csv = export_csv(&keys, CsvOptions::new()).unwrap();
20/// println!("{}", csv);
21/// ```
22pub fn export_csv(keys: &[PrivateKey], options: CsvOptions) -> Result<String> {
23    let mut lines = Vec::new();
24    
25    // Header row
26    if options.header {
27        let headers: Vec<&str> = options.columns.iter().map(|c| c.header()).collect();
28        lines.push(headers.join(","));
29    }
30    
31    // Data rows
32    for key in keys {
33        let row = build_csv_row(key, &options)?;
34        lines.push(row);
35    }
36    
37    Ok(lines.join("\n"))
38}
39
40/// Build a single CSV row for a key.
41fn build_csv_row(key: &PrivateKey, options: &CsvOptions) -> Result<String> {
42    let public_key = key.public_key();
43    
44    let addr_network = match options.network {
45        Network::Mainnet => AddrNetwork::BitcoinMainnet,
46        Network::Testnet => AddrNetwork::BitcoinTestnet,
47    };
48    
49    let mut values = Vec::new();
50    
51    for col in &options.columns {
52        let value = match col {
53            CsvColumn::Address => {
54                P2PKHAddress::from_public_key(&public_key, addr_network)
55                    .map_err(|e| ExportError::AddressError(e.to_string()))?
56                    .to_string()
57            }
58            CsvColumn::Wif => export_wif(key, options.network, true),
59            CsvColumn::Hex => export_hex(key, HexOptions::new()),
60            CsvColumn::PublicKey => public_key.to_hex(PublicKeyFormat::Compressed),
61            CsvColumn::Network => options.network.to_string(),
62        };
63        
64        // Escape value if it contains comma or quote
65        let escaped = escape_csv_value(&value);
66        values.push(escaped);
67    }
68    
69    Ok(values.join(","))
70}
71
72/// Escape a CSV value if needed.
73fn escape_csv_value(value: &str) -> String {
74    if value.contains(',') || value.contains('"') || value.contains('\n') {
75        format!("\"{}\"", value.replace('"', "\"\""))
76    } else {
77        value.to_string()
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    
85    #[test]
86    fn test_export_csv_default() {
87        let keys: Vec<PrivateKey> = (0..3).map(|_| PrivateKey::random()).collect();
88        let csv = export_csv(&keys, CsvOptions::new()).unwrap();
89        
90        let lines: Vec<&str> = csv.lines().collect();
91        assert_eq!(lines.len(), 4); // 1 header + 3 data rows
92        assert_eq!(lines[0], "address,wif,hex");
93    }
94    
95    #[test]
96    fn test_export_csv_no_header() {
97        let keys: Vec<PrivateKey> = (0..2).map(|_| PrivateKey::random()).collect();
98        let csv = export_csv(&keys, CsvOptions::new().with_header(false)).unwrap();
99        
100        let lines: Vec<&str> = csv.lines().collect();
101        assert_eq!(lines.len(), 2); // No header, just 2 data rows
102    }
103    
104    #[test]
105    fn test_export_csv_custom_columns() {
106        let keys: Vec<PrivateKey> = (0..1).map(|_| PrivateKey::random()).collect();
107        let options = CsvOptions::new()
108            .with_columns(vec![CsvColumn::Wif, CsvColumn::Address]);
109        let csv = export_csv(&keys, options).unwrap();
110        
111        let lines: Vec<&str> = csv.lines().collect();
112        assert_eq!(lines[0], "wif,address");
113    }
114    
115    #[test]
116    fn test_escape_csv_value() {
117        assert_eq!(escape_csv_value("simple"), "simple");
118        assert_eq!(escape_csv_value("with,comma"), "\"with,comma\"");
119        assert_eq!(escape_csv_value("with\"quote"), "\"with\"\"quote\"");
120    }
121}