Unified Y/Zcash

This commit is contained in:
Hanh 2022-03-07 22:47:06 +08:00
parent 766748ccfb
commit 056ffa454a
18 changed files with 401 additions and 300 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
src/generated/*.rs
docs/_site/
*.db
Cargo.lock

View File

@ -1,8 +1,8 @@
use criterion::{criterion_group, criterion_main, Criterion};
use sync::{scan_all, NETWORK};
use sync::scan_all;
use tokio::runtime::Runtime;
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
fn scan(c: &mut Criterion) {
dotenv::dotenv().unwrap();
@ -10,7 +10,7 @@ fn scan(c: &mut Criterion) {
let ivk = dotenv::var("IVK").unwrap();
let fvk =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &ivk)
decode_extended_full_viewing_key(Network::MainNetwork.hrp_sapling_extended_full_viewing_key(), &ivk)
.unwrap()
.unwrap();
@ -19,7 +19,7 @@ fn scan(c: &mut Criterion) {
c.bench_function("scan all", |b| {
b.iter(|| {
let r = Runtime::new().unwrap();
r.block_on(scan_all(fvks.clone().as_slice())).unwrap();
r.block_on(scan_all(&Network::MainNetwork, fvks.clone().as_slice())).unwrap();
});
});
}

View File

@ -2,7 +2,7 @@ use crate::commitment::{CTree, Witness};
use crate::db::AccountViewKey;
use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
use crate::lw_rpc::*;
use crate::{advance_tree, NETWORK};
use crate::advance_tree;
use ff::PrimeField;
use group::GroupEncoding;
use log::info;
@ -12,7 +12,7 @@ use std::time::Instant;
use thiserror::Error;
use tonic::transport::{Certificate, Channel, ClientTlsConfig};
use tonic::Request;
use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade, Parameters};
use zcash_primitives::consensus::{BlockHeight, Network, NetworkUpgrade, Parameters};
use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness};
use zcash_primitives::sapling::note_encryption::try_sapling_compact_note_decryption;
use zcash_primitives::sapling::{Node, Note, PaymentAddress};
@ -128,7 +128,8 @@ pub fn to_output_description(co: &CompactOutput) -> CompactOutputDescription {
od
}
fn decrypt_notes<'a>(
fn decrypt_notes<'a, N: Parameters>(
network: &N,
block: &'a CompactBlock,
vks: &HashMap<u32, AccountViewKey>,
) -> DecryptedBlock<'a> {
@ -147,7 +148,7 @@ fn decrypt_notes<'a>(
for (&account, vk) in vks.iter() {
let od = to_output_description(co);
if let Some((note, pa)) =
try_sapling_compact_note_decryption(&NETWORK, height, &vk.ivk, &od)
try_sapling_compact_note_decryption(network, height, &vk.ivk, &od)
{
notes.push(DecryptedNote {
account,
@ -180,10 +181,10 @@ impl DecryptNode {
DecryptNode { vks }
}
pub fn decrypt_blocks<'a>(&self, blocks: &'a [CompactBlock]) -> Vec<DecryptedBlock<'a>> {
pub fn decrypt_blocks<'a>(&self, network: &Network, blocks: &'a [CompactBlock]) -> Vec<DecryptedBlock<'a>> {
let mut decrypted_blocks: Vec<DecryptedBlock> = blocks
.par_iter()
.map(|b| decrypt_notes(b, &self.vks))
.map(|b| decrypt_notes(network, b, &self.vks))
.collect();
decrypted_blocks.sort_by(|a, b| a.height.cmp(&b.height));
decrypted_blocks
@ -307,10 +308,10 @@ pub async fn connect_lightwalletd(url: &str) -> anyhow::Result<CompactTxStreamer
Ok(client)
}
pub async fn sync(vks: HashMap<u32, AccountViewKey>, ld_url: &str) -> anyhow::Result<()> {
pub async fn sync(network: &Network, vks: HashMap<u32, AccountViewKey>, ld_url: &str) -> anyhow::Result<()> {
let decrypter = DecryptNode::new(vks);
let mut client = connect_lightwalletd(ld_url).await?;
let start_height: u32 = crate::NETWORK
let start_height: u32 = network
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
.into();
@ -321,7 +322,7 @@ pub async fn sync(vks: HashMap<u32, AccountViewKey>, ld_url: &str) -> anyhow::Re
eprintln!("Download chain: {} ms", start.elapsed().as_millis());
let start = Instant::now();
let blocks = decrypter.decrypt_blocks(&cbs);
let blocks = decrypter.decrypt_blocks(network, &cbs);
eprintln!("Decrypt Notes: {} ms", start.elapsed().as_millis());
let start = Instant::now();
@ -348,12 +349,13 @@ mod tests {
use crate::db::AccountViewKey;
use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
use crate::LWD_URL;
use crate::NETWORK;
use dotenv;
use std::collections::HashMap;
use std::time::Instant;
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::{NetworkUpgrade, Parameters};
use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters};
const NETWORK: &Network = &Network::MainNetwork;
#[tokio::test]
async fn test_get_latest_height() -> anyhow::Result<()> {
@ -376,7 +378,7 @@ mod tests {
fvks.insert(1, AccountViewKey::from_fvk(&fvk));
let decrypter = DecryptNode::new(fvks);
let mut client = CompactTxStreamerClient::connect(LWD_URL).await?;
let start_height: u32 = crate::NETWORK
let start_height: u32 = NETWORK
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
.into();
@ -387,7 +389,7 @@ mod tests {
eprintln!("Download chain: {} ms", start.elapsed().as_millis());
let start = Instant::now();
let blocks = decrypter.decrypt_blocks(&cbs);
let blocks = decrypter.decrypt_blocks(&Network::MainNetwork, &cbs);
eprintln!("Decrypt Notes: {} ms", start.elapsed().as_millis());
// no need to calculate tree before the first note if we can

View File

@ -87,13 +87,14 @@ impl ContactDecoder {
#[cfg(test)]
mod tests {
use zcash_params::coin::CoinType;
use crate::contact::{serialize_contacts, Contact};
use crate::db::DEFAULT_DB_PATH;
use crate::{DbAdapter, Wallet, LWD_URL};
#[test]
fn test_contacts() {
let db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
let contact = Contact {
id: 0,
name: "hanh".to_string(),
@ -106,14 +107,15 @@ mod tests {
#[tokio::test]
async fn test_serialize() {
let db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
let contacts = db.get_unsaved_contacts().unwrap();
let memos = serialize_contacts(&contacts).unwrap();
for m in memos.iter() {
println!("{:?}", m);
}
let mut wallet = Wallet::new("zec.db", LWD_URL);
let mut wallet = Wallet::new(CoinType::Zcash, "zec.db");
wallet.set_lwd_url(LWD_URL).unwrap();
let tx_id = wallet.save_contacts_tx(&memos, 1, 3).await.unwrap();
println!("{}", tx_id);
}

132
src/db.rs
View File

@ -3,19 +3,17 @@ use crate::contact::Contact;
use crate::prices::Quote;
use crate::taddr::derive_tkeys;
use crate::transaction::TransactionInfo;
use crate::{CTree, Witness, NETWORK};
use crate::{CTree, Witness};
use chrono::NaiveDateTime;
use rusqlite::{params, Connection, OptionalExtension, Transaction, NO_PARAMS};
use std::collections::HashMap;
use bech32::FromBase32;
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::{NetworkUpgrade, Parameters};
use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters};
use zcash_primitives::merkle_tree::IncrementalWitness;
use zcash_primitives::sapling::{Diversifier, Node, Note, Rseed, SaplingIvk};
use zcash_primitives::zip32::{DiversifierIndex, ExtendedFullViewingKey};
use serde::{Serialize, Deserialize};
use chacha20poly1305::{Key, ChaCha20Poly1305, Nonce};
use chacha20poly1305::aead::{Aead, NewAead};
use zcash_params::coin::{CoinType, get_coin_chain, get_coin_id};
mod migration;
@ -23,6 +21,7 @@ mod migration;
pub const DEFAULT_DB_PATH: &str = "zec.db";
pub struct DbAdapter {
pub coin_type: CoinType,
pub connection: Connection,
}
@ -63,8 +62,10 @@ impl AccountViewKey {
#[derive(Serialize, Deserialize)]
pub struct AccountBackup {
pub coin: u8,
pub name: String,
pub seed: Option<String>,
pub index: u32,
pub z_sk: Option<String>,
pub ivk: String,
pub z_addr: String,
@ -73,10 +74,10 @@ pub struct AccountBackup {
}
impl DbAdapter {
pub fn new(db_path: &str) -> anyhow::Result<DbAdapter> {
pub fn new(coin_type: CoinType, db_path: &str) -> anyhow::Result<DbAdapter> {
let connection = Connection::open(db_path)?;
connection.execute("PRAGMA synchronous = off", NO_PARAMS)?;
Ok(DbAdapter { connection })
Ok(DbAdapter { coin_type, connection })
}
pub fn begin_transaction(&mut self) -> anyhow::Result<Transaction> {
@ -103,6 +104,7 @@ impl DbAdapter {
&self,
name: &str,
seed: Option<&str>,
index: u32,
sk: Option<&str>,
ivk: &str,
address: &str,
@ -114,9 +116,9 @@ impl DbAdapter {
return Ok(-1);
}
self.connection.execute(
"INSERT INTO accounts(name, seed, sk, ivk, address) VALUES (?1, ?2, ?3, ?4, ?5)
"INSERT INTO accounts(name, seed, aindex, sk, ivk, address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT DO NOTHING",
params![name, seed, sk, ivk, address],
params![name, seed, index, sk, ivk, address],
)?;
let id_tx: i32 = self.connection.query_row(
"SELECT id_account FROM accounts WHERE ivk = ?1",
@ -126,6 +128,16 @@ impl DbAdapter {
Ok(id_tx)
}
pub fn next_account_id(&self, seed: &str) -> anyhow::Result<i32> {
let index = self
.connection
.query_row("SELECT MAX(aindex) FROM accounts WHERE seed = ?1", [seed], |row| {
let aindex: Option<i32> = row.get(0)?;
Ok(aindex.unwrap_or(-1))
})? + 1;
Ok(index)
}
pub fn get_fvks(&self) -> anyhow::Result<HashMap<u32, AccountViewKey>> {
let mut statement = self
.connection
@ -135,7 +147,7 @@ impl DbAdapter {
let ivk: String = row.get(1)?;
let sk: Option<String> = row.get(2)?;
let fvk = decode_extended_full_viewing_key(
NETWORK.hrp_sapling_extended_full_viewing_key(),
self.network().hrp_sapling_extended_full_viewing_key(),
&ivk,
)
.unwrap()
@ -327,7 +339,7 @@ impl DbAdapter {
pub fn get_db_height(&self) -> anyhow::Result<u32> {
let height: u32 = self.get_last_sync_height()?.unwrap_or_else(|| {
crate::NETWORK
self.network()
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
.into()
@ -696,8 +708,8 @@ impl DbAdapter {
pub fn create_taddr(&self, account: u32) -> anyhow::Result<()> {
let seed = self.get_seed(account)?;
if let Some(seed) = seed {
let bip44_path = format!("m/44'/{}'/0'/0/0", NETWORK.coin_type());
let (sk, address) = derive_tkeys(&seed, &bip44_path)?;
let bip44_path = format!("m/44'/{}'/0'/0/0", self.network().coin_type());
let (sk, address) = derive_tkeys(self.network(), &seed, &bip44_path)?;
self.connection.execute(
"INSERT INTO taddrs(account, sk, address) VALUES (?1, ?2, ?3) \
ON CONFLICT DO NOTHING",
@ -829,22 +841,24 @@ impl DbAdapter {
Ok(())
}
const NONCE: &'static[u8; 12] = b"unique nonce";
pub fn get_full_backup(&self, key: &str) -> anyhow::Result<String> {
pub fn get_full_backup(&self) -> anyhow::Result<Vec<AccountBackup>> {
let coin = get_coin_id(self.coin_type);
let mut statement = self.connection.prepare(
"SELECT name, seed, a.sk AS z_sk, ivk, a.address AS z_addr, t.sk as t_sk, t.address AS t_addr FROM accounts a LEFT JOIN taddrs t ON a.id_account = t.account")?;
"SELECT name, seed, aindex, a.sk AS z_sk, ivk, a.address AS z_addr, t.sk as t_sk, t.address AS t_addr FROM accounts a LEFT JOIN taddrs t ON a.id_account = t.account")?;
let rows = statement.query_map(NO_PARAMS, |r| {
let name: String = r.get(0)?;
let seed: Option<String> = r.get(1)?;
let z_sk: Option<String> = r.get(2)?;
let ivk: String = r.get(3)?;
let z_addr: String = r.get(4)?;
let t_sk: Option<String> = r.get(5)?;
let t_addr: Option<String> = r.get(6)?;
let index: u32 = r.get(2)?;
let z_sk: Option<String> = r.get(3)?;
let ivk: String = r.get(4)?;
let z_addr: String = r.get(5)?;
let t_sk: Option<String> = r.get(6)?;
let t_addr: Option<String> = r.get(7)?;
Ok(AccountBackup {
coin,
name,
seed,
index,
z_sk,
ivk,
z_addr,
@ -856,70 +870,48 @@ impl DbAdapter {
for r in rows {
accounts.push(r?);
}
let accounts_bin = bincode::serialize(&accounts)?;
let (hrp, key, _) = bech32::decode(key)?;
if hrp != "zwk" { anyhow::bail!("Invalid backup key") }
let key = Vec::<u8>::from_base32(&key)?;
let key = Key::from_slice(&key);
let cipher = ChaCha20Poly1305::new(key);
// nonce is constant because we always use a different key!
let cipher_text = cipher.encrypt(Nonce::from_slice(Self::NONCE), &*accounts_bin).map_err(|_e| anyhow::anyhow!("Failed to encrypt backup"))?;
let backup = base64::encode(cipher_text);
Ok(backup)
Ok(accounts)
}
pub fn restore_full_backup(&self, key: &str, backup: &str) -> anyhow::Result<()> {
let (hrp, key, _) = bech32::decode(key)?;
if hrp != "zwk" { anyhow::bail!("Not a valid decryption key"); }
let key = Vec::<u8>::from_base32(&key)?;
let key = Key::from_slice(&key);
let cipher = ChaCha20Poly1305::new(key);
let backup = base64::decode(backup)?;
let backup = cipher.decrypt(Nonce::from_slice(Self::NONCE), &*backup).map_err(|_e| anyhow::anyhow!("Failed to decrypt backup"))?;
let accounts: Vec<AccountBackup> = bincode::deserialize(&backup)?;
pub fn restore_full_backup(&self, accounts: &[AccountBackup]) -> anyhow::Result<()> {
let coin = get_coin_id(self.coin_type);
for a in accounts {
log::info!("{}", a.name);
let do_insert = || {
self.connection.execute("INSERT INTO accounts(name, seed, sk, ivk, address) VALUES (?1,?2,?3,?4,?5)",
params![a.name, a.seed, a.z_sk, a.ivk, a.z_addr])?;
let id_account = self.connection.last_insert_rowid() as u32;
if let Some(t_addr) = a.t_addr {
self.connection.execute("INSERT INTO taddrs(account, sk, address) VALUES (?1,?2,?3)",
params![id_account, a.t_sk, t_addr])?;
log::info!("{} {} {}", a.name, a.coin, coin);
if a.coin == coin {
let do_insert = || {
self.connection.execute("INSERT INTO accounts(name, seed, aindex, sk, ivk, address) VALUES (?1,?2,?3,?4,?5,?6)",
params![a.name, a.seed, a.index, a.z_sk, a.ivk, a.z_addr])?;
let id_account = self.connection.last_insert_rowid() as u32;
if let Some(t_addr) = &a.t_addr {
self.connection.execute("INSERT INTO taddrs(account, sk, address) VALUES (?1,?2,?3)",
params![id_account, a.t_sk, t_addr])?;
}
Ok::<_, anyhow::Error>(())
};
if let Err(e) = do_insert() {
log::info!("{:?}", e);
}
Ok::<_, anyhow::Error>(())
};
let _ = do_insert();
}
}
Ok(())
}
fn network(&self) -> &'static Network {
let chain = get_coin_chain(self.coin_type);
chain.network()
}
}
#[cfg(test)]
mod tests {
use bech32::{ToBase32, Variant};
use zcash_params::coin::CoinType;
use crate::db::{DbAdapter, ReceivedNote, DEFAULT_DB_PATH};
use crate::{CTree, Witness};
#[test]
fn test_db_backup() {
let db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let k = [0u8; 32];
let k = bech32::encode("zwk", k.to_base32(), Variant::Bech32).unwrap();
let b = db.get_full_backup(&k).unwrap();
println!("{} {}", k, b);
db.restore_full_backup(&k, &b).unwrap();
}
#[test]
fn test_db() {
let mut db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let mut db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
db.init_db().unwrap();
db.trim_to_height(0).unwrap();
@ -956,7 +948,7 @@ mod tests {
#[test]
fn test_balance() {
let db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
let balance = db.get_balance(1).unwrap();
println!("{}", balance);
}

View File

@ -48,6 +48,7 @@ pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
id_account INTEGER PRIMARY KEY,
name TEXT NOT NULL,
seed TEXT,
aindex INTEGER NOT NULL,
sk TEXT,
ivk TEXT NOT NULL UNIQUE,
address TEXT NOT NULL)",
@ -91,6 +92,7 @@ pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
rcm BLOB NOT NULL,
nf BLOB NOT NULL UNIQUE,
spent INTEGER,
excluded BOOL,
CONSTRAINT tx_output UNIQUE (tx, output_index))",
NO_PARAMS,
)?;
@ -119,24 +121,7 @@ pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
address TEXT NOT NULL)",
NO_PARAMS,
)?;
}
if version < 2 {
connection.execute("ALTER TABLE received_notes ADD excluded BOOL", NO_PARAMS)?;
}
if version < 3 {
connection.execute(
"CREATE TABLE IF NOT EXISTS contacts (
account INTEGER NOT NULL,
name TEXT NOT NULL,
address TEXT NOT NULL,
PRIMARY KEY (account, address))",
NO_PARAMS,
)?;
}
if version < 4 {
connection.execute(
"CREATE TABLE IF NOT EXISTS historical_prices (
currency TEXT NOT NULL,
@ -145,14 +130,7 @@ pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
PRIMARY KEY (currency, timestamp))",
NO_PARAMS,
)?;
}
if version < 5 {
connection.execute("DELETE FROM historical_prices", NO_PARAMS)?;
}
if version < 6 {
connection.execute("DROP TABLE contacts", NO_PARAMS)?;
connection.execute(
"CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY,
@ -163,19 +141,7 @@ pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
)?;
}
if version < 7 {
connection.execute(
"CREATE TABLE IF NOT EXISTS secret_shares (
account INTEGER PRIMARY KEY,
idx INTEGER NOT NULL,
participants INTEGER NOT NULL,
threshold INTEGER NOT NULL,
secret TEXT NOT NULL)",
NO_PARAMS,
)?;
}
update_schema_version(&connection, 7)?;
update_schema_version(&connection, 1)?;
Ok(())
}

View File

@ -1,79 +1,104 @@
use bech32::{ToBase32, Variant};
use crate::NETWORK;
use bip39::{Language, Mnemonic, Seed};
use rand::RngCore;
use rand::rngs::OsRng;
use zcash_client_backend::address::RecipientAddress;
use zcash_client_backend::encoding::{
decode_extended_full_viewing_key, decode_extended_spending_key,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
};
use zcash_primitives::consensus::Parameters;
use zcash_primitives::zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey};
use zcash_params::coin::{CoinChain, CoinType, get_coin_chain};
pub fn decode_key(key: &str) -> anyhow::Result<(Option<String>, Option<String>, String, String)> {
let res = if let Ok(mnemonic) = Mnemonic::from_phrase(&key, Language::English) {
let (sk, ivk, pa) = derive_secret_key(&mnemonic)?;
Ok((Some(key.to_string()), Some(sk), ivk, pa))
} else if let Ok(Some(sk)) =
decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &key)
{
let (ivk, pa) = derive_viewing_key(&sk)?;
Ok((None, Some(key.to_string()), ivk, pa))
} else if let Ok(Some(fvk)) =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &key)
{
let pa = derive_address(&fvk)?;
Ok((None, None, key.to_string(), pa))
} else {
Err(anyhow::anyhow!("Not a valid key"))
};
res
pub struct KeyHelpers {
coin_type: CoinType,
}
pub fn is_valid_key(key: &str) -> i8 {
if Mnemonic::from_phrase(&key, Language::English).is_ok() {
return 0;
impl KeyHelpers {
pub fn new(coin_type: CoinType) -> Self {
KeyHelpers {
coin_type
}
}
if let Ok(Some(_)) =
decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &key)
{
return 1;
fn chain(&self) -> &dyn CoinChain { get_coin_chain(self.coin_type) }
pub fn decode_key(&self, key: &str, index: u32) -> anyhow::Result<(Option<String>, Option<String>, String, String)> {
let network = self.chain().network();
let res = if let Ok(mnemonic) = Mnemonic::from_phrase(&key, Language::English) {
let (sk, ivk, pa) = self.derive_secret_key(&mnemonic, index)?;
Ok((Some(key.to_string()), Some(sk), ivk, pa))
} else if let Ok(Some(sk)) =
decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &key)
{
let (ivk, pa) = self.derive_viewing_key(&sk)?;
Ok((None, Some(key.to_string()), ivk, pa))
} else if let Ok(Some(fvk)) =
decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &key)
{
let pa = self.derive_address(&fvk)?;
Ok((None, None, key.to_string(), pa))
} else {
Err(anyhow::anyhow!("Not a valid key"))
};
res
}
if let Ok(Some(_)) =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &key)
{
return 2;
pub fn is_valid_key(&self, key: &str) -> i8 {
let network = self.chain().network();
if Mnemonic::from_phrase(&key, Language::English).is_ok() {
return 0;
}
if let Ok(Some(_)) =
decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &key)
{
return 1;
}
if let Ok(Some(_)) =
decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &key)
{
return 2;
}
-1
}
-1
}
pub fn derive_secret_key(mnemonic: &Mnemonic) -> anyhow::Result<(String, String, String)> {
let seed = Seed::new(&mnemonic, "");
let master = ExtendedSpendingKey::master(seed.as_bytes());
let path = [
ChildIndex::Hardened(32),
ChildIndex::Hardened(NETWORK.coin_type()),
ChildIndex::Hardened(0),
];
let extsk = ExtendedSpendingKey::from_path(&master, &path);
let sk = encode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &extsk);
pub fn derive_secret_key(&self, mnemonic: &Mnemonic, index: u32) -> anyhow::Result<(String, String, String)> {
let network = self.chain().network();
let seed = Seed::new(&mnemonic, "");
let master = ExtendedSpendingKey::master(seed.as_bytes());
let path = [
ChildIndex::Hardened(32),
ChildIndex::Hardened(network.coin_type()),
ChildIndex::Hardened(index),
];
let extsk = ExtendedSpendingKey::from_path(&master, &path);
let sk = encode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &extsk);
let (fvk, pa) = derive_viewing_key(&extsk)?;
Ok((sk, fvk, pa))
}
let (fvk, pa) = self.derive_viewing_key(&extsk)?;
Ok((sk, fvk, pa))
}
pub fn derive_viewing_key(extsk: &ExtendedSpendingKey) -> anyhow::Result<(String, String)> {
let fvk = ExtendedFullViewingKey::from(extsk);
let pa = derive_address(&fvk)?;
let fvk =
encode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &fvk);
Ok((fvk, pa))
}
pub fn derive_viewing_key(&self, extsk: &ExtendedSpendingKey) -> anyhow::Result<(String, String)> {
let network = self.chain().network();
let fvk = ExtendedFullViewingKey::from(extsk);
let pa = self.derive_address(&fvk)?;
let fvk =
encode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk);
Ok((fvk, pa))
}
pub fn derive_address(fvk: &ExtendedFullViewingKey) -> anyhow::Result<String> {
let (_, payment_address) = fvk.default_address().unwrap();
let address = encode_payment_address(NETWORK.hrp_sapling_payment_address(), &payment_address);
Ok(address)
pub fn derive_address(&self, fvk: &ExtendedFullViewingKey) -> anyhow::Result<String> {
let network = self.chain().network();
let (_, payment_address) = fvk.default_address().unwrap();
let address = encode_payment_address(network.hrp_sapling_payment_address(), &payment_address);
Ok(address)
}
pub fn valid_address(&self, address: &str) -> bool {
let recipient = RecipientAddress::decode(self.chain().network(), address);
recipient.is_some()
}
}
pub fn generate_random_enc_key() -> anyhow::Result<String> {

View File

@ -3,7 +3,7 @@
#[path = "generated/cash.z.wallet.sdk.rpc.rs"]
pub mod lw_rpc;
pub use zcash_params::coin::{get_branch, NETWORK, TICKER};
pub use zcash_params::coin::{CoinType, get_coin_type, get_branch};
// Mainnet
// pub const LWD_URL: &str = "https://mainnet.lightwalletd.com:9067";
@ -50,7 +50,7 @@ pub use crate::chain::{
pub use crate::commitment::{CTree, Witness};
pub use crate::db::DbAdapter;
pub use crate::hash::pedersen_hash;
pub use crate::key::{decode_key, is_valid_key, generate_random_enc_key};
pub use crate::key::{KeyHelpers, generate_random_enc_key};
pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
pub use crate::lw_rpc::*;
pub use crate::mempool::MemPool;
@ -58,4 +58,4 @@ pub use crate::pay::{broadcast_tx, Tx, TxIn, TxOut};
pub use crate::print::*;
pub use crate::scan::{latest_height, scan_all, sync_async};
pub use crate::ua::{get_sapling, get_ua};
pub use crate::wallet::{RecipientMemo, Wallet, WalletBalance};
pub use crate::wallet::{RecipientMemo, Wallet, WalletBalance, encrypt_backup, decrypt_backup};

View File

@ -1,10 +1,12 @@
use std::time::Instant;
use sync::{advance_tree, scan_all, CTree, Witness, NETWORK};
use sync::{advance_tree, scan_all, CTree, Witness};
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness};
use zcash_primitives::sapling::Node;
const NETWORK: Network = Network::MainNetwork;
#[tokio::main]
#[allow(dead_code)]
async fn main_scan() {
@ -17,7 +19,7 @@ async fn main_scan() {
.unwrap()
.unwrap();
scan_all(&vec![fvk]).await.unwrap();
scan_all(&NETWORK, &vec![fvk]).await.unwrap();
}
#[allow(dead_code)]

View File

@ -1,18 +1,23 @@
use clap::{App, Arg};
use std::fs::File;
use std::io::{Read, Write};
use sync::{decode_key, Tx, NETWORK};
use std::str::FromStr;
use sync::{KeyHelpers, Tx};
use zcash_client_backend::encoding::decode_extended_spending_key;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
use zcash_proofs::prover::LocalTxProver;
use zcash_params::coin::CoinType;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let key = dotenv::var("KEY").unwrap();
let (_seed, sk, _ivk, _address) = decode_key(&key)?;
let matches = App::new("Multisig CLI")
let matches = App::new("Cold wallet Signer CLI")
.version("1.0")
.arg(
Arg::with_name("coin")
.short("coin")
.long("coin")
.takes_value(true),
)
.arg(
Arg::with_name("tx_filename")
.short("tx")
@ -27,12 +32,23 @@ async fn main() -> anyhow::Result<()> {
)
.get_matches();
let tx_filename = matches.value_of("tx_filename").unwrap();
let out_filename = matches.value_of("out_filename").unwrap();
let coin = matches.value_of("coin").expect("coin argument missing");
let tx_filename = matches.value_of("tx_filename").expect("input filename missing");
let out_filename = matches.value_of("out_filename").expect("output filename missing");
let (coin_type, network) = match coin {
"zcash" => (CoinType::Zcash, Network::MainNetwork),
"ycash" => (CoinType::Ycash, Network::YCashMainNetwork),
_ => panic!("Invalid coin")
};
let key = dotenv::var("KEY").unwrap();
let index = u32::from_str(&dotenv::var("INDEX").unwrap_or_else(|_| "0".to_string())).unwrap();
let kh = KeyHelpers::new(coin_type);
let (_seed, sk, _ivk, _address) = kh.decode_key(&key, index)?;
let sk = sk.unwrap();
let sk =
decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &sk)?.unwrap();
decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &sk)?.unwrap();
let mut file = File::open(tx_filename)?;
let mut s = String::new();

View File

@ -10,11 +10,12 @@ use zcash_client_backend::data_api::wallet::ANCHOR_OFFSET;
use zcash_primitives::memo::Memo;
use zcash_primitives::merkle_tree::Hashable;
use zcash_primitives::sapling::Node;
use zcash_params::coin::CoinType;
const DB_NAME: &str = "zec.db";
fn init() {
let db = DbAdapter::new(DB_NAME).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DB_NAME).unwrap();
db.init_db().unwrap();
}
@ -31,8 +32,9 @@ async fn test() -> anyhow::Result<()> {
let progress = |height| {
log::info!("Height = {}", height);
};
let mut wallet = Wallet::new(DB_NAME, LWD_URL);
wallet.new_account_with_key("main", &seed).unwrap();
let mut wallet = Wallet::new(CoinType::Zcash, DB_NAME);
wallet.set_lwd_url(LWD_URL).unwrap();
wallet.new_account_with_key("main", &seed, 0).unwrap();
// wallet.new_account_with_key("test", &seed2).unwrap();
// wallet.new_account_with_key("zecpages", &ivk).unwrap();
@ -94,7 +96,8 @@ async fn test_sync() {
log::info!("Height = {}", height);
};
let wallet = Wallet::new(DB_NAME, LWD_URL);
let mut wallet = Wallet::new(CoinType::Zcash, DB_NAME);
wallet.set_lwd_url(LWD_URL).unwrap();
wallet.sync(true, ANCHOR_OFFSET, progress).await.unwrap();
}
@ -109,13 +112,13 @@ fn test_make_wallet() {
#[allow(dead_code)]
fn test_rewind() {
let mut db = DbAdapter::new(DB_NAME).unwrap();
let mut db = DbAdapter::new(CoinType::Zcash, DB_NAME).unwrap();
db.trim_to_height(1314000).unwrap();
}
#[allow(dead_code)]
fn test_get_balance() {
let db = DbAdapter::new(DB_NAME).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DB_NAME).unwrap();
let balance = db.get_balance(1).unwrap();
println!("Balance = {}", (balance as f64) / 100_000_000.0);
}
@ -138,7 +141,7 @@ fn test_invalid_witness() {
#[allow(dead_code)]
fn w() {
let db = DbAdapter::new("zec.db").unwrap();
let db = DbAdapter::new(CoinType::Zcash, "zec.db").unwrap();
// let w_b: Vec<u8> = db.connection.query_row("SELECT witness FROM sapling_witnesses WHERE note = 66 AND height = 1466097", NO_PARAMS, |row| row.get(0)).unwrap();
// let w = Witness::read(0, &*w_b).unwrap();
// print_witness2(&w);

View File

@ -1,7 +1,7 @@
use crate::chain::to_output_description;
use crate::{
connect_lightwalletd, get_latest_height, CompactTx, CompactTxStreamerClient, DbAdapter,
Exclude, NETWORK,
Exclude
};
use std::collections::HashMap;
use tonic::transport::Channel;
@ -10,6 +10,7 @@ use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::{BlockHeight, Parameters};
use zcash_primitives::sapling::note_encryption::try_sapling_compact_note_decryption;
use zcash_primitives::sapling::SaplingIvk;
use zcash_params::coin::{CoinChain, CoinType, get_coin_chain};
const DEFAULT_EXCLUDE_LEN: u8 = 1;
@ -20,6 +21,7 @@ struct MemPoolTransacton {
}
pub struct MemPool {
coin_type: CoinType,
db_path: String,
account: u32,
ivk: Option<SaplingIvk>,
@ -31,8 +33,9 @@ pub struct MemPool {
}
impl MemPool {
pub fn new(db_path: &str, ld_url: &str) -> MemPool {
pub fn new(coin_type: CoinType, db_path: &str) -> MemPool {
MemPool {
coin_type,
db_path: db_path.to_string(),
account: 0,
ivk: None,
@ -40,12 +43,12 @@ impl MemPool {
transactions: HashMap::new(),
nfs: HashMap::new(),
balance: 0,
ld_url: ld_url.to_string(),
ld_url: "".to_string(),
}
}
pub fn set_account(&mut self, account: u32) -> anyhow::Result<()> {
let db = DbAdapter::new(&self.db_path)?;
let db = DbAdapter::new(self.coin_type, &self.db_path)?;
let ivk = db.get_ivk(account)?;
self.account = account;
self.set_ivk(&ivk);
@ -55,7 +58,7 @@ impl MemPool {
fn set_ivk(&mut self, ivk: &str) {
let fvk =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &ivk)
decode_extended_full_viewing_key(self.chain().network().hrp_sapling_extended_full_viewing_key(), &ivk)
.unwrap()
.unwrap();
let ivk = fvk.fvk.vk.ivk();
@ -82,7 +85,7 @@ impl MemPool {
}
pub fn clear(&mut self, height: u32) -> anyhow::Result<()> {
let db = DbAdapter::new(&self.db_path)?;
let db = DbAdapter::new(self.coin_type, &self.db_path)?;
self.height = BlockHeight::from_u32(height);
self.nfs = db.get_nullifier_amounts(self.account, true)?;
self.transactions.clear();
@ -141,7 +144,7 @@ impl MemPool {
for co in tx.outputs.iter() {
let od = to_output_description(co);
if let Some((note, _)) =
try_sapling_compact_note_decryption(&NETWORK, self.height, ivk, &od)
try_sapling_compact_note_decryption(self.chain().network(), self.height, ivk, &od)
{
balance += note.value as i64; // value is incoming
}
@ -149,6 +152,13 @@ impl MemPool {
balance
}
pub fn set_lwd_url(&mut self, ld_url: &str) -> anyhow::Result<()> {
self.ld_url = ld_url.to_string();
Ok(())
}
fn chain(&self) -> &dyn CoinChain { get_coin_chain(self.coin_type) }
}
#[cfg(test)]
@ -157,12 +167,14 @@ mod tests {
use crate::mempool::MemPool;
use crate::{DbAdapter, LWD_URL};
use std::time::Duration;
use zcash_params::coin::CoinType;
#[tokio::test]
async fn test_mempool() {
let db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
let ivk = db.get_ivk(1).unwrap();
let mut mempool = MemPool::new("zec.db", LWD_URL);
let mut mempool = MemPool::new(CoinType::Zcash, "zec.db");
mempool.set_lwd_url(LWD_URL).unwrap();
mempool.set_ivk(&ivk);
loop {
mempool.scan().await.unwrap();

View File

@ -2,7 +2,7 @@ use crate::db::SpendableNote;
use crate::wallet::RecipientMemo;
use crate::{
connect_lightwalletd, get_branch, get_latest_height, hex_to_hash, GetAddressUtxosReply,
RawTransaction, NETWORK,
RawTransaction
};
use jubjub::Fr;
use rand::prelude::SliceRandom;
@ -27,9 +27,11 @@ use zcash_primitives::transaction::builder::{Builder, Progress};
use zcash_primitives::transaction::components::amount::{DEFAULT_FEE, MAX_MONEY};
use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut as ZTxOut};
use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
use zcash_params::coin::{CoinChain, CoinType, get_coin_chain};
#[derive(Serialize, Deserialize, Debug)]
pub struct Tx {
pub coin_type: CoinType,
pub height: u32,
pub t_inputs: Vec<TTxIn>,
pub inputs: Vec<TxIn>,
@ -39,8 +41,9 @@ pub struct Tx {
}
impl Tx {
pub fn new(height: u32) -> Self {
pub fn new(coin_type: CoinType, height: u32) -> Self {
Tx {
coin_type,
height,
t_inputs: vec![],
inputs: vec![],
@ -78,12 +81,14 @@ pub struct TxOut {
pub struct TxBuilder {
pub tx: Tx,
coin_type: CoinType,
}
impl TxBuilder {
pub fn new(height: u32) -> Self {
pub fn new(coin_type: CoinType, height: u32) -> Self {
TxBuilder {
tx: Tx::new(height),
coin_type,
tx: Tx::new(coin_type, height),
}
}
@ -107,7 +112,7 @@ impl TxBuilder {
let tx_in = TxIn {
diversifier: hex::encode(diversifier.0),
fvk: encode_extended_full_viewing_key(
NETWORK.hrp_sapling_extended_full_viewing_key(),
self.chain().network().hrp_sapling_extended_full_viewing_key(),
&fvk,
),
amount: u64::from(amount),
@ -151,7 +156,7 @@ impl TxBuilder {
ovk: &OutgoingViewingKey,
address: &PaymentAddress,
) -> anyhow::Result<()> {
self.tx.change = encode_payment_address(NETWORK.hrp_sapling_payment_address(), address);
self.tx.change = encode_payment_address(self.chain().network().hrp_sapling_payment_address(), address);
self.tx.ovk = hex::encode(ovk.0);
Ok(())
}
@ -241,7 +246,7 @@ impl TxBuilder {
self.set_change(&ovk, &change)?;
for r in recipients.iter() {
let to_addr = RecipientAddress::decode(&NETWORK, &r.address)
let to_addr = RecipientAddress::decode(self.chain().network(), &r.address)
.ok_or(anyhow::anyhow!("Invalid address"))?;
let memo = &r.memo;
@ -274,6 +279,8 @@ impl TxBuilder {
Ok(())
}
fn chain(&self) -> &dyn CoinChain { get_coin_chain(self.coin_type) }
}
impl Tx {
@ -287,13 +294,14 @@ impl Tx {
prover: &impl TxProver<OsRng>,
progress_callback: impl Fn(Progress) + Send + 'static,
) -> anyhow::Result<Vec<u8>> {
let chain = get_coin_chain(self.coin_type);
let last_height = BlockHeight::from_u32(self.height as u32);
let mut builder = Builder::new(NETWORK, last_height);
let mut builder = Builder::new(*chain.network(), last_height);
let ovk = hex_to_hash(&self.ovk)?;
builder.send_change_to(
OutgoingViewingKey(ovk),
decode_payment_address(NETWORK.hrp_sapling_payment_address(), &self.change)
decode_payment_address(chain.network().hrp_sapling_payment_address(), &self.change)
.unwrap()
.unwrap(),
);
@ -320,7 +328,7 @@ impl Tx {
hex::decode_to_slice(&txin.diversifier, &mut diversifier)?;
let diversifier = Diversifier(diversifier);
let fvk = decode_extended_full_viewing_key(
NETWORK.hrp_sapling_extended_full_viewing_key(),
chain.network().hrp_sapling_extended_full_viewing_key(),
&txin.fvk,
)?
.unwrap();
@ -339,7 +347,7 @@ impl Tx {
}
for txout in self.outputs.iter() {
let recipient = RecipientAddress::decode(&NETWORK, &txout.addr).unwrap();
let recipient = RecipientAddress::decode(chain.network(), &txout.addr).unwrap();
let amount = Amount::from_u64(txout.amount).unwrap();
match recipient {
RecipientAddress::Transparent(ta) => {
@ -367,7 +375,7 @@ impl Tx {
progress_callback(progress);
}
});
let consensus_branch_id = get_branch(u32::from(last_height));
let consensus_branch_id = get_branch(chain.network(), u32::from(last_height));
let (tx, _) = builder.build(consensus_branch_id, prover)?;
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;

View File

@ -1,5 +1,6 @@
use crate::{DbAdapter, TICKER};
use crate::DbAdapter;
use chrono::NaiveDateTime;
use zcash_params::coin::get_coin_chain;
const DAY_SEC: i64 = 24 * 3600;
@ -15,6 +16,7 @@ pub async fn fetch_historical_prices(
currency: &str,
db: &DbAdapter,
) -> anyhow::Result<Vec<Quote>> {
let chain = get_coin_chain(db.coin_type);
let json_error = || anyhow::anyhow!("Invalid JSON");
let today = now / DAY_SEC;
let from_day = today - days as i64;
@ -33,7 +35,7 @@ pub async fn fetch_historical_prices(
let client = reqwest::Client::new();
let url = format!(
"https://api.coingecko.com/api/v3/coins/{}/market_chart/range",
TICKER
chain.ticker()
);
let params = [
("from", from.to_string()),
@ -69,11 +71,12 @@ mod tests {
use crate::prices::fetch_historical_prices;
use crate::DbAdapter;
use std::time::SystemTime;
use zcash_params::coin::CoinType;
#[tokio::test]
async fn test_fetch_quotes() {
let currency = "EUR";
let mut db = DbAdapter::new(DEFAULT_DB_PATH).unwrap();
let mut db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()

View File

@ -17,11 +17,12 @@ use std::sync::Arc;
use std::time::Instant;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use zcash_primitives::consensus::{NetworkUpgrade, Parameters};
use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters};
use zcash_primitives::sapling::Node;
use zcash_primitives::zip32::ExtendedFullViewingKey;
use zcash_params::coin::{CoinType, get_coin_chain};
pub async fn scan_all(fvks: &[ExtendedFullViewingKey]) -> anyhow::Result<()> {
pub async fn scan_all(network: &Network, fvks: &[ExtendedFullViewingKey]) -> anyhow::Result<()> {
let fvks: HashMap<_, _> = fvks
.iter()
.enumerate()
@ -31,7 +32,7 @@ pub async fn scan_all(fvks: &[ExtendedFullViewingKey]) -> anyhow::Result<()> {
let total_start = Instant::now();
let mut client = CompactTxStreamerClient::connect(LWD_URL).await?;
let start_height: u32 = crate::NETWORK
let start_height: u32 = network
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
.into();
@ -42,7 +43,7 @@ pub async fn scan_all(fvks: &[ExtendedFullViewingKey]) -> anyhow::Result<()> {
info!("Download chain: {} ms", start.elapsed().as_millis());
let start = Instant::now();
let blocks = decrypter.decrypt_blocks(&cbs);
let blocks = decrypter.decrypt_blocks(network, &cbs);
info!("Decrypt Notes: {} ms", start.elapsed().as_millis());
let witnesses = calculate_tree_state_v2(&cbs, &blocks);
@ -85,6 +86,7 @@ pub struct TxIdHeight {
}
pub async fn sync_async(
coin_type: CoinType,
chunk_size: u32,
get_tx: bool,
db_path: &str,
@ -94,10 +96,14 @@ pub async fn sync_async(
) -> anyhow::Result<()> {
let ld_url = ld_url.to_owned();
let db_path = db_path.to_string();
let network = {
let chain = get_coin_chain(coin_type);
chain.network().clone()
};
let mut client = connect_lightwalletd(&ld_url).await?;
let (start_height, mut prev_hash, vks) = {
let db = DbAdapter::new(&db_path)?;
let db = DbAdapter::new(coin_type, &db_path)?;
let height = db.get_db_height()?;
let hash = db.get_db_hash(height)?;
let vks = db.get_fvks()?;
@ -137,7 +143,7 @@ pub async fn sync_async(
let proc_callback = progress_callback.clone();
let processor = tokio::spawn(async move {
let mut db = DbAdapter::new(&db_path)?;
let mut db = DbAdapter::new(coin_type, &db_path)?;
let mut nfs = db.get_nullifiers()?;
let (mut tree, mut witnesses) = db.get_tree()?;
@ -156,7 +162,7 @@ pub async fn sync_async(
{
// db tx scope
let db_tx = db.begin_transaction()?;
let dec_blocks = decrypter.decrypt_blocks(&blocks.0);
let dec_blocks = decrypter.decrypt_blocks(&network, &blocks.0);
log::info!("Dec start : {}", dec_blocks[0].height);
let start = Instant::now();
for b in dec_blocks.iter() {
@ -303,7 +309,7 @@ pub async fn sync_async(
return c;
});
let ids: Vec<_> = ids.into_iter().map(|e| e.id_tx).collect();
retrieve_tx_info(&mut client, &db_path2, &ids).await?;
retrieve_tx_info(coin_type, &mut client, &db_path2, &ids).await?;
}
log::info!("Transaction Details : {}", start.elapsed().as_millis());

View File

@ -1,6 +1,5 @@
use crate::{
AddressList, CompactTxStreamerClient, DbAdapter, GetAddressUtxosArg, GetAddressUtxosReply,
NETWORK,
};
use bip39::{Language, Mnemonic, Seed};
use ripemd160::{Digest, Ripemd160};
@ -10,7 +9,7 @@ use tiny_hderive::bip32::ExtendedPrivKey;
use tonic::transport::Channel;
use tonic::Request;
use zcash_client_backend::encoding::encode_transparent_address;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
use zcash_primitives::legacy::TransparentAddress;
pub async fn get_taddr_balance(
@ -49,7 +48,7 @@ pub async fn get_utxos(
}
}
pub fn derive_tkeys(phrase: &str, path: &str) -> anyhow::Result<(String, String)> {
pub fn derive_tkeys(network: &Network, phrase: &str, path: &str) -> anyhow::Result<(String, String)> {
let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?;
let seed = Seed::new(&mnemonic, "");
let secp = Secp256k1::<All>::new();
@ -60,8 +59,8 @@ pub fn derive_tkeys(phrase: &str, path: &str) -> anyhow::Result<(String, String)
let pub_key = Ripemd160::digest(&Sha256::digest(&pub_key));
let address = TransparentAddress::PublicKey(pub_key.into());
let address = encode_transparent_address(
&NETWORK.b58_pubkey_address_prefix(),
&NETWORK.b58_script_address_prefix(),
&network.b58_pubkey_address_prefix(),
&network.b58_script_address_prefix(),
&address,
);
let sk = secret_key.to_string();

View File

@ -1,5 +1,5 @@
use crate::contact::{Contact, ContactDecoder};
use crate::{CompactTxStreamerClient, DbAdapter, TxFilter, NETWORK};
use crate::{CompactTxStreamerClient, DbAdapter, TxFilter};
use futures::StreamExt;
use std::collections::HashMap;
use std::convert::TryFrom;
@ -10,13 +10,14 @@ use tonic::Request;
use zcash_client_backend::encoding::{
decode_extended_full_viewing_key, encode_payment_address, encode_transparent_address,
};
use zcash_primitives::consensus::{BlockHeight, Parameters};
use zcash_primitives::consensus::{BlockHeight, Network, Parameters};
use zcash_primitives::memo::Memo;
use zcash_primitives::sapling::note_encryption::{
try_sapling_note_decryption, try_sapling_output_recovery,
};
use zcash_primitives::transaction::Transaction;
use zcash_primitives::zip32::ExtendedFullViewingKey;
use zcash_params::coin::{CoinType, get_coin_chain};
#[derive(Debug)]
pub struct TransactionInfo {
@ -39,6 +40,7 @@ pub struct ContactRef {
}
pub async fn decode_transaction(
network: &Network,
client: &mut CompactTxStreamerClient<Channel>,
nfs: &HashMap<(u32, Vec<u8>), u64>,
id_tx: u32,
@ -79,29 +81,29 @@ pub async fn decode_transaction(
for output in tx.vout.iter() {
if let Some(taddr) = output.script_pubkey.address() {
address = encode_transparent_address(
&NETWORK.b58_pubkey_address_prefix(),
&NETWORK.b58_script_address_prefix(),
&network.b58_pubkey_address_prefix(),
&network.b58_script_address_prefix(),
&taddr,
);
}
}
for output in tx.shielded_outputs.iter() {
if let Some((note, pa, memo)) = try_sapling_note_decryption(&NETWORK, height, &ivk, output)
if let Some((note, pa, memo)) = try_sapling_note_decryption(network, height, &ivk, output)
{
amount += note.value as i64; // change or self transfer
let _ = contact_decoder.add_memo(&memo); // ignore memo that is not for contacts
let memo = Memo::try_from(memo)?;
if address.is_empty() {
address = encode_payment_address(NETWORK.hrp_sapling_payment_address(), &pa);
address = encode_payment_address(network.hrp_sapling_payment_address(), &pa);
}
if memo != Memo::Empty {
tx_memo = memo;
}
} else if let Some((_note, pa, memo)) =
try_sapling_output_recovery(&NETWORK, height, &ovk, &output)
try_sapling_output_recovery(network, height, &ovk, &output)
{
address = encode_payment_address(NETWORK.hrp_sapling_payment_address(), &pa);
address = encode_payment_address(network.hrp_sapling_payment_address(), &pa);
let memo = Memo::try_from(memo)?;
if memo != Memo::Empty {
tx_memo = memo;
@ -146,11 +148,16 @@ struct DecodeTxParams<'a> {
}
pub async fn retrieve_tx_info(
coin_type: CoinType,
client: &mut CompactTxStreamerClient<Channel>,
db_path: &str,
tx_ids: &[u32],
) -> anyhow::Result<()> {
let db = DbAdapter::new(db_path)?;
let network = {
let chain = get_coin_chain(coin_type);
chain.network().clone()
};
let db = DbAdapter::new(coin_type, db_path)?;
let nfs = db.get_nullifiers_raw()?;
let mut nf_map: HashMap<(u32, Vec<u8>), u64> = HashMap::new();
@ -163,7 +170,7 @@ pub async fn retrieve_tx_info(
for (index, &id_tx) in tx_ids.iter().enumerate() {
let (account, height, tx_hash, ivk) = db.get_txhash(id_tx)?;
let fvk: &ExtendedFullViewingKey = fvk_cache.entry(account).or_insert_with(|| {
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &ivk)
decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &ivk)
.unwrap()
.unwrap()
});
@ -183,6 +190,7 @@ pub async fn retrieve_tx_info(
let res = tokio_stream::iter(decode_tx_params).for_each_concurrent(None, |mut p| async move {
if let Ok(tx_info) = decode_transaction(
&network,
&mut p.client,
p.nf_map,
p.id_tx,
@ -229,10 +237,11 @@ pub async fn retrieve_tx_info(
#[cfg(test)]
mod tests {
use crate::transaction::decode_transaction;
use crate::{connect_lightwalletd, DbAdapter, LWD_URL, NETWORK};
use crate::{connect_lightwalletd, DbAdapter, LWD_URL};
use std::collections::HashMap;
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
use zcash_params::coin::CoinType;
#[tokio::test]
async fn test_decode_transaction() {
@ -240,7 +249,7 @@ mod tests {
hex::decode("b47da170329dc311b98892eac23e83025f8bb3ce10bb07535698c91fb37e1e54")
.unwrap();
let mut client = connect_lightwalletd(LWD_URL).await.unwrap();
let db = DbAdapter::new("./zec.db").unwrap();
let db = DbAdapter::new(CoinType::Zcash, "./zec.db").unwrap();
let account = 1;
let nfs = db.get_nullifiers_raw().unwrap();
let mut nf_map: HashMap<(u32, Vec<u8>), u64> = HashMap::new();
@ -251,11 +260,11 @@ mod tests {
}
let fvk = db.get_ivk(account).unwrap();
let fvk =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &fvk)
decode_extended_full_viewing_key(Network::MainNetwork.hrp_sapling_extended_full_viewing_key(), &fvk)
.unwrap()
.unwrap();
let tx_info =
decode_transaction(&mut client, &nf_map, 1, account, &fvk, &tx_hash, 1313212, 1)
decode_transaction(&Network::MainNetwork, &mut client, &nf_map, 1, account, &fvk, &tx_hash, 1313212, 1)
.await
.unwrap();
println!("{:?}", tx_info);

View File

@ -1,5 +1,5 @@
use crate::contact::{serialize_contacts, Contact};
use crate::key::{decode_key, is_valid_key};
use crate::key::KeyHelpers;
use crate::pay::Tx;
use crate::pay::TxBuilder;
use crate::prices::fetch_historical_prices;
@ -7,7 +7,7 @@ use crate::scan::ProgressCallback;
use crate::taddr::{get_taddr_balance, get_utxos};
use crate::{
broadcast_tx, connect_lightwalletd, get_latest_height, BlockId, CTree,
DbAdapter, NETWORK,
DbAdapter,
};
use bip39::{Language, Mnemonic};
use lazycell::AtomicLazyCell;
@ -19,6 +19,10 @@ use serde::Serialize;
use std::convert::TryFrom;
use std::str::FromStr;
use std::sync::Arc;
use anyhow::anyhow;
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use chacha20poly1305::aead::{Aead, NewAead};
use bech32::FromBase32;
use tokio::sync::Mutex;
use tonic::Request;
use zcash_client_backend::address::RecipientAddress;
@ -27,18 +31,21 @@ use zcash_client_backend::encoding::{
};
use zcash_client_backend::zip321::{Payment, TransactionRequest};
use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS};
use zcash_primitives::consensus::Parameters;
use zcash_primitives::consensus::{Network, Parameters};
use zcash_primitives::memo::Memo;
use zcash_primitives::transaction::builder::Progress;
use zcash_primitives::transaction::components::Amount;
use zcash_proofs::prover::LocalTxProver;
use zcash_params::coin::TICKER;
use zcash_params::coin::{CoinChain, CoinType, get_coin_chain};
use crate::db::AccountBackup;
const DEFAULT_CHUNK_SIZE: u32 = 100_000;
pub struct Wallet {
coin_type: CoinType,
pub db_path: String,
db: DbAdapter,
key_helpers: KeyHelpers,
prover: AtomicLazyCell<LocalTxProver>,
pub ld_url: String,
}
@ -92,14 +99,17 @@ impl From<&Recipient> for RecipientMemo {
}
impl Wallet {
pub fn new(db_path: &str, ld_url: &str) -> Wallet {
let db = DbAdapter::new(db_path).unwrap();
pub fn new(coin_type: CoinType, db_path: &str) -> Wallet {
let db = DbAdapter::new(coin_type, db_path).unwrap();
let key_helpers = KeyHelpers::new(coin_type);
db.init_db().unwrap();
Wallet {
coin_type,
db_path: db_path.to_string(),
db,
key_helpers,
prover: AtomicLazyCell::new(),
ld_url: ld_url.to_string(),
ld_url: "".to_string(),
}
}
@ -107,27 +117,29 @@ impl Wallet {
self.db.reset_db()
}
pub fn valid_key(key: &str) -> i8 {
is_valid_key(key)
pub fn valid_key(&self, key: &str) -> i8 {
self.key_helpers.is_valid_key(key)
}
pub fn valid_address(address: &str) -> bool {
let recipient = RecipientAddress::decode(&NETWORK, address);
recipient.is_some()
}
pub fn new_account(&self, name: &str, data: &str) -> anyhow::Result<i32> {
pub fn new_account(&self, name: &str, data: &str, index: u32) -> anyhow::Result<i32> {
if data.is_empty() {
let mut entropy = [0u8; 32];
OsRng.fill_bytes(&mut entropy);
let mnemonic = Mnemonic::from_entropy(&entropy, Language::English)?;
let seed = mnemonic.phrase();
self.new_account_with_key(name, seed)
self.new_account_with_key(name, seed, index)
} else {
self.new_account_with_key(name, data)
self.new_account_with_key(name, data, index)
}
}
pub fn new_sub_account(&self, id: u32, name: &str) -> anyhow::Result<i32> {
let seed = self.db.get_seed(id)?.ok_or_else(|| anyhow!("Account has no seed"))?;
let index = self.db.next_account_id(&seed)?;
let new_id = self.new_account_with_key(name, &seed, index as u32)?;
Ok(new_id)
}
pub fn get_backup(&self, account: u32) -> anyhow::Result<String> {
let (seed, sk, ivk) = self.db.get_backup(account)?;
if let Some(seed) = seed {
@ -144,11 +156,11 @@ impl Wallet {
Ok(sk)
}
pub fn new_account_with_key(&self, name: &str, key: &str) -> anyhow::Result<i32> {
let (seed, sk, ivk, pa) = decode_key(key)?;
pub fn new_account_with_key(&self, name: &str, key: &str, index: u32) -> anyhow::Result<i32> {
let (seed, sk, ivk, pa) = self.key_helpers.decode_key(key, index)?;
let account = self
.db
.store_account(name, seed.as_deref(), sk.as_deref(), &ivk, &pa)?;
.store_account(name, seed.as_deref(), index, sk.as_deref(), &ivk, &pa)?;
if account > 0 {
self.db.create_taddr(account as u32)?;
}
@ -156,6 +168,7 @@ impl Wallet {
}
async fn scan_async(
coin_type: CoinType,
get_tx: bool,
db_path: &str,
chunk_size: u32,
@ -164,6 +177,7 @@ impl Wallet {
ld_url: &str,
) -> anyhow::Result<()> {
crate::scan::sync_async(
coin_type,
chunk_size,
get_tx,
db_path,
@ -182,6 +196,7 @@ impl Wallet {
// Not a method in order to avoid locking the instance
pub async fn sync_ex(
coin_type: CoinType,
get_tx: bool,
anchor_offset: u32,
db_path: &str,
@ -190,6 +205,7 @@ impl Wallet {
) -> anyhow::Result<()> {
let cb = Arc::new(Mutex::new(progress_callback));
Self::scan_async(
coin_type,
get_tx,
db_path,
DEFAULT_CHUNK_SIZE,
@ -198,7 +214,7 @@ impl Wallet {
ld_url,
)
.await?;
Self::scan_async(get_tx, db_path, DEFAULT_CHUNK_SIZE, 0, cb.clone(), ld_url).await?;
Self::scan_async(coin_type, get_tx, db_path, DEFAULT_CHUNK_SIZE, 0, cb.clone(), ld_url).await?;
Ok(())
}
@ -209,6 +225,7 @@ impl Wallet {
progress_callback: impl Fn(u32) + Send + 'static,
) -> anyhow::Result<()> {
Self::sync_ex(
self.db.coin_type,
get_tx,
anchor_offset,
&self.db_path,
@ -249,11 +266,11 @@ impl Wallet {
use_transparent: bool,
anchor_offset: u32,
) -> anyhow::Result<(Tx, Vec<u32>)> {
let mut tx_builder = TxBuilder::new(last_height);
let mut tx_builder = TxBuilder::new(self.db.coin_type, last_height);
let fvk = self.db.get_ivk(account)?;
let fvk =
decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &fvk)
decode_extended_full_viewing_key(self.network().hrp_sapling_extended_full_viewing_key(), &fvk)
.unwrap()
.unwrap();
let utxos = if use_transparent {
@ -283,7 +300,7 @@ impl Wallet {
.db
.get_tsk(account)?
.map(|tsk| SecretKey::from_str(&tsk).unwrap());
let extsk = decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &zsk)
let extsk = decode_extended_spending_key(self.network().hrp_sapling_extended_spending_key(), &zsk)
.unwrap()
.unwrap();
let prover = self
@ -374,7 +391,7 @@ impl Wallet {
pub fn new_diversified_address(&self, account: u32) -> anyhow::Result<String> {
let ivk = self.get_ivk(account)?;
let fvk = decode_extended_full_viewing_key(
NETWORK.hrp_sapling_extended_full_viewing_key(),
self.network().hrp_sapling_extended_full_viewing_key(),
&ivk,
)?
.unwrap();
@ -384,7 +401,7 @@ impl Wallet {
.address(diversifier_index)
.map_err(|_| anyhow::anyhow!("Cannot generate new address"))?;
self.db.store_diversifier(account, &new_diversifier_index)?;
let pa = encode_payment_address(NETWORK.hrp_sapling_payment_address(), &pa);
let pa = encode_payment_address(self.network().hrp_sapling_payment_address(), &pa);
Ok(pa)
}
@ -484,8 +501,8 @@ impl Wallet {
self.db.truncate_data()
}
pub fn make_payment_uri(address: &str, amount: u64, memo: &str) -> anyhow::Result<String> {
let addr = RecipientAddress::decode(&NETWORK, address)
pub fn make_payment_uri(&self, address: &str, amount: u64, memo: &str) -> anyhow::Result<String> {
let addr = RecipientAddress::decode(self.network(), address)
.ok_or_else(|| anyhow::anyhow!("Invalid address"))?;
let payment = Payment {
recipient_address: addr,
@ -499,18 +516,18 @@ impl Wallet {
payments: vec![payment],
};
let uri = treq
.to_uri(&NETWORK)
.to_uri(self.network())
.ok_or_else(|| anyhow::anyhow!("Cannot build Payment URI"))?;
let uri = format!("{}{}", TICKER, &uri[5..]); // hack to replace the URI scheme
let uri = format!("{}{}", self.chain().ticker(), &uri[5..]); // hack to replace the URI scheme
Ok(uri)
}
pub fn parse_payment_uri(uri: &str) -> anyhow::Result<String> {
if uri[..5].ne(TICKER) {
pub fn parse_payment_uri(&self, uri: &str) -> anyhow::Result<String> {
if uri[..5].ne(self.chain().ticker()) {
anyhow::bail!("Invalid Payment URI");
}
let uri = format!("zcash{}", &uri[5..]); // hack to replace the URI scheme
let treq = TransactionRequest::from_uri(&NETWORK, &uri)
let treq = TransactionRequest::from_uri(self.network(), &uri)
.map_err(|_| anyhow::anyhow!("Invalid Payment URI"))?;
if treq.payments.len() != 1 {
anyhow::bail!("Invalid Payment URI")
@ -528,7 +545,7 @@ impl Wallet {
None => Ok(String::new()),
}?;
let payment = MyPayment {
address: payment.recipient_address.encode(&NETWORK),
address: payment.recipient_address.encode(self.network()),
amount: u64::from(payment.amount),
memo,
};
@ -538,12 +555,12 @@ impl Wallet {
Ok(payment_json)
}
pub fn get_full_backup(&self, key: &str) -> anyhow::Result<String> {
self.db.get_full_backup(key)
pub fn get_full_backup(&self) -> anyhow::Result<Vec<AccountBackup>> {
self.db.get_full_backup()
}
pub fn restore_full_backup(&self, key: &str, backup: &str) -> anyhow::Result<()> {
self.db.restore_full_backup(key, backup)
pub fn restore_full_backup(&self, accounts: &[AccountBackup]) -> anyhow::Result<()> {
self.db.restore_full_backup(accounts)
}
pub fn store_share_secret(&self, account: u32, secret: &str, index: usize, threshold: usize, participants: usize) -> anyhow::Result<()> {
@ -565,6 +582,40 @@ impl Wallet {
let recipient_memos: Vec<_> = recipients.iter().map(|r| RecipientMemo::from(r)).collect();
Ok(recipient_memos)
}
fn chain(&self) -> &dyn CoinChain { get_coin_chain(self.coin_type) }
fn network(&self) -> &Network { self.chain().network() }
}
const NONCE: &'static[u8; 12] = b"unique nonce";
pub fn encrypt_backup(accounts: &[AccountBackup], key: &str) -> anyhow::Result<String> {
let accounts_bin = bincode::serialize(&accounts)?;
let (hrp, key, _) = bech32::decode(key)?;
if hrp != "zwk" { anyhow::bail!("Invalid backup key") }
let key = Vec::<u8>::from_base32(&key)?;
let key = Key::from_slice(&key);
let cipher = ChaCha20Poly1305::new(key);
// nonce is constant because we always use a different key!
let cipher_text = cipher.encrypt(Nonce::from_slice(NONCE), &*accounts_bin).map_err(|_e| anyhow::anyhow!("Failed to encrypt backup"))?;
let backup = base64::encode(cipher_text);
Ok(backup)
}
pub fn decrypt_backup(key: &str, backup: &str) -> anyhow::Result<Vec<AccountBackup>> {
let (hrp, key, _) = bech32::decode(key)?;
if hrp != "zwk" { anyhow::bail!("Not a valid decryption key"); }
let key = Vec::<u8>::from_base32(&key)?;
let key = Key::from_slice(&key);
let cipher = ChaCha20Poly1305::new(key);
let backup = base64::decode(backup)?;
let backup = cipher.decrypt(Nonce::from_slice(NONCE), &*backup).map_err(|_e| anyhow::anyhow!("Failed to decrypt backup"))?;
let accounts: Vec<AccountBackup> = bincode::deserialize(&backup)?;
Ok(accounts)
}
#[derive(Serialize)]
@ -576,10 +627,11 @@ struct MyPayment {
#[cfg(test)]
mod tests {
use crate::key::derive_secret_key;
use crate::key::KeyHelpers;
use crate::wallet::Wallet;
use crate::LWD_URL;
use bip39::{Language, Mnemonic};
use zcash_params::coin::CoinType;
#[tokio::test]
async fn test_wallet_seed() {
@ -587,8 +639,9 @@ mod tests {
env_logger::init();
let seed = dotenv::var("SEED").unwrap();
let wallet = Wallet::new("zec.db", LWD_URL);
wallet.new_account_with_key("test", &seed).unwrap();
let mut wallet = Wallet::new(CoinType::Zcash, "zec.db");
wallet.set_lwd_url(LWD_URL).unwrap();
wallet.new_account_with_key("test", &seed, 0).unwrap();
}
#[tokio::test]
@ -597,8 +650,9 @@ mod tests {
env_logger::init();
let seed = dotenv::var("SEED").unwrap();
let kh = KeyHelpers::new(CoinType::Zcash);
let (sk, vk, pa) =
derive_secret_key(&Mnemonic::from_phrase(&seed, Language::English).unwrap()).unwrap();
kh.derive_secret_key(&Mnemonic::from_phrase(&seed, Language::English).unwrap(), 0).unwrap();
println!("{} {} {}", sk, vk, pa);
// let wallet = Wallet::new("zec.db");
//
@ -608,7 +662,8 @@ mod tests {
#[test]
pub fn test_diversified_address() {
let wallet = Wallet::new("zec.db", LWD_URL);
let mut wallet = Wallet::new(CoinType::Zcash, "zec.db");
wallet.set_lwd_url(LWD_URL).unwrap();
let address = wallet.new_diversified_address(1).unwrap();
println!("{}", address);
}