diff --git a/cli/src/main.rs b/cli/src/main.rs index 6f1df08..2b3f6bd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,7 +12,6 @@ fn main() { .version("1.0") .about("A command line Zcash Sapling paper wallet generator") .arg(Arg::with_name("testnet") - .short("t") .long("testnet") .help("Generate Testnet addresses")) .arg(Arg::with_name("format") @@ -37,6 +36,16 @@ fn main() { .long("entropy") .takes_value(true) .help("Provide additional entropy to the random number generator. Any random string, containing 32-64 characters")) + .arg(Arg::with_name("t_addresses") + .short("t") + .long("taddrs") + .help("Numbe rof T addresses to generate") + .takes_value(true) + .default_value("0") + .validator(|i:String| match i.parse::() { + Ok(_) => return Ok(()), + Err(_) => return Err(format!("Number of addresses '{}' is not a number", i)) + })) .arg(Arg::with_name("z_addresses") .short("z") .long("zaddrs") @@ -79,12 +88,25 @@ fn main() { entropy.extend(matches.value_of("entropy").unwrap().as_bytes()); } + // Get the filename and output format + let filename = matches.value_of("output"); + let format = matches.value_of("format").unwrap(); + + // Writing to PDF requires a filename + if format == "pdf" && filename.is_none() { + eprintln!("Need an output file name when writing to PDF"); + return; + } + + // Number of t addresses to generate + let t_addresses = matches.value_of("t_addresses").unwrap().parse::().unwrap(); + // Number of z addresses to generate - let num_addresses = matches.value_of("z_addresses").unwrap().parse::().unwrap(); + let z_addresses = matches.value_of("z_addresses").unwrap().parse::().unwrap(); - print!("Generating {} Sapling addresses.........", num_addresses); + print!("Generating {} Sapling addresses and {} Transparent addresses...", z_addresses, t_addresses); io::stdout().flush().ok(); - let addresses = generate_wallet(testnet, nohd, num_addresses, &entropy); + let addresses = generate_wallet(testnet, nohd, z_addresses, t_addresses, &entropy); println!("[OK]"); // If the default format is present, write to the console if the filename is absent @@ -99,7 +121,12 @@ fn main() { // We already know the output file name was specified print!("Writing {:?} as a PDF file...", filename.unwrap()); io::stdout().flush().ok(); - pdf::save_to_pdf(&addresses, filename.unwrap()); - println!("[OK]"); + match pdf::save_to_pdf(&addresses, filename.unwrap()) { + Ok(_) => { println!("[OK]");}, + Err(e) => { + eprintln!("[ERROR]"); + eprintln!("{}", e); + } + }; } } \ No newline at end of file diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8830493..248a58b 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,10 @@ zip32 = { git = "https://github.com/zcash/librustzcash", rev="3b6f5e3d5ede6469f json = "0.11.14" qrcode = { version = "0.8", default-features = false } printpdf = "0.2.8" -blake2-rfc = { git = "https://github.com/gtank/blake2-rfc", rev="7a5b5fc99ae483a0043db7547fb79a6fa44b88a9" } +secp256k1 = { version = "0.13.0", features = ["rand"] } +ripemd160 = "0.8.0" +sha2 = "0.8.0" +base58 = "0.1.0" [dev-dependencies] array2d = "0.1.0" diff --git a/lib/src/paper.rs b/lib/src/paper.rs index 10f2659..fe8427e 100644 --- a/lib/src/paper.rs +++ b/lib/src/paper.rs @@ -1,14 +1,77 @@ + use hex; +use secp256k1; +use ripemd160::{Ripemd160, Digest}; +use base58::{ToBase58}; use zip32::{ChildIndex, ExtendedSpendingKey}; use bech32::{Bech32, u5, ToBase32}; use rand::{Rng, ChaChaRng, FromEntropy, SeedableRng}; use json::{array, object}; -use blake2_rfc::blake2b::Blake2b; +use sha2; + + +/// A trait for converting a [u8] to base58 encoded string. +pub trait ToBase58Check { + /// Converts a value of `self` to a base58 value, returning the owned string. + /// The version is a coin-specific prefix that is added. + /// The suffix is any bytes that we want to add at the end (like the "iscompressed" flag for + /// Secret key encoding) + fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String; +} + +impl ToBase58Check for [u8] { + fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String { + let mut payload: Vec = Vec::new(); + payload.extend_from_slice(version); + payload.extend_from_slice(self); + payload.extend_from_slice(suffix); + + let mut checksum = double_sha256(&payload); + payload.append(&mut checksum[..4].to_vec()); + payload.to_base58() + } +} + +/// Sha256(Sha256(value)) +fn double_sha256(payload: &[u8]) -> Vec { + let h1 = sha2::Sha256::digest(&payload); + let h2 = sha2::Sha256::digest(&h1); + h2.to_vec() +} -/** - * Generate a series of `count` addresses and private keys. - */ -pub fn generate_wallet(testnet: bool, nohd: bool, count: u32, user_entropy: &[u8]) -> String { +/// Parameters used to generate addresses and private keys. Look in chainparams.cpp (in zcashd/src) +/// to get these values. +/// Usually these will be different for testnet and for mainnet. +struct CoinParams { + taddress_version: [u8; 2], + tsecret_prefix : [u8; 1], + zaddress_prefix : String, + zsecret_prefix : String, + cointype : u32, +} + +fn params(testnet: bool) -> CoinParams { + if testnet { + CoinParams { + taddress_version : [0x1D, 0x25], + tsecret_prefix : [0xEF], + zaddress_prefix : "ztestsapling".to_string(), + zsecret_prefix : "secret-extended-key-test".to_string(), + cointype : 1 + } + } else { + CoinParams { + taddress_version : [0x1C, 0xB8], + tsecret_prefix : [0x80], + zaddress_prefix : "zs".to_string(), + zsecret_prefix : "secret-extended-key-main".to_string(), + cointype : 133 + } + } +} + +/// Generate a series of `count` addresses and private keys. +pub fn generate_wallet(testnet: bool, nohd: bool, zcount: u32, tcount: u32, user_entropy: &[u8]) -> String { // Get 32 bytes of system entropy let mut system_entropy:[u8; 32] = [0; 32]; { @@ -17,12 +80,12 @@ pub fn generate_wallet(testnet: bool, nohd: bool, count: u32, user_entropy: &[u8 } // Add in user entropy to the system entropy, and produce a 32 byte hash... - let mut state = Blake2b::new(32); - state.update(&system_entropy); - state.update(&user_entropy); + let mut state = sha2::Sha256::new(); + state.input(&system_entropy); + state.input(&user_entropy); let mut final_entropy: [u8; 32] = [0; 32]; - final_entropy.clone_from_slice(&state.finalize().as_bytes()[0..32]); + final_entropy.clone_from_slice(&double_sha256(&state.result()[..])); // ...which will we use to seed the RNG let mut rng = ChaChaRng::from_seed(final_entropy); @@ -32,10 +95,10 @@ pub fn generate_wallet(testnet: bool, nohd: bool, count: u32, user_entropy: &[u8 let mut seed: [u8; 32] = [0; 32]; rng.fill(&mut seed); - return gen_addresses_with_seed_as_json(testnet, count, |i| (seed.to_vec(), i)); + return gen_addresses_with_seed_as_json(testnet, zcount, tcount, |i| (seed.to_vec(), i)); } else { // Not using HD addresses, so derive a new seed every time - return gen_addresses_with_seed_as_json(testnet, count, |_| { + return gen_addresses_with_seed_as_json(testnet, zcount, tcount, |_| { let mut seed:[u8; 32] = [0; 32]; rng.fill(&mut seed); @@ -44,53 +107,87 @@ pub fn generate_wallet(testnet: bool, nohd: bool, count: u32, user_entropy: &[u8 } } -/** - * Generate `count` addresses with the given seed. The addresses are derived from m/32'/cointype'/index' where - * index is 0..count - * - * Note that cointype is 1 for testnet and 133 for mainnet - * - * get_seed is a closure that will take the address number being derived, and return a tuple containing the - * seed and child number to use to derive this wallet. - * - * It is useful if we want to reuse (or not) the seed across multiple wallets. - */ -fn gen_addresses_with_seed_as_json(testnet: bool, count: u32, mut get_seed: F) -> String +/// Generate `count` addresses with the given seed. The addresses are derived from m/32'/cointype'/index' where +/// index is 0..count +/// +/// Note that cointype is 1 for testnet and 133 for mainnet +/// +/// get_seed is a closure that will take the address number being derived, and return a tuple cointaining the +/// seed and child number to use to derive this wallet. +/// It is useful if we want to reuse (or not) the seed across multiple wallets. +fn gen_addresses_with_seed_as_json(testnet: bool, zcount: u32, tcount: u32, mut get_seed: F) -> String where F: FnMut(u32) -> (Vec, u32) { let mut ans = array![]; - for i in 0..count { + // Note that for t-addresses, we don't use HD addresses + let (seed, _) = get_seed(0); + let mut rng_seed: [u8; 32] = [0; 32]; + rng_seed.clone_from_slice(&seed[0..32]); + + // derive a RNG from the seed + let mut rng = ChaChaRng::from_seed(rng_seed); + + // First generate the Z addresses + for i in 0..zcount { let (seed, child) = get_seed(i); - let (addr, pk, path) = get_address(testnet, &seed, child); + let (addr, pk, path) = get_zaddress(testnet, &seed, child); ans.push(object!{ "num" => i, "address" => addr, "private_key" => pk, + "type" => "zaddr", "seed" => path }).unwrap(); } + // Next generate the T addresses + for i in 0..tcount { + let (addr, pk_wif) = get_taddress(testnet, &mut rng); + + ans.push(object!{ + "num" => i, + "address" => addr, + "private_key" => pk_wif, + "type" => "taddr" + }).unwrap(); + } + return json::stringify_pretty(ans, 2); } -// Generate a standard ZIP-32 address from the given seed at 32'/44'/0'/index -fn get_address(testnet: bool, seed: &[u8], index: u32) -> (String, String, json::JsonValue) { - let addr_prefix = if testnet {"ztestsapling"} else {"zs"}; - let pk_prefix = if testnet {"secret-extended-key-test"} else {"secret-extended-key-main"}; - let cointype = if testnet {1} else {133}; - - let spk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( +/// Generate a t address +fn get_taddress(testnet: bool, mut rng: &mut ChaChaRng) -> (String, String) { + // SECP256k1 context + let ctx = secp256k1::Secp256k1::default(); + + let (sk, pubkey) = ctx.generate_keypair(&mut rng); + + // Address + let mut hash160 = Ripemd160::new(); + hash160.input(sha2::Sha256::digest(&pubkey.serialize().to_vec())); + let addr = hash160.result().to_base58check(¶ms(testnet).taddress_version, &[]); + + // Private Key + let sk_bytes: &[u8] = &sk[..]; + let pk_wif = sk_bytes.to_base58check(¶ms(testnet).tsecret_prefix, &[0x01]); + + return (addr, pk_wif); +} + +/// Generate a standard ZIP-32 address from the given seed at 32'/44'/0'/index +fn get_zaddress(testnet: bool, seed: &[u8], index: u32) -> (String, String, json::JsonValue) { + let spk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(seed), &[ ChildIndex::Hardened(32), - ChildIndex::Hardened(cointype), + ChildIndex::Hardened(params(testnet).cointype), ChildIndex::Hardened(index) ], ); let path = object!{ "HDSeed" => hex::encode(seed), - "path" => format!("m/32'/{}'/{}'", cointype, index) + "path" => format!("m/32'/{}'/{}'", params(testnet).cointype, index) }; let (_d, addr) = spk.default_address().expect("Cannot get result"); @@ -100,13 +197,13 @@ fn get_address(testnet: bool, seed: &[u8], index: u32) -> (String, String, json: v.get_mut(..11).unwrap().copy_from_slice(&addr.diversifier.0); addr.pk_d.write(v.get_mut(11..).unwrap()).expect("Cannot write!"); let checked_data: Vec = v.to_base32(); - let encoded = Bech32::new(addr_prefix.into(), checked_data).expect("bech32 failed").to_string(); + let encoded = Bech32::new(params(testnet).zaddress_prefix.into(), checked_data).expect("bech32 failed").to_string(); // Private Key is encoded as bech32 string let mut vp = Vec::new(); spk.write(&mut vp).expect("Can't write private key"); let c_d: Vec = vp.to_base32(); - let encoded_pk = Bech32::new(pk_prefix.into(), c_d).expect("bech32 failed").to_string(); + let encoded_pk = Bech32::new(params(testnet).zsecret_prefix.into(), c_d).expect("bech32 failed").to_string(); return (encoded.to_string(), encoded_pk.to_string(), path); } @@ -120,16 +217,14 @@ fn get_address(testnet: bool, seed: &[u8], index: u32) -> (String, String, json: #[cfg(test)] mod tests { - /** - * Test the wallet generation and that it is generating the right number and type of addresses - */ + /// Test the wallet generation and that it is generating the right number and type of addresses #[test] fn test_wallet_generation() { use crate::paper::generate_wallet; use std::collections::HashSet; // Testnet wallet - let w = generate_wallet(true, false, 1, &[]); + let w = generate_wallet(true, false, 1, 0, &[]); let j = json::parse(&w).unwrap(); assert_eq!(j.len(), 1); assert!(j[0]["address"].as_str().unwrap().starts_with("ztestsapling")); @@ -138,7 +233,7 @@ mod tests { // Mainnet wallet - let w = generate_wallet(false, false, 1, &[]); + let w = generate_wallet(false, false, 1, 0, &[]); let j = json::parse(&w).unwrap(); assert_eq!(j.len(), 1); assert!(j[0]["address"].as_str().unwrap().starts_with("zs")); @@ -146,7 +241,7 @@ mod tests { assert_eq!(j[0]["seed"]["path"].as_str().unwrap(), "m/32'/133'/0'"); // Check if all the addresses are the same - let w = generate_wallet(true, false, 3, &[]); + let w = generate_wallet(true, false, 3, 0, &[]); let j = json::parse(&w).unwrap(); assert_eq!(j.len(), 3); @@ -168,16 +263,61 @@ mod tests { assert_eq!(set2.len(), 1); } - /** - * Test nohd address generation, which does not use the same sed. - */ + #[test] + fn test_tandz_wallet_generation() { + use crate::paper::generate_wallet; + use std::collections::HashSet; + + // Testnet wallet + let w = generate_wallet(true, false, 1, 1, &[]); + let j = json::parse(&w).unwrap(); + assert_eq!(j.len(), 2); + + assert!(j[0]["address"].as_str().unwrap().starts_with("ztestsapling")); + assert!(j[0]["private_key"].as_str().unwrap().starts_with("secret-extended-key-test")); + assert_eq!(j[0]["seed"]["path"].as_str().unwrap(), "m/32'/1'/0'"); + + assert!(j[1]["address"].as_str().unwrap().starts_with("tm")); + let pk = j[1]["private_key"].as_str().unwrap(); + assert!(pk.starts_with("c") || pk.starts_with("9")); + + // Mainnet wallet + let w = generate_wallet(false, false, 1, 1, &[]); + let j = json::parse(&w).unwrap(); + assert_eq!(j.len(), 2); + + assert!(j[0]["address"].as_str().unwrap().starts_with("zs")); + assert!(j[0]["private_key"].as_str().unwrap().starts_with("secret-extended-key-main")); + assert_eq!(j[0]["seed"]["path"].as_str().unwrap(), "m/32'/133'/0'"); + + assert!(j[1]["address"].as_str().unwrap().starts_with("t1")); + let pk = j[1]["private_key"].as_str().unwrap(); + assert!(pk.starts_with("L") || pk.starts_with("K") || pk.starts_with("5")); + + // Check if all the addresses are the same + let w = generate_wallet(true, false, 3, 3, &[]); + let j = json::parse(&w).unwrap(); + assert_eq!(j.len(), 6); + + let mut set1 = HashSet::new(); + for i in 0..6 { + set1.insert(j[i]["address"].as_str().unwrap()); + set1.insert(j[i]["private_key"].as_str().unwrap()); + } + + // There should be 6 + 6 distinct addresses and private keys + assert_eq!(set1.len(), 12); + } + + + /// Test nohd address generation, which does not use the same sed. #[test] fn test_nohd() { use crate::paper::generate_wallet; use std::collections::HashSet; // Check if all the addresses use a different seed - let w = generate_wallet(true, true, 3, &[]); + let w = generate_wallet(true, true, 3, 0, &[]); let j = json::parse(&w).unwrap(); assert_eq!(j.len(), 3); @@ -199,7 +339,7 @@ mod tests { assert_eq!(set2.len(), 3); } - // Test the address derivation against the test data (see below) + /// Test the address derivation against the test data (see below) fn test_address_derivation(testdata: &str, testnet: bool) { use crate::paper::gen_addresses_with_seed_as_json; let td = json::parse(&testdata.replace("'", "\"")).unwrap(); @@ -208,7 +348,7 @@ mod tests { let seed = hex::decode(i["seed"].as_str().unwrap()).unwrap(); let num = i["num"].as_u32().unwrap(); - let addresses = gen_addresses_with_seed_as_json(testnet, num+1, |child| (seed.clone(), child)); + let addresses = gen_addresses_with_seed_as_json(testnet, num+1, 0, |child| (seed.clone(), child)); let j = json::parse(&addresses).unwrap(); assert_eq!(j[num as usize]["address"], i["addr"]); @@ -216,44 +356,89 @@ mod tests { } } - /* - Test data was derived from zcashd. It contains 20 sets of seeds, and for each seed, it contains 5 accounts that are derived for the testnet and mainnet. - We'll use the same seed and derive the same set of addresses here, and then make sure that both the address and private key matches up. - - To derive the test data, add something like this in test_wallet.cpp and run with - ./src/zcash-gtest --gtest_filter=WalletTests.* - - ``` - void print_wallet(std::string seed, std::string pk, std::string addr, int num) { - std::cout << "{'seed': '" << seed << "', 'pk': '" << pk << "', 'addr': '" << addr << "', 'num': " << num << "}," << std::endl; - } - - void gen_addresses() { - for (int i=0; i < 20; i++) { - HDSeed seed = HDSeed::Random(); - for (int j=0; j < 5; j++) { - auto m = libzcash::SaplingExtendedSpendingKey::Master(seed); - auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT) - .Derive(Params().BIP44CoinType() | ZIP32_HARDENED_KEY_LIMIT) - .Derive(j | ZIP32_HARDENED_KEY_LIMIT); - - auto rawSeed = seed.RawSeed(); - print_wallet(HexStr(rawSeed.begin(), rawSeed.end()), - EncodeSpendingKey(xsk), EncodePaymentAddress(xsk.DefaultAddress()), j); - } - } - } + #[test] + fn test_taddr_testnet() { + use crate::paper::get_taddress; + use rand::{ChaChaRng, SeedableRng}; + + // 0-seeded, for predictable outcomes + let seed : [u8; 32] = [0; 32]; + let mut rng = ChaChaRng::from_seed(seed); + + let testdata = [ + ["tmEw65eREGVhneyqwB442UnjeVaTaJVWvi9", "cRZUuqfYFZ6bv7QxEjDMHpnxQmJG2oncZ2DAZsfVXmB2SCts8Z2N"], + ["tmXJQzrFTRAPpmVhrWTVUwFp7X4sisUdw2X", "cUtxiJ8n67Au9eM7WnTyRQNewfcW9bJZkKWkUkKgwqdsp2eayU57"], + ["tmGb1FcP31uFVtKU319thMiR2J7krABDWku", "cSuqVYsMGutnxjYNeL1DMQpiv2isMwF8gVG2oLNTnECWVGjTpB5N"], + ["tmHQ9fDGWqk684tjWvvEWZ8BSkpNrQ162Yb", "cNynpdfzR4jgZi5E6ihAQhzeKB2w7NXNbVvznr9oW26VoJCGHiLW"], + ["tmNS3LoTEFgUuEwzyYinoan4AceJ4dc21SR", "cP6FPTWbehuiXBpUnDW5iYVayEKeboxFQftx97GfSGwBs1HgPYjS"] + ]; + + for i in 0..5 { + let (a, sk) = get_taddress(true, &mut rng); + assert_eq!(a, testdata[i][0]); + assert_eq!(sk, testdata[i][1]); + } + } - TEST(WalletTests, SaplingAddressTest) { - SelectParams(CBaseChainParams::TESTNET); - gen_addresses(); - - SelectParams(CBaseChainParams::MAIN); - gen_addresses(); + #[test] + fn test_taddr_mainnet() { + use crate::paper::get_taddress; + use rand::{ChaChaRng, SeedableRng}; + + // 0-seeded, for predictable outcomes + let seed : [u8; 32] = [0; 32]; + let mut rng = ChaChaRng::from_seed(seed); + + let testdata = [ + ["t1P6LkovpsqCHWjeVWKkHd84ttbNksfW6k6", "L1CVSvfgpVQLkfwgrKQDvWHtnXzrNMgvUz4hTTCz2eX2BTmWSCaE"], + ["t1fTfg1m42VtKdFWQqjBk5b9Mv5nuPo7XLL", "L4XyFP8vf3UdzCsr8Ner45sbKSK6V9CsgHNHNKsBSiysZHaeQDq7"], + ["t1QkFvmtddEjzk5GbLRaxW3kGh8g2jDqSHP", "L2Yr2dsVqrCXoJ57FvC5z6KfHoRThV9ScT7ZguuxH7YWEXboHTY6"], + ["t1RZQLNn7T5acveY5GBvmhTWh9qJ2vy9hC9", "KxcoMig8z13RQGbxiJt33PVagwjXSvRgXTnXgRhHzuSVYZ9KdGUh"], + ["t1WbJ1xxps1yQ6hoXszV4j7PR1fDF7WogPz", "KxjFvYWkDeDTMkMDPogxMDzXM12EwMrZLdkV2gp9wAHBcGEcBPqZ"], + ]; + + for i in 0..5 { + let (a, sk) = get_taddress(false, &mut rng); + assert_eq!(a, testdata[i][0]); + assert_eq!(sk, testdata[i][1]); } - ``` - */ + } + + + /// Test data was derived from zcashd. It cointains 20 sets of seeds, and for each seed, it contains 5 accounts that are derived for the testnet and mainnet. + /// We'll use the same seed and derive the same set of addresses here, and then make sure that both the address and private key matches up. + /// To derive the test data, add something like this in test_wallet.cpp and run with + /// ./src/zcash-gtest --gtest_filter=WalletTests.* + /// + /// ``` + /// void print_wallet(std::string seed, std::string pk, std::string addr, int num) { + /// std::cout << "{'seed': '" << seed << "', 'pk': '" << pk << "', 'addr': '" << addr << "', 'num': " << num << "}," << std::endl; + /// } + /// + /// void gen_addresses() { + /// for (int i=0; i < 20; i++) { + /// HDSeed seed = HDSeed::Random(); + /// for (int j=0; j < 5; j++) { + /// auto m = libzcash::SaplingExtendedSpendingKey::Master(seed); + /// auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT) + /// .Derive(Params().BIP44CoinType() | ZIP32_HARDENED_KEY_LIMIT) + /// .Derive(j | ZIP32_HARDENED_KEY_LIMIT); + /// auto rawSeed = seed.RawSeed(); + /// print_wallet(HexStr(rawSeed.begin(), rawSeed.end()), + /// EncodeSpendingKey(xsk), EncodePaymentAddress(xsk.DefaultAddress()), j); + /// } + /// } + /// } + /// + /// TEST(WalletTests, SaplingAddressTest) { + /// SelectParams(CBaseChainParams::TESTNET); + /// gen_addresses(); + /// + /// SelectParams(CBaseChainParams::MAIN); + /// gen_addresses(); + /// } + /// ``` #[test] fn test_address_derivation_testnet() { let testdata = "[ diff --git a/lib/src/pdf.rs b/lib/src/pdf.rs index b897a69..697a424 100644 --- a/lib/src/pdf.rs +++ b/lib/src/pdf.rs @@ -12,7 +12,7 @@ use printpdf::*; /** * Save the list of wallets (address + private keys) to the given PDF file name. */ -pub fn save_to_pdf(addresses: &str, filename: &str) { +pub fn save_to_pdf(addresses: &str, filename: &str) -> Result<(), String> { let (doc, page1, layer1) = PdfDocument::new("Zec Sapling Paper Wallet", Mm(210.0), Mm(297.0), "Layer 1"); let font = doc.add_builtin_font(BuiltinFont::Courier).unwrap(); @@ -39,11 +39,19 @@ pub fn save_to_pdf(addresses: &str, filename: &str) { current_layer = doc.get_page(page2).add_layer("Layer 3"); } - // Add address + private key - add_address_to_page(¤t_layer, &font, &font_bold, kv["address"].as_str().unwrap(), pos); - add_pk_to_page(¤t_layer, &font, &font_bold, kv["private_key"].as_str().unwrap(), kv["seed"]["HDSeed"].as_str().unwrap(), kv["seed"]["path"].as_str().unwrap(), pos); + let address = kv["address"].as_str().unwrap(); + let pk = kv["private_key"].as_str().unwrap(); - // Is the shape stroked? Is the shape closed? Is the shape filled? + let (seed, hdpath, is_taddr) = if kv["type"].as_str().unwrap() == "zaddr" { + (kv["seed"]["HDSeed"].as_str().unwrap(), kv["seed"]["path"].as_str().unwrap(), false) + } else { + ("", "", true) + }; + + // Add address + private key + add_address_to_page(¤t_layer, &font, &font_bold, address, is_taddr, pos); + add_pk_to_page(¤t_layer, &font, &font_bold, pk, address, is_taddr, seed, hdpath, pos); + let line1 = Line { points: vec![(Point::new(Mm(5.0), Mm(160.0)), false), (Point::new(Mm(205.0), Mm(160.0)), false)], is_closed: true, @@ -70,7 +78,21 @@ pub fn save_to_pdf(addresses: &str, filename: &str) { pos = pos + 1; }; - doc.save(&mut BufWriter::new(File::create(filename).unwrap())).unwrap(); + let file = match File::create(filename) { + Ok(f) => f, + Err(e) => { + return Err(format!("Couldn't open {} for writing. Aborting. {}", filename, e)); + } + }; + + match doc.save(&mut BufWriter::new(file)) { + Ok(_) => (), + Err(e) => { + return Err(format!("Couldn't save {}. Aborting. {}", filename, e)); + } + }; + + return Ok(()); } /** @@ -112,14 +134,19 @@ fn add_footer_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef, /** * Add the address section to the PDF at `pos`. Note that each page can fit only 2 wallets, so pos has to effectively be either 0 or 1. */ -fn add_address_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef, font_bold: &IndirectFontRef, address: &str, pos: u32) { - let (scaledimg, finalsize) = qrcode_scaled(address, 10); +fn add_address_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef, font_bold: &IndirectFontRef, address: &str, is_taddr: bool, pos: u32) { + let (scaledimg, finalsize) = qrcode_scaled(address, if is_taddr {13} else {10}); - // page_height top_margin vertical_padding position - let ypos = 297.0 - 5.0 - 50.0 - (140.0 * pos as f64); - add_qrcode_image_to_page(current_layer, scaledimg, finalsize, Mm(10.0), Mm(ypos)); + // page_height top_margin vertical_padding position + let ypos = 297.0 - 5.0 - 35.0 - (140.0 * pos as f64); + let title = if is_taddr {"T Address"} else {"ZEC Address (Sapling)"}; + + add_address_at(current_layer, font, font_bold, title, address, &scaledimg, finalsize, ypos); +} - current_layer.use_text("ZEC Address (Sapling)", 14, Mm(55.0), Mm(ypos+27.5), &font_bold); +fn add_address_at(current_layer: &PdfLayerReference, font: &IndirectFontRef, font_bold: &IndirectFontRef, title: &str, address: &str, qrcode: &Vec, finalsize: usize, ypos: f64) { + add_qrcode_image_to_page(current_layer, qrcode, finalsize, Mm(10.0), Mm(ypos)); + current_layer.use_text(title, 14, Mm(55.0), Mm(ypos+27.5), &font_bold); let strs = split_to_max(&address, 39, 39); // No spaces, so user can copy the address for i in 0..strs.len() { @@ -130,28 +157,60 @@ fn add_address_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef /** * Add the private key section to the PDF at `pos`, which can effectively be only 0 or 1. */ -fn add_pk_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef, font_bold: &IndirectFontRef, pk: &str, seed: &str, path: &str, pos: u32) { - let (scaledimg, finalsize) = qrcode_scaled(pk, 10); +fn add_pk_to_page(current_layer: &PdfLayerReference, font: &IndirectFontRef, font_bold: &IndirectFontRef, pk: &str, address: &str, is_taddr: bool, seed: &str, path: &str, pos: u32) { + // page_height top_margin vertical_padding position + let ypos = 297.0 - 5.0 - 90.0 - (140.0 * pos as f64); + + let line1 = Line { + points: vec![(Point::new(Mm(5.0), Mm(ypos + 50.0)), false), (Point::new(Mm(205.0), Mm(ypos + 50.0)), false)], + is_closed: true, + has_fill: false, + has_stroke: true, + is_clipping_path: false, + }; + + let outline_color = printpdf::Color::Rgb(Rgb::new(0.0, 0.0, 0.0, None)); + + current_layer.set_outline_color(outline_color); + let mut dash_pattern = LineDashPattern::default(); + dash_pattern.dash_1 = Some(5); + current_layer.set_line_dash_pattern(dash_pattern); + current_layer.set_outline_thickness(1.0); + + // Draw first line + current_layer.add_shape(line1); + + // Reset the dashed line pattern + current_layer.set_line_dash_pattern(LineDashPattern::default()); - // page_height top_margin vertical_padding position - let ypos = 297.0 - 5.0 - 100.0 - (140.0 * pos as f64); - add_qrcode_image_to_page(current_layer, scaledimg, finalsize, Mm(145.0), Mm(ypos-17.5)); + let (scaledimg, finalsize) = qrcode_scaled(pk, if is_taddr {20} else {10}); - current_layer.use_text("Private Key", 14, Mm(10.0), Mm(ypos+32.5), &font_bold); + add_qrcode_image_to_page(current_layer, &scaledimg, finalsize, Mm(145.0), Mm(ypos-17.5)); + + current_layer.use_text("Private Key", 14, Mm(10.0), Mm(ypos+37.5), &font_bold); let strs = split_to_max(&pk, 45, 45); // No spaces, so user can copy the private key for i in 0..strs.len() { - current_layer.use_text(strs[i].clone(), 12, Mm(10.0), Mm(ypos+25.0-((i*5) as f64)), &font); + current_layer.use_text(strs[i].clone(), 12, Mm(10.0), Mm(ypos+32.5-((i*5) as f64)), &font); } - // And add the seed too. + // Add the address a second time below the private key + let title = if is_taddr {"T Address"} else {"ZEC Address (Sapling)"}; + current_layer.use_text(title, 12, Mm(10.0), Mm(ypos-10.0), &font_bold); + let strs = split_to_max(&address, 39, 39); // No spaces, so user can copy the address + for i in 0..strs.len() { + current_layer.use_text(strs[i].clone(), 12, Mm(10.0), Mm(ypos-15.0-((i*5) as f64)), &font); + } - current_layer.use_text(format!("HDSeed: {}, Path: {}", seed, path).as_str(), 8, Mm(10.0), Mm(ypos-25.0), &font); + // And add the seed too. + if !is_taddr { + current_layer.use_text(format!("HDSeed: {}, Path: {}", seed, path).as_str(), 8, Mm(10.0), Mm(ypos-35.0), &font); + } } /** * Insert the given QRCode into the PDF at the given x,y co-ordinates. The qr code is a vector of RGB values. */ -fn add_qrcode_image_to_page(current_layer: &PdfLayerReference, qr: Vec, qrsize: usize, x: Mm, y: Mm) { +fn add_qrcode_image_to_page(current_layer: &PdfLayerReference, qr: &Vec, qrsize: usize, x: Mm, y: Mm) { // you can also construct images manually from your data: let image_file_2 = ImageXObject { width: Px(qrsize), @@ -162,7 +221,7 @@ fn add_qrcode_image_to_page(current_layer: &PdfLayerReference, qr: Vec, qrsi /* put your bytes here. Make sure the total number of bytes = width * height * (bytes per component * number of components) (e.g. 2 (bytes) x 3 (colors) for RGB 16bit) */ - image_data: qr, + image_data: qr.to_vec(), image_filter: None, /* does not work yet */ clipping_bbox: None, /* doesn't work either, untested */ }; diff --git a/qtlib/src/lib.rs b/qtlib/src/lib.rs index 29bbe98..3231861 100644 --- a/qtlib/src/lib.rs +++ b/qtlib/src/lib.rs @@ -8,14 +8,14 @@ use zecpaperlib::paper; * after using it to free it properly */ #[no_mangle] -pub extern fn rust_generate_wallet(testnet: bool, count: u32, entropy: *const c_char) -> *mut c_char { +pub extern fn rust_generate_wallet(testnet: bool, zcount: u32, tcount: u32, entropy: *const c_char) -> *mut c_char { let entropy_str = unsafe { assert!(!entropy.is_null()); CStr::from_ptr(entropy) }; - let c_str = CString::new(paper::generate_wallet(testnet, false, count, entropy_str.to_bytes())).unwrap(); + let c_str = CString::new(paper::generate_wallet(testnet, false, zcount, tcount, entropy_str.to_bytes())).unwrap(); return c_str.into_raw(); } diff --git a/qtlib/src/main.cpp b/qtlib/src/main.cpp index 4435dec..752b873 100644 --- a/qtlib/src/main.cpp +++ b/qtlib/src/main.cpp @@ -6,7 +6,7 @@ using namespace std; int main() { - char * from_rust = rust_generate_wallet(true, 1, "user-provided-entropy"); + char * from_rust = rust_generate_wallet(true, 1, 1, "user-provided-entropy"); auto stri = string(from_rust); cout << stri << endl; rust_free_string(from_rust); diff --git a/qtlib/src/zecpaperrust.h b/qtlib/src/zecpaperrust.h index 6c538f8..a077130 100644 --- a/qtlib/src/zecpaperrust.h +++ b/qtlib/src/zecpaperrust.h @@ -5,7 +5,7 @@ extern "C"{ #endif -extern char * rust_generate_wallet(bool testnet, unsigned int count, const char* entropy); +extern char * rust_generate_wallet(bool testnet, unsigned int zcount, unsigned int tcount, const char* entropy); extern void rust_free_string(char* s); #ifdef __cplusplus