2021-01-12 17:24:18 -08:00
|
|
|
//! Functions for querying information in the wdb database.
|
2019-03-08 18:23:31 -08:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
use ff::PrimeField;
|
|
|
|
use rusqlite::{params, OptionalExtension, ToSql, NO_PARAMS};
|
2021-01-11 17:13:40 -08:00
|
|
|
use std::collections::HashMap;
|
2020-08-06 13:11:25 -07:00
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
use zcash_primitives::{
|
2020-08-25 14:20:12 -07:00
|
|
|
block::BlockHash,
|
|
|
|
consensus::{self, BlockHeight, NetworkUpgrade},
|
2020-08-18 12:48:59 -07:00
|
|
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
2020-08-05 16:01:22 -07:00
|
|
|
note_encryption::Memo,
|
2021-01-08 20:49:38 -08:00
|
|
|
primitives::{Nullifier, PaymentAddress},
|
2020-08-06 17:13:24 -07:00
|
|
|
sapling::Node,
|
2021-01-12 17:24:18 -08:00
|
|
|
transaction::{components::Amount, Transaction, TxId},
|
2020-08-06 13:11:25 -07:00
|
|
|
zip32::ExtendedFullViewingKey,
|
2020-08-05 16:01:22 -07:00
|
|
|
};
|
2019-03-08 18:23:31 -08:00
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
use zcash_client_backend::{
|
2021-01-12 17:24:18 -08:00
|
|
|
address::RecipientAddress,
|
|
|
|
data_api::{error::Error, ShieldedOutput},
|
2020-08-26 14:47:47 -07:00
|
|
|
encoding::{
|
|
|
|
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
2021-01-12 17:24:18 -08:00
|
|
|
encode_payment_address,
|
2020-08-26 14:47:47 -07:00
|
|
|
},
|
2021-01-12 17:24:18 -08:00
|
|
|
wallet::{AccountId, WalletTx},
|
2021-01-12 20:10:34 -08:00
|
|
|
DecryptedOutput,
|
2020-08-05 16:01:22 -07:00
|
|
|
};
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
use crate::{
|
2021-01-13 14:20:11 -08:00
|
|
|
error::{SqliteClientError},
|
2021-01-12 17:24:18 -08:00
|
|
|
DataConnStmtCache, NoteId, WalletDB,
|
|
|
|
};
|
2019-03-08 18:23:31 -08:00
|
|
|
|
2020-08-25 14:29:01 -07:00
|
|
|
pub mod init;
|
2020-08-25 14:43:28 -07:00
|
|
|
pub mod transact;
|
2020-08-25 14:29:01 -07:00
|
|
|
|
2019-03-08 18:23:31 -08:00
|
|
|
/// Returns the address for the account.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
2020-08-05 16:01:22 -07:00
|
|
|
/// use zcash_primitives::{
|
|
|
|
/// consensus::{self, Network},
|
|
|
|
/// };
|
2020-11-23 08:53:17 -08:00
|
|
|
/// use zcash_client_backend::wallet::AccountId;
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-10-21 12:08:09 -07:00
|
|
|
/// WalletDB,
|
2020-09-11 15:17:43 -07:00
|
|
|
/// wallet::get_address,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
|
|
|
|
/// let addr = get_address(&db, AccountId(0));
|
2019-03-08 18:23:31 -08:00
|
|
|
/// ```
|
2020-08-05 16:01:22 -07:00
|
|
|
pub fn get_address<P: consensus::Parameters>(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb: &WalletDB<P>,
|
2020-08-06 13:11:25 -07:00
|
|
|
account: AccountId,
|
2020-08-05 16:01:22 -07:00
|
|
|
) -> Result<Option<PaymentAddress>, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let addr: String = wdb.conn.query_row(
|
2019-03-08 18:23:31 -08:00
|
|
|
"SELECT address FROM accounts
|
|
|
|
WHERE account = ?",
|
2020-08-05 16:01:22 -07:00
|
|
|
&[account.0],
|
2019-03-08 18:23:31 -08:00
|
|
|
|row| row.get(0),
|
|
|
|
)?;
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
decode_payment_address(wdb.params.hrp_sapling_payment_address(), &addr)
|
|
|
|
.map_err(SqliteClientError::Bech32)
|
2019-03-08 18:23:31 -08:00
|
|
|
}
|
2019-03-08 18:53:38 -08:00
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
pub fn get_extended_full_viewing_keys<P: consensus::Parameters>(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb: &WalletDB<P>,
|
2021-01-11 17:13:40 -08:00
|
|
|
) -> Result<HashMap<AccountId, ExtendedFullViewingKey>, SqliteClientError> {
|
2020-08-26 14:47:47 -07:00
|
|
|
// Fetch the ExtendedFullViewingKeys we are tracking
|
2021-01-12 17:24:18 -08:00
|
|
|
let mut stmt_fetch_accounts = wdb
|
|
|
|
.conn
|
2021-01-11 17:13:40 -08:00
|
|
|
.prepare("SELECT account, extfvk FROM accounts ORDER BY account ASC")?;
|
2020-08-26 14:47:47 -07:00
|
|
|
|
|
|
|
let rows = stmt_fetch_accounts
|
|
|
|
.query_map(NO_PARAMS, |row| {
|
2021-01-11 17:13:40 -08:00
|
|
|
let acct = row.get(0).map(AccountId)?;
|
|
|
|
let extfvk = row.get(1).map(|extfvk: String| {
|
2020-08-26 14:47:47 -07:00
|
|
|
decode_extended_full_viewing_key(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.params.hrp_sapling_extended_full_viewing_key(),
|
2020-08-26 14:47:47 -07:00
|
|
|
&extfvk,
|
|
|
|
)
|
2021-01-12 17:24:18 -08:00
|
|
|
.map_err(SqliteClientError::Bech32)
|
|
|
|
.and_then(|k| k.ok_or(SqliteClientError::IncorrectHRPExtFVK))
|
2021-01-11 17:13:40 -08:00
|
|
|
})?;
|
|
|
|
|
|
|
|
Ok((acct, extfvk))
|
2020-08-26 14:47:47 -07:00
|
|
|
})
|
|
|
|
.map_err(SqliteClientError::from)?;
|
|
|
|
|
2021-01-11 17:13:40 -08:00
|
|
|
let mut res: HashMap<AccountId, ExtendedFullViewingKey> = HashMap::new();
|
|
|
|
for row in rows {
|
2021-01-12 17:24:18 -08:00
|
|
|
let (account_id, efvkr) = row?;
|
2021-01-11 17:13:40 -08:00
|
|
|
res.insert(account_id, efvkr?);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(res)
|
2020-08-26 14:47:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_valid_account_extfvk<P: consensus::Parameters>(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb: &WalletDB<P>,
|
2020-08-26 14:47:47 -07:00
|
|
|
account: AccountId,
|
|
|
|
extfvk: &ExtendedFullViewingKey,
|
|
|
|
) -> Result<bool, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.conn
|
2020-08-26 14:47:47 -07:00
|
|
|
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
|
|
|
.exists(&[
|
|
|
|
account.0.to_sql()?,
|
|
|
|
encode_extended_full_viewing_key(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.params.hrp_sapling_extended_full_viewing_key(),
|
2020-08-26 14:47:47 -07:00
|
|
|
extfvk,
|
|
|
|
)
|
|
|
|
.to_sql()?,
|
|
|
|
])
|
|
|
|
.map_err(SqliteClientError::from)
|
|
|
|
}
|
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
/// Returns the balance for the account, including all mined unspent notes that we know
|
|
|
|
/// about.
|
|
|
|
///
|
2020-07-09 14:49:30 -07:00
|
|
|
/// WARNING: This balance is potentially unreliable, as mined notes may become unmined due
|
|
|
|
/// to chain reorgs. You should generally not show this balance to users without some
|
|
|
|
/// caveat. Use [`get_verified_balance`] where you need a more reliable indication of the
|
|
|
|
/// wallet balance.
|
|
|
|
///
|
2019-03-08 18:53:38 -08:00
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
2021-01-12 17:24:18 -08:00
|
|
|
/// use zcash_primitives::consensus::Network;
|
2020-11-23 08:53:17 -08:00
|
|
|
/// use zcash_client_backend::wallet::AccountId;
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-10-21 12:08:09 -07:00
|
|
|
/// WalletDB,
|
2020-09-11 15:17:43 -07:00
|
|
|
/// wallet::get_balance,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
/// let addr = get_balance(&db, AccountId(0));
|
2019-03-08 18:53:38 -08:00
|
|
|
/// ```
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_balance<P>(wdb: &WalletDB<P>, account: AccountId) -> Result<Amount, SqliteClientError> {
|
|
|
|
let balance = wdb.conn.query_row(
|
2019-03-08 18:53:38 -08:00
|
|
|
"SELECT SUM(value) FROM received_notes
|
|
|
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
|
|
|
WHERE account = ? AND spent IS NULL AND transactions.block IS NOT NULL",
|
2020-08-06 09:23:15 -07:00
|
|
|
&[account.0],
|
2019-03-08 18:53:38 -08:00
|
|
|
|row| row.get(0).or(Ok(0)),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Amount::from_i64(balance) {
|
|
|
|
Ok(amount) if !amount.is_negative() => Ok(amount),
|
2021-01-12 17:24:18 -08:00
|
|
|
_ => Err(SqliteClientError::CorruptedData(
|
2020-10-19 15:52:48 -07:00
|
|
|
"Sum of values in received_notes is out of range".to_string(),
|
2021-01-12 17:24:18 -08:00
|
|
|
)),
|
2019-03-08 18:53:38 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-11 17:13:40 -08:00
|
|
|
/// Returns the verified balance for the account at the specified height,
|
2021-01-08 11:49:10 -08:00
|
|
|
/// This may be used to obtain a balance that ignores notes that have been
|
|
|
|
/// received so recently that they are not yet deemed spendable.
|
2019-03-08 18:53:38 -08:00
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
2021-01-12 17:24:18 -08:00
|
|
|
/// use zcash_primitives::consensus::{BlockHeight, Network};
|
2020-11-23 08:53:17 -08:00
|
|
|
/// use zcash_client_backend::wallet::AccountId;
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-10-21 12:08:09 -07:00
|
|
|
/// WalletDB,
|
2020-09-11 15:17:43 -07:00
|
|
|
/// wallet::get_verified_balance,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
|
2020-09-11 15:17:43 -07:00
|
|
|
/// let addr = get_verified_balance(&db, AccountId(0), BlockHeight::from_u32(0));
|
2019-03-08 18:53:38 -08:00
|
|
|
/// ```
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_verified_balance<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-06 13:11:25 -07:00
|
|
|
account: AccountId,
|
2020-08-26 14:47:47 -07:00
|
|
|
anchor_height: BlockHeight,
|
2020-08-05 18:14:45 -07:00
|
|
|
) -> Result<Amount, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let balance = wdb.conn.query_row(
|
2019-03-08 18:53:38 -08:00
|
|
|
"SELECT SUM(value) FROM received_notes
|
|
|
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
|
|
|
WHERE account = ? AND spent IS NULL AND transactions.block <= ?",
|
2020-08-06 09:23:15 -07:00
|
|
|
&[account.0, u32::from(anchor_height)],
|
2019-03-08 18:53:38 -08:00
|
|
|
|row| row.get(0).or(Ok(0)),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Amount::from_i64(balance) {
|
|
|
|
Ok(amount) if !amount.is_negative() => Ok(amount),
|
2021-01-12 17:24:18 -08:00
|
|
|
_ => Err(SqliteClientError::CorruptedData(
|
2020-10-19 15:52:48 -07:00
|
|
|
"Sum of values in received_notes is out of range".to_string(),
|
2021-01-12 17:24:18 -08:00
|
|
|
)),
|
2019-03-08 18:53:38 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-08 19:22:35 -08:00
|
|
|
/// Returns the memo for a received note, if it is known and a valid UTF-8 string.
|
|
|
|
///
|
2021-01-12 17:24:18 -08:00
|
|
|
/// The note is identified by its row index in the `received_notes` table within the wdb
|
2019-03-08 19:22:35 -08:00
|
|
|
/// database.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
2021-01-12 17:24:18 -08:00
|
|
|
/// use zcash_primitives::consensus::Network;
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-08-06 13:11:25 -07:00
|
|
|
/// NoteId,
|
2020-10-21 12:08:09 -07:00
|
|
|
/// WalletDB,
|
2020-09-11 15:17:43 -07:00
|
|
|
/// wallet::get_received_memo_as_utf8,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
/// let memo = get_received_memo_as_utf8(&db, NoteId(27));
|
2020-08-05 18:14:45 -07:00
|
|
|
/// ```
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_received_memo_as_utf8<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-06 13:11:25 -07:00
|
|
|
id_note: NoteId,
|
2020-08-05 18:14:45 -07:00
|
|
|
) -> Result<Option<String>, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let memo: Vec<_> = wdb.conn.query_row(
|
2019-03-08 19:22:35 -08:00
|
|
|
"SELECT memo FROM received_notes
|
|
|
|
WHERE id_note = ?",
|
2020-08-06 13:11:25 -07:00
|
|
|
&[id_note.0],
|
2019-03-08 19:22:35 -08:00
|
|
|
|row| row.get(0),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Memo::from_bytes(&memo) {
|
|
|
|
Some(memo) => match memo.to_utf8() {
|
|
|
|
Some(Ok(res)) => Ok(Some(res)),
|
2021-01-12 17:24:18 -08:00
|
|
|
Some(Err(e)) => Err(SqliteClientError::InvalidMemo(e)),
|
2019-03-08 19:22:35 -08:00
|
|
|
None => Ok(None),
|
|
|
|
},
|
|
|
|
None => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the memo for a sent note, if it is known and a valid UTF-8 string.
|
|
|
|
///
|
2021-01-12 17:24:18 -08:00
|
|
|
/// The note is identified by its row index in the `sent_notes` table within the wdb
|
2019-03-08 19:22:35 -08:00
|
|
|
/// database.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
2021-01-12 17:24:18 -08:00
|
|
|
/// use zcash_primitives::consensus::Network;
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-08-06 13:11:25 -07:00
|
|
|
/// NoteId,
|
2020-10-21 12:08:09 -07:00
|
|
|
/// WalletDB,
|
2020-09-11 15:17:43 -07:00
|
|
|
/// wallet::get_sent_memo_as_utf8,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
/// let memo = get_sent_memo_as_utf8(&db, NoteId(12));
|
2020-08-05 18:14:45 -07:00
|
|
|
/// ```
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_sent_memo_as_utf8<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-06 13:11:25 -07:00
|
|
|
id_note: NoteId,
|
2020-08-05 18:14:45 -07:00
|
|
|
) -> Result<Option<String>, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let memo: Vec<_> = wdb.conn.query_row(
|
2019-03-08 19:22:35 -08:00
|
|
|
"SELECT memo FROM sent_notes
|
|
|
|
WHERE id_note = ?",
|
2020-08-06 13:11:25 -07:00
|
|
|
&[id_note.0],
|
2019-03-08 19:22:35 -08:00
|
|
|
|row| row.get(0),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Memo::from_bytes(&memo) {
|
|
|
|
Some(memo) => match memo.to_utf8() {
|
|
|
|
Some(Ok(res)) => Ok(Some(res)),
|
2021-01-12 17:24:18 -08:00
|
|
|
Some(Err(e)) => Err(SqliteClientError::InvalidMemo(e)),
|
2019-03-08 19:22:35 -08:00
|
|
|
None => Ok(None),
|
|
|
|
},
|
|
|
|
None => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn block_height_extrema<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-25 14:20:12 -07:00
|
|
|
) -> Result<Option<(BlockHeight, BlockHeight)>, rusqlite::Error> {
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.conn
|
2020-08-25 14:20:12 -07:00
|
|
|
.query_row(
|
|
|
|
"SELECT MIN(height), MAX(height) FROM blocks",
|
|
|
|
NO_PARAMS,
|
|
|
|
|row| {
|
|
|
|
let min_height: u32 = row.get(0)?;
|
|
|
|
let max_height: u32 = row.get(1)?;
|
|
|
|
Ok(Some((
|
|
|
|
BlockHeight::from(min_height),
|
|
|
|
BlockHeight::from(max_height),
|
|
|
|
)))
|
|
|
|
},
|
|
|
|
)
|
|
|
|
//.optional() doesn't work here because a failed aggregate function
|
|
|
|
//produces a runtime error, not an empty set of rows.
|
|
|
|
.or(Ok(None))
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_tx_height<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
|
|
|
txid: TxId,
|
|
|
|
) -> Result<Option<BlockHeight>, rusqlite::Error> {
|
|
|
|
wdb.conn
|
2020-08-25 14:20:12 -07:00
|
|
|
.query_row(
|
|
|
|
"SELECT block FROM transactions WHERE txid = ?",
|
|
|
|
&[txid.0.to_vec()],
|
|
|
|
|row| row.get(0).map(u32::into),
|
|
|
|
)
|
|
|
|
.optional()
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_block_hash<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-25 14:20:12 -07:00
|
|
|
block_height: BlockHeight,
|
|
|
|
) -> Result<Option<BlockHash>, rusqlite::Error> {
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.conn
|
2020-08-25 14:20:12 -07:00
|
|
|
.query_row(
|
|
|
|
"SELECT hash FROM blocks WHERE height = ?",
|
|
|
|
&[u32::from(block_height)],
|
|
|
|
|row| {
|
|
|
|
let row_data = row.get::<_, Vec<_>>(0)?;
|
|
|
|
Ok(BlockHash::from_slice(&row_data))
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.optional()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Rewinds the database to the given height.
|
|
|
|
///
|
|
|
|
/// If the requested height is greater than or equal to the height of the last scanned
|
|
|
|
/// block, this function does nothing.
|
2020-12-02 13:36:56 -08:00
|
|
|
///
|
|
|
|
/// This should only be executed inside a transactional context.
|
2020-08-25 14:20:12 -07:00
|
|
|
pub fn rewind_to_height<P: consensus::Parameters>(
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb: &WalletDB<P>,
|
2020-08-25 14:20:12 -07:00
|
|
|
block_height: BlockHeight,
|
2021-01-13 14:20:11 -08:00
|
|
|
) -> Result<(), SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let sapling_activation_height = wdb
|
|
|
|
.params
|
2020-08-25 14:20:12 -07:00
|
|
|
.activation_height(NetworkUpgrade::Sapling)
|
2021-01-13 14:20:11 -08:00
|
|
|
.ok_or(SqliteClientError::BackendError(Error::SaplingNotActive))?;
|
2020-08-25 14:20:12 -07:00
|
|
|
|
|
|
|
// Recall where we synced up to previously.
|
2021-01-12 17:24:18 -08:00
|
|
|
let last_scanned_height = wdb
|
|
|
|
.conn
|
|
|
|
.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
|
|
|
row.get(0)
|
|
|
|
.map(|h: u32| h.into())
|
|
|
|
.or(Ok(sapling_activation_height - 1))
|
2021-01-13 14:20:11 -08:00
|
|
|
})?;
|
2020-08-25 14:20:12 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
// nothing to do if we're deleting back down to the max height
|
2020-08-25 14:20:12 -07:00
|
|
|
if block_height >= last_scanned_height {
|
2021-01-12 17:24:18 -08:00
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
// Decrement witnesses.
|
|
|
|
wdb.conn
|
|
|
|
.execute(
|
|
|
|
"DELETE FROM sapling_witnesses WHERE block > ?",
|
|
|
|
&[u32::from(block_height)],
|
2021-01-13 14:20:11 -08:00
|
|
|
)?;
|
2020-08-25 14:20:12 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
// Un-mine transactions.
|
|
|
|
wdb.conn
|
|
|
|
.execute(
|
|
|
|
"UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?",
|
|
|
|
&[u32::from(block_height)],
|
2021-01-13 14:20:11 -08:00
|
|
|
)?;
|
2020-08-25 14:20:12 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
// Now that they aren't depended on, delete scanned blocks.
|
|
|
|
wdb.conn
|
|
|
|
.execute(
|
|
|
|
"DELETE FROM blocks WHERE height > ?",
|
|
|
|
&[u32::from(block_height)],
|
2021-01-13 14:20:11 -08:00
|
|
|
)?;
|
2020-08-25 14:20:12 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-08-25 14:20:12 -07:00
|
|
|
}
|
2020-08-06 13:11:25 -07:00
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_commitment_tree<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-06 17:13:24 -07:00
|
|
|
block_height: BlockHeight,
|
|
|
|
) -> Result<Option<CommitmentTree<Node>>, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
wdb.conn
|
2020-08-06 17:13:24 -07:00
|
|
|
.query_row_and_then(
|
|
|
|
"SELECT sapling_tree FROM blocks WHERE height = ?",
|
|
|
|
&[u32::from(block_height)],
|
|
|
|
|row| {
|
|
|
|
let row_data: Vec<u8> = row.get(0)?;
|
|
|
|
CommitmentTree::read(&row_data[..]).map_err(|e| {
|
|
|
|
rusqlite::Error::FromSqlConversionFailure(
|
|
|
|
row_data.len(),
|
|
|
|
rusqlite::types::Type::Blob,
|
|
|
|
Box::new(e),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.optional()
|
|
|
|
.map_err(SqliteClientError::from)
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_witnesses<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
2020-08-18 12:48:59 -07:00
|
|
|
block_height: BlockHeight,
|
|
|
|
) -> Result<Vec<(NoteId, IncrementalWitness<Node>)>, SqliteClientError> {
|
2021-01-12 17:24:18 -08:00
|
|
|
let mut stmt_fetch_witnesses = wdb
|
|
|
|
.conn
|
2020-08-18 12:48:59 -07:00
|
|
|
.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
|
|
|
let witnesses = stmt_fetch_witnesses
|
|
|
|
.query_map(&[u32::from(block_height)], |row| {
|
|
|
|
let id_note = NoteId(row.get(0)?);
|
2021-01-12 17:24:18 -08:00
|
|
|
let wdb: Vec<u8> = row.get(1)?;
|
|
|
|
Ok(IncrementalWitness::read(&wdb[..]).map(|witness| (id_note, witness)))
|
2020-08-18 12:48:59 -07:00
|
|
|
})
|
|
|
|
.map_err(SqliteClientError::from)?;
|
|
|
|
|
2021-01-12 20:04:42 -08:00
|
|
|
// unwrap database error & IO error from IncrementalWitness::read
|
|
|
|
let res: Vec<_> = witnesses.collect::<Result<Result<_, _>, _>>()??;
|
2020-08-18 12:48:59 -07:00
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn get_nullifiers<P>(
|
|
|
|
wdb: &WalletDB<P>,
|
|
|
|
) -> Result<Vec<(Nullifier, AccountId)>, SqliteClientError> {
|
2020-08-18 13:49:00 -07:00
|
|
|
// Get the nullifiers for the notes we are tracking
|
2021-01-12 17:24:18 -08:00
|
|
|
let mut stmt_fetch_nullifiers = wdb
|
|
|
|
.conn
|
2020-08-18 13:49:00 -07:00
|
|
|
.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?;
|
|
|
|
let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| {
|
2021-01-08 20:49:38 -08:00
|
|
|
let nf_bytes: Vec<u8> = row.get(1)?;
|
2020-08-18 13:49:00 -07:00
|
|
|
let account = AccountId(row.get(2)?);
|
2021-01-08 20:49:38 -08:00
|
|
|
Ok((Nullifier::from_slice(&nf_bytes), account))
|
2020-08-18 13:49:00 -07:00
|
|
|
})?;
|
|
|
|
|
2021-01-12 20:04:42 -08:00
|
|
|
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
2020-08-18 13:49:00 -07:00
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
2021-01-12 17:24:18 -08:00
|
|
|
pub fn insert_block<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
block_height: BlockHeight,
|
|
|
|
block_hash: BlockHash,
|
|
|
|
block_time: u32,
|
|
|
|
commitment_tree: &CommitmentTree<Node>,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
let mut encoded_tree = Vec::new();
|
|
|
|
commitment_tree.write(&mut encoded_tree).unwrap();
|
|
|
|
|
2021-01-12 20:10:34 -08:00
|
|
|
stmts.stmt_insert_block.execute(params![
|
|
|
|
u32::from(block_height),
|
|
|
|
&block_hash.0[..],
|
|
|
|
block_time,
|
|
|
|
encoded_tree
|
|
|
|
])?;
|
2021-01-12 17:24:18 -08:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn put_tx_meta<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
tx: &WalletTx,
|
|
|
|
height: BlockHeight,
|
|
|
|
) -> Result<i64, SqliteClientError> {
|
|
|
|
let txid = tx.txid.0.to_vec();
|
|
|
|
if stmts
|
|
|
|
.stmt_update_tx_meta
|
|
|
|
.execute(params![u32::from(height), (tx.index as i64), txid])?
|
|
|
|
== 0
|
|
|
|
{
|
|
|
|
// It isn't there, so insert our transaction into the database.
|
|
|
|
stmts
|
|
|
|
.stmt_insert_tx_meta
|
|
|
|
.execute(params![txid, u32::from(height), (tx.index as i64),])?;
|
|
|
|
|
|
|
|
Ok(stmts.wallet_db.conn.last_insert_rowid())
|
|
|
|
} else {
|
|
|
|
// It was there, so grab its row number.
|
|
|
|
stmts
|
|
|
|
.stmt_select_tx_ref
|
|
|
|
.query_row(&[txid], |row| row.get(0))
|
|
|
|
.map_err(SqliteClientError::from)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn put_tx_data<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
tx: &Transaction,
|
|
|
|
created_at: Option<time::OffsetDateTime>,
|
|
|
|
) -> Result<i64, SqliteClientError> {
|
|
|
|
let txid = tx.txid().0.to_vec();
|
|
|
|
|
|
|
|
let mut raw_tx = vec![];
|
|
|
|
tx.write(&mut raw_tx)?;
|
|
|
|
|
|
|
|
if stmts
|
|
|
|
.stmt_update_tx_data
|
|
|
|
.execute(params![u32::from(tx.expiry_height), raw_tx, txid,])?
|
|
|
|
== 0
|
|
|
|
{
|
|
|
|
// It isn't there, so insert our transaction into the database.
|
|
|
|
stmts.stmt_insert_tx_data.execute(params![
|
|
|
|
txid,
|
|
|
|
created_at,
|
|
|
|
u32::from(tx.expiry_height),
|
|
|
|
raw_tx
|
|
|
|
])?;
|
|
|
|
|
|
|
|
Ok(stmts.wallet_db.conn.last_insert_rowid())
|
|
|
|
} else {
|
|
|
|
// It was there, so grab its row number.
|
|
|
|
stmts
|
|
|
|
.stmt_select_tx_ref
|
|
|
|
.query_row(&[txid], |row| row.get(0))
|
|
|
|
.map_err(SqliteClientError::from)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn mark_spent<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
tx_ref: i64,
|
|
|
|
nf: &Nullifier,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
stmts
|
|
|
|
.stmt_mark_recived_note_spent
|
|
|
|
.execute(&[tx_ref.to_sql()?, nf.0.to_sql()?])?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assumptions:
|
|
|
|
// - A transaction will not contain more than 2^63 shielded outputs.
|
|
|
|
// - A note value will never exceed 2^63 zatoshis.
|
|
|
|
pub fn put_received_note<'a, P, T: ShieldedOutput>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
output: &T,
|
|
|
|
nf_opt: &Option<Nullifier>,
|
|
|
|
tx_ref: i64,
|
|
|
|
) -> Result<NoteId, SqliteClientError> {
|
|
|
|
let rcm = output.note().rcm().to_repr();
|
|
|
|
let account = output.account().0 as i64;
|
|
|
|
let diversifier = output.to().diversifier().0.to_vec();
|
|
|
|
let value = output.note().value as i64;
|
|
|
|
let rcm = rcm.as_ref();
|
|
|
|
let memo = output.memo().map(|m| m.as_bytes());
|
|
|
|
let is_change = output.is_change();
|
|
|
|
let tx = tx_ref;
|
|
|
|
let output_index = output.index() as i64;
|
|
|
|
let nf_bytes = nf_opt.map(|nf| nf.0.to_vec());
|
|
|
|
|
2021-01-12 20:06:57 -08:00
|
|
|
let sql_args: &[(&str, &dyn ToSql)] = &[
|
2021-01-12 17:24:18 -08:00
|
|
|
(&":account", &account),
|
|
|
|
(&":diversifier", &diversifier),
|
|
|
|
(&":value", &value),
|
|
|
|
(&":rcm", &rcm),
|
|
|
|
(&":nf", &nf_bytes),
|
|
|
|
(&":memo", &memo),
|
|
|
|
(&":is_change", &is_change),
|
|
|
|
(&":tx", &tx),
|
|
|
|
(&":output_index", &output_index),
|
|
|
|
];
|
|
|
|
|
|
|
|
// First try updating an existing received note into the database.
|
|
|
|
if stmts.stmt_update_received_note.execute_named(&sql_args)? == 0 {
|
|
|
|
// It isn't there, so insert our note into the database.
|
|
|
|
stmts.stmt_insert_received_note.execute_named(&sql_args)?;
|
|
|
|
|
|
|
|
Ok(NoteId(stmts.wallet_db.conn.last_insert_rowid()))
|
|
|
|
} else {
|
|
|
|
// It was there, so grab its row number.
|
|
|
|
stmts
|
|
|
|
.stmt_select_received_note
|
|
|
|
.query_row(params![tx_ref, (output.index() as i64)], |row| {
|
|
|
|
row.get(0).map(NoteId)
|
|
|
|
})
|
|
|
|
.map_err(SqliteClientError::from)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert_witness<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
note_id: NoteId,
|
|
|
|
witness: &IncrementalWitness<Node>,
|
|
|
|
height: BlockHeight,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
let mut encoded = Vec::new();
|
|
|
|
witness.write(&mut encoded).unwrap();
|
|
|
|
|
|
|
|
stmts
|
|
|
|
.stmt_insert_witness
|
|
|
|
.execute(params![note_id.0, u32::from(height), encoded])?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn prune_witnesses<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
below_height: BlockHeight,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
stmts
|
|
|
|
.stmt_prune_witnesses
|
|
|
|
.execute(&[u32::from(below_height)])?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_expired_notes<'a, P>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
height: BlockHeight,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
stmts.stmt_update_expired.execute(&[u32::from(height)])?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn put_sent_note<'a, P: consensus::Parameters>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
output: &DecryptedOutput,
|
|
|
|
tx_ref: i64,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
let output_index = output.index as i64;
|
|
|
|
let account = output.account.0 as i64;
|
|
|
|
let value = output.note.value as i64;
|
|
|
|
let to_str = encode_payment_address(
|
|
|
|
stmts.wallet_db.params.hrp_sapling_payment_address(),
|
|
|
|
&output.to,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Try updating an existing sent note.
|
|
|
|
if stmts.stmt_update_sent_note.execute(params![
|
|
|
|
account,
|
|
|
|
to_str,
|
|
|
|
value,
|
|
|
|
&output.memo.as_bytes(),
|
|
|
|
tx_ref,
|
|
|
|
output_index
|
|
|
|
])? == 0
|
|
|
|
{
|
|
|
|
// It isn't there, so insert.
|
|
|
|
insert_sent_note(
|
|
|
|
stmts,
|
|
|
|
tx_ref,
|
|
|
|
output.index,
|
|
|
|
output.account,
|
|
|
|
&RecipientAddress::Shielded(output.to.clone()),
|
|
|
|
Amount::from_u64(output.note.value)
|
|
|
|
.map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?,
|
|
|
|
Some(output.memo.clone()),
|
|
|
|
)?
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert_sent_note<'a, P: consensus::Parameters>(
|
|
|
|
stmts: &mut DataConnStmtCache<'a, P>,
|
|
|
|
tx_ref: i64,
|
|
|
|
output_index: usize,
|
|
|
|
account: AccountId,
|
|
|
|
to: &RecipientAddress,
|
|
|
|
value: Amount,
|
|
|
|
memo: Option<Memo>,
|
|
|
|
) -> Result<(), SqliteClientError> {
|
|
|
|
let to_str = to.encode(&stmts.wallet_db.params);
|
|
|
|
let ivalue: i64 = value.into();
|
|
|
|
stmts.stmt_insert_sent_note.execute(params![
|
|
|
|
tx_ref,
|
|
|
|
(output_index as i64),
|
|
|
|
account.0,
|
|
|
|
to_str,
|
|
|
|
ivalue,
|
|
|
|
memo.map(|m| m.as_bytes().to_vec()),
|
|
|
|
])?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use tempfile::NamedTempFile;
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
use zcash_primitives::{
|
|
|
|
transaction::components::Amount,
|
|
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
|
|
|
};
|
|
|
|
|
2020-10-19 14:20:34 -07:00
|
|
|
use zcash_client_backend::data_api::WalletRead;
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
use crate::{
|
2020-08-25 14:29:01 -07:00
|
|
|
tests,
|
2021-01-12 20:32:23 -08:00
|
|
|
wallet::init::{init_accounts_table, init_wallet_db},
|
2020-10-21 12:08:09 -07:00
|
|
|
AccountId, WalletDB,
|
2019-03-08 18:53:38 -08:00
|
|
|
};
|
|
|
|
|
2020-09-10 15:55:34 -07:00
|
|
|
use super::{get_address, get_balance};
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
#[test]
|
|
|
|
fn empty_database_has_no_balance() {
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
2021-01-12 17:24:18 -08:00
|
|
|
let db_data = WalletDB::for_path(data_file.path(), tests::network()).unwrap();
|
2021-01-12 20:32:23 -08:00
|
|
|
init_wallet_db(&db_data).unwrap();
|
2019-03-08 18:53:38 -08:00
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
2021-01-12 17:24:18 -08:00
|
|
|
init_accounts_table(&db_data, &extfvks).unwrap();
|
2019-03-08 18:53:38 -08:00
|
|
|
|
|
|
|
// The account should be empty
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
|
2019-03-08 18:53:38 -08:00
|
|
|
|
2020-09-10 15:55:34 -07:00
|
|
|
// We can't get an anchor height, as we have not scanned any blocks.
|
|
|
|
assert_eq!((&db_data).get_target_and_anchor_heights().unwrap(), None);
|
2019-03-08 18:53:38 -08:00
|
|
|
|
|
|
|
// An invalid account has zero balance
|
2021-01-12 17:24:18 -08:00
|
|
|
assert!(get_address(&db_data, AccountId(1)).is_err());
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
|
2019-03-08 18:53:38 -08:00
|
|
|
}
|
|
|
|
}
|