2019-03-08 18:23:31 -08:00
|
|
|
//! Functions for querying information in the data database.
|
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
use zcash_primitives::{
|
|
|
|
consensus::{self},
|
|
|
|
note_encryption::Memo,
|
|
|
|
primitives::PaymentAddress,
|
|
|
|
transaction::components::Amount,
|
|
|
|
};
|
2019-03-08 18:23:31 -08:00
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
use zcash_client_backend::{
|
|
|
|
data_api::{chain::get_target_and_anchor_heights, error::Error},
|
|
|
|
encoding::decode_payment_address,
|
|
|
|
};
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
use crate::{error::SqliteClientError, Account, DataConnection};
|
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-08-05 18:14:45 -07:00
|
|
|
/// use zcash_client_sqlite::{
|
2020-08-05 16:01:22 -07:00
|
|
|
/// Account,
|
2020-08-05 18:14:45 -07:00
|
|
|
/// DataConnection,
|
|
|
|
/// query::get_address,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
|
|
/// let db = DataConnection::for_path(data_file).unwrap();
|
2020-08-05 16:01:22 -07:00
|
|
|
/// let addr = get_address(&db, &Network::TestNetwork, Account(0));
|
2019-03-08 18:23:31 -08:00
|
|
|
/// ```
|
2020-08-05 16:01:22 -07:00
|
|
|
pub fn get_address<P: consensus::Parameters>(
|
|
|
|
data: &DataConnection,
|
|
|
|
params: &P,
|
|
|
|
account: Account,
|
|
|
|
) -> Result<Option<PaymentAddress>, SqliteClientError> {
|
|
|
|
let addr: String = data.0.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),
|
|
|
|
)?;
|
|
|
|
|
2020-08-05 16:01:22 -07:00
|
|
|
decode_payment_address(params.hrp_sapling_payment_address(), &addr)
|
|
|
|
.map_err(|e| SqliteClientError(e.into()))
|
2019-03-08 18:23:31 -08:00
|
|
|
}
|
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;
|
|
|
|
/// use zcash_client_sqlite::{
|
|
|
|
/// DataConnection,
|
|
|
|
/// query::get_balance,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
|
|
/// let db = DataConnection::for_path(data_file).unwrap();
|
|
|
|
/// let addr = get_balance(&db, 0);
|
2019-03-08 18:53:38 -08:00
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
pub fn get_balance(data: &DataConnection, account: u32) -> Result<Amount, SqliteClientError> {
|
|
|
|
let balance = data.0.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",
|
|
|
|
&[account],
|
|
|
|
|row| row.get(0).or(Ok(0)),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Amount::from_i64(balance) {
|
|
|
|
Ok(amount) if !amount.is_negative() => Ok(amount),
|
2020-08-05 18:14:45 -07:00
|
|
|
_ => Err(SqliteClientError(Error::CorruptedData(
|
2019-03-08 18:53:38 -08:00
|
|
|
"Sum of values in received_notes is out of range",
|
|
|
|
))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the verified balance for the account, which ignores notes that have been
|
|
|
|
/// received too recently and are not yet deemed spendable.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
|
|
|
/// use zcash_client_sqlite::{
|
|
|
|
/// DataConnection,
|
|
|
|
/// query::get_verified_balance,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
|
|
/// let db = DataConnection::for_path(data_file).unwrap();
|
|
|
|
/// let addr = get_verified_balance(&db, 0);
|
2019-03-08 18:53:38 -08:00
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
pub fn get_verified_balance(
|
|
|
|
data: &DataConnection,
|
|
|
|
account: u32,
|
|
|
|
) -> Result<Amount, SqliteClientError> {
|
2020-08-04 14:52:56 -07:00
|
|
|
let (_, anchor_height) = get_target_and_anchor_heights(data)?;
|
2019-03-08 18:53:38 -08:00
|
|
|
|
2020-08-05 18:14:45 -07:00
|
|
|
let balance = data.0.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-05 13:08:58 -07:00
|
|
|
&[account, 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),
|
2020-08-05 18:14:45 -07:00
|
|
|
_ => Err(SqliteClientError(Error::CorruptedData(
|
2019-03-08 18:53:38 -08:00
|
|
|
"Sum of values in received_notes is out of range",
|
|
|
|
))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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.
|
|
|
|
///
|
|
|
|
/// The note is identified by its row index in the `received_notes` table within the data
|
|
|
|
/// database.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
|
|
|
/// use zcash_client_sqlite::{
|
|
|
|
/// DataConnection,
|
|
|
|
/// query::get_received_memo_as_utf8,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
|
|
/// let db = DataConnection::for_path(data_file).unwrap();
|
|
|
|
/// let memo = get_received_memo_as_utf8(&db, 27);
|
|
|
|
/// ```
|
|
|
|
pub fn get_received_memo_as_utf8(
|
|
|
|
data: &DataConnection,
|
2019-03-08 19:22:35 -08:00
|
|
|
id_note: i64,
|
2020-08-05 18:14:45 -07:00
|
|
|
) -> Result<Option<String>, SqliteClientError> {
|
|
|
|
let memo: Vec<_> = data.0.query_row(
|
2019-03-08 19:22:35 -08:00
|
|
|
"SELECT memo FROM received_notes
|
|
|
|
WHERE id_note = ?",
|
|
|
|
&[id_note],
|
|
|
|
|row| row.get(0),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Memo::from_bytes(&memo) {
|
|
|
|
Some(memo) => match memo.to_utf8() {
|
|
|
|
Some(Ok(res)) => Ok(Some(res)),
|
2020-08-05 18:14:45 -07:00
|
|
|
Some(Err(e)) => Err(SqliteClientError(Error::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.
|
|
|
|
///
|
|
|
|
/// The note is identified by its row index in the `sent_notes` table within the data
|
|
|
|
/// database.
|
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
2020-08-05 18:14:45 -07:00
|
|
|
/// use tempfile::NamedTempFile;
|
|
|
|
/// use zcash_client_sqlite::{
|
|
|
|
/// DataConnection,
|
|
|
|
/// query::get_sent_memo_as_utf8,
|
|
|
|
/// };
|
|
|
|
///
|
|
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
|
|
/// let db = DataConnection::for_path(data_file).unwrap();
|
|
|
|
/// let memo = get_sent_memo_as_utf8(&db, 12);
|
|
|
|
/// ```
|
|
|
|
pub fn get_sent_memo_as_utf8(
|
|
|
|
data: &DataConnection,
|
2019-03-08 19:22:35 -08:00
|
|
|
id_note: i64,
|
2020-08-05 18:14:45 -07:00
|
|
|
) -> Result<Option<String>, SqliteClientError> {
|
|
|
|
let memo: Vec<_> = data.0.query_row(
|
2019-03-08 19:22:35 -08:00
|
|
|
"SELECT memo FROM sent_notes
|
|
|
|
WHERE id_note = ?",
|
|
|
|
&[id_note],
|
|
|
|
|row| row.get(0),
|
|
|
|
)?;
|
|
|
|
|
|
|
|
match Memo::from_bytes(&memo) {
|
|
|
|
Some(memo) => match memo.to_utf8() {
|
|
|
|
Some(Ok(res)) => Ok(Some(res)),
|
2020-08-05 18:14:45 -07:00
|
|
|
Some(Err(e)) => Err(SqliteClientError(Error::InvalidMemo(e))),
|
2019-03-08 19:22:35 -08:00
|
|
|
None => Ok(None),
|
|
|
|
},
|
|
|
|
None => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2020-08-05 18:14:45 -07:00
|
|
|
use rusqlite::Connection;
|
2019-03-08 18:53:38 -08:00
|
|
|
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-08-05 18:14:45 -07:00
|
|
|
use zcash_client_backend::data_api::error::Error;
|
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
use crate::{
|
|
|
|
init::{init_accounts_table, init_data_database},
|
2020-08-05 16:01:22 -07:00
|
|
|
tests, Account, DataConnection,
|
2019-03-08 18:53:38 -08:00
|
|
|
};
|
|
|
|
|
2020-08-05 18:14:45 -07:00
|
|
|
use super::{get_address, get_balance, get_verified_balance};
|
|
|
|
|
2019-03-08 18:53:38 -08:00
|
|
|
#[test]
|
|
|
|
fn empty_database_has_no_balance() {
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 18:53:38 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
|
2019-03-08 18:53:38 -08:00
|
|
|
|
|
|
|
// The account should be empty
|
2020-08-05 18:14:45 -07:00
|
|
|
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
2019-03-08 18:53:38 -08:00
|
|
|
|
|
|
|
// The account should have no verified balance, as we haven't scanned any blocks
|
2020-08-05 18:14:45 -07:00
|
|
|
let e = get_verified_balance(&db_data, 0).unwrap_err();
|
|
|
|
match e.0 {
|
|
|
|
Error::ScanRequired => (),
|
2019-03-08 18:53:38 -08:00
|
|
|
_ => panic!("Unexpected error: {:?}", e),
|
|
|
|
}
|
|
|
|
|
|
|
|
// An invalid account has zero balance
|
2020-08-05 16:01:22 -07:00
|
|
|
assert!(get_address(&db_data, &tests::network(), Account(1)).is_err());
|
2020-08-05 18:14:45 -07:00
|
|
|
assert_eq!(get_balance(&db_data, 1).unwrap(), Amount::zero());
|
2019-03-08 18:53:38 -08:00
|
|
|
}
|
|
|
|
}
|