rustywallet_export/
csv.rs1use 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
10pub fn export_csv(keys: &[PrivateKey], options: CsvOptions) -> Result<String> {
23 let mut lines = Vec::new();
24
25 if options.header {
27 let headers: Vec<&str> = options.columns.iter().map(|c| c.header()).collect();
28 lines.push(headers.join(","));
29 }
30
31 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
40fn 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 let escaped = escape_csv_value(&value);
66 values.push(escaped);
67 }
68
69 Ok(values.join(","))
70}
71
72fn 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); 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); }
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}