diff --git a/benches/scan_all.rs b/benches/scan_all.rs index 267a608..8fd532d 100644 --- a/benches/scan_all.rs +++ b/benches/scan_all.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use sync::scan_all; +use warp_api_ffi::scan_all; use tokio::runtime::Runtime; use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_primitives::consensus::{Network, Parameters}; diff --git a/integrations/rust/Cargo.toml b/integrations/rust/Cargo.toml new file mode 100644 index 0000000..425e083 --- /dev/null +++ b/integrations/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "warpsync-rust-example" +version = "1.2.15" +authors = ["Hanh "] +edition = "2021" + +[[bin]] +name = "warp-example" +path = "src/main.rs" + +[dependencies] +tokio = { version = "^1.6", features = ["macros", "rt-multi-thread"] } +lazy_static = "1.4.0" +env_logger = "0.9" +log = "0.4" + +[dependencies.zcash-warpsync] +path = "../.." + diff --git a/integrations/rust/src/main.rs b/integrations/rust/src/main.rs new file mode 100644 index 0000000..f87fb71 --- /dev/null +++ b/integrations/rust/src/main.rs @@ -0,0 +1,42 @@ +use warp_api_ffi::api::account::{get_backup, new_account}; +use warp_api_ffi::api::sync::coin_sync; +use warp_api_ffi::{CoinConfig, init_coin, set_coin_lwd_url}; +use lazy_static::lazy_static; +use std::sync::Mutex; + +lazy_static! { + static ref CANCEL: Mutex = Mutex::new(false); +} + +const FVK: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz"; + +#[tokio::main] +async fn main() { + env_logger::init(); + + // Initialize the library for Zcash (coin = 0) + init_coin(0, "./zec.db").unwrap(); + set_coin_lwd_url(0, "https://lwdv3.zecwallet.co:443"); // ZecWallet Lightwalletd URL + + // Create a new account with the ZEC pages viewing key + let id_account = new_account(0, "test_account", Some(FVK.to_string()), + None).unwrap(); + + // Synchronize + coin_sync(0 /* zcash */, + true /* retrieve tx details */, + 0 /* sync to tip */, + 100 /* spam filter threshold */, |p| { + log::info!("Progress: {}", p.height); + }, &CANCEL).await.unwrap(); + + // Grab the database accessor + let cc = &CoinConfig::get(0 /* zcash */); + let db = cc.db.as_ref().unwrap().clone(); + let db = db.lock().unwrap(); + + // Query the account balance + let balance = db.get_balance(id_account).unwrap(); + + println!("Balance = {}", balance) +} \ No newline at end of file diff --git a/src/api/account.rs b/src/api/account.rs index 50b9fb1..204c5ac 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -1,3 +1,5 @@ +//! Account related API + // Account creation use crate::coinconfig::CoinConfig; @@ -5,7 +7,7 @@ use crate::db::AccountData; use crate::key2::decode_key; use crate::taddr::{derive_taddr, derive_tkeys}; use crate::transaction::retrieve_tx_info; -use crate::{connect_lightwalletd, derive_zip32, AccountInfo, KeyPack}; +use crate::{connect_lightwalletd, AccountInfo, KeyPack}; use anyhow::anyhow; use bip39::{Language, Mnemonic}; use rand::rngs::OsRng; @@ -14,7 +16,21 @@ use std::fs::File; use std::io::BufReader; use zcash_client_backend::encoding::{decode_extended_full_viewing_key, encode_payment_address}; use zcash_primitives::consensus::Parameters; +use crate::zip32::derive_zip32; +/// Create a new account +/// # Arguments +/// +/// * `coin`: 0 for zcash, 1 for ycash +/// * `name`: account name +/// * `key`: `Some(key)` where key is either a passphrase, +/// a secret key or a viewing key for an existing account, +/// or `None` for a new randomly generated account +/// * `index`: `Some(x)` for account at index `x` or +/// `None` for main account (same as x = 0) +/// +/// # Returns +/// `account id` pub fn new_account( coin: u8, name: &str, @@ -34,6 +50,17 @@ pub fn new_account( Ok(id_account) } +/// Create one or many sub accounts of the current account +/// +/// # Example +/// ```rust +/// crate::api::account::new_sub_account("test", None, 5) +/// ``` +/// +/// # Arguments +/// * `name`: name of the sub accounts. Every sub account will have the same name +/// * `index`: Starting index. If `None`, use the index following the highest used index +/// * `count`: Number of subaccounts to create pub fn new_sub_account(name: &str, index: Option, count: u32) -> anyhow::Result<()> { let c = CoinConfig::get_active(); let db = c.db()?; @@ -59,6 +86,14 @@ fn new_account_with_key(coin: u8, name: &str, key: &str, index: u32) -> anyhow:: Ok(account) } +/// Update the transparent secret key for the given account from a derivation path +/// +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account] +/// * `path`: derivation path +/// +/// Account must have a seed phrase pub fn import_transparent_key(coin: u8, id_account: u32, path: &str) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let db = c.db()?; @@ -69,6 +104,12 @@ pub fn import_transparent_key(coin: u8, id_account: u32, path: &str) -> anyhow:: Ok(()) } +/// Update the transparent secret key for the given account +/// +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account] +/// * `sk`: secret key pub fn import_transparent_secret_key(coin: u8, id_account: u32, sk: &str) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let db = c.db()?; @@ -77,6 +118,7 @@ pub fn import_transparent_secret_key(coin: u8, id_account: u32, sk: &str) -> any Ok(()) } +/// Generate a new diversified address pub fn new_diversified_address() -> anyhow::Result { let c = CoinConfig::get_active(); let db = c.db()?; @@ -96,11 +138,16 @@ pub fn new_diversified_address() -> anyhow::Result { Ok(pa) } +/// Retrieve the transparent balance for the current account from the LWD server pub async fn get_taddr_balance_default() -> anyhow::Result { let c = CoinConfig::get_active(); get_taddr_balance(c.coin, c.id_account).await } +/// Retrieve the transparent balance from the LWD server +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account] pub async fn get_taddr_balance(coin: u8, id_account: u32) -> anyhow::Result { let c = CoinConfig::get(coin); let mut client = c.connect_lwd().await?; @@ -112,6 +159,10 @@ pub async fn get_taddr_balance(coin: u8, id_account: u32) -> anyhow::Result Ok(balance) } +/// Look for accounts that have some transparent balance. Stop when the gap limit +/// is exceeded and no balance was found +/// # Arguments +/// * `gap_limit`: number of accounts with 0 balance before the scan stops pub async fn scan_transparent_accounts(gap_limit: usize) -> anyhow::Result<()> { let c = CoinConfig::get_active(); let mut client = c.connect_lwd().await?; @@ -119,8 +170,12 @@ pub async fn scan_transparent_accounts(gap_limit: usize) -> anyhow::Result<()> { Ok(()) } -// Account backup - +/// Get the backup string. It is either the passphrase, the secret key or the viewing key +/// depending on how the account was created +/// # Arguments +/// * `id_account`: account id as returned from [new_account] +/// +/// Use the current active coin pub fn get_backup(account: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let AccountData { seed, sk, fvk, .. } = c.db()?.get_account_info(account)?; @@ -133,30 +188,44 @@ pub fn get_backup(account: u32) -> anyhow::Result { Ok(fvk) } +/// Get the secret key. Returns empty string if the account has no secret key +/// # Arguments +/// * `id_account`: account id as returned from [new_account] +/// +/// Use the current active coin pub fn get_sk(account: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let AccountData { sk, .. } = c.db()?.get_account_info(account)?; Ok(sk.unwrap_or(String::new())) } +/// Reset the database +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash pub fn reset_db(coin: u8) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let db = c.db()?; db.reset_db() } +/// Truncate all non account data for the current active coin pub fn truncate_data() -> anyhow::Result<()> { let c = CoinConfig::get_active(); let db = c.db()?; db.truncate_data() } +/// Truncate all synchronization data for the current active coin pub fn truncate_sync_data() -> anyhow::Result<()> { let c = CoinConfig::get_active(); let db = c.db()?; db.truncate_sync_data() } +/// Delete an account +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account] pub fn delete_account(coin: u8, account: u32) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let db = c.db()?; @@ -164,9 +233,14 @@ pub fn delete_account(coin: u8, account: u32) -> anyhow::Result<()> { Ok(()) } +/// Import a ZWL data file +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `name`: prefix for the imported accounts +/// * `data`: data file pub fn import_from_zwl(coin: u8, name: &str, data: &str) -> anyhow::Result<()> { let c = CoinConfig::get(coin); - let sks = crate::read_zwl(data)?; + let sks = crate::misc::read_zwl(data)?; let db = c.db()?; for (i, key) in sks.iter().enumerate() { let name = format!("{}-{}", name, i + 1); @@ -176,6 +250,13 @@ pub fn import_from_zwl(coin: u8, name: &str, data: &str) -> anyhow::Result<()> { Ok(()) } +/// Derive keys using Zip-32 +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `id_account`: account id as returned from [new_account]. Must have a passphrase +/// * `account`: derived account index +/// * `external`: external/internal +/// * `address`: address index pub fn derive_keys( coin: u8, id_account: u32, @@ -190,6 +271,10 @@ pub fn derive_keys( derive_zip32(c.chain.network(), &seed, account, external, address) } +/// Import synchronization data obtained from external source +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `file`: file that contains the synchronization data pub async fn import_sync_data(coin: u8, file: &str) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let mut db = c.db()?; diff --git a/src/api/contact.rs b/src/api/contact.rs index 43bc29a..54e65ac 100644 --- a/src/api/contact.rs +++ b/src/api/contact.rs @@ -1,3 +1,5 @@ +//! Contact Address book + use crate::api::payment::{build_sign_send_multi_payment, RecipientMemo}; use crate::api::sync::get_latest_height; use crate::coinconfig::CoinConfig; @@ -5,6 +7,12 @@ use crate::contact::{serialize_contacts, Contact}; use crate::db::AccountData; use zcash_primitives::memo::Memo; +/// Store contact in the database +/// # Arguments +/// * `id`: contact id +/// * `name`: contact name +/// * `address`: contact address +/// * `dirty`: true if the database hasn't been saved to the blockchain yet pub fn store_contact(id: u32, name: &str, address: &str, dirty: bool) -> anyhow::Result<()> { let c = CoinConfig::get_active(); let contact = Contact { @@ -16,6 +24,9 @@ pub fn store_contact(id: u32, name: &str, address: &str, dirty: bool) -> anyhow: Ok(()) } +/// Save the new/modified contacts to the blockchain +/// # Arguments +/// * `anchor_offset`: minimum confirmations required for note selection pub async fn commit_unsaved_contacts(anchor_offset: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let contacts = c.db()?.get_unsaved_contacts()?; @@ -24,7 +35,7 @@ pub async fn commit_unsaved_contacts(anchor_offset: u32) -> anyhow::Result anyhow::Result { +async fn save_contacts_tx(memos: &[Memo], anchor_offset: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let last_height = get_latest_height().await?; let AccountData { address, .. } = c.db()?.get_account_info(c.id_account)?; diff --git a/src/api/dart_ffi.rs b/src/api/dart_ffi.rs index 4fa1973..803920b 100644 --- a/src/api/dart_ffi.rs +++ b/src/api/dart_ffi.rs @@ -8,13 +8,11 @@ use log::Level; use std::cell::RefCell; use std::ffi::{CStr, CString}; use std::os::raw::c_char; -use std::sync::atomic::AtomicBool; use std::sync::Mutex; use tokio::sync::Semaphore; use zcash_primitives::transaction::builder::Progress; static mut POST_COBJ: Option = None; -static IS_ERROR: AtomicBool = AtomicBool::new(false); const MAX_COINS: u8 = 3; @@ -90,56 +88,12 @@ fn try_init_logger() { let _ = env_logger::try_init(); } -// fn log_result(result: anyhow::Result) -> T { -// match result { -// Err(err) => { -// log::error!("ERROR: {}", err); -// let last_error = LAST_ERROR.lock().unwrap(); -// last_error.replace(err.to_string()); -// IS_ERROR.store(true, Ordering::Release); -// T::default() -// } -// Ok(v) => { -// IS_ERROR.store(false, Ordering::Release); -// v -// } -// } -// } -// -// fn log_string(result: anyhow::Result) -> String { -// match result { -// Err(err) => { -// log::error!("{}", err); -// let last_error = LAST_ERROR.lock().unwrap(); -// last_error.replace(err.to_string()); -// IS_ERROR.store(true, Ordering::Release); -// format!("{}", err) -// } -// Ok(v) => { -// IS_ERROR.store(false, Ordering::Release); -// v -// } -// } -// } - #[repr(C)] pub struct CResult { value: T, error: *mut c_char, } -// #[no_mangle] -// pub unsafe extern "C" fn get_error() -> bool { -// IS_ERROR.load(Ordering::Acquire) -// } -// -// #[no_mangle] -// pub unsafe extern "C" fn get_error_msg() -> *mut c_char { -// let error = LAST_ERROR.lock().unwrap(); -// let e = error.take(); -// to_c_str(e) -// } - #[no_mangle] pub unsafe extern "C" fn init_wallet(db_path: *mut c_char) { try_init_logger(); @@ -254,14 +208,6 @@ pub async unsafe extern "C" fn warp( get_tx, anchor_offset, max_cost, - move |downloaded| { - let mut downloaded = downloaded.into_dart(); - if port != 0 { - if let Some(p) = POST_COBJ { - p(port, &mut downloaded); - } - } - }, move |progress| { let mut progress = serde_json::to_string(&progress).unwrap().into_dart(); if port != 0 { @@ -588,7 +534,7 @@ pub unsafe extern "C" fn parse_payment_uri(uri: *mut c_char) -> CResult<*mut c_c #[no_mangle] pub unsafe extern "C" fn generate_random_enc_key() -> CResult<*mut c_char> { - to_cresult_str(crate::key::generate_random_enc_key()) + to_cresult_str(crate::api::fullbackup::generate_random_enc_key()) } #[no_mangle] @@ -624,7 +570,7 @@ pub unsafe extern "C" fn restore_full_backup(key: *mut c_char, backup: *mut c_ch pub unsafe extern "C" fn split_data(id: u32, data: *mut c_char) -> CResult<*mut c_char> { from_c_str!(data); let res = || { - let res = crate::FountainCodes::encode_into_drops(id, &base64::decode(&*data)?)?; + let res = crate::fountain::FountainCodes::encode_into_drops(id, &base64::decode(&*data)?)?; let output = serde_json::to_string(&res)?; Ok(output) }; @@ -632,11 +578,10 @@ pub unsafe extern "C" fn split_data(id: u32, data: *mut c_char) -> CResult<*mut } #[no_mangle] -// TODO: who uses this? pub unsafe extern "C" fn merge_data(drop: *mut c_char) -> CResult<*mut c_char> { from_c_str!(drop); let res = || { - let res = crate::put_drop(&*drop)? + let res = crate::fountain::put_drop(&*drop)? .map(|d| base64::encode(&d)) .unwrap_or(String::new()); Ok::<_, anyhow::Error>(res) @@ -650,7 +595,7 @@ pub unsafe extern "C" fn get_tx_summary(tx: *mut c_char) -> CResult<*mut c_char> from_c_str!(tx); let res = || { let tx: Tx = serde_json::from_str(&tx)?; - let summary = crate::get_tx_summary(&tx)?; + let summary = crate::pay::get_tx_summary(&tx)?; let summary = serde_json::to_string(&summary)?; Ok::<_, anyhow::Error>(summary) }; @@ -710,22 +655,22 @@ pub unsafe extern "C" fn disable_wal(db_path: *mut c_char) { #[no_mangle] pub unsafe extern "C" fn has_cuda() -> bool { - crate::has_cuda() + crate::gpu::has_cuda() } #[no_mangle] pub unsafe extern "C" fn has_metal() -> bool { - crate::has_metal() + crate::gpu::has_metal() } #[no_mangle] pub unsafe extern "C" fn has_gpu() -> bool { - crate::has_gpu() + crate::gpu::has_gpu() } #[no_mangle] pub unsafe extern "C" fn use_gpu(v: bool) { - crate::use_gpu(v) + crate::gpu::use_gpu(v) } #[tokio::main] diff --git a/src/api/fullbackup.rs b/src/api/fullbackup.rs index 0068a34..2d19372 100644 --- a/src/api/fullbackup.rs +++ b/src/api/fullbackup.rs @@ -1,23 +1,37 @@ +//! Save/Load account data as JSON use crate::coinconfig::CoinConfig; use crate::db::AccountBackup; -use bech32::FromBase32; +use bech32::{FromBase32, ToBase32, Variant}; use chacha20poly1305::aead::{Aead, NewAead}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use rand::RngCore; +use rand::rngs::OsRng; const NONCE: &[u8; 12] = b"unique nonce"; +/// Return backup data of every account for a given coin +/// # Argument +/// * `coin`: 0 for zcash, 1 for ycash pub fn get_full_backup(coin: u8) -> anyhow::Result> { let c = CoinConfig::get(coin); let db = c.db()?; db.get_full_backup(coin) } +/// Import backup data for a given coin +/// # Argument +/// * `coin`: 0 for zcash, 1 for ycash +/// * `accounts`: list of backups pub fn restore_full_backup(coin: u8, accounts: &[AccountBackup]) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let db = c.db()?; db.restore_full_backup(accounts) } +/// Encrypt a list of account backups +/// # Argument +/// * `accounts`: list of backups +/// * `key`: encryption key pub fn encrypt_backup(accounts: &[AccountBackup], key: &str) -> anyhow::Result { let accounts_bin = bincode::serialize(&accounts)?; let backup = if !key.is_empty() { @@ -40,6 +54,10 @@ pub fn encrypt_backup(accounts: &[AccountBackup], key: &str) -> anyhow::Result anyhow::Result> { let backup = if !key.is_empty() { let (hrp, key, _) = bech32::decode(key)?; @@ -61,3 +79,11 @@ pub fn decrypt_backup(key: &str, backup: &str) -> anyhow::Result = bincode::deserialize(&backup)?; Ok(accounts) } + +/// Generate a random encryption key +pub fn generate_random_enc_key() -> anyhow::Result { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + let key = bech32::encode("zwk", key.to_base32(), Variant::Bech32)?; + Ok(key) +} diff --git a/src/api/historical_prices.rs b/src/api/historical_prices.rs index 5d0b05d..a54d6d8 100644 --- a/src/api/historical_prices.rs +++ b/src/api/historical_prices.rs @@ -1,5 +1,12 @@ +//! Retrieve Historical Prices from coingecko + use crate::coinconfig::CoinConfig; +/// Retrieve historical prices +/// # Arguments +/// * `now`: current timestamp +/// * `days`: how many days to fetch +/// * `currency`: base currency pub async fn sync_historical_prices(now: i64, days: u32, currency: &str) -> anyhow::Result { let c = CoinConfig::get_active(); let mut db = c.db()?; diff --git a/src/api/mempool.rs b/src/api/mempool.rs index 8ef8918..f4f51a9 100644 --- a/src/api/mempool.rs +++ b/src/api/mempool.rs @@ -1,15 +1,17 @@ +//! Access to server mempool + use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_primitives::consensus::Parameters; +use crate::api::sync::get_latest_height; use crate::coinconfig::CoinConfig; use crate::db::AccountData; -use crate::get_latest_height; +/// Scan the mempool and return the unconfirmed balance pub async fn scan() -> anyhow::Result { let c = CoinConfig::get_active(); let AccountData { fvk, .. } = c.db()?.get_account_info(c.id_account)?; - let mut client = c.connect_lwd().await?; - let height = get_latest_height(&mut client).await?; + let height = get_latest_height().await?; let mut mempool = c.mempool.lock().unwrap(); let current_height = c.height; if height != current_height { @@ -21,6 +23,7 @@ pub async fn scan() -> anyhow::Result { &fvk, )? .unwrap(); + let mut client = c.connect_lwd().await?; mempool .update(&mut client, height, &fvk.fvk.vk.ivk()) .await?; diff --git a/src/api/message.rs b/src/api/message.rs index 1055503..3169cb6 100644 --- a/src/api/message.rs +++ b/src/api/message.rs @@ -1,11 +1,20 @@ +//! Mark messages read + use crate::coinconfig::CoinConfig; +/// Mark a given message as read or unread +/// # Arguments +/// * `message`: message id +/// * `read`: read or unread pub fn mark_message_read(message: u32, read: bool) -> anyhow::Result<()> { let c = CoinConfig::get_active(); c.db()?.mark_message_read(message, read)?; Ok(()) } +/// Mark all messages as read or unread +/// # Arguments +/// * `read`: read or unread pub fn mark_all_messages_read(read: bool) -> anyhow::Result<()> { let c = CoinConfig::get_active(); c.db()?.mark_all_messages_read(c.id_account, read)?; diff --git a/src/api/payment.rs b/src/api/payment.rs index e07b10a..9aa9580 100644 --- a/src/api/payment.rs +++ b/src/api/payment.rs @@ -1,3 +1,5 @@ +//! Payments + use anyhow::anyhow; use std::str::FromStr; @@ -77,6 +79,11 @@ fn sign(tx: &Tx, progress_callback: PaymentProgressCallback) -> anyhow::Result anyhow::Result { let last_height = get_latest_height().await?; let tx_id = build_sign_send_multi_payment(last_height, &[], true, 0, Box::new(|_| {})).await?; Ok(tx_id) } +/// Parse a json document that contains a list of recipients pub fn parse_recipients(recipients: &str) -> anyhow::Result> { let c = CoinConfig::get_active(); let AccountData { address, .. } = c.db()?.get_account_info(c.id_account)?; @@ -135,12 +154,14 @@ pub fn parse_recipients(recipients: &str) -> anyhow::Result> Ok(recipient_memos) } +/// Encode a message into a memo 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 } +/// Decode a memo into a message pub fn decode_memo( id_tx: u32, memo: &str, diff --git a/src/api/payment_uri.rs b/src/api/payment_uri.rs index a4af5fb..b903933 100644 --- a/src/api/payment_uri.rs +++ b/src/api/payment_uri.rs @@ -1,3 +1,5 @@ +//! encode and decode Payment URI + use crate::coinconfig::CoinConfig; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -7,6 +9,11 @@ use zcash_client_backend::zip321::{Payment, TransactionRequest}; use zcash_primitives::memo::Memo; use zcash_primitives::transaction::components::Amount; +/// Build a payment URI +/// # Arguments +/// * `address`: recipient address +/// * `amount`: amount in zats +/// * `memo`: memo text pub fn make_payment_uri(address: &str, amount: u64, memo: &str) -> anyhow::Result { let c = CoinConfig::get_active(); let addr = RecipientAddress::decode(c.chain.network(), address) @@ -29,6 +36,9 @@ pub fn make_payment_uri(address: &str, amount: u64, memo: &str) -> anyhow::Resul Ok(uri) } +/// Decode a payment uri +/// # Arguments +/// * `uri`: payment uri pub fn parse_payment_uri(uri: &str) -> anyhow::Result { let c = CoinConfig::get_active(); let scheme = c.chain.ticker(); diff --git a/src/api/sync.rs b/src/api/sync.rs index d353470..eaee19e 100644 --- a/src/api/sync.rs +++ b/src/api/sync.rs @@ -1,17 +1,26 @@ -// Sync +//! Warp Synchronize use crate::coinconfig::CoinConfig; use crate::db::PlainNote; use crate::scan::{AMProgressCallback, Progress}; -use crate::{AccountData, BlockId, CTree, CompactTxStreamerClient, DbAdapter}; +use crate::{AccountData, BlockId, CompactTxStreamerClient, DbAdapter}; use std::sync::Arc; use tokio::sync::Mutex; use tonic::transport::Channel; use tonic::Request; use zcash_primitives::sapling::Note; +use crate::commitment::CTree; const DEFAULT_CHUNK_SIZE: u32 = 100_000; +/// Asynchronously perform warp sync +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash +/// * `get_tx`: true to retrieve transaction details +/// * `anchor_offset`: minimum number of confirmations for note selection +/// * `max_cost`: tx that have a higher spending cost are excluded +/// * `progress_callback`: function callback during synchronization +/// * `cancel`: cancellation mutex, set to true to abort pub async fn coin_sync( coin: u8, get_tx: bool, @@ -69,6 +78,7 @@ async fn coin_sync_impl( Ok(()) } +/// Return the latest block height pub async fn get_latest_height() -> anyhow::Result { let c = CoinConfig::get_active(); let mut client = c.connect_lwd().await?; @@ -76,12 +86,17 @@ pub async fn get_latest_height() -> anyhow::Result { Ok(last_height) } +/// Return the latest block height synchronized pub fn get_synced_height() -> anyhow::Result { let c = CoinConfig::get_active(); let db = c.db()?; db.get_last_sync_height().map(|h| h.unwrap_or(0)) } +/// Skip block synchronization and directly mark the chain synchronized +/// Used for new accounts that have no transaction history +/// # Arguments +/// * `coin`: 0 for zcash, 1 for ycash pub async fn skip_to_last_height(coin: u8) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let mut client = c.connect_lwd().await?; @@ -90,16 +105,17 @@ pub async fn skip_to_last_height(coin: u8) -> anyhow::Result<()> { Ok(()) } -// if exact = true, do not snap the height to a pre existing checkpoint -// and get the tree_state from the server -// this option is used when we rescan from a given height -// We ignore any transaction that occurred before +/// Rewind to a previous block height +/// +/// Height is snapped to a closest earlier checkpoint. +/// The effective height is returned pub async fn rewind_to(height: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let height = c.db()?.trim_to_height(height)?; Ok(height) } +/// Synchronize from a given height pub async fn rescan_from(height: u32) -> anyhow::Result<()> { let c = CoinConfig::get_active(); c.db()?.truncate_sync_data()?; @@ -129,6 +145,7 @@ async fn fetch_and_store_tree_state( Ok(()) } +/// Return the date of sapling activation pub async fn get_activation_date() -> anyhow::Result { let c = CoinConfig::get_active(); let mut client = c.connect_lwd().await?; @@ -136,6 +153,9 @@ pub async fn get_activation_date() -> anyhow::Result { Ok(date_time) } +/// Return the block height for a given timestamp +/// # Arguments +/// * `time`: seconds since epoch pub async fn get_block_by_time(time: u32) -> anyhow::Result { let c = CoinConfig::get_active(); let mut client = c.connect_lwd().await?; @@ -143,7 +163,7 @@ pub async fn get_block_by_time(time: u32) -> anyhow::Result { Ok(date_time) } -pub fn trial_decrypt( +fn trial_decrypt( height: u32, cmu: &[u8], epk: &[u8], diff --git a/src/chain.rs b/src/chain.rs index f0ba5f9..dd2deb9 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -3,10 +3,9 @@ use crate::db::AccountViewKey; use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient; use crate::lw_rpc::*; use crate::scan::Blocks; -use crate::{advance_tree, has_cuda}; +use crate::builder::advance_tree; use ff::PrimeField; use futures::{future, FutureExt}; -use lazy_static::lazy_static; use log::info; use rand::prelude::SliceRandom; use rand::rngs::OsRng; @@ -470,7 +469,7 @@ impl DecryptNode { if blocks.is_empty() { return vec![]; } - if has_cuda() { + if crate::gpu::has_cuda() { let processor = CudaProcessor::setup_decrypt(network, blocks).unwrap(); return trial_decrypt(processor, self.vks.iter()).unwrap(); } @@ -546,7 +545,8 @@ fn calculate_tree_state_v1( witnesses } -pub fn calculate_tree_state_v2(cbs: &[CompactBlock], blocks: &[DecryptedBlock]) -> Vec { +#[allow(dead_code)] +fn calculate_tree_state_v2(cbs: &[CompactBlock], blocks: &[DecryptedBlock]) -> Vec { let mut p = 0usize; let mut nodes: Vec = vec![]; let mut positions: Vec = vec![]; @@ -593,6 +593,7 @@ pub fn calculate_tree_state_v2(cbs: &[CompactBlock], blocks: &[DecryptedBlock]) new_witnesses } +/// Connect to a lightwalletd server pub async fn connect_lightwalletd(url: &str) -> anyhow::Result> { log::info!("LWD URL: {}", url); let mut channel = tonic::transport::Channel::from_shared(url.to_owned())?; @@ -613,6 +614,8 @@ async fn get_height(server: String) -> Option<(String, u32)> { Some((server, height)) } +/// Return the URL of the best server given a list of servers +/// The best server is the one that has the highest height pub async fn get_best_server(servers: &[String]) -> Option { let mut server_heights = vec![]; for s in servers.iter() { diff --git a/src/coinconfig.rs b/src/coinconfig.rs index 931f5e3..6e5fdf2 100644 --- a/src/coinconfig.rs +++ b/src/coinconfig.rs @@ -1,4 +1,6 @@ -use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter, FountainCodes, MemPool}; +use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter}; +use crate::fountain::FountainCodes; +use crate::mempool::MemPool; use anyhow::anyhow; use lazy_static::lazy_static; use lazycell::AtomicLazyCell; @@ -21,10 +23,12 @@ lazy_static! { pub static ACTIVE_COIN: AtomicU8 = AtomicU8::new(0); +/// Set the active coin pub fn set_active(active: u8) { ACTIVE_COIN.store(active, Ordering::Release); } +/// Set the active account for a given coin pub fn set_active_account(coin: u8, id: u32) { let mempool = { let mut c = COIN_CONFIG[coin as usize].lock().unwrap(); @@ -35,16 +39,19 @@ pub fn set_active_account(coin: u8, id: u32) { let _ = mempool.clear(); } +/// Set the lightwalletd url for a given coin pub fn set_coin_lwd_url(coin: u8, lwd_url: &str) { let mut c = COIN_CONFIG[coin as usize].lock().unwrap(); c.lwd_url = Some(lwd_url.to_string()); } +/// Get the URL of the lightwalletd server for a given coin pub fn get_coin_lwd_url(coin: u8) -> String { let c = COIN_CONFIG[coin as usize].lock().unwrap(); c.lwd_url.clone().unwrap_or_default() } +/// Initialize a coin with a database path pub fn init_coin(coin: u8, db_path: &str) -> anyhow::Result<()> { let mut c = COIN_CONFIG[coin as usize].lock().unwrap(); c.set_db_path(db_path)?; diff --git a/src/db.rs b/src/db.rs index 51c3af4..74d2e6d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,7 +3,7 @@ use crate::contact::Contact; use crate::prices::Quote; use crate::taddr::{derive_tkeys, TBalance}; use crate::transaction::TransactionInfo; -use crate::{CTree, Witness}; +use crate::commitment::{CTree, Witness}; use rusqlite::Error::QueryReturnedNoRows; use rusqlite::{params, Connection, OptionalExtension, Transaction}; use serde::{Deserialize, Serialize}; @@ -1148,7 +1148,7 @@ pub struct AccountData { #[cfg(test)] mod tests { use crate::db::{DbAdapter, ReceivedNote, DEFAULT_DB_PATH}; - use crate::{CTree, Witness}; + use crate::commitment::{CTree, Witness}; use zcash_params::coin::CoinType; #[test] @@ -1157,8 +1157,8 @@ mod tests { db.init_db().unwrap(); db.trim_to_height(0).unwrap(); - db.store_block(1, &[0u8; 32], 0, &CTree::new()).unwrap(); let db_tx = db.begin_transaction().unwrap(); + DbAdapter::store_block(&db_tx, 1, &[0u8; 32], 0, &CTree::new()).unwrap(); let id_tx = DbAdapter::store_transaction(&[0; 32], 1, 1, 0, 20, &db_tx).unwrap(); DbAdapter::store_received_note( &ReceivedNote { @@ -1184,8 +1184,8 @@ mod tests { filled: vec![], cursor: CTree::new(), }; + DbAdapter::store_witnesses(&db_tx, &witness, 1000, 1).unwrap(); db_tx.commit().unwrap(); - Db::store_witnesses(&witness, 1000, 1).unwrap(); } #[test] diff --git a/src/gpu/cuda.rs b/src/gpu/cuda.rs index 4326dbb..caf71aa 100644 --- a/src/gpu/cuda.rs +++ b/src/gpu/cuda.rs @@ -1,7 +1,7 @@ use crate::chain::DecryptedBlock; use crate::gpu::{collect_nf, GPUProcessor}; use crate::lw_rpc::CompactBlock; -use crate::{Hash, GENERATORS_EXP}; +use crate::{Hash, hash::GENERATORS_EXP}; use anyhow::Result; use ff::BatchInverter; use jubjub::Fq; diff --git a/src/key.rs b/src/key.rs index e4d5541..1e5b31c 100644 --- a/src/key.rs +++ b/src/key.rs @@ -112,10 +112,3 @@ impl KeyHelpers { recipient.is_some() } } - -pub fn generate_random_enc_key() -> anyhow::Result { - let mut key = [0u8; 32]; - OsRng.fill_bytes(&mut key); - let key = bech32::encode("zwk", key.to_base32(), Variant::Bech32)?; - Ok(key) -} diff --git a/src/lib.rs b/src/lib.rs index c55c9ca..de74177 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,65 @@ // #![allow(dead_code)] // #![allow(unused_imports)] +// #![warn(missing_docs)] + +//! A library for fast synchronization of y/zcash blockchain +//! +//! - Implements the warp sync algorithm for sapling +//! - Multi Account management + +//! # Example +//! ```rust +//! use warp_api_ffi::api::account::{get_backup, new_account}; +//! use warp_api_ffi::api::sync::coin_sync; +//! use warp_api_ffi::{CoinConfig, init_coin, set_coin_lwd_url}; +//! use lazy_static::lazy_static; +//! use std::sync::Mutex; +//! +//! lazy_static! { +//! static ref CANCEL: Mutex = Mutex::new(false); +//! } +//! +//! const FVK: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz"; +//! +//! #[tokio::main] +//! async fn main() { +//! env_logger::init(); +//! +//! // Initialize the library for Zcash (coin = 0) +//! init_coin(0, "./zec.db").unwrap(); +//! set_coin_lwd_url(0, "https://lwdv3.zecwallet.co:443"); // ZecWallet Lightwalletd URL +//! +//! // Create a new account with the ZEC pages viewing key +//! let id_account = new_account(0, "test_account", Some(FVK.to_string()), +//! None).unwrap(); +//! +//! // Synchronize +//! coin_sync(0 /* zcash */, +//! true /* retrieve tx details */, +//! 0 /* sync to tip */, +//! 100 /* spam filter threshold */, |p| { +//! log::info!("Progress: {}", p.height); +//! }, &CANCEL).await.unwrap(); +//! +//! // Grab the database accessor +//! let cc = &CoinConfig::get(0 /* zcash */); +//! let db = cc.db.as_ref().unwrap().clone(); +//! let db = db.lock().unwrap(); +//! +//! // Query the account balance +//! let balance = db.get_balance(id_account).unwrap(); +//! +//! println!("Balance = {}", balance) +//! } +//! ``` + #[path = "generated/cash.z.wallet.sdk.rpc.rs"] pub mod lw_rpc; -pub use zcash_params::coin::{get_branch, get_coin_type, CoinType}; +use zcash_params::coin::{get_branch, get_coin_type, CoinType}; // Mainnet -pub const LWD_URL: &str = "https://mainnet.lightwalletd.com:9067"; +const LWD_URL: &str = "https://mainnet.lightwalletd.com:9067"; // pub const LWD_URL: &str = "https://lwdv3.zecwallet.co"; // pub const LWD_URL: &str = "http://lwd.hanh.me:9067"; // pub const LWD_URL: &str = "http://127.0.0.1:9067"; @@ -40,6 +93,7 @@ mod transaction; mod ua; mod zip32; // mod wallet; +/// accounts, sync, payments, etc. pub mod api; #[cfg(feature = "ledger")] @@ -56,34 +110,21 @@ mod ledger { } } -pub fn hex_to_hash(hex: &str) -> anyhow::Result<[u8; 32]> { - let mut hash = [0u8; 32]; - hex::decode_to_slice(hex, &mut hash)?; - Ok(hash) -} - -pub use crate::builder::advance_tree; pub use crate::chain::{ - calculate_tree_state_v2, connect_lightwalletd, download_chain, get_best_server, - get_latest_height, ChainError, DecryptNode, + connect_lightwalletd, get_best_server, + ChainError, }; pub use crate::coinconfig::{ init_coin, set_active, set_active_account, set_coin_lwd_url, CoinConfig, }; -pub use crate::commitment::{CTree, Witness}; pub use crate::db::{AccountData, AccountInfo, AccountRec, DbAdapter, TxRec}; -pub use crate::fountain::{put_drop, FountainCodes, RaptorQDrops}; -pub use crate::hash::{pedersen_hash, Hash, GENERATORS_EXP}; -pub use crate::key::{generate_random_enc_key, KeyHelpers}; +// pub use crate::fountain::FountainCodes; +pub use crate::hash::Hash; +pub use crate::key::KeyHelpers; pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient; pub use crate::lw_rpc::*; -pub use crate::mempool::MemPool; -pub use crate::misc::read_zwl; -pub use crate::pay::{broadcast_tx, get_tx_summary, Tx, TxIn, TxOut}; -pub use crate::print::*; -pub use crate::scan::{latest_height, sync_async}; -pub use crate::ua::{get_sapling, get_ua}; -pub use zip32::{derive_zip32, KeyPack}; +pub use crate::pay::{broadcast_tx, Tx, TxIn, TxOut}; +pub use zip32::KeyPack; // pub use crate::wallet::{decrypt_backup, encrypt_backup, RecipientMemo, Wallet, WalletBalance}; #[cfg(feature = "ledger_sapling")] @@ -97,4 +138,3 @@ pub mod nodejs; mod gpu; -pub use gpu::{has_cuda, has_gpu, has_metal, use_gpu}; diff --git a/src/pay.rs b/src/pay.rs index dc4fa37..4d3661a 100644 --- a/src/pay.rs +++ b/src/pay.rs @@ -2,7 +2,7 @@ use crate::db::SpendableNote; // use crate::wallet::RecipientMemo; use crate::api::payment::RecipientMemo; use crate::coinconfig::CoinConfig; -use crate::{get_latest_height, hex_to_hash, GetAddressUtxosReply, RawTransaction}; +use crate::{GetAddressUtxosReply, Hash, RawTransaction}; use anyhow::anyhow; use jubjub::Fr; use rand::prelude::SliceRandom; @@ -28,6 +28,7 @@ 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 crate::chain::get_latest_height; #[derive(Serialize, Deserialize, Debug)] pub struct Tx { @@ -318,7 +319,7 @@ impl Tx { let mut builder = Builder::new(*chain.network(), last_height); let efvk = ExtendedFullViewingKey::from(zsk); - let ovk = hex_to_hash(&self.ovk)?; + let ovk: Hash = hex::decode(&self.ovk)?.try_into().unwrap(); builder.send_change_to( OutgoingViewingKey(ovk), decode_payment_address(chain.network().hrp_sapling_payment_address(), &self.change) @@ -406,6 +407,7 @@ impl Tx { } } +/// Broadcast a raw signed transaction to the network pub async fn broadcast_tx(tx: &[u8]) -> anyhow::Result { let c = CoinConfig::get_active(); let mut client = c.connect_lwd().await?; diff --git a/src/print.rs b/src/print.rs index ce7a55e..f315ce9 100644 --- a/src/print.rs +++ b/src/print.rs @@ -1,4 +1,4 @@ -use crate::{CTree, Witness}; +use crate::commitment::{CTree, Witness}; use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; use zcash_primitives::sapling::Node; diff --git a/src/scan.rs b/src/scan.rs index e121654..22b15fb 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -1,14 +1,16 @@ use crate::builder::BlockProcessor; -use crate::chain::{DecryptedBlock, Nf, NfRef}; +use crate::chain::{DecryptedBlock, get_latest_height, Nf, NfRef}; use crate::db::{AccountViewKey, DbAdapter, PlainNote, ReceivedNote}; use serde::Serialize; use std::cmp::Ordering; use crate::transaction::retrieve_tx_info; use crate::{ - connect_lightwalletd, download_chain, get_latest_height, CompactBlock, CompactSaplingOutput, - CompactTx, DecryptNode, Witness, + connect_lightwalletd, CompactBlock, CompactSaplingOutput, + CompactTx }; +use crate::chain::{DecryptNode, download_chain}; +use crate::commitment::Witness; use ff::PrimeField; use anyhow::anyhow; @@ -43,9 +45,9 @@ impl std::fmt::Debug for Blocks { #[derive(Clone, Serialize)] pub struct Progress { - height: u32, - trial_decryptions: u64, - downloaded: usize, + pub height: u32, + pub trial_decryptions: u64, + pub downloaded: usize, } pub type ProgressCallback = dyn Fn(Progress) + Send; @@ -326,7 +328,8 @@ pub async fn sync_async( db_transaction.commit()?; // db_transaction is dropped here } - log::info!("progress: {}", dec_block.height); + progress.height = dec_block.height; + log::info!("progress: {}", progress.height); let callback = proc_callback.lock().await; callback(progress.clone()); } diff --git a/src/transaction.rs b/src/transaction.rs index 31ee22b..077931c 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -292,7 +292,8 @@ mod tests { nf_map.insert((nf.0, nf.2.clone()), nf.1); } } - let fvk = db.get_ivk(account).unwrap(); + let account = db.get_account_info(account).unwrap(); + let fvk = account.fvk.clone(); let fvk = decode_extended_full_viewing_key( Network::MainNetwork.hrp_sapling_extended_full_viewing_key(), &fvk, diff --git a/src/ua.rs b/src/ua.rs index eeb7f6b..a992df5 100644 --- a/src/ua.rs +++ b/src/ua.rs @@ -38,7 +38,7 @@ impl FromAddress for MyReceiver { } } -pub fn get_ua(_sapling_addr: &str, _transparent_addr: &str) -> anyhow::Result { +fn get_ua(_sapling_addr: &str, _transparent_addr: &str) -> anyhow::Result { todo!() // let sapling_addr = ZcashAddress::try_from_encoded(sapling_addr)?; // let transparent_addr = ZcashAddress::try_from_encoded(transparent_addr)?; @@ -53,7 +53,7 @@ pub fn get_ua(_sapling_addr: &str, _transparent_addr: &str) -> anyhow::Result anyhow::Result { +fn get_sapling(ua_addr: &str) -> anyhow::Result { let ua_addr = ZcashAddress::try_from_encoded(ua_addr)?; let r = ua_addr.convert::()?; if let Receiver::Sapling(data) = r.receiver { @@ -64,7 +64,7 @@ pub fn get_sapling(ua_addr: &str) -> anyhow::Result { #[cfg(test)] mod tests { - use crate::ua::{get_sapling, get_ua}; + use super::{get_sapling, get_ua}; #[test] fn test_ua() -> anyhow::Result<()> {