Browse Source

Merge pull request #2 from adityapk00/taddr

Transparent Address Support
duke
adityapk00 5 years ago
committed by GitHub
parent
commit
d92b9076d1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 39
      cli/src/main.rs
  2. 5
      lib/Cargo.toml
  3. 351
      lib/src/paper.rs
  4. 105
      lib/src/pdf.rs
  5. 4
      qtlib/src/lib.rs
  6. 2
      qtlib/src/main.cpp
  7. 2
      qtlib/src/zecpaperrust.h

39
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::<i32>() {
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::<u32>().unwrap();
// Number of z addresses to generate
let num_addresses = matches.value_of("z_addresses").unwrap().parse::<u32>().unwrap();
let z_addresses = matches.value_of("z_addresses").unwrap().parse::<u32>().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);
}
};
}
}

5
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"

351
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<u8> = 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<u8> {
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<F>(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<F>(testnet: bool, zcount: u32, tcount: u32, mut get_seed: F) -> String
where F: FnMut(u32) -> (Vec<u8>, 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(&params(testnet).taddress_version, &[]);
// Private Key
let sk_bytes: &[u8] = &sk[..];
let pk_wif = sk_bytes.to_base58check(&params(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<u5> = 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<u5> = 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 = "[

105
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(&current_layer, &font, &font_bold, kv["address"].as_str().unwrap(), pos);
add_pk_to_page(&current_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(&current_layer, &font, &font_bold, address, is_taddr, pos);
add_pk_to_page(&current_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<u8>, 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<u8>, qrsize: usize, x: Mm, y: Mm) {
fn add_qrcode_image_to_page(current_layer: &PdfLayerReference, qr: &Vec<u8>, 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<u8>, 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 */
};

4
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();
}

2
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);

2
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

Loading…
Cancel
Save