librustzcash/zcash_client_sqlite/src/wallet/transact.rs

819 lines
30 KiB
Rust

//! Functions for creating transactions.
//!
use rusqlite::{named_params, Row};
use std::convert::TryInto;
use ff::PrimeField;
use zcash_primitives::{
consensus::BlockHeight,
merkle_tree::IncrementalWitness,
sapling::{Diversifier, Rseed},
transaction::components::Amount,
zip32::AccountId,
};
use zcash_client_backend::wallet::SpendableNote;
use crate::{error::SqliteClientError, WalletDb};
fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
let diversifier = {
let d: Vec<_> = row.get(0)?;
if d.len() != 11 {
return Err(SqliteClientError::CorruptedData(
"Invalid diversifier length".to_string(),
));
}
let mut tmp = [0; 11];
tmp.copy_from_slice(&d);
Diversifier(tmp)
};
let note_value = Amount::from_i64(row.get(1)?).unwrap();
let rseed = {
let rcm_bytes: Vec<_> = row.get(2)?;
// We store rcm directly in the data DB, regardless of whether the note
// used a v1 or v2 note plaintext, so for the purposes of spending let's
// pretend this is a pre-ZIP 212 note.
let rcm = Option::from(jubjub::Fr::from_repr(
rcm_bytes[..]
.try_into()
.map_err(|_| SqliteClientError::InvalidNote)?,
))
.ok_or(SqliteClientError::InvalidNote)?;
Rseed::BeforeZip212(rcm)
};
let witness = {
let d: Vec<_> = row.get(3)?;
IncrementalWitness::read(&d[..])?
};
Ok(SpendableNote {
diversifier,
note_value,
rseed,
witness,
})
}
#[deprecated(
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletRead::get_spendable_sapling_notes instead."
)]
pub fn get_spendable_sapling_notes<P>(
wdb: &WalletDb<P>,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Vec<SpendableNote>, SqliteClientError> {
let mut stmt_select_notes = wdb.conn.prepare(
"SELECT diversifier, value, rcm, witness
FROM received_notes
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note
WHERE account = :account
AND spent IS NULL
AND transactions.block <= :anchor_height
AND sapling_witnesses.block = :anchor_height",
)?;
// Select notes
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
named_params![
":account": &u32::from(account),
":anchor_height": &u32::from(anchor_height),
],
to_spendable_note,
)?;
notes.collect::<Result<_, _>>()
}
#[deprecated(
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletRead::select_spendable_sapling_notes instead."
)]
pub fn select_spendable_sapling_notes<P>(
wdb: &WalletDb<P>,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
) -> Result<Vec<SpendableNote>, SqliteClientError> {
// The goal of this SQL statement is to select the oldest notes until the required
// value has been reached, and then fetch the witnesses at the desired height for the
// selected notes. This is achieved in several steps:
//
// 1) Use a window function to create a view of all notes, ordered from oldest to
// newest, with an additional column containing a running sum:
// - Unspent notes accumulate the values of all unspent notes in that note's
// account, up to itself.
// - Spent notes accumulate the values of all notes in the transaction they were
// spent in, up to itself.
//
// 2) Select all unspent notes in the desired account, along with their running sum.
//
// 3) Select all notes for which the running sum was less than the required value, as
// well as a single note for which the sum was greater than or equal to the
// 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(
"WITH selected AS (
WITH eligible AS (
SELECT id_note, diversifier, value, rcm,
SUM(value) OVER
(PARTITION BY account, spent ORDER BY id_note) AS so_far
FROM received_notes
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
)
SELECT * FROM eligible WHERE so_far < :target_value
UNION
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1)
), witnesses AS (
SELECT note, witness FROM sapling_witnesses
WHERE block = :anchor_height
)
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
FROM selected
INNER JOIN witnesses ON selected.id_note = witnesses.note",
)?;
// Select notes
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
named_params![
":account": &u32::from(account),
":anchor_height": &u32::from(anchor_height),
":target_value": &i64::from(target_value),
],
to_spendable_note,
)?;
notes.collect::<Result<_, _>>()
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use rusqlite::Connection;
use std::collections::HashMap;
use tempfile::NamedTempFile;
use zcash_proofs::prover::LocalTxProver;
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, BranchId, Parameters},
legacy::TransparentAddress,
sapling::{
keys::DiversifiableFullViewingKey, note_encryption::try_sapling_output_recovery,
prover::TxProver,
},
transaction::{components::Amount, Transaction},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys as transparent;
use zcash_client_backend::{
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead},
keys::{sapling, UnifiedFullViewingKey},
wallet::OvkPolicy,
};
use crate::{
chain::init::init_cache_database,
tests::{self, fake_compact_block, insert_into_cache, network, sapling_activation_height},
wallet::{
get_balance, get_balance_at,
init::{init_accounts_table, init_blocks_table, init_wallet_db},
},
AccountId, BlockDb, DataConnStmtCache, WalletDb,
};
fn test_prover() -> impl TxProver {
match LocalTxProver::with_default_location() {
Some(tx_prover) => tx_prover,
None => {
panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
}
}
}
#[test]
fn create_to_address_fails_on_incorrect_extsk() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let acct0 = AccountId::from(0);
let acct1 = AccountId::from(1);
// Add two accounts to the wallet
let extsk0 = sapling::spending_key(&[0u8; 32], network().coin_type(), acct0);
let extsk1 = sapling::spending_key(&[1u8; 32], network().coin_type(), acct1);
let dfvk0 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk0));
let dfvk1 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk1));
#[cfg(feature = "transparent-inputs")]
let ufvks = {
let tsk0 =
transparent::AccountPrivKey::from_seed(&network(), &[0u8; 32], acct0).unwrap();
let tsk1 =
transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], acct1).unwrap();
HashMap::from([
(
acct0,
UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None)
.unwrap(),
),
(
acct1,
UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None)
.unwrap(),
),
])
};
#[cfg(not(feature = "transparent-inputs"))]
let ufvks = HashMap::from([
(
acct0,
UnifiedFullViewingKey::new(Some(dfvk0), None).unwrap(),
),
(
acct1,
UnifiedFullViewingKey::new(Some(dfvk1), None).unwrap(),
),
]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk0.default_address().1.into();
// Invalid extsk for the given account should cause an error
let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk1,
&to,
Amount::from_u64(1).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
}
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(1),
&extsk0,
&to,
Amount::from_u64(1).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"),
}
}
#[test]
fn create_to_address_fails_with_no_blocks() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk.default_address().1.into();
// We cannot do anything if we aren't synchronised
let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(1).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"),
}
}
#[test]
fn create_to_address_fails_on_insufficient_balance() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
init_blocks_table(
&db_data,
BlockHeight::from(1u32),
BlockHash([1; 32]),
1,
&[],
)
.unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk.default_address().1.into();
// Account balance should be zero
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
Amount::zero()
);
// We cannot spend anything
let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(1).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 1001 including fee)"
),
}
}
#[test]
fn create_to_address_fails_on_unverified_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
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();
// 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(),
value
);
// Add more funds to the wallet in a second note
let (cb, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value);
insert_into_cache(&db_cache, &cb);
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, 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(),
(value + value).unwrap()
);
assert_eq!(
get_balance_at(&db_data, AccountId::from(0), anchor_height2).unwrap(),
value
);
// Spend fails because there are insufficient verified notes
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into();
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 50000, need 71000 including fee)"
),
}
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
// note is verified
for i in 2..10 {
let (cb, _) =
fake_compact_block(sapling_activation_height() + i, cb.hash(), &dfvk, value);
insert_into_cache(&db_cache, &cb);
}
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend still fails
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 50000, need 71000 including fee)"
),
}
// Mine block 11 so that the second note becomes verified
let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value);
insert_into_cache(&db_cache, &cb);
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend should now succeed
create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
OvkPolicy::Sender,
10,
)
.unwrap();
}
#[test]
fn create_to_address_fails_on_locked_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
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);
// Send some of the funds to another address
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into();
create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(15000).unwrap(),
None,
OvkPolicy::Sender,
10,
)
.unwrap();
// A second spend fails because there are no usable notes
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 3000 including fee)"
),
}
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
// until just before the first transaction expires
for i in 1..22 {
let (cb, _) = fake_compact_block(
sapling_activation_height() + i,
cb.hash(),
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
value,
);
insert_into_cache(&db_cache, &cb);
}
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend still fails
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 3000 including fee)"
),
}
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
let (cb, _) = fake_compact_block(
sapling_activation_height() + 22,
cb.hash(),
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])).into(),
value,
);
insert_into_cache(&db_cache, &cb);
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend should now succeed
create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
OvkPolicy::Sender,
10,
)
.unwrap();
}
#[test]
fn ovk_policy_prevents_recovery_from_chain() {
let network = tests::network();
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap();
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
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);
let extsk2 = ExtendedSpendingKey::master(&[]);
let addr2 = extsk2.default_address().1;
let to = addr2.clone().into();
let send_and_recover_with_policy = |db_write: &mut DataConnStmtCache<'_, _>, ovk_policy| {
let tx_row = create_spend_to_address(
db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(15000).unwrap(),
None,
ovk_policy,
10,
)
.unwrap();
// Fetch the transaction from the database
let raw_tx: Vec<_> = db_write
.wallet_db
.conn
.query_row(
"SELECT raw FROM transactions
WHERE id_tx = ?",
&[tx_row],
|row| row.get(0),
)
.unwrap();
let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap();
// Fetch the output index from the database
let output_index: i64 = db_write
.wallet_db
.conn
.query_row(
"SELECT output_index FROM sent_notes
WHERE tx = ?",
&[tx_row],
|row| row.get(0),
)
.unwrap();
let output = &tx.sapling_bundle().unwrap().shielded_outputs[output_index as usize];
try_sapling_output_recovery(
&network,
sapling_activation_height(),
&dfvk.fvk().ovk,
output,
)
};
// 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();
assert_eq!(&recovered_to, &addr2);
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 22 (that don't send us funds)
// so that the first transaction expires
for i in 1..=22 {
let (cb, _) = fake_compact_block(
sapling_activation_height() + i,
cb.hash(),
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
value,
);
insert_into_cache(&db_cache, &cb);
}
scan_cached_blocks(&network, &db_cache, &mut db_write, 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());
}
#[test]
fn create_to_address_succeeds_to_t_addr_zero_change() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note
let value = Amount::from_u64(51000).unwrap();
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
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();
// 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(),
value
);
let to = TransparentAddress::PublicKey([7; 20]).into();
create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(0),
&extsk,
&to,
Amount::from_u64(50000).unwrap(),
None,
OvkPolicy::Sender,
10,
)
.unwrap();
}
}