commit
d9b068195a
|
@ -35,7 +35,7 @@ use zcash_client_backend::{
|
||||||
wallet::OvkPolicy,
|
wallet::OvkPolicy,
|
||||||
zip321,
|
zip321,
|
||||||
};
|
};
|
||||||
use zcash_note_encryption::Domain;
|
use zcash_note_encryption::{Domain, COMPACT_NOTE_SIZE};
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters},
|
consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters},
|
||||||
|
@ -52,7 +52,7 @@ use zcash_primitives::{
|
||||||
Amount,
|
Amount,
|
||||||
},
|
},
|
||||||
fees::FeeRule,
|
fees::FeeRule,
|
||||||
TxId,
|
Transaction, TxId,
|
||||||
},
|
},
|
||||||
zip32::{sapling::DiversifiableFullViewingKey, DiversifierIndex},
|
zip32::{sapling::DiversifiableFullViewingKey, DiversifierIndex},
|
||||||
};
|
};
|
||||||
|
@ -270,6 +270,34 @@ where
|
||||||
(height, res)
|
(height, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a fake block at the expected next height containing only the given
|
||||||
|
/// transaction, and inserts it into the cache.
|
||||||
|
/// This assumes that the transaction only has Sapling spends and outputs.
|
||||||
|
///
|
||||||
|
/// This generated block will be treated as the latest block, and subsequent calls to
|
||||||
|
/// [`Self::generate_next_block`] will build on it.
|
||||||
|
pub(crate) fn generate_next_block_from_tx(
|
||||||
|
&mut self,
|
||||||
|
tx: &Transaction,
|
||||||
|
) -> (BlockHeight, Cache::InsertResult) {
|
||||||
|
let (height, prev_hash, initial_sapling_tree_size) = self
|
||||||
|
.latest_cached_block
|
||||||
|
.map(|(prev_height, prev_hash, end_size)| (prev_height + 1, prev_hash, end_size))
|
||||||
|
.unwrap_or_else(|| (self.sapling_activation_height(), BlockHash([0; 32]), 0));
|
||||||
|
|
||||||
|
let cb = fake_compact_block_from_tx(height, prev_hash, tx, initial_sapling_tree_size);
|
||||||
|
let res = self.cache.insert(&cb);
|
||||||
|
|
||||||
|
self.latest_cached_block = Some((
|
||||||
|
height,
|
||||||
|
cb.hash(),
|
||||||
|
initial_sapling_tree_size
|
||||||
|
+ cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
|
||||||
|
));
|
||||||
|
|
||||||
|
(height, res)
|
||||||
|
}
|
||||||
|
|
||||||
/// Invokes [`scan_cached_blocks`] with the given arguments, expecting success.
|
/// Invokes [`scan_cached_blocks`] with the given arguments, expecting success.
|
||||||
pub(crate) fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) {
|
pub(crate) fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) {
|
||||||
assert_matches!(self.try_scan_cached_blocks(from_height, limit), Ok(_));
|
assert_matches!(self.try_scan_cached_blocks(from_height, limit), Ok(_));
|
||||||
|
@ -295,6 +323,38 @@ where
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the wallet using a new wallet database but with the same cache of blocks,
|
||||||
|
/// and returns the old wallet database file.
|
||||||
|
///
|
||||||
|
/// This does not recreate accounts, nor does it rescan the cached blocks.
|
||||||
|
/// The resulting wallet has no test account.
|
||||||
|
/// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`.
|
||||||
|
pub(crate) fn reset(&mut self) -> NamedTempFile {
|
||||||
|
let network = self.network();
|
||||||
|
self.latest_cached_block = None;
|
||||||
|
let tf = std::mem::replace(&mut self._data_file, NamedTempFile::new().unwrap());
|
||||||
|
self.db_data = WalletDb::for_path(self._data_file.path(), network).unwrap();
|
||||||
|
self.test_account = None;
|
||||||
|
init_wallet_db(&mut self.db_data, None).unwrap();
|
||||||
|
tf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the latest cached block to the most recent one in the cache database.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn reset_latest_cached_block(&mut self) {
|
||||||
|
self.cache
|
||||||
|
.block_source()
|
||||||
|
.with_blocks::<_, Infallible>(None, None, |block: CompactBlock| {
|
||||||
|
self.latest_cached_block = Some((
|
||||||
|
BlockHeight::from_u32(block.height.try_into().unwrap()),
|
||||||
|
BlockHash::from_slice(block.hash.as_slice()),
|
||||||
|
block.chain_metadata.unwrap().sapling_commitment_tree_size,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Cache> TestState<Cache> {
|
impl<Cache> TestState<Cache> {
|
||||||
|
@ -667,6 +727,38 @@ pub(crate) fn fake_compact_block<P: consensus::Parameters>(
|
||||||
(cb, note.nf(&dfvk.fvk().vk.nk, 0))
|
(cb, note.nf(&dfvk.fvk().vk.nk, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a fake CompactBlock at the given height containing only the given transaction.
|
||||||
|
/// This assumes that the transaction only has Sapling spends and outputs.
|
||||||
|
pub(crate) fn fake_compact_block_from_tx(
|
||||||
|
height: BlockHeight,
|
||||||
|
prev_hash: BlockHash,
|
||||||
|
tx: &Transaction,
|
||||||
|
initial_sapling_tree_size: u32,
|
||||||
|
) -> CompactBlock {
|
||||||
|
// Create a fake CompactTx
|
||||||
|
let mut ctx = CompactTx {
|
||||||
|
hash: tx.txid().as_ref().to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(bundle) = tx.sapling_bundle() {
|
||||||
|
for spend in bundle.shielded_spends() {
|
||||||
|
ctx.spends.push(CompactSaplingSpend {
|
||||||
|
nf: spend.nullifier().to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for output in bundle.shielded_outputs() {
|
||||||
|
ctx.outputs.push(CompactSaplingOutput {
|
||||||
|
cmu: output.cmu().to_bytes().to_vec(),
|
||||||
|
ephemeral_key: output.ephemeral_key().0.to_vec(),
|
||||||
|
ciphertext: output.enc_ciphertext()[..COMPACT_NOTE_SIZE].to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a fake CompactBlock at the given height, spending a single note from the
|
/// Create a fake CompactBlock at the given height, spending a single note from the
|
||||||
/// given address.
|
/// given address.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -741,6 +833,16 @@ pub(crate) fn fake_compact_block_spending<P: consensus::Parameters>(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fake_compact_block_from_compact_tx(
|
||||||
|
ctx: CompactTx,
|
||||||
|
height: BlockHeight,
|
||||||
|
prev_hash: BlockHash,
|
||||||
|
initial_sapling_tree_size: u32,
|
||||||
|
) -> CompactBlock {
|
||||||
|
let mut rng = OsRng;
|
||||||
let mut cb = CompactBlock {
|
let mut cb = CompactBlock {
|
||||||
hash: {
|
hash: {
|
||||||
let mut hash = vec![0; 32];
|
let mut hash = vec![0; 32];
|
||||||
|
|
|
@ -434,6 +434,7 @@ pub(crate) mod tests {
|
||||||
use std::{convert::Infallible, num::NonZeroU32};
|
use std::{convert::Infallible, num::NonZeroU32};
|
||||||
|
|
||||||
use incrementalmerkletree::Hashable;
|
use incrementalmerkletree::Hashable;
|
||||||
|
use secrecy::Secret;
|
||||||
use zcash_proofs::prover::LocalTxProver;
|
use zcash_proofs::prover::LocalTxProver;
|
||||||
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
|
@ -464,6 +465,7 @@ pub(crate) mod tests {
|
||||||
error::Error,
|
error::Error,
|
||||||
wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError},
|
wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError},
|
||||||
AccountBirthday, Ratio, ShieldedProtocol, WalletCommitmentTrees, WalletRead,
|
AccountBirthday, Ratio, ShieldedProtocol, WalletCommitmentTrees, WalletRead,
|
||||||
|
WalletWrite,
|
||||||
},
|
},
|
||||||
decrypt_transaction,
|
decrypt_transaction,
|
||||||
fees::{fixed, zip317, DustOutputPolicy},
|
fees::{fixed, zip317, DustOutputPolicy},
|
||||||
|
@ -484,7 +486,7 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use {
|
use {
|
||||||
zcash_client_backend::{data_api::WalletWrite, wallet::WalletTransparentOutput},
|
zcash_client_backend::wallet::WalletTransparentOutput,
|
||||||
zcash_primitives::transaction::components::{OutPoint, TxOut},
|
zcash_primitives::transaction::components::{OutPoint, TxOut},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -512,8 +514,10 @@ pub(crate) mod tests {
|
||||||
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h, 1);
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
// Verified balance matches total balance
|
// Spendable balance matches total balance
|
||||||
assert_eq!(st.get_total_balance(account), value);
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
block_max_scanned(&st.wallet().conn)
|
block_max_scanned(&st.wallet().conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -709,11 +713,16 @@ pub(crate) mod tests {
|
||||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h1, 1);
|
st.scan_cached_blocks(h1, 1);
|
||||||
|
|
||||||
// Verified balance matches total balance
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
assert_eq!(st.get_total_balance(account), value);
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
// Value is considered pending
|
// Value is considered pending at 10 confirmations.
|
||||||
assert_eq!(st.get_pending_shielded_balance(account, 10), value);
|
assert_eq!(st.get_pending_shielded_balance(account, 10), value);
|
||||||
|
assert_eq!(
|
||||||
|
st.get_spendable_balance(account, 10),
|
||||||
|
NonNegativeAmount::ZERO
|
||||||
|
);
|
||||||
|
|
||||||
// Wallet is fully scanned
|
// Wallet is fully scanned
|
||||||
let summary = st.get_wallet_summary(1);
|
let summary = st.get_wallet_summary(1);
|
||||||
|
@ -766,7 +775,10 @@ pub(crate) mod tests {
|
||||||
}
|
}
|
||||||
st.scan_cached_blocks(h2 + 1, 8);
|
st.scan_cached_blocks(h2 + 1, 8);
|
||||||
|
|
||||||
// Second spend still fails
|
// Total balance is value * number of blocks scanned (10).
|
||||||
|
assert_eq!(st.get_total_balance(account), (value * 10).unwrap());
|
||||||
|
|
||||||
|
// Spend still fails
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
st.create_spend_to_address(
|
st.create_spend_to_address(
|
||||||
&usk,
|
&usk,
|
||||||
|
@ -788,17 +800,38 @@ pub(crate) mod tests {
|
||||||
let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h11, 1);
|
st.scan_cached_blocks(h11, 1);
|
||||||
|
|
||||||
// Second spend should now succeed
|
// Total balance is value * number of blocks scanned (11).
|
||||||
assert_matches!(
|
assert_eq!(st.get_total_balance(account), (value * 11).unwrap());
|
||||||
st.create_spend_to_address(
|
// Spendable balance at 10 confirmations is value * 2.
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 10), (value * 2).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
st.get_pending_shielded_balance(account, 10),
|
||||||
|
(value * 9).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spend should now succeed
|
||||||
|
let amount_sent = NonNegativeAmount::from_u64(70000).unwrap();
|
||||||
|
let txid = st
|
||||||
|
.create_spend_to_address(
|
||||||
&usk,
|
&usk,
|
||||||
&to,
|
&to,
|
||||||
Amount::from_u64(70000).unwrap(),
|
amount_sent.into(),
|
||||||
None,
|
None,
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
NonZeroU32::new(10).unwrap(),
|
NonZeroU32::new(10).unwrap(),
|
||||||
),
|
)
|
||||||
Ok(_)
|
.unwrap();
|
||||||
|
let tx = &st.wallet().get_transaction(txid).unwrap();
|
||||||
|
|
||||||
|
let (h, _) = st.generate_next_block_from_tx(tx);
|
||||||
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
|
// TODO: send to an account so that we can check its balance.
|
||||||
|
assert_eq!(
|
||||||
|
st.get_total_balance(account),
|
||||||
|
((value * 11).unwrap()
|
||||||
|
- (amount_sent + NonNegativeAmount::from_u64(10000).unwrap()).unwrap())
|
||||||
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -816,9 +849,12 @@ pub(crate) mod tests {
|
||||||
let value = NonNegativeAmount::from_u64(50000).unwrap();
|
let value = NonNegativeAmount::from_u64(50000).unwrap();
|
||||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h1, 1);
|
st.scan_cached_blocks(h1, 1);
|
||||||
assert_eq!(st.get_total_balance(account), value);
|
|
||||||
|
|
||||||
// Send some of the funds to another address
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
|
// Send some of the funds to another address, but don't mine the tx.
|
||||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
let to = extsk2.default_address().1.into();
|
let to = extsk2.default_address().1.into();
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
|
@ -886,16 +922,33 @@ pub(crate) mod tests {
|
||||||
);
|
);
|
||||||
st.scan_cached_blocks(h43, 1);
|
st.scan_cached_blocks(h43, 1);
|
||||||
|
|
||||||
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
// Second spend should now succeed
|
// Second spend should now succeed
|
||||||
st.create_spend_to_address(
|
let amount_sent2 = NonNegativeAmount::from_u64(2000).unwrap();
|
||||||
&usk,
|
let txid2 = st
|
||||||
&to,
|
.create_spend_to_address(
|
||||||
Amount::from_u64(2000).unwrap(),
|
&usk,
|
||||||
None,
|
&to,
|
||||||
OvkPolicy::Sender,
|
amount_sent2.into(),
|
||||||
NonZeroU32::new(1).unwrap(),
|
None,
|
||||||
)
|
OvkPolicy::Sender,
|
||||||
.unwrap();
|
NonZeroU32::new(1).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tx2 = &st.wallet().get_transaction(txid2).unwrap();
|
||||||
|
|
||||||
|
let (h, _) = st.generate_next_block_from_tx(tx2);
|
||||||
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
|
// TODO: send to an account so that we can check its balance.
|
||||||
|
assert_eq!(
|
||||||
|
st.get_total_balance(account),
|
||||||
|
(value - (amount_sent2 + NonNegativeAmount::from_u64(10000).unwrap()).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -912,7 +965,10 @@ pub(crate) mod tests {
|
||||||
let value = NonNegativeAmount::from_u64(50000).unwrap();
|
let value = NonNegativeAmount::from_u64(50000).unwrap();
|
||||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h1, 1);
|
st.scan_cached_blocks(h1, 1);
|
||||||
|
|
||||||
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
assert_eq!(st.get_total_balance(account), value);
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||||
let addr2 = extsk2.default_address().1;
|
let addr2 = extsk2.default_address().1;
|
||||||
|
@ -1007,16 +1063,15 @@ pub(crate) mod tests {
|
||||||
let dfvk = st.test_account_sapling().unwrap();
|
let dfvk = st.test_account_sapling().unwrap();
|
||||||
|
|
||||||
// Add funds to the wallet in a single note
|
// Add funds to the wallet in a single note
|
||||||
let value = Amount::from_u64(60000).unwrap();
|
let value = NonNegativeAmount::from_u64(60000).unwrap();
|
||||||
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
st.scan_cached_blocks(h, 1);
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
// Verified balance matches total balance
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
assert_eq!(
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
st.get_total_balance(account),
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
NonNegativeAmount::try_from(value).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// TODO: generate_next_block_from_tx does not currently support transparent outputs.
|
||||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
st.create_spend_to_address(
|
st.create_spend_to_address(
|
||||||
|
@ -1042,22 +1097,22 @@ pub(crate) mod tests {
|
||||||
let dfvk = st.test_account_sapling().unwrap();
|
let dfvk = st.test_account_sapling().unwrap();
|
||||||
|
|
||||||
// Add funds to the wallet in a single note owned by the internal spending key
|
// Add funds to the wallet in a single note owned by the internal spending key
|
||||||
let value = Amount::from_u64(60000).unwrap();
|
let value = NonNegativeAmount::from_u64(60000).unwrap();
|
||||||
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::Internal, value);
|
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::Internal, value.into());
|
||||||
st.scan_cached_blocks(h, 1);
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
// Verified balance matches total balance
|
// Spendable balance matches total balance at 1 confirmation.
|
||||||
|
assert_eq!(st.get_total_balance(account), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(account, 1), value);
|
||||||
|
|
||||||
|
// Value is considered pending at 10 confirmations.
|
||||||
|
assert_eq!(st.get_pending_shielded_balance(account, 10), value);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.get_total_balance(account),
|
st.get_spendable_balance(account, 10),
|
||||||
NonNegativeAmount::try_from(value).unwrap()
|
NonNegativeAmount::ZERO
|
||||||
);
|
|
||||||
|
|
||||||
// the balance is considered pending
|
|
||||||
assert_eq!(
|
|
||||||
st.get_pending_shielded_balance(account, 10),
|
|
||||||
NonNegativeAmount::try_from(value).unwrap()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: generate_next_block_from_tx does not currently support transparent outputs.
|
||||||
let to = TransparentAddress::PublicKey([7; 20]).into();
|
let to = TransparentAddress::PublicKey([7; 20]).into();
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
st.create_spend_to_address(
|
st.create_spend_to_address(
|
||||||
|
@ -1072,6 +1127,126 @@ pub(crate) mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_address_change_spends_detected_in_restore_from_seed() {
|
||||||
|
let mut st = TestBuilder::new().with_block_cache().build();
|
||||||
|
|
||||||
|
// Add two accounts to the wallet.
|
||||||
|
let seed = Secret::new([0u8; 32].to_vec());
|
||||||
|
let birthday = AccountBirthday::from_sapling_activation(&st.network());
|
||||||
|
let (_, usk) = st
|
||||||
|
.wallet_mut()
|
||||||
|
.create_account(&seed, birthday.clone())
|
||||||
|
.unwrap();
|
||||||
|
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||||
|
|
||||||
|
let (_, usk2) = st
|
||||||
|
.wallet_mut()
|
||||||
|
.create_account(&seed, birthday.clone())
|
||||||
|
.unwrap();
|
||||||
|
let dfvk2 = usk2.sapling().to_diversifiable_full_viewing_key();
|
||||||
|
|
||||||
|
// Add funds to the wallet in a single note
|
||||||
|
let value = NonNegativeAmount::from_u64(100000).unwrap();
|
||||||
|
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value.into());
|
||||||
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
|
// Spendable balance matches total balance
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||||
|
assert_eq!(st.get_spendable_balance(AccountId::from(0), 1), value);
|
||||||
|
assert_eq!(
|
||||||
|
st.get_total_balance(AccountId::from(1)),
|
||||||
|
NonNegativeAmount::ZERO
|
||||||
|
);
|
||||||
|
|
||||||
|
let amount_sent = NonNegativeAmount::from_u64(20000).unwrap();
|
||||||
|
let amount_legacy_change = NonNegativeAmount::from_u64(30000).unwrap();
|
||||||
|
let addr = dfvk.default_address().1;
|
||||||
|
let addr2 = dfvk2.default_address().1;
|
||||||
|
let req = TransactionRequest::new(vec![
|
||||||
|
// payment to an external recipient
|
||||||
|
Payment {
|
||||||
|
recipient_address: RecipientAddress::Shielded(addr2),
|
||||||
|
amount: amount_sent.into(),
|
||||||
|
memo: None,
|
||||||
|
label: None,
|
||||||
|
message: None,
|
||||||
|
other_params: vec![],
|
||||||
|
},
|
||||||
|
// payment back to the originating wallet, simulating legacy change
|
||||||
|
Payment {
|
||||||
|
recipient_address: RecipientAddress::Shielded(addr),
|
||||||
|
amount: amount_legacy_change.into(),
|
||||||
|
memo: None,
|
||||||
|
label: None,
|
||||||
|
message: None,
|
||||||
|
other_params: vec![],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let fee_rule = FixedFeeRule::standard();
|
||||||
|
let input_selector = GreedyInputSelector::new(
|
||||||
|
fixed::SingleOutputChangeStrategy::new(fee_rule),
|
||||||
|
DustOutputPolicy::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let txid = st
|
||||||
|
.spend(
|
||||||
|
&input_selector,
|
||||||
|
&usk,
|
||||||
|
req,
|
||||||
|
OvkPolicy::Sender,
|
||||||
|
NonZeroU32::new(1).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let tx = &st.wallet().get_transaction(txid).unwrap();
|
||||||
|
|
||||||
|
let amount_left =
|
||||||
|
(value - (amount_sent + fee_rule.fixed_fee().try_into().unwrap()).unwrap()).unwrap();
|
||||||
|
let pending_change = (amount_left - amount_legacy_change).unwrap();
|
||||||
|
|
||||||
|
// The "legacy change" is not counted by get_pending_change().
|
||||||
|
assert_eq!(st.get_pending_change(AccountId::from(0), 1), pending_change);
|
||||||
|
// We spent the only note so we only have pending change.
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(0)), pending_change);
|
||||||
|
|
||||||
|
let (h, _) = st.generate_next_block_from_tx(tx);
|
||||||
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(1)), amount_sent);
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(0)), amount_left);
|
||||||
|
|
||||||
|
st.reset();
|
||||||
|
|
||||||
|
// Account creation and DFVK derivation should be deterministic.
|
||||||
|
let (_, restored_usk) = st
|
||||||
|
.wallet_mut()
|
||||||
|
.create_account(&seed, birthday.clone())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
restored_usk
|
||||||
|
.sapling()
|
||||||
|
.to_diversifiable_full_viewing_key()
|
||||||
|
.to_bytes(),
|
||||||
|
dfvk.to_bytes()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, restored_usk2) = st.wallet_mut().create_account(&seed, birthday).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
restored_usk2
|
||||||
|
.sapling()
|
||||||
|
.to_diversifiable_full_viewing_key()
|
||||||
|
.to_bytes(),
|
||||||
|
dfvk2.to_bytes()
|
||||||
|
);
|
||||||
|
|
||||||
|
st.scan_cached_blocks(st.sapling_activation_height(), 2);
|
||||||
|
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(1)), amount_sent);
|
||||||
|
assert_eq!(st.get_total_balance(AccountId::from(0)), amount_left);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zip317_spend() {
|
fn zip317_spend() {
|
||||||
let mut st = TestBuilder::new()
|
let mut st = TestBuilder::new()
|
||||||
|
@ -1100,12 +1275,10 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
st.scan_cached_blocks(h1, 11);
|
st.scan_cached_blocks(h1, 11);
|
||||||
|
|
||||||
// Verified balance matches total balance
|
// Spendable balance matches total balance
|
||||||
let total = Amount::from_u64(60000).unwrap();
|
let total = NonNegativeAmount::from_u64(60000).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(st.get_total_balance(account), total);
|
||||||
st.get_total_balance(account),
|
assert_eq!(st.get_spendable_balance(account, 1), total);
|
||||||
NonNegativeAmount::try_from(total).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let input_selector = GreedyInputSelector::new(
|
let input_selector = GreedyInputSelector::new(
|
||||||
zip317::SingleOutputChangeStrategy::new(Zip317FeeRule::standard()),
|
zip317::SingleOutputChangeStrategy::new(Zip317FeeRule::standard()),
|
||||||
|
@ -1148,15 +1321,26 @@ pub(crate) mod tests {
|
||||||
}])
|
}])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_matches!(
|
let txid = st
|
||||||
st.spend(
|
.spend(
|
||||||
&input_selector,
|
&input_selector,
|
||||||
&usk,
|
&usk,
|
||||||
req,
|
req,
|
||||||
OvkPolicy::Sender,
|
OvkPolicy::Sender,
|
||||||
NonZeroU32::new(1).unwrap(),
|
NonZeroU32::new(1).unwrap(),
|
||||||
),
|
)
|
||||||
Ok(_)
|
.unwrap();
|
||||||
|
let tx = &st.wallet().get_transaction(txid).unwrap();
|
||||||
|
|
||||||
|
let (h, _) = st.generate_next_block_from_tx(tx);
|
||||||
|
st.scan_cached_blocks(h, 1);
|
||||||
|
|
||||||
|
// TODO: send to an account so that we can check its balance.
|
||||||
|
// We sent back to the same account so the amount_sent should be included
|
||||||
|
// in the total balance.
|
||||||
|
assert_eq!(
|
||||||
|
st.get_total_balance(account),
|
||||||
|
(total - NonNegativeAmount::from_u64(10000).unwrap()).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ and this library adheres to Rust's notion of
|
||||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Trait implementation `Mul<usize>` for `NonNegativeAmount`.
|
||||||
|
|
||||||
## [0.13.0-rc.1] - 2023-09-08
|
## [0.13.0-rc.1] - 2023-09-08
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -306,6 +306,14 @@ impl Sub<NonNegativeAmount> for Option<NonNegativeAmount> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Mul<usize> for NonNegativeAmount {
|
||||||
|
type Output = Option<Self>;
|
||||||
|
|
||||||
|
fn mul(self, rhs: usize) -> Option<NonNegativeAmount> {
|
||||||
|
(self.0 * rhs).map(NonNegativeAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A type for balance violations in amount addition and subtraction
|
/// A type for balance violations in amount addition and subtraction
|
||||||
/// (overflow and underflow of allowed ranges)
|
/// (overflow and underflow of allowed ranges)
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
Loading…
Reference in New Issue