Merge branch 'wallet/sqlite_cached_statements'
This commit is contained in:
commit
d5166b5134
|
@ -47,6 +47,7 @@ uuid = "1.1"
|
|||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
incrementalmerkletree = { version = "0.4", features = ["legacy-api", "test-dependencies"] }
|
||||
proptest = "1.0.0"
|
||||
rand_core = "0.6"
|
||||
regex = "1.4"
|
||||
|
@ -59,6 +60,7 @@ zcash_address = { version = "0.3", path = "../components/zcash_address", feature
|
|||
[features]
|
||||
mainnet = []
|
||||
test-dependencies = [
|
||||
"incrementalmerkletree/test-dependencies",
|
||||
"zcash_primitives/test-dependencies",
|
||||
"zcash_client_backend/test-dependencies",
|
||||
]
|
||||
|
|
|
@ -299,7 +299,7 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Empty chain should return None
|
||||
assert_matches!(db_data.get_max_height_hash(), Ok(None));
|
||||
|
@ -328,8 +328,7 @@ mod tests {
|
|||
assert_matches!(validate_chain_result, Ok(()));
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap();
|
||||
|
@ -348,7 +347,7 @@ mod tests {
|
|||
validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap();
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap();
|
||||
|
@ -365,7 +364,7 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Create some fake CompactBlocks
|
||||
let (cb, _) = fake_compact_block(
|
||||
|
@ -386,8 +385,7 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap();
|
||||
|
@ -427,7 +425,7 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Create some fake CompactBlocks
|
||||
let (cb, _) = fake_compact_block(
|
||||
|
@ -448,8 +446,7 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_chain(&db_cache, db_data.get_max_height_hash().unwrap(), None).unwrap();
|
||||
|
@ -489,11 +486,11 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
Amount::zero()
|
||||
);
|
||||
|
||||
|
@ -519,36 +516,46 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
|
||||
// "Rewind" to height of last scanned block
|
||||
truncate_to_height(&db_data, sapling_activation_height() + 1).unwrap();
|
||||
db_data
|
||||
.transactionally(|wdb| {
|
||||
truncate_to_height(&wdb.conn.0, &wdb.params, sapling_activation_height() + 1)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Account balance should be unaltered
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
|
||||
// Rewind so that one block is dropped
|
||||
truncate_to_height(&db_data, sapling_activation_height()).unwrap();
|
||||
db_data
|
||||
.transactionally(|wdb| {
|
||||
truncate_to_height(&wdb.conn.0, &wdb.params, sapling_activation_height())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Account balance should only contain the first received note
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should again reflect both received notes
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -564,7 +571,7 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Create a block with height SAPLING_ACTIVATION_HEIGHT
|
||||
let value = Amount::from_u64(50000).unwrap();
|
||||
|
@ -576,9 +583,11 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb1);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next
|
||||
let (cb2, _) = fake_compact_block(
|
||||
|
@ -596,7 +605,7 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb3);
|
||||
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) {
|
||||
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None) {
|
||||
Err(Error::Chain(e)) => {
|
||||
assert_matches!(
|
||||
e.cause(),
|
||||
|
@ -609,9 +618,9 @@ mod tests {
|
|||
|
||||
// If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
Amount::from_u64(150_000).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -627,11 +636,11 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
Amount::zero()
|
||||
);
|
||||
|
||||
|
@ -647,11 +656,13 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Create a second fake CompactBlock sending more value to the address
|
||||
let value2 = Amount::from_u64(7).unwrap();
|
||||
|
@ -665,11 +676,11 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -685,11 +696,11 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
|
||||
let (dfvk, _taddr) = init_test_accounts_table(&mut db_data);
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
Amount::zero()
|
||||
);
|
||||
|
||||
|
@ -705,11 +716,13 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Create a second fake CompactBlock spending value from the address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[0]);
|
||||
|
@ -728,11 +741,11 @@ mod tests {
|
|||
);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Account balance should equal the change
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value - value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,18 +32,16 @@
|
|||
// Catch documentation errors caused by code changes.
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
|
||||
use rusqlite::Connection;
|
||||
use rusqlite::{self, Connection};
|
||||
use secrecy::{ExposeSecret, SecretVec};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::{borrow::Borrow, collections::HashMap, convert::AsRef, fmt, path::Path};
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
legacy::TransparentAddress,
|
||||
memo::{Memo, MemoBytes},
|
||||
sapling::{self},
|
||||
sapling,
|
||||
transaction::{
|
||||
components::{amount::Amount, OutPoint},
|
||||
Transaction, TxId,
|
||||
|
@ -72,9 +70,6 @@ use {
|
|||
std::{fs, io},
|
||||
};
|
||||
|
||||
mod prepared;
|
||||
pub use prepared::DataConnStmtCache;
|
||||
|
||||
pub mod chain;
|
||||
pub mod error;
|
||||
pub mod wallet;
|
||||
|
@ -107,12 +102,21 @@ impl fmt::Display for NoteId {
|
|||
pub struct UtxoId(pub i64);
|
||||
|
||||
/// A wrapper for the SQLite connection to the wallet database.
|
||||
pub struct WalletDb<P> {
|
||||
conn: Connection,
|
||||
pub struct WalletDb<C, P> {
|
||||
conn: C,
|
||||
params: P,
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters> WalletDb<P> {
|
||||
/// A wrapper for a SQLite transaction affecting the wallet database.
|
||||
pub struct SqlTransaction<'conn>(pub(crate) rusqlite::Transaction<'conn>);
|
||||
|
||||
impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
|
||||
fn borrow(&self) -> &rusqlite::Connection {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters + Clone> WalletDb<Connection, P> {
|
||||
/// Construct a connection to the wallet database stored at the specified path.
|
||||
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
|
||||
Connection::open(path).and_then(move |conn| {
|
||||
|
@ -121,53 +125,60 @@ impl<P: consensus::Parameters> WalletDb<P> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Given a wallet database connection, obtain a handle for the write operations
|
||||
/// for that database. This operation may eagerly initialize and cache sqlite
|
||||
/// prepared statements that are used in write operations.
|
||||
pub fn get_update_ops(&self) -> Result<DataConnStmtCache<'_, P>, SqliteClientError> {
|
||||
DataConnStmtCache::new(self)
|
||||
pub fn transactionally<F, A>(&mut self, f: F) -> Result<A, SqliteClientError>
|
||||
where
|
||||
F: FnOnce(&WalletDb<SqlTransaction<'_>, P>) -> Result<A, SqliteClientError>,
|
||||
{
|
||||
let wdb = WalletDb {
|
||||
conn: SqlTransaction(self.conn.transaction()?),
|
||||
params: self.params.clone(),
|
||||
};
|
||||
let result = f(&wdb)?;
|
||||
wdb.conn.0.commit()?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
||||
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
|
||||
type Error = SqliteClientError;
|
||||
type NoteRef = NoteId;
|
||||
type TxRef = i64;
|
||||
|
||||
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
||||
wallet::block_height_extrema(self).map_err(SqliteClientError::from)
|
||||
wallet::block_height_extrema(self.conn.borrow()).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
wallet::get_min_unspent_height(self).map_err(SqliteClientError::from)
|
||||
wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
|
||||
wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from)
|
||||
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
wallet::get_tx_height(self, txid).map_err(SqliteClientError::from)
|
||||
wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_unified_full_viewing_keys(
|
||||
&self,
|
||||
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
|
||||
wallet::get_unified_full_viewing_keys(self)
|
||||
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
|
||||
}
|
||||
|
||||
fn get_account_for_ufvk(
|
||||
&self,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<AccountId>, Self::Error> {
|
||||
wallet::get_account_for_ufvk(self, ufvk)
|
||||
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
|
||||
}
|
||||
|
||||
fn get_current_address(
|
||||
&self,
|
||||
account: AccountId,
|
||||
) -> Result<Option<UnifiedAddress>, Self::Error> {
|
||||
wallet::get_current_address(self, account).map(|res| res.map(|(addr, _)| addr))
|
||||
wallet::get_current_address(self.conn.borrow(), &self.params, account)
|
||||
.map(|res| res.map(|(addr, _)| addr))
|
||||
}
|
||||
|
||||
fn is_valid_account_extfvk(
|
||||
|
@ -175,7 +186,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error> {
|
||||
wallet::is_valid_account_extfvk(self, account, extfvk)
|
||||
wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk)
|
||||
}
|
||||
|
||||
fn get_balance_at(
|
||||
|
@ -183,17 +194,19 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
wallet::get_balance_at(self, account, anchor_height)
|
||||
wallet::get_balance_at(self.conn.borrow(), account, anchor_height)
|
||||
}
|
||||
|
||||
fn get_transaction(&self, id_tx: i64) -> Result<Transaction, Self::Error> {
|
||||
wallet::get_transaction(self, id_tx)
|
||||
wallet::get_transaction(self.conn.borrow(), &self.params, id_tx)
|
||||
}
|
||||
|
||||
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Option<Memo>, Self::Error> {
|
||||
match id_note {
|
||||
NoteId::SentNoteId(id_note) => wallet::get_sent_memo(self, id_note),
|
||||
NoteId::ReceivedNoteId(id_note) => wallet::get_received_memo(self, id_note),
|
||||
NoteId::SentNoteId(id_note) => wallet::get_sent_memo(self.conn.borrow(), id_note),
|
||||
NoteId::ReceivedNoteId(id_note) => {
|
||||
wallet::get_received_memo(self.conn.borrow(), id_note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,7 +214,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Option<sapling::CommitmentTree>, Self::Error> {
|
||||
wallet::sapling::get_sapling_commitment_tree(self, block_height)
|
||||
wallet::sapling::get_sapling_commitment_tree(self.conn.borrow(), block_height)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -209,7 +222,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Vec<(Self::NoteRef, sapling::IncrementalWitness)>, Self::Error> {
|
||||
wallet::sapling::get_sapling_witnesses(self, block_height)
|
||||
wallet::sapling::get_sapling_witnesses(self.conn.borrow(), block_height)
|
||||
}
|
||||
|
||||
fn get_sapling_nullifiers(
|
||||
|
@ -217,8 +230,8 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
query: data_api::NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error> {
|
||||
match query {
|
||||
NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self),
|
||||
NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self),
|
||||
NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self.conn.borrow()),
|
||||
NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self.conn.borrow()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,7 +241,12 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
wallet::sapling::get_spendable_sapling_notes(self, account, anchor_height, exclude)
|
||||
wallet::sapling::get_spendable_sapling_notes(
|
||||
self.conn.borrow(),
|
||||
account,
|
||||
anchor_height,
|
||||
exclude,
|
||||
)
|
||||
}
|
||||
|
||||
fn select_spendable_sapling_notes(
|
||||
|
@ -239,7 +257,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
wallet::sapling::select_spendable_sapling_notes(
|
||||
self,
|
||||
self.conn.borrow(),
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
|
@ -252,7 +270,7 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
_account: AccountId,
|
||||
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return wallet::get_transparent_receivers(&self.params, &self.conn, _account);
|
||||
return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account);
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
panic!(
|
||||
|
@ -267,7 +285,13 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
_exclude: &[OutPoint],
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return wallet::get_unspent_transparent_outputs(self, _address, _max_height, _exclude);
|
||||
return wallet::get_unspent_transparent_outputs(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
_address,
|
||||
_max_height,
|
||||
_exclude,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
panic!(
|
||||
|
@ -281,7 +305,12 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
_max_height: BlockHeight,
|
||||
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return wallet::get_transparent_balances(self, _account, _max_height);
|
||||
return wallet::get_transparent_balances(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
_account,
|
||||
_max_height,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
panic!(
|
||||
|
@ -290,177 +319,15 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
||||
type Error = SqliteClientError;
|
||||
type NoteRef = NoteId;
|
||||
type TxRef = i64;
|
||||
|
||||
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
||||
self.wallet_db.block_height_extrema()
|
||||
}
|
||||
|
||||
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
self.wallet_db.get_min_unspent_height()
|
||||
}
|
||||
|
||||
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
|
||||
self.wallet_db.get_block_hash(block_height)
|
||||
}
|
||||
|
||||
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
self.wallet_db.get_tx_height(txid)
|
||||
}
|
||||
|
||||
fn get_unified_full_viewing_keys(
|
||||
&self,
|
||||
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
|
||||
self.wallet_db.get_unified_full_viewing_keys()
|
||||
}
|
||||
|
||||
fn get_account_for_ufvk(
|
||||
&self,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<AccountId>, Self::Error> {
|
||||
self.wallet_db.get_account_for_ufvk(ufvk)
|
||||
}
|
||||
|
||||
fn get_current_address(
|
||||
&self,
|
||||
account: AccountId,
|
||||
) -> Result<Option<UnifiedAddress>, Self::Error> {
|
||||
self.wallet_db.get_current_address(account)
|
||||
}
|
||||
|
||||
fn is_valid_account_extfvk(
|
||||
&self,
|
||||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.wallet_db.is_valid_account_extfvk(account, extfvk)
|
||||
}
|
||||
|
||||
fn get_balance_at(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
self.wallet_db.get_balance_at(account, anchor_height)
|
||||
}
|
||||
|
||||
fn get_transaction(&self, id_tx: i64) -> Result<Transaction, Self::Error> {
|
||||
self.wallet_db.get_transaction(id_tx)
|
||||
}
|
||||
|
||||
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Option<Memo>, Self::Error> {
|
||||
self.wallet_db.get_memo(id_note)
|
||||
}
|
||||
|
||||
fn get_commitment_tree(
|
||||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Option<sapling::CommitmentTree>, Self::Error> {
|
||||
self.wallet_db.get_commitment_tree(block_height)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn get_witnesses(
|
||||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Vec<(Self::NoteRef, sapling::IncrementalWitness)>, Self::Error> {
|
||||
self.wallet_db.get_witnesses(block_height)
|
||||
}
|
||||
|
||||
fn get_sapling_nullifiers(
|
||||
&self,
|
||||
query: data_api::NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error> {
|
||||
self.wallet_db.get_sapling_nullifiers(query)
|
||||
}
|
||||
|
||||
fn get_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
self.wallet_db
|
||||
.get_spendable_sapling_notes(account, anchor_height, exclude)
|
||||
}
|
||||
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
self.wallet_db
|
||||
.select_spendable_sapling_notes(account, target_value, anchor_height, exclude)
|
||||
}
|
||||
|
||||
fn get_transparent_receivers(
|
||||
&self,
|
||||
account: AccountId,
|
||||
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> {
|
||||
self.wallet_db.get_transparent_receivers(account)
|
||||
}
|
||||
|
||||
fn get_unspent_transparent_outputs(
|
||||
&self,
|
||||
address: &TransparentAddress,
|
||||
max_height: BlockHeight,
|
||||
exclude: &[OutPoint],
|
||||
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
|
||||
self.wallet_db
|
||||
.get_unspent_transparent_outputs(address, max_height, exclude)
|
||||
}
|
||||
|
||||
fn get_transparent_balances(
|
||||
&self,
|
||||
account: AccountId,
|
||||
max_height: BlockHeight,
|
||||
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
|
||||
self.wallet_db.get_transparent_balances(account, max_height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
|
||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, SqliteClientError>
|
||||
where
|
||||
F: FnOnce(&mut Self) -> Result<A, SqliteClientError>,
|
||||
{
|
||||
self.wallet_db.conn.execute("BEGIN IMMEDIATE", [])?;
|
||||
match f(self) {
|
||||
Ok(result) => {
|
||||
self.wallet_db.conn.execute("COMMIT", [])?;
|
||||
Ok(result)
|
||||
}
|
||||
Err(error) => {
|
||||
match self.wallet_db.conn.execute("ROLLBACK", []) {
|
||||
Ok(_) => Err(error),
|
||||
Err(e) =>
|
||||
// Panicking here is probably the right thing to do, because it
|
||||
// means the database is corrupt.
|
||||
panic!(
|
||||
"Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
|
||||
e,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
||||
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
|
||||
type UtxoRef = UtxoId;
|
||||
|
||||
fn create_account(
|
||||
&mut self,
|
||||
seed: &SecretVec<u8>,
|
||||
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
|
||||
self.transactionally(|stmts| {
|
||||
let account = wallet::get_max_account_id(stmts.wallet_db)?
|
||||
self.transactionally(|wdb| {
|
||||
let account = wallet::get_max_account_id(&wdb.conn.0)?
|
||||
.map(|a| AccountId::from(u32::from(a) + 1))
|
||||
.unwrap_or_else(|| AccountId::from(0));
|
||||
|
||||
|
@ -468,15 +335,11 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
return Err(SqliteClientError::AccountIdOutOfRange);
|
||||
}
|
||||
|
||||
let usk = UnifiedSpendingKey::from_seed(
|
||||
&stmts.wallet_db.params,
|
||||
seed.expose_secret(),
|
||||
account,
|
||||
)
|
||||
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
|
||||
let usk = UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account)
|
||||
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
|
||||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
|
||||
wallet::add_account(stmts.wallet_db, account, &ufvk)?;
|
||||
wallet::add_account(&wdb.conn.0, &wdb.params, account, &ufvk)?;
|
||||
|
||||
Ok((account, usk))
|
||||
})
|
||||
|
@ -486,28 +349,37 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
&mut self,
|
||||
account: AccountId,
|
||||
) -> Result<Option<UnifiedAddress>, Self::Error> {
|
||||
match self.get_unified_full_viewing_keys()?.get(&account) {
|
||||
Some(ufvk) => {
|
||||
let search_from = match wallet::get_current_address(self.wallet_db, account)? {
|
||||
Some((_, mut last_diversifier_index)) => {
|
||||
last_diversifier_index
|
||||
.increment()
|
||||
.map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?;
|
||||
last_diversifier_index
|
||||
}
|
||||
None => DiversifierIndex::default(),
|
||||
};
|
||||
self.transactionally(
|
||||
|wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) {
|
||||
Some(ufvk) => {
|
||||
let search_from =
|
||||
match wallet::get_current_address(&wdb.conn.0, &wdb.params, account)? {
|
||||
Some((_, mut last_diversifier_index)) => {
|
||||
last_diversifier_index
|
||||
.increment()
|
||||
.map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?;
|
||||
last_diversifier_index
|
||||
}
|
||||
None => DiversifierIndex::default(),
|
||||
};
|
||||
|
||||
let (addr, diversifier_index) = ufvk
|
||||
.find_address(search_from)
|
||||
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
|
||||
let (addr, diversifier_index) = ufvk
|
||||
.find_address(search_from)
|
||||
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
|
||||
|
||||
self.stmt_insert_address(account, diversifier_index, &addr)?;
|
||||
wallet::insert_address(
|
||||
&wdb.conn.0,
|
||||
&wdb.params,
|
||||
account,
|
||||
diversifier_index,
|
||||
&addr,
|
||||
)?;
|
||||
|
||||
Ok(Some(addr))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
Ok(Some(addr))
|
||||
}
|
||||
None => Ok(None),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(height = u32::from(block.block_height)))]
|
||||
|
@ -517,11 +389,10 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
block: &PrunedBlock,
|
||||
updated_witnesses: &[(Self::NoteRef, sapling::IncrementalWitness)],
|
||||
) -> Result<Vec<(Self::NoteRef, sapling::IncrementalWitness)>, Self::Error> {
|
||||
// database updates for each block are transactional
|
||||
self.transactionally(|up| {
|
||||
self.transactionally(|wdb| {
|
||||
// Insert the block into the database.
|
||||
wallet::insert_block(
|
||||
up,
|
||||
&wdb.conn.0,
|
||||
block.block_height,
|
||||
block.block_hash,
|
||||
block.block_time,
|
||||
|
@ -530,15 +401,16 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
|
||||
let mut new_witnesses = vec![];
|
||||
for tx in block.transactions {
|
||||
let tx_row = wallet::put_tx_meta(up, tx, block.block_height)?;
|
||||
let tx_row = wallet::put_tx_meta(&wdb.conn.0, tx, block.block_height)?;
|
||||
|
||||
// Mark notes as spent and remove them from the scanning cache
|
||||
for spend in &tx.sapling_spends {
|
||||
wallet::sapling::mark_sapling_note_spent(up, tx_row, spend.nf())?;
|
||||
wallet::sapling::mark_sapling_note_spent(&wdb.conn.0, tx_row, spend.nf())?;
|
||||
}
|
||||
|
||||
for output in &tx.sapling_outputs {
|
||||
let received_note_id = wallet::sapling::put_received_note(up, output, tx_row)?;
|
||||
let received_note_id =
|
||||
wallet::sapling::put_received_note(&wdb.conn.0, output, tx_row)?;
|
||||
|
||||
// Save witness for note.
|
||||
new_witnesses.push((received_note_id, output.witness().clone()));
|
||||
|
@ -549,17 +421,22 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
for (received_note_id, witness) in updated_witnesses.iter().chain(new_witnesses.iter())
|
||||
{
|
||||
if let NoteId::ReceivedNoteId(rnid) = *received_note_id {
|
||||
wallet::sapling::insert_witness(up, rnid, witness, block.block_height)?;
|
||||
wallet::sapling::insert_witness(
|
||||
&wdb.conn.0,
|
||||
rnid,
|
||||
witness,
|
||||
block.block_height,
|
||||
)?;
|
||||
} else {
|
||||
return Err(SqliteClientError::InvalidNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune the stored witnesses (we only expect rollbacks of at most PRUNING_HEIGHT blocks).
|
||||
wallet::prune_witnesses(up, block.block_height - PRUNING_HEIGHT)?;
|
||||
wallet::prune_witnesses(&wdb.conn.0, block.block_height - PRUNING_HEIGHT)?;
|
||||
|
||||
// Update now-expired transactions that didn't get mined.
|
||||
wallet::update_expired_notes(up, block.block_height)?;
|
||||
wallet::update_expired_notes(&wdb.conn.0, block.block_height)?;
|
||||
|
||||
Ok(new_witnesses)
|
||||
})
|
||||
|
@ -569,91 +446,114 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
&mut self,
|
||||
d_tx: DecryptedTransaction,
|
||||
) -> Result<Self::TxRef, Self::Error> {
|
||||
self.transactionally(|up| {
|
||||
let tx_ref = wallet::put_tx_data(up, d_tx.tx, None, None)?;
|
||||
self.transactionally(|wdb| {
|
||||
let tx_ref = wallet::put_tx_data(&wdb.conn.0, d_tx.tx, None, None)?;
|
||||
|
||||
let mut spending_account_id: Option<AccountId> = None;
|
||||
for output in d_tx.sapling_outputs {
|
||||
match output.transfer_type {
|
||||
TransferType::Outgoing | TransferType::WalletInternal => {
|
||||
let recipient = if output.transfer_type == TransferType::Outgoing {
|
||||
Recipient::Sapling(output.note.recipient())
|
||||
} else {
|
||||
Recipient::InternalAccount(output.account, PoolType::Sapling)
|
||||
};
|
||||
let mut spending_account_id: Option<AccountId> = None;
|
||||
for output in d_tx.sapling_outputs {
|
||||
match output.transfer_type {
|
||||
TransferType::Outgoing | TransferType::WalletInternal => {
|
||||
let recipient = if output.transfer_type == TransferType::Outgoing {
|
||||
Recipient::Sapling(output.note.recipient())
|
||||
} else {
|
||||
Recipient::InternalAccount(output.account, PoolType::Sapling)
|
||||
};
|
||||
|
||||
wallet::put_sent_output(
|
||||
up,
|
||||
output.account,
|
||||
tx_ref,
|
||||
output.index,
|
||||
&recipient,
|
||||
Amount::from_u64(output.note.value().inner()).map_err(|_|
|
||||
SqliteClientError::CorruptedData("Note value is not a valid Zcash amount.".to_string()))?,
|
||||
Some(&output.memo),
|
||||
)?;
|
||||
wallet::put_sent_output(
|
||||
&wdb.conn.0,
|
||||
&wdb.params,
|
||||
output.account,
|
||||
tx_ref,
|
||||
output.index,
|
||||
&recipient,
|
||||
Amount::from_u64(output.note.value().inner()).map_err(|_| {
|
||||
SqliteClientError::CorruptedData(
|
||||
"Note value is not a valid Zcash amount.".to_string(),
|
||||
)
|
||||
})?,
|
||||
Some(&output.memo),
|
||||
)?;
|
||||
|
||||
if matches!(recipient, Recipient::InternalAccount(_, _)) {
|
||||
wallet::sapling::put_received_note(up, output, tx_ref)?;
|
||||
}
|
||||
if matches!(recipient, Recipient::InternalAccount(_, _)) {
|
||||
wallet::sapling::put_received_note(&wdb.conn.0, output, tx_ref)?;
|
||||
}
|
||||
TransferType::Incoming => {
|
||||
match spending_account_id {
|
||||
Some(id) =>
|
||||
if id != output.account {
|
||||
panic!("Unable to determine a unique account identifier for z->t spend.");
|
||||
}
|
||||
None => {
|
||||
spending_account_id = Some(output.account);
|
||||
}
|
||||
TransferType::Incoming => {
|
||||
match spending_account_id {
|
||||
Some(id) => {
|
||||
if id != output.account {
|
||||
panic!("Unable to determine a unique account identifier for z->t spend.");
|
||||
}
|
||||
}
|
||||
|
||||
wallet::sapling::put_received_note(up, output, tx_ref)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any of the utxos spent in the transaction are ours, mark them as spent.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
|
||||
wallet::mark_transparent_utxo_spent(up, tx_ref, &txin.prevout)?;
|
||||
}
|
||||
|
||||
// If we have some transparent outputs:
|
||||
if !d_tx.tx.transparent_bundle().iter().any(|b| b.vout.is_empty()) {
|
||||
let nullifiers = self.wallet_db.get_sapling_nullifiers(data_api::NullifierQuery::All)?;
|
||||
// If the transaction contains shielded spends from our wallet, we will store z->t
|
||||
// transactions we observe in the same way they would be stored by
|
||||
// create_spend_to_address.
|
||||
if let Some((account_id, _)) = nullifiers.iter().find(
|
||||
|(_, nf)|
|
||||
d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter())
|
||||
.any(|input| nf == input.nullifier())
|
||||
) {
|
||||
for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
|
||||
if let Some(address) = txout.recipient_address() {
|
||||
wallet::put_sent_output(
|
||||
up,
|
||||
*account_id,
|
||||
tx_ref,
|
||||
output_index,
|
||||
&Recipient::Transparent(address),
|
||||
txout.value,
|
||||
None
|
||||
)?;
|
||||
None => {
|
||||
spending_account_id = Some(output.account);
|
||||
}
|
||||
}
|
||||
|
||||
wallet::sapling::put_received_note(&wdb.conn.0, output, tx_ref)?;
|
||||
}
|
||||
}
|
||||
Ok(tx_ref)
|
||||
}
|
||||
|
||||
// If any of the utxos spent in the transaction are ours, mark them as spent.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
for txin in d_tx
|
||||
.tx
|
||||
.transparent_bundle()
|
||||
.iter()
|
||||
.flat_map(|b| b.vin.iter())
|
||||
{
|
||||
wallet::mark_transparent_utxo_spent(&wdb.conn.0, tx_ref, &txin.prevout)?;
|
||||
}
|
||||
|
||||
// If we have some transparent outputs:
|
||||
if !d_tx
|
||||
.tx
|
||||
.transparent_bundle()
|
||||
.iter()
|
||||
.any(|b| b.vout.is_empty())
|
||||
{
|
||||
let nullifiers = wdb.get_sapling_nullifiers(data_api::NullifierQuery::All)?;
|
||||
// If the transaction contains shielded spends from our wallet, we will store z->t
|
||||
// transactions we observe in the same way they would be stored by
|
||||
// create_spend_to_address.
|
||||
if let Some((account_id, _)) = nullifiers.iter().find(|(_, nf)| {
|
||||
d_tx.tx
|
||||
.sapling_bundle()
|
||||
.iter()
|
||||
.flat_map(|b| b.shielded_spends().iter())
|
||||
.any(|input| nf == input.nullifier())
|
||||
}) {
|
||||
for (output_index, txout) in d_tx
|
||||
.tx
|
||||
.transparent_bundle()
|
||||
.iter()
|
||||
.flat_map(|b| b.vout.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if let Some(address) = txout.recipient_address() {
|
||||
wallet::put_sent_output(
|
||||
&wdb.conn.0,
|
||||
&wdb.params,
|
||||
*account_id,
|
||||
tx_ref,
|
||||
output_index,
|
||||
&Recipient::Transparent(address),
|
||||
txout.value,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tx_ref)
|
||||
})
|
||||
}
|
||||
|
||||
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error> {
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
self.transactionally(|up| {
|
||||
self.transactionally(|wdb| {
|
||||
let tx_ref = wallet::put_tx_data(
|
||||
up,
|
||||
&wdb.conn.0,
|
||||
sent_tx.tx,
|
||||
Some(sent_tx.fee_amount),
|
||||
Some(sent_tx.created),
|
||||
|
@ -669,21 +569,31 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
// reasonable assumption for a light client such as a mobile phone.
|
||||
if let Some(bundle) = sent_tx.tx.sapling_bundle() {
|
||||
for spend in bundle.shielded_spends() {
|
||||
wallet::sapling::mark_sapling_note_spent(up, tx_ref, spend.nullifier())?;
|
||||
wallet::sapling::mark_sapling_note_spent(
|
||||
&wdb.conn.0,
|
||||
tx_ref,
|
||||
spend.nullifier(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
for utxo_outpoint in &sent_tx.utxos_spent {
|
||||
wallet::mark_transparent_utxo_spent(up, tx_ref, utxo_outpoint)?;
|
||||
wallet::mark_transparent_utxo_spent(&wdb.conn.0, tx_ref, utxo_outpoint)?;
|
||||
}
|
||||
|
||||
for output in &sent_tx.outputs {
|
||||
wallet::insert_sent_output(up, tx_ref, sent_tx.account, output)?;
|
||||
wallet::insert_sent_output(
|
||||
&wdb.conn.0,
|
||||
&wdb.params,
|
||||
tx_ref,
|
||||
sent_tx.account,
|
||||
output,
|
||||
)?;
|
||||
|
||||
if let Some((account, note)) = output.sapling_change_to() {
|
||||
wallet::sapling::put_received_note(
|
||||
up,
|
||||
&wdb.conn.0,
|
||||
&DecryptedOutput {
|
||||
index: output.output_index(),
|
||||
note: note.clone(),
|
||||
|
@ -704,7 +614,9 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
}
|
||||
|
||||
fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> {
|
||||
wallet::truncate_to_height(self.wallet_db, block_height)
|
||||
self.transactionally(|wdb| {
|
||||
wallet::truncate_to_height(&wdb.conn.0, &wdb.params, block_height)
|
||||
})
|
||||
}
|
||||
|
||||
fn put_received_transparent_utxo(
|
||||
|
@ -712,7 +624,7 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
_output: &WalletTransparentOutput,
|
||||
) -> Result<Self::UtxoRef, Self::Error> {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return wallet::put_received_transparent_utxo(self, _output);
|
||||
return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output);
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
panic!(
|
||||
|
@ -1056,7 +968,7 @@ mod tests {
|
|||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn init_test_accounts_table(
|
||||
db_data: &WalletDb<Network>,
|
||||
db_data: &mut WalletDb<rusqlite::Connection, Network>,
|
||||
) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) {
|
||||
let (ufvk, taddr) = init_test_accounts_table_ufvk(db_data);
|
||||
(ufvk.sapling().unwrap().clone(), taddr)
|
||||
|
@ -1064,7 +976,7 @@ mod tests {
|
|||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn init_test_accounts_table_ufvk(
|
||||
db_data: &WalletDb<Network>,
|
||||
db_data: &mut WalletDb<rusqlite::Connection, Network>,
|
||||
) -> (UnifiedFullViewingKey, Option<TransparentAddress>) {
|
||||
let seed = [0u8; 32];
|
||||
let account = AccountId::from(0);
|
||||
|
@ -1291,13 +1203,12 @@ mod tests {
|
|||
|
||||
let account = AccountId::from(0);
|
||||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
let _ = init_test_accounts_table_ufvk(&db_data);
|
||||
init_test_accounts_table_ufvk(&mut db_data);
|
||||
|
||||
let current_addr = db_data.get_current_address(account).unwrap();
|
||||
assert!(current_addr.is_some());
|
||||
|
||||
let mut update_ops = db_data.get_update_ops().unwrap();
|
||||
let addr2 = update_ops.get_next_available_address(account).unwrap();
|
||||
let addr2 = db_data.get_next_available_address(account).unwrap();
|
||||
assert!(addr2.is_some());
|
||||
assert_ne!(current_addr, addr2);
|
||||
|
||||
|
@ -1322,7 +1233,7 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet.
|
||||
let (ufvk, taddr) = init_test_accounts_table_ufvk(&db_data);
|
||||
let (ufvk, taddr) = init_test_accounts_table_ufvk(&mut db_data);
|
||||
let taddr = taddr.unwrap();
|
||||
|
||||
let receivers = db_data.get_transparent_receivers(0.into()).unwrap();
|
||||
|
|
|
@ -1,812 +0,0 @@
|
|||
//! Prepared SQL statements used by the wallet.
|
||||
//!
|
||||
//! Some `rusqlite` crate APIs are only available on prepared statements; these are stored
|
||||
//! inside the [`DataConnStmtCache`]. When adding a new prepared statement:
|
||||
//!
|
||||
//! - Add it as a private field of `DataConnStmtCache`.
|
||||
//! - Build the statement in [`DataConnStmtCache::new`].
|
||||
//! - Add a crate-private helper method to `DataConnStmtCache` for running the statement.
|
||||
|
||||
use rusqlite::{named_params, params, Statement, ToSql};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
merkle_tree::{write_commitment_tree, write_incremental_witness},
|
||||
sapling::{self, Diversifier, Nullifier},
|
||||
transaction::{components::Amount, TxId},
|
||||
zip32::{AccountId, DiversifierIndex},
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::UnifiedAddress,
|
||||
data_api::{PoolType, Recipient},
|
||||
encoding::AddressCodec,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, wallet::pool_code, NoteId, WalletDb};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
crate::UtxoId, rusqlite::OptionalExtension,
|
||||
zcash_client_backend::wallet::WalletTransparentOutput,
|
||||
zcash_primitives::transaction::components::transparent::OutPoint,
|
||||
};
|
||||
|
||||
pub(crate) struct InsertAddress<'a> {
|
||||
stmt: Statement<'a>,
|
||||
}
|
||||
|
||||
impl<'a> InsertAddress<'a> {
|
||||
pub(crate) fn new(conn: &'a rusqlite::Connection) -> Result<Self, rusqlite::Error> {
|
||||
Ok(InsertAddress {
|
||||
stmt: conn.prepare(
|
||||
"INSERT INTO addresses (
|
||||
account,
|
||||
diversifier_index_be,
|
||||
address,
|
||||
cached_transparent_receiver_address
|
||||
)
|
||||
VALUES (
|
||||
:account,
|
||||
:diversifier_index_be,
|
||||
:address,
|
||||
:cached_transparent_receiver_address
|
||||
)",
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds the given address and diversifier index to the addresses table.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted address.
|
||||
pub(crate) fn execute<P: consensus::Parameters>(
|
||||
&mut self,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
mut diversifier_index: DiversifierIndex,
|
||||
address: &UnifiedAddress,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
// the diversifier index is stored in big-endian order to allow sorting
|
||||
diversifier_index.0.reverse();
|
||||
self.stmt.execute(named_params![
|
||||
":account": &u32::from(account),
|
||||
":diversifier_index_be": &&diversifier_index.0[..],
|
||||
":address": &address.encode(params),
|
||||
":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)),
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The primary type used to implement [`WalletWrite`] for the SQLite database.
|
||||
///
|
||||
/// A data structure that stores the SQLite prepared statements that are
|
||||
/// required for the implementation of [`WalletWrite`] against the backing
|
||||
/// store.
|
||||
///
|
||||
/// [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
|
||||
pub struct DataConnStmtCache<'a, P> {
|
||||
pub(crate) wallet_db: &'a WalletDb<P>,
|
||||
stmt_insert_block: Statement<'a>,
|
||||
|
||||
stmt_insert_tx_meta: Statement<'a>,
|
||||
stmt_update_tx_meta: Statement<'a>,
|
||||
|
||||
stmt_insert_tx_data: Statement<'a>,
|
||||
stmt_update_tx_data: Statement<'a>,
|
||||
stmt_select_tx_ref: Statement<'a>,
|
||||
|
||||
stmt_mark_sapling_note_spent: Statement<'a>,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_mark_transparent_utxo_spent: Statement<'a>,
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_insert_received_transparent_utxo: Statement<'a>,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_update_received_transparent_utxo: Statement<'a>,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_insert_legacy_transparent_utxo: Statement<'a>,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_update_legacy_transparent_utxo: Statement<'a>,
|
||||
stmt_insert_received_note: Statement<'a>,
|
||||
stmt_update_received_note: Statement<'a>,
|
||||
stmt_select_received_note: Statement<'a>,
|
||||
|
||||
stmt_insert_sent_output: Statement<'a>,
|
||||
stmt_update_sent_output: Statement<'a>,
|
||||
|
||||
stmt_insert_witness: Statement<'a>,
|
||||
stmt_prune_witnesses: Statement<'a>,
|
||||
stmt_update_expired: Statement<'a>,
|
||||
|
||||
stmt_insert_address: InsertAddress<'a>,
|
||||
}
|
||||
|
||||
impl<'a, P> DataConnStmtCache<'a, P> {
|
||||
pub(crate) fn new(wallet_db: &'a WalletDb<P>) -> Result<Self, SqliteClientError> {
|
||||
Ok(
|
||||
DataConnStmtCache {
|
||||
wallet_db,
|
||||
stmt_insert_block: wallet_db.conn.prepare(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_insert_tx_meta: wallet_db.conn.prepare(
|
||||
"INSERT INTO transactions (txid, block, tx_index)
|
||||
VALUES (?, ?, ?)",
|
||||
)?,
|
||||
stmt_update_tx_meta: wallet_db.conn.prepare(
|
||||
"UPDATE transactions
|
||||
SET block = ?, tx_index = ? WHERE txid = ?",
|
||||
)?,
|
||||
stmt_insert_tx_data: wallet_db.conn.prepare(
|
||||
"INSERT INTO transactions (txid, created, expiry_height, raw, fee)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_update_tx_data: wallet_db.conn.prepare(
|
||||
"UPDATE transactions
|
||||
SET expiry_height = :expiry_height,
|
||||
raw = :raw,
|
||||
fee = IFNULL(:fee, fee)
|
||||
WHERE txid = :txid",
|
||||
)?,
|
||||
stmt_select_tx_ref: wallet_db.conn.prepare(
|
||||
"SELECT id_tx FROM transactions WHERE txid = ?",
|
||||
)?,
|
||||
stmt_mark_sapling_note_spent: wallet_db.conn.prepare(
|
||||
"UPDATE sapling_received_notes SET spent = ? WHERE nf = ?"
|
||||
)?,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_mark_transparent_utxo_spent: wallet_db.conn.prepare(
|
||||
"UPDATE utxos SET spent_in_tx = :spent_in_tx
|
||||
WHERE prevout_txid = :prevout_txid
|
||||
AND prevout_idx = :prevout_idx"
|
||||
)?,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_insert_received_transparent_utxo: wallet_db.conn.prepare(
|
||||
"INSERT INTO utxos (
|
||||
received_by_account, address,
|
||||
prevout_txid, prevout_idx, script,
|
||||
value_zat, height)
|
||||
SELECT
|
||||
addresses.account, :address,
|
||||
:prevout_txid, :prevout_idx, :script,
|
||||
:value_zat, :height
|
||||
FROM addresses
|
||||
WHERE addresses.cached_transparent_receiver_address = :address
|
||||
RETURNING id_utxo"
|
||||
)?,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_update_received_transparent_utxo: wallet_db.conn.prepare(
|
||||
"UPDATE utxos
|
||||
SET received_by_account = addresses.account,
|
||||
height = :height,
|
||||
address = :address,
|
||||
script = :script,
|
||||
value_zat = :value_zat
|
||||
FROM addresses
|
||||
WHERE prevout_txid = :prevout_txid
|
||||
AND prevout_idx = :prevout_idx
|
||||
AND addresses.cached_transparent_receiver_address = :address
|
||||
RETURNING id_utxo"
|
||||
)?,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_insert_legacy_transparent_utxo: wallet_db.conn.prepare(
|
||||
"INSERT INTO utxos (
|
||||
received_by_account, address,
|
||||
prevout_txid, prevout_idx, script,
|
||||
value_zat, height)
|
||||
VALUES
|
||||
(:received_by_account, :address,
|
||||
:prevout_txid, :prevout_idx, :script,
|
||||
:value_zat, :height)
|
||||
RETURNING id_utxo"
|
||||
)?,
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
stmt_update_legacy_transparent_utxo: wallet_db.conn.prepare(
|
||||
"UPDATE utxos
|
||||
SET received_by_account = :received_by_account,
|
||||
height = :height,
|
||||
address = :address,
|
||||
script = :script,
|
||||
value_zat = :value_zat
|
||||
WHERE prevout_txid = :prevout_txid
|
||||
AND prevout_idx = :prevout_idx
|
||||
RETURNING id_utxo"
|
||||
)?,
|
||||
stmt_insert_received_note: wallet_db.conn.prepare(
|
||||
"INSERT INTO sapling_received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
|
||||
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
|
||||
)?,
|
||||
stmt_update_received_note: wallet_db.conn.prepare(
|
||||
"UPDATE sapling_received_notes
|
||||
SET account = :account,
|
||||
diversifier = :diversifier,
|
||||
value = :value,
|
||||
rcm = :rcm,
|
||||
nf = IFNULL(:nf, nf),
|
||||
memo = IFNULL(:memo, memo),
|
||||
is_change = IFNULL(:is_change, is_change)
|
||||
WHERE tx = :tx AND output_index = :output_index",
|
||||
)?,
|
||||
stmt_select_received_note: wallet_db.conn.prepare(
|
||||
"SELECT id_note FROM sapling_received_notes WHERE tx = ? AND output_index = ?"
|
||||
)?,
|
||||
stmt_update_sent_output: wallet_db.conn.prepare(
|
||||
"UPDATE sent_notes
|
||||
SET from_account = :from_account,
|
||||
to_address = :to_address,
|
||||
to_account = :to_account,
|
||||
value = :value,
|
||||
memo = IFNULL(:memo, memo)
|
||||
WHERE tx = :tx
|
||||
AND output_pool = :output_pool
|
||||
AND output_index = :output_index",
|
||||
)?,
|
||||
stmt_insert_sent_output: wallet_db.conn.prepare(
|
||||
"INSERT INTO sent_notes (
|
||||
tx, output_pool, output_index, from_account,
|
||||
to_address, to_account, value, memo)
|
||||
VALUES (
|
||||
:tx, :output_pool, :output_index, :from_account,
|
||||
:to_address, :to_account, :value, :memo)"
|
||||
)?,
|
||||
stmt_insert_witness: wallet_db.conn.prepare(
|
||||
"INSERT INTO sapling_witnesses (note, block, witness)
|
||||
VALUES (?, ?, ?)",
|
||||
)?,
|
||||
stmt_prune_witnesses: wallet_db.conn.prepare(
|
||||
"DELETE FROM sapling_witnesses WHERE block < ?"
|
||||
)?,
|
||||
stmt_update_expired: wallet_db.conn.prepare(
|
||||
"UPDATE sapling_received_notes SET spent = NULL WHERE EXISTS (
|
||||
SELECT id_tx FROM transactions
|
||||
WHERE id_tx = sapling_received_notes.spent AND block IS NULL AND expiry_height < ?
|
||||
)",
|
||||
)?,
|
||||
stmt_insert_address: InsertAddress::new(&wallet_db.conn)?
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Inserts information about a scanned block into the database.
|
||||
pub fn stmt_insert_block(
|
||||
&mut self,
|
||||
block_height: BlockHeight,
|
||||
block_hash: BlockHash,
|
||||
block_time: u32,
|
||||
commitment_tree: &sapling::CommitmentTree,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let mut encoded_tree = Vec::new();
|
||||
write_commitment_tree(commitment_tree, &mut encoded_tree).unwrap();
|
||||
|
||||
self.stmt_insert_block.execute(params![
|
||||
u32::from(block_height),
|
||||
&block_hash.0[..],
|
||||
block_time,
|
||||
encoded_tree
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts the given transaction and its block metadata into the wallet.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted transaction, or an error if the
|
||||
/// transaction exists.
|
||||
pub(crate) fn stmt_insert_tx_meta(
|
||||
&mut self,
|
||||
txid: &TxId,
|
||||
height: BlockHeight,
|
||||
tx_index: usize,
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
self.stmt_insert_tx_meta.execute(params![
|
||||
&txid.as_ref()[..],
|
||||
u32::from(height),
|
||||
(tx_index as i64),
|
||||
])?;
|
||||
|
||||
Ok(self.wallet_db.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Updates the block metadata for the given transaction.
|
||||
///
|
||||
/// Returns `false` if the transaction doesn't exist in the wallet.
|
||||
pub(crate) fn stmt_update_tx_meta(
|
||||
&mut self,
|
||||
height: BlockHeight,
|
||||
tx_index: usize,
|
||||
txid: &TxId,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
match self.stmt_update_tx_meta.execute(params![
|
||||
u32::from(height),
|
||||
(tx_index as i64),
|
||||
&txid.as_ref()[..],
|
||||
])? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("txid column is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts the given transaction and its data into the wallet.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted transaction, or an error if the
|
||||
/// transaction exists.
|
||||
pub(crate) fn stmt_insert_tx_data(
|
||||
&mut self,
|
||||
txid: &TxId,
|
||||
created_at: Option<time::OffsetDateTime>,
|
||||
expiry_height: BlockHeight,
|
||||
raw_tx: &[u8],
|
||||
fee: Option<Amount>,
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
self.stmt_insert_tx_data.execute(params![
|
||||
&txid.as_ref()[..],
|
||||
created_at,
|
||||
u32::from(expiry_height),
|
||||
raw_tx,
|
||||
fee.map(i64::from)
|
||||
])?;
|
||||
|
||||
Ok(self.wallet_db.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Updates the data for the given transaction.
|
||||
///
|
||||
/// Returns `false` if the transaction doesn't exist in the wallet.
|
||||
pub(crate) fn stmt_update_tx_data(
|
||||
&mut self,
|
||||
expiry_height: BlockHeight,
|
||||
raw_tx: &[u8],
|
||||
fee: Option<Amount>,
|
||||
txid: &TxId,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(":expiry_height", &u32::from(expiry_height)),
|
||||
(":raw", &raw_tx),
|
||||
(":fee", &fee.map(i64::from)),
|
||||
(":txid", &&txid.as_ref()[..]),
|
||||
];
|
||||
match self.stmt_update_tx_data.execute(sql_args)? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("txid column is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the database row for the given `txid`, if the transaction is in the wallet.
|
||||
pub(crate) fn stmt_select_tx_ref(&mut self, txid: &TxId) -> Result<i64, SqliteClientError> {
|
||||
self.stmt_select_tx_ref
|
||||
.query_row([&txid.as_ref()[..]], |row| row.get(0))
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Marks a given nullifier as having been revealed in the construction of the
|
||||
/// specified transaction.
|
||||
///
|
||||
/// Marking a note spent in this fashion does NOT imply that the spending transaction
|
||||
/// has been mined.
|
||||
///
|
||||
/// Returns `false` if the nullifier does not correspond to any received note.
|
||||
pub(crate) fn stmt_mark_sapling_note_spent(
|
||||
&mut self,
|
||||
tx_ref: i64,
|
||||
nf: &Nullifier,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
match self
|
||||
.stmt_mark_sapling_note_spent
|
||||
.execute(params![tx_ref, &nf.0[..]])?
|
||||
{
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("nf column is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the given UTXO as having been spent.
|
||||
///
|
||||
/// Returns `false` if `outpoint` does not correspond to any tracked UTXO.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn stmt_mark_transparent_utxo_spent(
|
||||
&mut self,
|
||||
tx_ref: i64,
|
||||
outpoint: &OutPoint,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(":spent_in_tx", &tx_ref),
|
||||
(":prevout_txid", &outpoint.hash().to_vec()),
|
||||
(":prevout_idx", &outpoint.n()),
|
||||
];
|
||||
|
||||
match self.stmt_mark_transparent_utxo_spent.execute(sql_args)? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("tx_outpoint constraint is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
|
||||
/// Inserts a sent note into the wallet database.
|
||||
///
|
||||
/// `output_index` is the index within the transaction that contains the recipient output:
|
||||
///
|
||||
/// - If `to` is a Unified address, this is an index into the outputs of the transaction
|
||||
/// within the bundle associated with the recipient's output pool.
|
||||
/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the
|
||||
/// transaction.
|
||||
/// - If `to` is a transparent address, this is an index into the transparent outputs of
|
||||
/// the transaction.
|
||||
/// - If `to` is an internal account, this is an index into the Sapling outputs of the
|
||||
/// transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn stmt_insert_sent_output(
|
||||
&mut self,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
from_account: AccountId,
|
||||
to: &Recipient,
|
||||
value: Amount,
|
||||
memo: Option<&MemoBytes>,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let (to_address, to_account, pool_type) = match to {
|
||||
Recipient::Transparent(addr) => (
|
||||
Some(addr.encode(&self.wallet_db.params)),
|
||||
None,
|
||||
PoolType::Transparent,
|
||||
),
|
||||
Recipient::Sapling(addr) => (
|
||||
Some(addr.encode(&self.wallet_db.params)),
|
||||
None,
|
||||
PoolType::Sapling,
|
||||
),
|
||||
Recipient::Unified(addr, pool) => {
|
||||
(Some(addr.encode(&self.wallet_db.params)), None, *pool)
|
||||
}
|
||||
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool),
|
||||
};
|
||||
|
||||
self.stmt_insert_sent_output.execute(named_params![
|
||||
":tx": &tx_ref,
|
||||
":output_pool": &pool_code(pool_type),
|
||||
":output_index": &i64::try_from(output_index).unwrap(),
|
||||
":from_account": &u32::from(from_account),
|
||||
":to_address": &to_address,
|
||||
":to_account": &to_account,
|
||||
":value": &i64::from(value),
|
||||
":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()),
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the data for the given sent note.
|
||||
///
|
||||
/// Returns `false` if the transaction doesn't exist in the wallet.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn stmt_update_sent_output(
|
||||
&mut self,
|
||||
from_account: AccountId,
|
||||
to: &Recipient,
|
||||
value: Amount,
|
||||
memo: Option<&MemoBytes>,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let (to_address, to_account, pool_type) = match to {
|
||||
Recipient::Transparent(addr) => (
|
||||
Some(addr.encode(&self.wallet_db.params)),
|
||||
None,
|
||||
PoolType::Transparent,
|
||||
),
|
||||
Recipient::Sapling(addr) => (
|
||||
Some(addr.encode(&self.wallet_db.params)),
|
||||
None,
|
||||
PoolType::Sapling,
|
||||
),
|
||||
Recipient::Unified(addr, pool) => {
|
||||
(Some(addr.encode(&self.wallet_db.params)), None, *pool)
|
||||
}
|
||||
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool),
|
||||
};
|
||||
|
||||
match self.stmt_update_sent_output.execute(named_params![
|
||||
":from_account": &u32::from(from_account),
|
||||
":to_address": &to_address,
|
||||
":to_account": &to_account,
|
||||
":value": &i64::from(value),
|
||||
":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()),
|
||||
":tx": &tx_ref,
|
||||
":output_pool": &pool_code(pool_type),
|
||||
":output_index": &i64::try_from(output_index).unwrap(),
|
||||
])? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given received UTXO to the datastore.
|
||||
///
|
||||
/// Returns the database identifier for the newly-inserted UTXO if the address to which the
|
||||
/// UTXO was sent corresponds to a cached transparent receiver in the addresses table, or
|
||||
/// Ok(None) if the address is unknown. Returns an error if the UTXO exists.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn stmt_insert_received_transparent_utxo(
|
||||
&mut self,
|
||||
output: &WalletTransparentOutput,
|
||||
) -> Result<Option<UtxoId>, SqliteClientError> {
|
||||
self.stmt_insert_received_transparent_utxo
|
||||
.query_row(
|
||||
named_params![
|
||||
":address": &output.recipient_address().encode(&self.wallet_db.params),
|
||||
":prevout_txid": &output.outpoint().hash().to_vec(),
|
||||
":prevout_idx": &output.outpoint().n(),
|
||||
":script": &output.txout().script_pubkey.0,
|
||||
":value_zat": &i64::from(output.txout().value),
|
||||
":height": &u32::from(output.height()),
|
||||
],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
Ok(UtxoId(id))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Adds the given received UTXO to the datastore.
|
||||
///
|
||||
/// Returns the database identifier for the updated UTXO if the address to which the UTXO was
|
||||
/// sent corresponds to a cached transparent receiver in the addresses table, or Ok(None) if
|
||||
/// the address is unknown.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn stmt_update_received_transparent_utxo(
|
||||
&mut self,
|
||||
output: &WalletTransparentOutput,
|
||||
) -> Result<Option<UtxoId>, SqliteClientError> {
|
||||
self.stmt_update_received_transparent_utxo
|
||||
.query_row(
|
||||
named_params![
|
||||
":prevout_txid": &output.outpoint().hash().to_vec(),
|
||||
":prevout_idx": &output.outpoint().n(),
|
||||
":address": &output.recipient_address().encode(&self.wallet_db.params),
|
||||
":script": &output.txout().script_pubkey.0,
|
||||
":value_zat": &i64::from(output.txout().value),
|
||||
":height": &u32::from(output.height()),
|
||||
],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
Ok(UtxoId(id))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Adds the given legacy UTXO to the datastore.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted UTXO, or an error if the UTXO
|
||||
/// exists.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn stmt_insert_legacy_transparent_utxo(
|
||||
&mut self,
|
||||
output: &WalletTransparentOutput,
|
||||
received_by_account: AccountId,
|
||||
) -> Result<UtxoId, SqliteClientError> {
|
||||
self.stmt_insert_legacy_transparent_utxo
|
||||
.query_row(
|
||||
named_params![
|
||||
":received_by_account": &u32::from(received_by_account),
|
||||
":address": &output.recipient_address().encode(&self.wallet_db.params),
|
||||
":prevout_txid": &output.outpoint().hash().to_vec(),
|
||||
":prevout_idx": &output.outpoint().n(),
|
||||
":script": &output.txout().script_pubkey.0,
|
||||
":value_zat": &i64::from(output.txout().value),
|
||||
":height": &u32::from(output.height()),
|
||||
],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
Ok(UtxoId(id))
|
||||
},
|
||||
)
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Adds the given legacy UTXO to the datastore.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted UTXO, or an error if the UTXO
|
||||
/// exists.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub(crate) fn stmt_update_legacy_transparent_utxo(
|
||||
&mut self,
|
||||
output: &WalletTransparentOutput,
|
||||
received_by_account: AccountId,
|
||||
) -> Result<Option<UtxoId>, SqliteClientError> {
|
||||
self.stmt_update_legacy_transparent_utxo
|
||||
.query_row(
|
||||
named_params![
|
||||
":received_by_account": &u32::from(received_by_account),
|
||||
":prevout_txid": &output.outpoint().hash().to_vec(),
|
||||
":prevout_idx": &output.outpoint().n(),
|
||||
":address": &output.recipient_address().encode(&self.wallet_db.params),
|
||||
":script": &output.txout().script_pubkey.0,
|
||||
":value_zat": &i64::from(output.txout().value),
|
||||
":height": &u32::from(output.height()),
|
||||
],
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
Ok(UtxoId(id))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Adds the given address and diversifier index to the addresses table.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted address.
|
||||
pub(crate) fn stmt_insert_address(
|
||||
&mut self,
|
||||
account: AccountId,
|
||||
diversifier_index: DiversifierIndex,
|
||||
address: &UnifiedAddress,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
self.stmt_insert_address.execute(
|
||||
&self.wallet_db.params,
|
||||
account,
|
||||
diversifier_index,
|
||||
address,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P> DataConnStmtCache<'a, P> {
|
||||
/// Inserts the given received note into the wallet.
|
||||
///
|
||||
/// This implementation relies on the facts that:
|
||||
/// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
/// - A note value will never exceed 2^63 zatoshis.
|
||||
///
|
||||
/// Returns the database row for the newly-inserted note, or an error if the note
|
||||
/// exists.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn stmt_insert_received_note(
|
||||
&mut self,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
account: AccountId,
|
||||
diversifier: &Diversifier,
|
||||
value: u64,
|
||||
rcm: [u8; 32],
|
||||
nf: Option<&Nullifier>,
|
||||
memo: Option<&MemoBytes>,
|
||||
is_change: bool,
|
||||
) -> Result<NoteId, SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(":tx", &tx_ref),
|
||||
(":output_index", &(output_index as i64)),
|
||||
(":account", &u32::from(account)),
|
||||
(":diversifier", &diversifier.0.as_ref()),
|
||||
(":value", &(value as i64)),
|
||||
(":rcm", &rcm.as_ref()),
|
||||
(":nf", &nf.map(|nf| nf.0.as_ref())),
|
||||
(
|
||||
":memo",
|
||||
&memo
|
||||
.filter(|m| *m != &MemoBytes::empty())
|
||||
.map(|m| m.as_slice()),
|
||||
),
|
||||
(":is_change", &is_change),
|
||||
];
|
||||
|
||||
self.stmt_insert_received_note.execute(sql_args)?;
|
||||
|
||||
Ok(NoteId::ReceivedNoteId(
|
||||
self.wallet_db.conn.last_insert_rowid(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Updates the data for the given transaction.
|
||||
///
|
||||
/// This implementation relies on the facts that:
|
||||
/// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
/// - A note value will never exceed 2^63 zatoshis.
|
||||
///
|
||||
/// Returns `false` if the transaction doesn't exist in the wallet.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn stmt_update_received_note(
|
||||
&mut self,
|
||||
account: AccountId,
|
||||
diversifier: &Diversifier,
|
||||
value: u64,
|
||||
rcm: [u8; 32],
|
||||
nf: Option<&Nullifier>,
|
||||
memo: Option<&MemoBytes>,
|
||||
is_change: bool,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||
(":account", &u32::from(account)),
|
||||
(":diversifier", &diversifier.0.as_ref()),
|
||||
(":value", &(value as i64)),
|
||||
(":rcm", &rcm.as_ref()),
|
||||
(":nf", &nf.map(|nf| nf.0.as_ref())),
|
||||
(
|
||||
":memo",
|
||||
&memo
|
||||
.filter(|m| *m != &MemoBytes::empty())
|
||||
.map(|m| m.as_slice()),
|
||||
),
|
||||
(":is_change", &is_change),
|
||||
(":tx", &tx_ref),
|
||||
(":output_index", &(output_index as i64)),
|
||||
];
|
||||
|
||||
match self.stmt_update_received_note.execute(sql_args)? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the database row for the given `txid`, if the transaction is in the wallet.
|
||||
pub(crate) fn stmt_select_received_note(
|
||||
&mut self,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
) -> Result<NoteId, SqliteClientError> {
|
||||
self.stmt_select_received_note
|
||||
.query_row(params![tx_ref, (output_index as i64)], |row| {
|
||||
row.get(0).map(NoteId::ReceivedNoteId)
|
||||
})
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Records the incremental witness for the specified note, as of the given block
|
||||
/// height.
|
||||
///
|
||||
/// Returns `SqliteClientError::InvalidNoteId` if the note ID is for a sent note.
|
||||
pub(crate) fn stmt_insert_witness(
|
||||
&mut self,
|
||||
note_id: NoteId,
|
||||
height: BlockHeight,
|
||||
witness: &sapling::IncrementalWitness,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let note_id = match note_id {
|
||||
NoteId::ReceivedNoteId(note_id) => Ok(note_id),
|
||||
NoteId::SentNoteId(_) => Err(SqliteClientError::InvalidNoteId),
|
||||
}?;
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
write_incremental_witness(witness, &mut encoded).unwrap();
|
||||
|
||||
self.stmt_insert_witness
|
||||
.execute(params![note_id, u32::from(height), encoded])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes old incremental witnesses up to the given block height.
|
||||
pub(crate) fn stmt_prune_witnesses(
|
||||
&mut self,
|
||||
below_height: BlockHeight,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
self.stmt_prune_witnesses
|
||||
.execute([u32::from(below_height)])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks notes that have not been mined in transactions as expired, up to the given
|
||||
/// block height.
|
||||
pub fn stmt_update_expired(&mut self, height: BlockHeight) -> Result<(), SqliteClientError> {
|
||||
self.stmt_update_expired.execute([u32::from(height)])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -111,14 +111,14 @@ impl std::error::Error for WalletMigrationError {
|
|||
// check for unspent transparent outputs whenever running initialization with a version of the
|
||||
// library *not* compiled with the `transparent-inputs` feature flag, and fail if any are present.
|
||||
pub fn init_wallet_db<P: consensus::Parameters + 'static>(
|
||||
wdb: &mut WalletDb<P>,
|
||||
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
||||
seed: Option<SecretVec<u8>>,
|
||||
) -> Result<(), MigratorError<WalletMigrationError>> {
|
||||
init_wallet_db_internal(wdb, seed, &[])
|
||||
}
|
||||
|
||||
fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
|
||||
wdb: &mut WalletDb<P>,
|
||||
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
||||
seed: Option<SecretVec<u8>>,
|
||||
target_migrations: &[Uuid],
|
||||
) -> Result<(), MigratorError<WalletMigrationError>> {
|
||||
|
@ -200,7 +200,7 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
|
|||
/// let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
|
||||
/// let ufvks = HashMap::from([(account, ufvk)]);
|
||||
/// init_accounts_table(&db_data, &ufvks).unwrap();
|
||||
/// init_accounts_table(&mut db_data, &ufvks).unwrap();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
|
@ -208,29 +208,29 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
|
|||
/// [`scan_cached_blocks`]: zcash_client_backend::data_api::chain::scan_cached_blocks
|
||||
/// [`create_spend_to_address`]: zcash_client_backend::data_api::wallet::create_spend_to_address
|
||||
pub fn init_accounts_table<P: consensus::Parameters>(
|
||||
wdb: &WalletDb<P>,
|
||||
wallet_db: &mut WalletDb<rusqlite::Connection, P>,
|
||||
keys: &HashMap<AccountId, UnifiedFullViewingKey>,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let mut empty_check = wdb.conn.prepare("SELECT * FROM accounts LIMIT 1")?;
|
||||
if empty_check.exists([])? {
|
||||
return Err(SqliteClientError::TableNotEmpty);
|
||||
}
|
||||
|
||||
// Ensure that the account identifiers are sequential and begin at zero.
|
||||
if let Some(account_id) = keys.keys().max() {
|
||||
if usize::try_from(u32::from(*account_id)).unwrap() >= keys.len() {
|
||||
return Err(SqliteClientError::AccountIdDiscontinuity);
|
||||
wallet_db.transactionally(|wdb| {
|
||||
let mut empty_check = wdb.conn.0.prepare("SELECT * FROM accounts LIMIT 1")?;
|
||||
if empty_check.exists([])? {
|
||||
return Err(SqliteClientError::TableNotEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert accounts atomically
|
||||
wdb.conn.execute("BEGIN IMMEDIATE", [])?;
|
||||
for (account, key) in keys.iter() {
|
||||
wallet::add_account(wdb, *account, key)?;
|
||||
}
|
||||
wdb.conn.execute("COMMIT", [])?;
|
||||
// Ensure that the account identifiers are sequential and begin at zero.
|
||||
if let Some(account_id) = keys.keys().max() {
|
||||
if usize::try_from(u32::from(*account_id)).unwrap() >= keys.len() {
|
||||
return Err(SqliteClientError::AccountIdDiscontinuity);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Insert accounts atomically
|
||||
for (account, key) in keys.iter() {
|
||||
wallet::add_account(&wdb.conn.0, &wdb.params, *account, key)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialises the data database with the given block.
|
||||
|
@ -262,33 +262,35 @@ pub fn init_accounts_table<P: consensus::Parameters>(
|
|||
/// let sapling_tree = &[];
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
||||
/// init_blocks_table(&db, height, hash, time, sapling_tree);
|
||||
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
||||
/// init_blocks_table(&mut db, height, hash, time, sapling_tree);
|
||||
/// ```
|
||||
pub fn init_blocks_table<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub fn init_blocks_table<P: consensus::Parameters>(
|
||||
wallet_db: &mut WalletDb<rusqlite::Connection, P>,
|
||||
height: BlockHeight,
|
||||
hash: BlockHash,
|
||||
time: u32,
|
||||
sapling_tree: &[u8],
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let mut empty_check = wdb.conn.prepare("SELECT * FROM blocks LIMIT 1")?;
|
||||
if empty_check.exists([])? {
|
||||
return Err(SqliteClientError::TableNotEmpty);
|
||||
}
|
||||
wallet_db.transactionally(|wdb| {
|
||||
let mut empty_check = wdb.conn.0.prepare("SELECT * FROM blocks LIMIT 1")?;
|
||||
if empty_check.exists([])? {
|
||||
return Err(SqliteClientError::TableNotEmpty);
|
||||
}
|
||||
|
||||
wdb.conn.execute(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
wdb.conn.0.execute(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
[
|
||||
u32::from(height).to_sql()?,
|
||||
hash.0.to_sql()?,
|
||||
time.to_sql()?,
|
||||
sapling_tree.to_sql()?,
|
||||
],
|
||||
)?;
|
||||
[
|
||||
u32::from(height).to_sql()?,
|
||||
hash.0.to_sql()?,
|
||||
time.to_sql()?,
|
||||
sapling_tree.to_sql()?,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -606,7 +608,7 @@ mod tests {
|
|||
#[test]
|
||||
fn init_migrate_from_0_3_0() {
|
||||
fn init_0_3_0<P>(
|
||||
wdb: &mut WalletDb<P>,
|
||||
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
account: AccountId,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
|
@ -722,7 +724,7 @@ mod tests {
|
|||
#[test]
|
||||
fn init_migrate_from_autoshielding_poc() {
|
||||
fn init_autoshielding<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
account: AccountId,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
|
@ -878,14 +880,14 @@ mod tests {
|
|||
let extfvk = secret_key.to_extended_full_viewing_key();
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||
init_autoshielding(&db_data, &extfvk, account).unwrap();
|
||||
init_autoshielding(&mut db_data, &extfvk, account).unwrap();
|
||||
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_migrate_from_main_pre_migrations() {
|
||||
fn init_main<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
account: AccountId,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
|
@ -1025,7 +1027,12 @@ mod tests {
|
|||
let secret_key = UnifiedSpendingKey::from_seed(&tests::network(), &seed, account).unwrap();
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||
init_main(&db_data, &secret_key.to_unified_full_viewing_key(), account).unwrap();
|
||||
init_main(
|
||||
&mut db_data,
|
||||
&secret_key.to_unified_full_viewing_key(),
|
||||
account,
|
||||
)
|
||||
.unwrap();
|
||||
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
|
||||
}
|
||||
|
||||
|
@ -1036,8 +1043,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// We can call the function as many times as we want with no data
|
||||
init_accounts_table(&db_data, &HashMap::new()).unwrap();
|
||||
init_accounts_table(&db_data, &HashMap::new()).unwrap();
|
||||
init_accounts_table(&mut db_data, &HashMap::new()).unwrap();
|
||||
init_accounts_table(&mut db_data, &HashMap::new()).unwrap();
|
||||
|
||||
let seed = [0u8; 32];
|
||||
let account = AccountId::from(0);
|
||||
|
@ -1062,11 +1069,11 @@ mod tests {
|
|||
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
|
||||
let ufvks = HashMap::from([(account, ufvk)]);
|
||||
|
||||
init_accounts_table(&db_data, &ufvks).unwrap();
|
||||
init_accounts_table(&mut db_data, &ufvks).unwrap();
|
||||
|
||||
// Subsequent calls should return an error
|
||||
init_accounts_table(&db_data, &HashMap::new()).unwrap_err();
|
||||
init_accounts_table(&db_data, &ufvks).unwrap_err();
|
||||
init_accounts_table(&mut db_data, &HashMap::new()).unwrap_err();
|
||||
init_accounts_table(&mut db_data, &ufvks).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1090,12 +1097,12 @@ mod tests {
|
|||
|
||||
// should fail if we have a gap
|
||||
assert_matches!(
|
||||
init_accounts_table(&db_data, &ufvks(&[0, 2])),
|
||||
init_accounts_table(&mut db_data, &ufvks(&[0, 2])),
|
||||
Err(SqliteClientError::AccountIdDiscontinuity)
|
||||
);
|
||||
|
||||
// should succeed if there are no gaps
|
||||
assert!(init_accounts_table(&db_data, &ufvks(&[0, 1, 2])).is_ok());
|
||||
assert!(init_accounts_table(&mut db_data, &ufvks(&[0, 1, 2])).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1106,7 +1113,7 @@ mod tests {
|
|||
|
||||
// First call with data should initialise the blocks table
|
||||
init_blocks_table(
|
||||
&db_data,
|
||||
&mut db_data,
|
||||
BlockHeight::from(1u32),
|
||||
BlockHash([1; 32]),
|
||||
1,
|
||||
|
@ -1116,7 +1123,7 @@ mod tests {
|
|||
|
||||
// Subsequent calls should return an error
|
||||
init_blocks_table(
|
||||
&db_data,
|
||||
&mut db_data,
|
||||
BlockHeight::from(2u32),
|
||||
BlockHash([2; 32]),
|
||||
2,
|
||||
|
@ -1139,7 +1146,7 @@ mod tests {
|
|||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
let expected_address = ufvk.sapling().unwrap().default_address().1;
|
||||
let ufvks = HashMap::from([(account_id, ufvk)]);
|
||||
init_accounts_table(&db_data, &ufvks).unwrap();
|
||||
init_accounts_table(&mut db_data, &ufvks).unwrap();
|
||||
|
||||
// The account's address should be in the data DB
|
||||
let ua = db_data.get_current_address(AccountId::from(0)).unwrap();
|
||||
|
@ -1153,16 +1160,15 @@ mod tests {
|
|||
let mut db_data = WalletDb::for_path(data_file.path(), Network::MainNetwork).unwrap();
|
||||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = test_vectors::UNIFIED[0].root_seed;
|
||||
let (account, _usk) = ops.create_account(&Secret::new(seed.to_vec())).unwrap();
|
||||
let (account, _usk) = db_data.create_account(&Secret::new(seed.to_vec())).unwrap();
|
||||
assert_eq!(account, AccountId::from(0u32));
|
||||
|
||||
for tv in &test_vectors::UNIFIED[..3] {
|
||||
if let Some(RecipientAddress::Unified(tvua)) =
|
||||
RecipientAddress::decode(&Network::MainNetwork, tv.unified_addr)
|
||||
{
|
||||
let (ua, di) = wallet::get_current_address(&db_data, account)
|
||||
let (ua, di) = wallet::get_current_address(&db_data.conn, &db_data.params, account)
|
||||
.unwrap()
|
||||
.expect("create_account generated the first address");
|
||||
assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
|
||||
|
@ -1170,7 +1176,8 @@ mod tests {
|
|||
assert_eq!(tvua.sapling(), ua.sapling());
|
||||
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
|
||||
|
||||
ops.get_next_available_address(account)
|
||||
db_data
|
||||
.get_next_available_address(account)
|
||||
.unwrap()
|
||||
.expect("get_next_available_address generated an address");
|
||||
} else {
|
||||
|
|
|
@ -67,7 +67,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
while let Some(row) = rows.next()? {
|
||||
let account: u32 = row.get(0)?;
|
||||
let taddrs =
|
||||
get_transparent_receivers(&self._params, transaction, AccountId::from(account))
|
||||
get_transparent_receivers(transaction, &self._params, AccountId::from(account))
|
||||
.map_err(|e| match e {
|
||||
SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
|
||||
SqliteClientError::CorruptedData(s) => {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
//! Functions for Sapling support in the wallet.
|
||||
use group::ff::PrimeField;
|
||||
use rusqlite::{named_params, types::Value, OptionalExtension, Row};
|
||||
use rusqlite::{named_params, params, types::Value, Connection, OptionalExtension, Row};
|
||||
use std::rc::Rc;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight,
|
||||
memo::MemoBytes,
|
||||
merkle_tree::{read_commitment_tree, read_incremental_witness},
|
||||
merkle_tree::{read_commitment_tree, read_incremental_witness, write_incremental_witness},
|
||||
sapling::{self, Diversifier, Note, Nullifier, Rseed},
|
||||
transaction::components::Amount,
|
||||
zip32::AccountId,
|
||||
|
@ -17,7 +17,9 @@ use zcash_client_backend::{
|
|||
DecryptedOutput, TransferType,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb};
|
||||
use crate::{error::SqliteClientError, NoteId};
|
||||
|
||||
use super::memo_repr;
|
||||
|
||||
/// This trait provides a generalization over shielded output representations.
|
||||
pub(crate) trait ReceivedSaplingOutput {
|
||||
|
@ -117,13 +119,13 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<NoteId>, SqliteCli
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_spendable_sapling_notes<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn get_spendable_sapling_notes(
|
||||
conn: &Connection,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[NoteId],
|
||||
) -> Result<Vec<ReceivedSaplingNote<NoteId>>, SqliteClientError> {
|
||||
let mut stmt_select_notes = wdb.conn.prepare(
|
||||
let mut stmt_select_notes = conn.prepare_cached(
|
||||
"SELECT id_note, diversifier, value, rcm, witness
|
||||
FROM sapling_received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
|
||||
|
@ -156,8 +158,8 @@ pub(crate) fn get_spendable_sapling_notes<P>(
|
|||
notes.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
pub(crate) fn select_spendable_sapling_notes<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn select_spendable_sapling_notes(
|
||||
conn: &Connection,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
|
@ -181,7 +183,7 @@ pub(crate) fn select_spendable_sapling_notes<P>(
|
|||
// required value, bringing the sum of all selected notes across the threshold.
|
||||
//
|
||||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let mut stmt_select_notes = wdb.conn.prepare(
|
||||
let mut stmt_select_notes = conn.prepare_cached(
|
||||
"WITH selected AS (
|
||||
WITH eligible AS (
|
||||
SELECT id_note, diversifier, value, rcm,
|
||||
|
@ -189,8 +191,8 @@ pub(crate) fn select_spendable_sapling_notes<P>(
|
|||
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||
FROM sapling_received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
|
||||
WHERE account = :account
|
||||
AND spent IS NULL
|
||||
WHERE account = :account
|
||||
AND spent IS NULL
|
||||
AND transactions.block <= :anchor_height
|
||||
AND id_note NOT IN rarray(:exclude)
|
||||
)
|
||||
|
@ -230,43 +232,42 @@ pub(crate) fn select_spendable_sapling_notes<P>(
|
|||
|
||||
/// Returns the commitment tree for the block at the specified height,
|
||||
/// if any.
|
||||
pub(crate) fn get_sapling_commitment_tree<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn get_sapling_commitment_tree(
|
||||
conn: &Connection,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Option<sapling::CommitmentTree>, SqliteClientError> {
|
||||
wdb.conn
|
||||
.query_row_and_then(
|
||||
"SELECT sapling_tree FROM blocks WHERE height = ?",
|
||||
[u32::from(block_height)],
|
||||
|row| {
|
||||
let row_data: Vec<u8> = row.get(0)?;
|
||||
read_commitment_tree(&row_data[..]).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
row_data.len(),
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(e),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
conn.query_row_and_then(
|
||||
"SELECT sapling_tree FROM blocks WHERE height = ?",
|
||||
[u32::from(block_height)],
|
||||
|row| {
|
||||
let row_data: Vec<u8> = row.get(0)?;
|
||||
read_commitment_tree(&row_data[..]).map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
row_data.len(),
|
||||
rusqlite::types::Type::Blob,
|
||||
Box::new(e),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Returns the incremental witnesses for the block at the specified height,
|
||||
/// if any.
|
||||
pub(crate) fn get_sapling_witnesses<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn get_sapling_witnesses(
|
||||
conn: &Connection,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Vec<(NoteId, sapling::IncrementalWitness)>, SqliteClientError> {
|
||||
let mut stmt_fetch_witnesses = wdb
|
||||
.conn
|
||||
.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
||||
let mut stmt_fetch_witnesses =
|
||||
conn.prepare_cached("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
||||
|
||||
let witnesses = stmt_fetch_witnesses
|
||||
.query_map([u32::from(block_height)], |row| {
|
||||
let id_note = NoteId::ReceivedNoteId(row.get(0)?);
|
||||
let wdb: Vec<u8> = row.get(1)?;
|
||||
Ok(read_incremental_witness(&wdb[..]).map(|witness| (id_note, witness)))
|
||||
let witness_data: Vec<u8> = row.get(1)?;
|
||||
Ok(read_incremental_witness(&witness_data[..]).map(|witness| (id_note, witness)))
|
||||
})
|
||||
.map_err(SqliteClientError::from)?;
|
||||
|
||||
|
@ -277,13 +278,23 @@ pub(crate) fn get_sapling_witnesses<P>(
|
|||
|
||||
/// Records the incremental witness for the specified note,
|
||||
/// as of the given block height.
|
||||
pub(crate) fn insert_witness<'a, P>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
pub(crate) fn insert_witness(
|
||||
conn: &Connection,
|
||||
note_id: i64,
|
||||
witness: &sapling::IncrementalWitness,
|
||||
height: BlockHeight,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
stmts.stmt_insert_witness(NoteId::ReceivedNoteId(note_id), height, witness)
|
||||
let mut stmt_insert_witness = conn.prepare_cached(
|
||||
"INSERT INTO sapling_witnesses (note, block, witness)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
write_incremental_witness(witness, &mut encoded).unwrap();
|
||||
|
||||
stmt_insert_witness.execute(params![note_id, u32::from(height), encoded])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the
|
||||
|
@ -292,11 +303,11 @@ pub(crate) fn insert_witness<'a, P>(
|
|||
/// "Potentially spendable" means:
|
||||
/// - The transaction in which the note was created has been observed as mined.
|
||||
/// - No transaction in which the note's nullifier appears has been observed as mined.
|
||||
pub(crate) fn get_sapling_nullifiers<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn get_sapling_nullifiers(
|
||||
conn: &Connection,
|
||||
) -> Result<Vec<(AccountId, Nullifier)>, SqliteClientError> {
|
||||
// Get the nullifiers for the notes we are tracking
|
||||
let mut stmt_fetch_nullifiers = wdb.conn.prepare(
|
||||
let mut stmt_fetch_nullifiers = conn.prepare(
|
||||
"SELECT rn.id_note, rn.account, rn.nf, tx.block as block
|
||||
FROM sapling_received_notes rn
|
||||
LEFT OUTER JOIN transactions tx
|
||||
|
@ -318,11 +329,11 @@ pub(crate) fn get_sapling_nullifiers<P>(
|
|||
}
|
||||
|
||||
/// Returns the nullifiers for the notes that this wallet is tracking.
|
||||
pub(crate) fn get_all_sapling_nullifiers<P>(
|
||||
wdb: &WalletDb<P>,
|
||||
pub(crate) fn get_all_sapling_nullifiers(
|
||||
conn: &Connection,
|
||||
) -> Result<Vec<(AccountId, Nullifier)>, SqliteClientError> {
|
||||
// Get the nullifiers for the notes we are tracking
|
||||
let mut stmt_fetch_nullifiers = wdb.conn.prepare(
|
||||
let mut stmt_fetch_nullifiers = conn.prepare(
|
||||
"SELECT rn.id_note, rn.account, rn.nf
|
||||
FROM sapling_received_notes rn
|
||||
WHERE nf IS NOT NULL",
|
||||
|
@ -345,13 +356,19 @@ pub(crate) fn get_all_sapling_nullifiers<P>(
|
|||
///
|
||||
/// Marking a note spent in this fashion does NOT imply that the
|
||||
/// spending transaction has been mined.
|
||||
pub(crate) fn mark_sapling_note_spent<'a, P>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
pub(crate) fn mark_sapling_note_spent(
|
||||
conn: &Connection,
|
||||
tx_ref: i64,
|
||||
nf: &Nullifier,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
stmts.stmt_mark_sapling_note_spent(tx_ref, nf)?;
|
||||
Ok(())
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
let mut stmt_mark_sapling_note_spent =
|
||||
conn.prepare_cached("UPDATE sapling_received_notes SET spent = ? WHERE nf = ?")?;
|
||||
|
||||
match stmt_mark_sapling_note_spent.execute(params![tx_ref, &nf.0[..]])? {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => unreachable!("nf column is marked as UNIQUE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the specified shielded output as having been received.
|
||||
|
@ -359,49 +376,48 @@ pub(crate) fn mark_sapling_note_spent<'a, P>(
|
|||
/// This implementation relies on the facts that:
|
||||
/// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
/// - A note value will never exceed 2^63 zatoshis.
|
||||
pub(crate) fn put_received_note<'a, P, T: ReceivedSaplingOutput>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||
conn: &Connection,
|
||||
output: &T,
|
||||
tx_ref: i64,
|
||||
) -> Result<NoteId, SqliteClientError> {
|
||||
let mut stmt_upsert_received_note = conn.prepare_cached(
|
||||
"INSERT INTO sapling_received_notes
|
||||
(tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
|
||||
VALUES
|
||||
(:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)
|
||||
ON CONFLICT (tx, output_index) DO UPDATE
|
||||
SET account = :account,
|
||||
diversifier = :diversifier,
|
||||
value = :value,
|
||||
rcm = :rcm,
|
||||
nf = IFNULL(:nf, nf),
|
||||
memo = IFNULL(:memo, memo),
|
||||
is_change = IFNULL(:is_change, is_change)
|
||||
RETURNING id_note",
|
||||
)?;
|
||||
|
||||
let rcm = output.note().rcm().to_repr();
|
||||
let account = output.account();
|
||||
let to = output.note().recipient();
|
||||
let diversifier = to.diversifier();
|
||||
let value = output.note().value();
|
||||
let memo = output.memo();
|
||||
let is_change = output.is_change();
|
||||
let output_index = output.index();
|
||||
let nf = output.nullifier();
|
||||
|
||||
// First try updating an existing received note into the database.
|
||||
if !stmts.stmt_update_received_note(
|
||||
account,
|
||||
diversifier,
|
||||
value.inner(),
|
||||
rcm,
|
||||
nf,
|
||||
memo,
|
||||
is_change,
|
||||
tx_ref,
|
||||
output_index,
|
||||
)? {
|
||||
// It isn't there, so insert our note into the database.
|
||||
stmts.stmt_insert_received_note(
|
||||
tx_ref,
|
||||
output_index,
|
||||
account,
|
||||
diversifier,
|
||||
value.inner(),
|
||||
rcm,
|
||||
nf,
|
||||
memo,
|
||||
is_change,
|
||||
)
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmts.stmt_select_received_note(tx_ref, output.index())
|
||||
}
|
||||
let sql_args = named_params![
|
||||
":tx": &tx_ref,
|
||||
":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"),
|
||||
":account": u32::from(output.account()),
|
||||
":diversifier": &diversifier.0.as_ref(),
|
||||
":value": output.note().value().inner(),
|
||||
":rcm": &rcm.as_ref(),
|
||||
":nf": output.nullifier().map(|nf| nf.0.as_ref()),
|
||||
":memo": memo_repr(output.memo()),
|
||||
":is_change": output.is_change()
|
||||
];
|
||||
|
||||
stmt_upsert_received_note
|
||||
.query_row(sql_args, |row| {
|
||||
row.get::<_, i64>(0).map(NoteId::ReceivedNoteId)
|
||||
})
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -447,7 +463,7 @@ mod tests {
|
|||
get_balance, get_balance_at,
|
||||
init::{init_blocks_table, init_wallet_db},
|
||||
},
|
||||
AccountId, BlockDb, DataConnStmtCache, WalletDb,
|
||||
AccountId, BlockDb, WalletDb,
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -481,9 +497,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
let to = dfvk.default_address().1.into();
|
||||
|
||||
|
@ -492,10 +507,9 @@ mod tests {
|
|||
let usk1 = UnifiedSpendingKey::from_seed(&network(), &[1u8; 32], acct1).unwrap();
|
||||
|
||||
// Attempting to spend with a USK that is not in the wallet results in an error
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk1,
|
||||
|
@ -516,17 +530,15 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
let to = dfvk.default_address().1.into();
|
||||
|
||||
// We cannot do anything if we aren't synchronised
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -546,7 +558,7 @@ mod tests {
|
|||
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
init_blocks_table(
|
||||
&db_data,
|
||||
&mut db_data,
|
||||
BlockHeight::from(1u32),
|
||||
BlockHash([1; 32]),
|
||||
1,
|
||||
|
@ -555,23 +567,21 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
let to = dfvk.default_address().1.into();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
Amount::zero()
|
||||
);
|
||||
|
||||
// We cannot spend anything
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -600,9 +610,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -615,14 +624,16 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = db_data.get_target_and_anchor_heights(10).unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data.conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
|
@ -635,16 +646,16 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Verified balance does not include the second note
|
||||
let (_, anchor_height2) = db_data.get_target_and_anchor_heights(10).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
(value + value).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data, AccountId::from(0), anchor_height2).unwrap(),
|
||||
get_balance_at(&db_data.conn, AccountId::from(0), anchor_height2).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
|
@ -653,7 +664,7 @@ mod tests {
|
|||
let to = extsk2.default_address().1.into();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -683,12 +694,12 @@ mod tests {
|
|||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -715,12 +726,12 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -745,9 +756,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -760,16 +770,18 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Send some of the funds to another address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().1.into();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -785,7 +797,7 @@ mod tests {
|
|||
// A second spend fails because there are no usable notes
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -814,12 +826,12 @@ mod tests {
|
|||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -845,11 +857,11 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -874,9 +886,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -889,17 +900,19 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let addr2 = extsk2.default_address().1;
|
||||
let to = addr2.into();
|
||||
|
||||
let send_and_recover_with_policy = |db_write: &mut DataConnStmtCache<'_, _>, ovk_policy| {
|
||||
let send_and_recover_with_policy = |db_data: &mut WalletDb<Connection, _>, ovk_policy| {
|
||||
let tx_row = create_spend_to_address(
|
||||
db_write,
|
||||
db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -912,8 +925,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Fetch the transaction from the database
|
||||
let raw_tx: Vec<_> = db_write
|
||||
.wallet_db
|
||||
let raw_tx: Vec<_> = db_data
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT raw FROM transactions
|
||||
|
@ -944,7 +956,7 @@ mod tests {
|
|||
// Send some of the funds to another address, keeping history.
|
||||
// The recipient output is decryptable by the sender.
|
||||
let (_, recovered_to, _) =
|
||||
send_and_recover_with_policy(&mut db_write, OvkPolicy::Sender).unwrap();
|
||||
send_and_recover_with_policy(&mut db_data, OvkPolicy::Sender).unwrap();
|
||||
assert_eq!(&recovered_to, &addr2);
|
||||
|
||||
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 42 (that don't send us funds)
|
||||
|
@ -959,11 +971,11 @@ mod tests {
|
|||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&network, &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&network, &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Send the funds again, discarding history.
|
||||
// Neither transaction output is decryptable by the sender.
|
||||
assert!(send_and_recover_with_policy(&mut db_write, OvkPolicy::Discard).is_none());
|
||||
assert!(send_and_recover_with_policy(&mut db_data, OvkPolicy::Discard).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -977,9 +989,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -992,21 +1003,23 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = db_data.get_target_and_anchor_heights(10).unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data.conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -1031,9 +1044,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet in a single note
|
||||
|
@ -1046,21 +1058,23 @@ mod tests {
|
|||
value,
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = db_data.get_target_and_anchor_heights(10).unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data.conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||
assert_matches!(
|
||||
create_spend_to_address(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&usk,
|
||||
|
@ -1085,9 +1099,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut ops = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (_, usk) = ops.create_account(&seed).unwrap();
|
||||
let (_, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Add funds to the wallet
|
||||
|
@ -1112,15 +1125,17 @@ mod tests {
|
|||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
let total = Amount::from_u64(60000).unwrap();
|
||||
let (_, anchor_height) = db_data.get_target_and_anchor_heights(1).unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), total);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
||||
get_balance(&db_data.conn, AccountId::from(0)).unwrap(),
|
||||
total
|
||||
);
|
||||
assert_eq!(
|
||||
get_balance_at(&db_data.conn, AccountId::from(0), anchor_height).unwrap(),
|
||||
total
|
||||
);
|
||||
|
||||
|
@ -1142,7 +1157,7 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
spend(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&input_selector,
|
||||
|
@ -1170,7 +1185,7 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
spend(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&input_selector,
|
||||
|
@ -1195,9 +1210,8 @@ mod tests {
|
|||
init_wallet_db(&mut db_data, None).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
let mut db_write = db_data.get_update_ops().unwrap();
|
||||
let seed = Secret::new([0u8; 32].to_vec());
|
||||
let (account_id, usk) = db_write.create_account(&seed).unwrap();
|
||||
let (account_id, usk) = db_data.create_account(&seed).unwrap();
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
let uaddr = db_data.get_current_address(account_id).unwrap().unwrap();
|
||||
let taddr = uaddr.transparent().unwrap();
|
||||
|
@ -1212,7 +1226,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let res0 = db_write.put_received_transparent_utxo(&utxo);
|
||||
let res0 = db_data.put_received_transparent_utxo(&utxo);
|
||||
assert!(matches!(res0, Ok(_)));
|
||||
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
|
@ -1229,11 +1243,11 @@ mod tests {
|
|||
Amount::from_u64(50000).unwrap(),
|
||||
);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &mut db_data, None).unwrap();
|
||||
|
||||
assert_matches!(
|
||||
shield_transparent_funds(
|
||||
&mut db_write,
|
||||
&mut db_data,
|
||||
&tests::network(),
|
||||
test_prover(),
|
||||
&input_selector,
|
||||
|
|
Loading…
Reference in New Issue