use crate::chain::{get_activation_date, get_block_by_time}; use crate::contact::{serialize_contacts, Contact}; use crate::db::{AccountBackup, ZMessage}; use crate::key::KeyHelpers; use crate::pay::Tx; use crate::pay::TxBuilder; use crate::prices::fetch_historical_prices; use crate::scan::AM_ProgressCallback; use crate::taddr::{get_taddr_balance, get_utxos, scan_transparent_accounts}; use crate::{ broadcast_tx, connect_lightwalletd, get_latest_height, BlockId, CTree, CompactTxStreamerClient, DbAdapter, }; use anyhow::anyhow; use bech32::FromBase32; use bip39::{Language, Mnemonic}; use chacha20poly1305::aead::{Aead, NewAead}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use lazycell::AtomicLazyCell; use rand::rngs::OsRng; use rand::RngCore; use secp256k1::SecretKey; use serde::Deserialize; use serde::Serialize; use std::convert::TryFrom; use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; use tonic::transport::Channel; use tonic::Request; use zcash_client_backend::address::RecipientAddress; use zcash_client_backend::encoding::{ decode_extended_full_viewing_key, decode_extended_spending_key, encode_payment_address, }; use zcash_client_backend::zip321::{Payment, TransactionRequest}; use zcash_params::coin::{get_coin_chain, CoinChain, CoinType}; use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS}; 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; const DEFAULT_CHUNK_SIZE: u32 = 100_000; pub struct Wallet { coin_type: CoinType, pub db_path: String, db: DbAdapter, key_helpers: KeyHelpers, prover: AtomicLazyCell, pub ld_url: String, } #[repr(C)] pub struct WalletBalance { pub confirmed: u64, pub unconfirmed: i64, pub spendable: u64, } impl Default for WalletBalance { fn default() -> Self { WalletBalance { confirmed: 0, unconfirmed: 0, spendable: 0, } } } // to_address: &str, // amount: u64, // memo: &str, // max_amount_per_note: u64, #[derive(Deserialize)] pub struct Recipient { pub address: String, pub amount: u64, pub reply_to: bool, pub subject: String, pub memo: String, pub max_amount_per_note: u64, } pub struct RecipientMemo { pub address: String, pub amount: u64, pub memo: Memo, pub max_amount_per_note: u64, } impl RecipientMemo { pub fn from_recipient(from: &str, r: &Recipient) -> Self { let memo = if !r.reply_to && r.subject.is_empty() { r.memo.clone() } else { encode_memo(from, r.reply_to, &r.subject, &r.memo) }; RecipientMemo { address: r.address.clone(), amount: r.amount, memo: Memo::from_str(&memo).unwrap(), max_amount_per_note: r.max_amount_per_note, } } } pub fn encode_memo(from: &str, include_from: bool, subject: &str, body: &str) -> String { let from = if include_from { from } else { &"" }; let msg = format!("\u{1F6E1}MSG\n{}\n{}\n{}", from, subject, body); msg } pub fn decode_memo(memo: &str, recipient: &str, timestamp: u32, height: u32) -> ZMessage { let memo_lines: Vec<_> = memo.splitn(4, '\n').collect(); let msg = if memo_lines[0] == "\u{1F6E1}MSG" { ZMessage { sender: if memo_lines[1].is_empty() { None } else { Some(memo_lines[1].to_string()) }, recipient: recipient.to_string(), subject: memo_lines[2].to_string(), body: memo_lines[3].to_string(), timestamp, height, } } else { ZMessage { sender: None, recipient: recipient.to_string(), subject: memo_lines[0].chars().take(20).collect(), body: memo.to_string(), timestamp, height, } }; msg } impl Wallet { 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: "".to_string(), } } pub fn reset_db(&self) -> anyhow::Result<()> { self.db.reset_db() } pub fn valid_key(&self, key: &str) -> i8 { self.key_helpers.is_valid_key(key) } pub fn new_account(&self, name: &str, data: &str, index: u32) -> anyhow::Result { 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, index) } else { self.new_account_with_key(name, data, index) } } pub fn new_sub_account(&self, id: u32, name: &str) -> anyhow::Result { let (seed, _) = self.db.get_seed(id)?; let seed = seed.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 new_sub_account_index(&self, id: u32, name: &str, index: u32) -> anyhow::Result { let (seed, _) = self.db.get_seed(id)?; let seed = seed.ok_or_else(|| anyhow!("Account has no seed"))?; let new_id = self.new_account_with_key(name, &seed, index)?; Ok(new_id) } pub fn get_backup(&self, account: u32) -> anyhow::Result { let (seed, sk, ivk) = self.db.get_backup(account)?; if let Some(seed) = seed { return Ok(seed); } if let Some(sk) = sk { return Ok(sk); } Ok(ivk) } pub fn get_sk(&self, account: u32) -> anyhow::Result { let sk = self.db.get_sk(account)?; Ok(sk) } pub fn new_account_with_key(&self, name: &str, key: &str, index: u32) -> anyhow::Result { let (seed, sk, ivk, pa) = self.key_helpers.decode_key(key, index)?; let account = self.db .store_account(name, seed.as_deref(), index, sk.as_deref(), &ivk, &pa)?; if account > 0 { self.db.create_taddr(account as u32)?; } Ok(account) } async fn scan_async( coin_type: CoinType, get_tx: bool, db_path: &str, chunk_size: u32, target_height_offset: u32, progress_callback: AM_ProgressCallback, ld_url: &str, ) -> anyhow::Result<()> { crate::scan::sync_async( coin_type, chunk_size, get_tx, db_path, target_height_offset, progress_callback, ld_url, ) .await } pub async fn get_latest_height(&self) -> anyhow::Result { let mut client = connect_lightwalletd(&self.ld_url).await?; let last_height = get_latest_height(&mut client).await?; Ok(last_height) } // 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, progress_callback: impl Fn(u32) + Send + 'static, ld_url: &str, ) -> anyhow::Result<()> { let cb = Arc::new(Mutex::new(progress_callback)); Self::scan_async( coin_type, get_tx, db_path, DEFAULT_CHUNK_SIZE, anchor_offset, cb.clone(), ld_url, ) .await?; Self::scan_async( coin_type, get_tx, db_path, DEFAULT_CHUNK_SIZE, 0, cb.clone(), ld_url, ) .await?; Ok(()) } pub async fn sync( &self, get_tx: bool, anchor_offset: u32, progress_callback: impl Fn(u32) + Send + 'static, ) -> anyhow::Result<()> { Self::sync_ex( self.db.coin_type, get_tx, anchor_offset, &self.db_path, progress_callback, &self.ld_url, ) .await } pub async fn skip_to_last_height(&self) -> anyhow::Result<()> { let mut client = connect_lightwalletd(&self.ld_url).await?; let last_height = get_latest_height(&mut client).await?; self.store_tree_state(&mut client, last_height).await?; Ok(()) } async fn store_tree_state( &self, client: &mut CompactTxStreamerClient, height: u32, ) -> anyhow::Result<()> { let block_id = BlockId { height: height as u64, hash: vec![], }; let block = client.get_block(block_id.clone()).await?.into_inner(); let tree_state = client .get_tree_state(Request::new(block_id)) .await? .into_inner(); let tree = CTree::read(&*hex::decode(&tree_state.tree)?)?; self.db .store_block(height, &block.hash, block.time, &tree)?; Ok(()) } pub async fn rewind_to_height(&mut self, height: u32) -> anyhow::Result<()> { let mut client = connect_lightwalletd(&self.ld_url).await?; self.db.trim_to_height(height)?; self.store_tree_state(&mut client, height).await?; Ok(()) } async fn prepare_multi_payment( &self, account: u32, last_height: u32, recipients: &[RecipientMemo], use_transparent: bool, anchor_offset: u32, ) -> anyhow::Result<(Tx, Vec)> { 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( self.network().hrp_sapling_extended_full_viewing_key(), &fvk, ) .unwrap() .unwrap(); let utxos = if use_transparent { let mut client = connect_lightwalletd(&self.ld_url).await?; get_utxos(&mut client, &self.db, account).await? } else { vec![] }; let target_amount: u64 = recipients.iter().map(|r| r.amount).sum(); let anchor_height = last_height.saturating_sub(anchor_offset); let spendable_notes = self.db.get_spendable_notes(account, anchor_height, &fvk)?; let note_ids = tx_builder.select_inputs(&fvk, &spendable_notes, &utxos, target_amount)?; tx_builder.select_outputs(&fvk, recipients)?; Ok((tx_builder.tx, note_ids)) } fn sign( &mut self, tx: &Tx, account: u32, progress_callback: impl Fn(Progress) + Send + 'static, ) -> anyhow::Result> { self._ensure_prover()?; let zsk = self.db.get_sk(account)?; let tsk = self .db .get_tsk(account)? .map(|tsk| SecretKey::from_str(&tsk).unwrap()); let extsk = decode_extended_spending_key(self.network().hrp_sapling_extended_spending_key(), &zsk) .unwrap() .unwrap(); let prover = self .prover .borrow() .ok_or_else(|| anyhow::anyhow!("Prover not initialized"))?; let raw_tx = tx.sign(tsk, &extsk, prover, progress_callback)?; Ok(raw_tx) } fn mark_spend(&mut self, selected_notes: &[u32]) -> anyhow::Result<()> { let db_tx = self.db.begin_transaction()?; for id_note in selected_notes.iter() { DbAdapter::mark_spent(*id_note, 0, &db_tx)?; } db_tx.commit()?; Ok(()) } /// Build a multi payment for offline signing pub async fn build_only_multi_payment( &mut self, account: u32, last_height: u32, recipients: &[RecipientMemo], use_transparent: bool, anchor_offset: u32, ) -> anyhow::Result { let (tx, _) = self .prepare_multi_payment( account, last_height, recipients, use_transparent, anchor_offset, ) .await?; let tx_str = serde_json::to_string(&tx)?; Ok(tx_str) } pub async fn sign_only_multi_payment( &mut self, tx_string: &str, account: u32, progress_callback: impl Fn(Progress) + Send + 'static, ) -> anyhow::Result> { let tx = serde_json::from_str::(tx_string)?; let raw_tx = self.sign(&tx, account, progress_callback)?; Ok(raw_tx) } /// Build, sign and broadcast a multi payment pub async fn build_sign_send_multi_payment( &mut self, account: u32, last_height: u32, recipients: &[RecipientMemo], use_transparent: bool, anchor_offset: u32, progress_callback: impl Fn(Progress) + Send + 'static, ) -> anyhow::Result { let (tx, note_ids) = self .prepare_multi_payment( account, last_height, recipients, use_transparent, anchor_offset, ) .await?; let raw_tx = self.sign(&tx, account, progress_callback)?; let tx_id = broadcast_tx(&raw_tx, &self.ld_url).await?; self.mark_spend(¬e_ids)?; Ok(tx_id) } pub async fn shield_taddr(&mut self, account: u32, last_height: u32) -> anyhow::Result { let tx_id = self .build_sign_send_multi_payment(account, last_height, &[], true, 0, |_| {}) .await?; Ok(tx_id) } fn _ensure_prover(&mut self) -> anyhow::Result<()> { if !self.prover.filled() { let prover = LocalTxProver::from_bytes(SPEND_PARAMS, OUTPUT_PARAMS); self.prover .fill(prover) .map_err(|_| anyhow::anyhow!("dup prover"))?; } Ok(()) } pub fn get_ivk(&self, account: u32) -> anyhow::Result { self.db.get_ivk(account) } pub fn new_diversified_address(&self, account: u32) -> anyhow::Result { let ivk = self.get_ivk(account)?; let fvk = decode_extended_full_viewing_key( self.network().hrp_sapling_extended_full_viewing_key(), &ivk, )? .unwrap(); let mut diversifier_index = self.db.get_diversifier(account)?; diversifier_index.increment().unwrap(); let (new_diversifier_index, pa) = fvk .find_address(diversifier_index) .ok_or_else(|| anyhow::anyhow!("Cannot generate new address"))?; self.db.store_diversifier(account, &new_diversifier_index)?; let pa = encode_payment_address(self.network().hrp_sapling_payment_address(), &pa); Ok(pa) } pub async fn get_taddr_balance(&self, account: u32) -> anyhow::Result { let mut client = connect_lightwalletd(&self.ld_url).await?; let address = self.db.get_taddr(account)?; let balance = match address { None => 0u64, Some(address) => get_taddr_balance(&mut client, &address).await?, }; Ok(balance) } pub fn store_contact( &self, id: u32, name: &str, address: &str, dirty: bool, ) -> anyhow::Result<()> { let contact = Contact { id, name: name.to_string(), address: address.to_string(), }; self.db.store_contact(&contact, dirty)?; Ok(()) } pub async fn commit_unsaved_contacts( &mut self, account: u32, anchor_offset: u32, ) -> anyhow::Result { let contacts = self.db.get_unsaved_contacts()?; let memos = serialize_contacts(&contacts)?; let tx_id = self .save_contacts_tx(&memos, account, anchor_offset) .await?; Ok(tx_id) } pub async fn save_contacts_tx( &mut self, memos: &[Memo], account: u32, anchor_offset: u32, ) -> anyhow::Result { let mut client = connect_lightwalletd(&self.ld_url).await?; let last_height = get_latest_height(&mut client).await?; let address = self.db.get_address(account)?; let recipients: Vec<_> = memos .iter() .map(|m| RecipientMemo { address: address.clone(), amount: 0, memo: m.clone(), max_amount_per_note: 0, }) .collect(); let tx_id = self .build_sign_send_multi_payment( account, last_height, &recipients, false, anchor_offset, |_| {}, ) .await?; Ok(tx_id) } pub fn mark_message_read(&self, _account: u32, message: u32, read: bool) -> anyhow::Result<()> { self.db.mark_message_read(message, read)?; Ok(()) } pub fn mark_all_messages_read(&self, account: u32, read: bool) -> anyhow::Result<()> { self.db.mark_all_messages_read(account, read)?; Ok(()) } pub fn set_lwd_url(&mut self, ld_url: &str) -> anyhow::Result<()> { self.ld_url = ld_url.to_string(); Ok(()) } pub async fn sync_historical_prices( &mut self, now: i64, days: u32, currency: &str, ) -> anyhow::Result { let quotes = fetch_historical_prices(now, days, currency, &self.db).await?; self.db.store_historical_prices("es, currency)?; Ok(quotes.len() as u32) } pub fn delete_account(&self, account: u32) -> anyhow::Result<()> { self.db.delete_account(account)?; Ok(()) } pub fn truncate_data(&self) -> anyhow::Result<()> { self.db.truncate_data() } pub fn make_payment_uri( &self, address: &str, amount: u64, memo: &str, ) -> anyhow::Result { let addr = RecipientAddress::decode(self.network(), address) .ok_or_else(|| anyhow::anyhow!("Invalid address"))?; let payment = Payment { recipient_address: addr, amount: Amount::from_u64(amount).map_err(|_| anyhow::anyhow!("Invalid amount"))?, memo: Some(Memo::from_str(memo)?.into()), label: None, message: None, other_params: vec![], }; let treq = TransactionRequest { payments: vec![payment], }; let uri = treq .to_uri(self.network()) .ok_or_else(|| anyhow::anyhow!("Cannot build Payment URI"))?; let uri = format!("{}{}", self.chain().ticker(), &uri[5..]); // hack to replace the URI scheme Ok(uri) } pub fn parse_payment_uri(&self, uri: &str) -> anyhow::Result { 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(self.network(), &uri) .map_err(|_| anyhow::anyhow!("Invalid Payment URI"))?; if treq.payments.len() != 1 { anyhow::bail!("Invalid Payment URI") } let payment = &treq.payments[0]; let memo = match payment.memo { Some(ref memo) => { let memo = Memo::try_from(memo.clone())?; match memo { Memo::Text(text) => Ok(text.to_string()), Memo::Empty => Ok(String::new()), _ => Err(anyhow::anyhow!("Invalid Memo")), } } None => Ok(String::new()), }?; let payment = MyPayment { address: payment.recipient_address.encode(self.network()), amount: u64::from(payment.amount), memo, }; let payment_json = serde_json::to_string(&payment)?; Ok(payment_json) } pub fn get_full_backup(&self) -> anyhow::Result> { self.db.get_full_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<()> { self.db .store_share_secret(account, secret, index, threshold, participants) } pub fn get_share_secret(&self, account: u32) -> anyhow::Result { self.db.get_share_secret(account) } pub fn parse_recipients( &self, account: u32, recipients: &str, ) -> anyhow::Result> { let address = self.db.get_address(account)?; let recipients: Vec = serde_json::from_str(recipients)?; let recipient_memos: Vec<_> = recipients .iter() .map(|r| RecipientMemo::from_recipient(&address, r)) .collect(); Ok(recipient_memos) } #[cfg(feature = "ledger_sapling")] pub async fn ledger_sign(&mut self, tx_filename: &str) -> anyhow::Result { self._ensure_prover()?; let file = std::file::File::open(tx_filename)?; let mut tx: Tx = serde_json::from_reader(&file)?; let raw_tx = crate::build_tx_ledger(&mut tx, self.prover.borrow().unwrap()).await?; let tx_id = broadcast_tx(&raw_tx, &self.ld_url).await?; Ok(tx_id) } #[cfg(not(feature = "ledger_sapling"))] pub async fn ledger_sign(&mut self, _tx_filename: &str) -> anyhow::Result { unimplemented!() } pub async fn get_activation_date(&self) -> anyhow::Result { let mut client = connect_lightwalletd(&self.ld_url).await?; let date_time = get_activation_date(self.network(), &mut client).await?; Ok(date_time) } pub async fn get_block_by_time(&self, time: u32) -> anyhow::Result { let mut client = connect_lightwalletd(&self.ld_url).await?; let date_time = get_block_by_time(self.network(), &mut client, time).await?; Ok(date_time) } pub async fn scan_transparent_accounts( &self, account: u32, gap_limit: usize, ) -> anyhow::Result<()> { let mut client = connect_lightwalletd(&self.ld_url).await?; scan_transparent_accounts(self.network(), &mut client, &self.db, account, gap_limit) .await?; Ok(()) } 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 { let accounts_bin = bincode::serialize(&accounts)?; let backup = if !key.is_empty() { let (hrp, key, _) = bech32::decode(key)?; if hrp != "zwk" { anyhow::bail!("Invalid backup key") } let key = Vec::::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"))?; base64::encode(cipher_text) } else { base64::encode(accounts_bin) }; Ok(backup) } pub fn decrypt_backup(key: &str, backup: &str) -> anyhow::Result> { let backup = if !key.is_empty() { let (hrp, key, _) = bech32::decode(key)?; if hrp != "zwk" { anyhow::bail!("Not a valid decryption key"); } let key = Vec::::from_base32(&key)?; let key = Key::from_slice(&key); let cipher = ChaCha20Poly1305::new(key); let backup = base64::decode(backup)?; cipher .decrypt(Nonce::from_slice(NONCE), &*backup) .map_err(|_e| anyhow::anyhow!("Failed to decrypt backup"))? } else { base64::decode(backup)? }; let accounts: Vec = bincode::deserialize(&backup)?; Ok(accounts) } #[derive(Serialize)] struct MyPayment { address: String, amount: u64, memo: String, } #[cfg(test)] mod tests { 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() { dotenv::dotenv().unwrap(); env_logger::init(); let seed = dotenv::var("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] async fn test_payment() { dotenv::dotenv().unwrap(); env_logger::init(); let seed = dotenv::var("SEED").unwrap(); let kh = KeyHelpers::new(CoinType::Zcash); let (sk, vk, pa) = kh .derive_secret_key(&Mnemonic::from_phrase(&seed, Language::English).unwrap(), 0) .unwrap(); println!("{} {} {}", sk, vk, pa); // let wallet = Wallet::new("zec.db"); // // let tx_id = wallet.send_payment(1, &pa, 1000).await.unwrap(); // println!("TXID = {}", tx_id); } #[test] pub fn test_diversified_address() { 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); } }