Merge branch 'main' into 1044-extract-zip32
This commit is contained in:
commit
d332aacf98
|
@ -12,8 +12,16 @@ and this library adheres to Rust's notion of
|
|||
- `BlockMetadata::orchard_tree_size`.
|
||||
- `TransparentInputSource`
|
||||
- `SaplingInputSource`
|
||||
- `ScannedBlock::sapling_tree_size`.
|
||||
- `ScannedBlock::orchard_tree_size`.
|
||||
- `ScannedBlock::{
|
||||
sapling_tree_size, orchard_tree_size, orchard_nullifier_map,
|
||||
orchard_commitments, into_commitments
|
||||
}`
|
||||
- `Balance::{add_spendable_value, add_pending_change_value, add_pending_spendable_value}`
|
||||
- `AccountBalance::{
|
||||
with_sapling_balance_mut,
|
||||
with_orchard_balance_mut,
|
||||
add_unshielded_value
|
||||
}`
|
||||
- `wallet::propose_standard_transfer_to_address`
|
||||
- `wallet::input_selection::Proposal::from_parts`
|
||||
- `wallet::input_selection::SaplingInputs`
|
||||
|
@ -22,8 +30,9 @@ and this library adheres to Rust's notion of
|
|||
functionality and move it behind the `transparent-inputs` feature flag.
|
||||
- `zcash_client_backend::fees::standard`
|
||||
- `zcash_client_backend::wallet`:
|
||||
- `ReceivedSaplingNote::from_parts`
|
||||
- `ReceivedSaplingNote::{txid, output_index, diversifier, rseed, note_commitment_tree_position}`
|
||||
- `WalletNote`
|
||||
- `ReceivedNote`
|
||||
- `WalletSaplingOutput::recipient_key_scope`
|
||||
- `zcash_client_backend::zip321::TransactionRequest::total`
|
||||
- `zcash_client_backend::zip321::parse::Param::name`
|
||||
- `zcash_client_backend::proto::`
|
||||
|
@ -41,17 +50,33 @@ and this library adheres to Rust's notion of
|
|||
wallet::input_selection::{Proposal, SaplingInputs},
|
||||
}`
|
||||
|
||||
### Moved
|
||||
- `zcash_client_backend::data_api::{PoolType, ShieldedProtocol}` have
|
||||
been moved into the `zcash_client_backend` root module.
|
||||
- `zcash_client_backend::data_api::{NoteId, Recipient}` have
|
||||
been moved into the `zcash_client_backend::wallet` module.
|
||||
|
||||
### Changed
|
||||
- `zcash_client_backend::data_api`:
|
||||
- Arguments to `BlockMetadata::from_parts` have changed to include Orchard.
|
||||
- `BlockMetadata::sapling_tree_size` now returns an `Option<u32>` instead of
|
||||
a `u32` for consistency with Orchard.
|
||||
- `WalletShieldedOutput` has an additional type parameter which is used for
|
||||
key scope. `WalletShieldedOutput::from_parts` now takes an additional
|
||||
argument of this type.
|
||||
- `WalletTx` has an additional type parameter as a consequence of the
|
||||
`WalletShieldedOutput` change.
|
||||
- `ScannedBlock` has an additional type parameter as a consequence of the
|
||||
`WalletTx` change.
|
||||
- Arguments to `ScannedBlock::from_parts` have changed.
|
||||
- `ScannedBlock::metadata` has been renamed to `to_block_metadata` and now
|
||||
returns an owned value rather than a reference.
|
||||
- `ShieldedProtocol` has a new variant for `Orchard`, allowing for better
|
||||
reporting to callers trying to perform actions using `Orchard` before it is
|
||||
fully supported.
|
||||
- Fields of `Balance` and `AccountBalance` have been made private and the values
|
||||
of these fields have been made available via methods having the same names
|
||||
as the previously-public fields.
|
||||
- `chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata
|
||||
about the scanned blocks on success.
|
||||
- `error::Error` enum changes:
|
||||
|
@ -157,6 +182,8 @@ and this library adheres to Rust's notion of
|
|||
- `WalletTransparentOutput::value`
|
||||
|
||||
### Removed
|
||||
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
|
||||
`zcash_client_backend::ReceivedNote`.
|
||||
- `zcash_client_backend::data_api::WalletRead::is_valid_account_extfvk` has been
|
||||
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by
|
||||
`get_account_for_ufvk`.
|
||||
|
@ -164,6 +191,9 @@ and this library adheres to Rust's notion of
|
|||
removed without replacement as it was unused, and its functionality will be
|
||||
fully reproduced by `SaplingInputSource::select_spendable_sapling_notes` in a future
|
||||
change.
|
||||
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
|
||||
replaced by `into_commitments` which returns both Sapling and Orchard note commitments
|
||||
and associated note commitment retention information for the block.
|
||||
|
||||
## [0.10.0] - 2023-09-25
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::{self, Debug},
|
||||
fmt::Debug,
|
||||
io,
|
||||
num::{NonZeroU32, TryFromIntError},
|
||||
};
|
||||
|
@ -18,12 +18,12 @@ use zcash_primitives::{
|
|||
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
|
||||
transaction::{
|
||||
components::{
|
||||
amount::{Amount, NonNegativeAmount},
|
||||
amount::{Amount, BalanceError, NonNegativeAmount},
|
||||
OutPoint,
|
||||
},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::AccountId,
|
||||
zip32::{AccountId, Scope},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -31,7 +31,7 @@ use crate::{
|
|||
decrypt::DecryptedOutput,
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::service::TreeState,
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx},
|
||||
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
|
||||
};
|
||||
|
||||
use self::chain::CommitmentTreeRoot;
|
||||
|
@ -58,19 +58,9 @@ pub enum NullifierQuery {
|
|||
/// Balance information for a value within a single pool in an account.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Balance {
|
||||
/// The value in the account that may currently be spent; it is possible to compute witnesses
|
||||
/// for all the notes that comprise this value, and all of this value is confirmed to the
|
||||
/// required confirmation depth.
|
||||
pub spendable_value: NonNegativeAmount,
|
||||
|
||||
/// The value in the account of shielded change notes that do not yet have sufficient
|
||||
/// confirmations to be spendable.
|
||||
pub change_pending_confirmation: NonNegativeAmount,
|
||||
|
||||
/// The value in the account of all remaining received notes that either do not have sufficient
|
||||
/// confirmations to be spendable, or for which witnesses cannot yet be constructed without
|
||||
/// additional scanning.
|
||||
pub value_pending_spendability: NonNegativeAmount,
|
||||
spendable_value: NonNegativeAmount,
|
||||
change_pending_confirmation: NonNegativeAmount,
|
||||
value_pending_spendability: NonNegativeAmount,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
|
@ -81,6 +71,64 @@ impl Balance {
|
|||
value_pending_spendability: NonNegativeAmount::ZERO,
|
||||
};
|
||||
|
||||
fn check_total_adding(
|
||||
&self,
|
||||
value: NonNegativeAmount,
|
||||
) -> Result<NonNegativeAmount, BalanceError> {
|
||||
(self.spendable_value
|
||||
+ self.change_pending_confirmation
|
||||
+ self.value_pending_spendability
|
||||
+ value)
|
||||
.ok_or(BalanceError::Overflow)
|
||||
}
|
||||
|
||||
/// Returns the value in the account that may currently be spent; it is possible to compute
|
||||
/// witnesses for all the notes that comprise this value, and all of this value is confirmed to
|
||||
/// the required confirmation depth.
|
||||
pub fn spendable_value(&self) -> NonNegativeAmount {
|
||||
self.spendable_value
|
||||
}
|
||||
|
||||
/// Adds the specified value to the spendable total, checking for overflow.
|
||||
pub fn add_spendable_value(&mut self, value: NonNegativeAmount) -> Result<(), BalanceError> {
|
||||
self.check_total_adding(value)?;
|
||||
self.spendable_value = (self.spendable_value + value).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the value in the account of shielded change notes that do not yet have sufficient
|
||||
/// confirmations to be spendable.
|
||||
pub fn change_pending_confirmation(&self) -> NonNegativeAmount {
|
||||
self.change_pending_confirmation
|
||||
}
|
||||
|
||||
/// Adds the specified value to the pending change total, checking for overflow.
|
||||
pub fn add_pending_change_value(
|
||||
&mut self,
|
||||
value: NonNegativeAmount,
|
||||
) -> Result<(), BalanceError> {
|
||||
self.check_total_adding(value)?;
|
||||
self.change_pending_confirmation = (self.change_pending_confirmation + value).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the value in the account of all remaining received notes that either do not have
|
||||
/// sufficient confirmations to be spendable, or for which witnesses cannot yet be constructed
|
||||
/// without additional scanning.
|
||||
pub fn value_pending_spendability(&self) -> NonNegativeAmount {
|
||||
self.value_pending_spendability
|
||||
}
|
||||
|
||||
/// Adds the specified value to the pending spendable total, checking for overflow.
|
||||
pub fn add_pending_spendable_value(
|
||||
&mut self,
|
||||
value: NonNegativeAmount,
|
||||
) -> Result<(), BalanceError> {
|
||||
self.check_total_adding(value)?;
|
||||
self.value_pending_spendability = (self.value_pending_spendability + value).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the total value of funds represented by this [`Balance`].
|
||||
pub fn total(&self) -> NonNegativeAmount {
|
||||
(self.spendable_value + self.change_pending_confirmation + self.value_pending_spendability)
|
||||
|
@ -93,7 +141,10 @@ impl Balance {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AccountBalance {
|
||||
/// The value of unspent Sapling outputs belonging to the account.
|
||||
pub sapling_balance: Balance,
|
||||
sapling_balance: Balance,
|
||||
|
||||
/// The value of unspent Orchard outputs belonging to the account.
|
||||
orchard_balance: Balance,
|
||||
|
||||
/// The value of all unspent transparent outputs belonging to the account, irrespective of
|
||||
/// confirmation depth.
|
||||
|
@ -102,19 +153,95 @@ pub struct AccountBalance {
|
|||
/// possible operation on a transparent balance is to shield it, it is possible to create a
|
||||
/// zero-conf transaction to perform that shielding, and the resulting shielded notes will be
|
||||
/// subject to normal confirmation rules.
|
||||
pub unshielded: NonNegativeAmount,
|
||||
unshielded: NonNegativeAmount,
|
||||
}
|
||||
|
||||
impl AccountBalance {
|
||||
/// The [`Balance`] value having zero values for all its fields.
|
||||
pub const ZERO: Self = Self {
|
||||
sapling_balance: Balance::ZERO,
|
||||
orchard_balance: Balance::ZERO,
|
||||
unshielded: NonNegativeAmount::ZERO,
|
||||
};
|
||||
|
||||
fn check_total(&self) -> Result<NonNegativeAmount, BalanceError> {
|
||||
(self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded)
|
||||
.ok_or(BalanceError::Overflow)
|
||||
}
|
||||
|
||||
/// Returns the [`Balance`] of Sapling funds in the account.
|
||||
pub fn sapling_balance(&self) -> &Balance {
|
||||
&self.sapling_balance
|
||||
}
|
||||
|
||||
/// Provides a `mutable reference to the [`Balance`] of Sapling funds in the account
|
||||
/// to the specified callback, checking invariants after the callback's action has been
|
||||
/// evaluated.
|
||||
pub fn with_sapling_balance_mut<A, E: From<BalanceError>>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut Balance) -> Result<A, E>,
|
||||
) -> Result<A, E> {
|
||||
let result = f(&mut self.sapling_balance)?;
|
||||
self.check_total()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns the [`Balance`] of Orchard funds in the account.
|
||||
pub fn orchard_balance(&self) -> &Balance {
|
||||
&self.orchard_balance
|
||||
}
|
||||
|
||||
/// Provides a `mutable reference to the [`Balance`] of Orchard funds in the account
|
||||
/// to the specified callback, checking invariants after the callback's action has been
|
||||
/// evaluated.
|
||||
pub fn with_orchard_balance_mut<A, E: From<BalanceError>>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut Balance) -> Result<A, E>,
|
||||
) -> Result<A, E> {
|
||||
let result = f(&mut self.orchard_balance)?;
|
||||
self.check_total()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns the total value of unspent transparent transaction outputs belonging to the wallet.
|
||||
pub fn unshielded(&self) -> NonNegativeAmount {
|
||||
self.unshielded
|
||||
}
|
||||
|
||||
/// Adds the specified value to the unshielded total, checking for overflow of
|
||||
/// the total account balance.
|
||||
pub fn add_unshielded_value(&mut self, value: NonNegativeAmount) -> Result<(), BalanceError> {
|
||||
self.unshielded = (self.unshielded + value).ok_or(BalanceError::Overflow)?;
|
||||
self.check_total()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the total value of funds belonging to the account.
|
||||
pub fn total(&self) -> NonNegativeAmount {
|
||||
(self.sapling_balance.total() + self.unshielded)
|
||||
(self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded)
|
||||
.expect("Account balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
|
||||
/// Returns the total value of shielded (Sapling and Orchard) funds that may immediately be
|
||||
/// spent.
|
||||
pub fn spendable_value(&self) -> NonNegativeAmount {
|
||||
(self.sapling_balance.spendable_value + self.orchard_balance.spendable_value)
|
||||
.expect("Account balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
|
||||
/// Returns the total value of change and/or shielding transaction outputs that are awaiting
|
||||
/// sufficient confirmations for spendability.
|
||||
pub fn change_pending_confirmation(&self) -> NonNegativeAmount {
|
||||
(self.sapling_balance.change_pending_confirmation
|
||||
+ self.orchard_balance.change_pending_confirmation)
|
||||
.expect("Account balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
|
||||
/// Returns the value of shielded funds that are not yet spendable because additional scanning
|
||||
/// is required before it will be possible to derive witnesses for the associated notes.
|
||||
pub fn value_pending_spendability(&self) -> NonNegativeAmount {
|
||||
(self.sapling_balance.value_pending_spendability
|
||||
+ self.orchard_balance.value_pending_spendability)
|
||||
.expect("Account balance cannot overflow MAX_MONEY")
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +358,7 @@ pub trait SaplingInputSource {
|
|||
&self,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error>;
|
||||
|
||||
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
|
||||
/// if possible.
|
||||
|
@ -241,7 +368,7 @@ pub trait SaplingInputSource {
|
|||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error>;
|
||||
}
|
||||
|
||||
/// A trait representing the capability to query a data store for unspent transparent UTXOs
|
||||
|
@ -475,18 +602,20 @@ impl BlockMetadata {
|
|||
/// decrypted and extracted from a [`CompactBlock`].
|
||||
///
|
||||
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
|
||||
pub struct ScannedBlock<Nf> {
|
||||
pub struct ScannedBlock<Nf, S> {
|
||||
block_height: BlockHeight,
|
||||
block_hash: BlockHash,
|
||||
block_time: u32,
|
||||
sapling_tree_size: u32,
|
||||
orchard_tree_size: u32,
|
||||
transactions: Vec<WalletTx<Nf>>,
|
||||
transactions: Vec<WalletTx<Nf, S>>,
|
||||
sapling_nullifier_map: Vec<(TxId, u16, Vec<sapling::Nullifier>)>,
|
||||
sapling_commitments: Vec<(sapling::Node, Retention<BlockHeight>)>,
|
||||
orchard_nullifier_map: Vec<(TxId, u16, Vec<orchard::note::Nullifier>)>,
|
||||
orchard_commitments: Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
|
||||
}
|
||||
|
||||
impl<Nf> ScannedBlock<Nf> {
|
||||
impl<Nf, S> ScannedBlock<Nf, S> {
|
||||
/// Constructs a new `ScannedBlock`
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_parts(
|
||||
|
@ -495,9 +624,11 @@ impl<Nf> ScannedBlock<Nf> {
|
|||
block_time: u32,
|
||||
sapling_tree_size: u32,
|
||||
orchard_tree_size: u32,
|
||||
transactions: Vec<WalletTx<Nf>>,
|
||||
transactions: Vec<WalletTx<Nf, S>>,
|
||||
sapling_nullifier_map: Vec<(TxId, u16, Vec<sapling::Nullifier>)>,
|
||||
sapling_commitments: Vec<(sapling::Node, Retention<BlockHeight>)>,
|
||||
orchard_nullifier_map: Vec<(TxId, u16, Vec<orchard::note::Nullifier>)>,
|
||||
orchard_commitments: Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
block_height,
|
||||
|
@ -508,6 +639,8 @@ impl<Nf> ScannedBlock<Nf> {
|
|||
transactions,
|
||||
sapling_nullifier_map,
|
||||
sapling_commitments,
|
||||
orchard_nullifier_map,
|
||||
orchard_commitments,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -537,7 +670,7 @@ impl<Nf> ScannedBlock<Nf> {
|
|||
}
|
||||
|
||||
/// Returns the list of transactions from the block that are relevant to the wallet.
|
||||
pub fn transactions(&self) -> &[WalletTx<Nf>] {
|
||||
pub fn transactions(&self) -> &[WalletTx<Nf, S>] {
|
||||
&self.transactions
|
||||
}
|
||||
|
||||
|
@ -557,10 +690,34 @@ impl<Nf> ScannedBlock<Nf> {
|
|||
&self.sapling_commitments
|
||||
}
|
||||
|
||||
/// Consumes `self` and returns the list of Sapling note commitments associated with the
|
||||
/// scanned block as an owned value.
|
||||
pub fn into_sapling_commitments(self) -> Vec<(sapling::Node, Retention<BlockHeight>)> {
|
||||
self.sapling_commitments
|
||||
/// Returns the vector of Orchard nullifiers for each transaction in the block.
|
||||
///
|
||||
/// The returned tuple is keyed by both transaction ID and the index of the transaction within
|
||||
/// the block, so that either the txid or the combination of the block hash available from
|
||||
/// [`Self::block_hash`] and returned transaction index may be used to uniquely identify the
|
||||
/// transaction, depending upon the needs of the caller.
|
||||
pub fn orchard_nullifier_map(&self) -> &[(TxId, u16, Vec<orchard::note::Nullifier>)] {
|
||||
&self.orchard_nullifier_map
|
||||
}
|
||||
|
||||
/// Returns the ordered list of Orchard note commitments to be added to the note commitment
|
||||
/// tree.
|
||||
pub fn orchard_commitments(
|
||||
&self,
|
||||
) -> &[(orchard::note::NoteCommitment, Retention<BlockHeight>)] {
|
||||
&self.orchard_commitments
|
||||
}
|
||||
|
||||
/// Consumes `self` and returns the lists of Sapling and Orchard note commitments associated
|
||||
/// with the scanned block as an owned value.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn into_commitments(
|
||||
self,
|
||||
) -> (
|
||||
Vec<(sapling::Node, Retention<BlockHeight>)>,
|
||||
Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
|
||||
) {
|
||||
(self.sapling_commitments, self.orchard_commitments)
|
||||
}
|
||||
|
||||
/// Returns the [`BlockMetadata`] corresponding to the scanned block.
|
||||
|
@ -599,81 +756,6 @@ pub struct SentTransaction<'a> {
|
|||
pub utxos_spent: Vec<OutPoint>,
|
||||
}
|
||||
|
||||
/// A shielded transfer protocol supported by the wallet.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ShieldedProtocol {
|
||||
/// The Sapling protocol
|
||||
Sapling,
|
||||
/// The Orchard protocol
|
||||
Orchard,
|
||||
}
|
||||
|
||||
/// A unique identifier for a shielded transaction output
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NoteId {
|
||||
txid: TxId,
|
||||
protocol: ShieldedProtocol,
|
||||
output_index: u16,
|
||||
}
|
||||
|
||||
impl NoteId {
|
||||
/// Constructs a new `NoteId` from its parts.
|
||||
pub fn new(txid: TxId, protocol: ShieldedProtocol, output_index: u16) -> Self {
|
||||
Self {
|
||||
txid,
|
||||
protocol,
|
||||
output_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ID of the transaction containing this note.
|
||||
pub fn txid(&self) -> &TxId {
|
||||
&self.txid
|
||||
}
|
||||
|
||||
/// Returns the shielded protocol used by this note.
|
||||
pub fn protocol(&self) -> ShieldedProtocol {
|
||||
self.protocol
|
||||
}
|
||||
|
||||
/// Returns the index of this note within its transaction's corresponding list of
|
||||
/// shielded outputs.
|
||||
pub fn output_index(&self) -> u16 {
|
||||
self.output_index
|
||||
}
|
||||
}
|
||||
|
||||
/// A value pool to which the wallet supports sending transaction outputs.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PoolType {
|
||||
/// The transparent value pool
|
||||
Transparent,
|
||||
/// A shielded value pool.
|
||||
Shielded(ShieldedProtocol),
|
||||
}
|
||||
|
||||
impl fmt::Display for PoolType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PoolType::Transparent => f.write_str("Transparent"),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that represents the recipient of a transaction output; a recipient address (and, for
|
||||
/// unified addresses, the pool to which the payment is sent) in the case of outgoing output, or an
|
||||
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
|
||||
/// output.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Recipient {
|
||||
Transparent(TransparentAddress),
|
||||
Sapling(sapling::PaymentAddress),
|
||||
Unified(UnifiedAddress, PoolType),
|
||||
InternalAccount(AccountId, PoolType),
|
||||
}
|
||||
|
||||
/// A type that represents an output (either Sapling or transparent) that was sent by the wallet.
|
||||
pub struct SentTransactionOutput {
|
||||
output_index: usize,
|
||||
|
@ -899,7 +981,7 @@ pub trait WalletWrite: WalletRead {
|
|||
/// `blocks` must be sequential, in order of increasing block height
|
||||
fn put_blocks(
|
||||
&mut self,
|
||||
blocks: Vec<ScannedBlock<sapling::Nullifier>>,
|
||||
blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Updates the wallet's view of the blockchain.
|
||||
|
@ -986,20 +1068,19 @@ pub mod testing {
|
|||
memo::Memo,
|
||||
sapling,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
zip32::AccountId,
|
||||
zip32::{AccountId, Scope},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::{AddressMetadata, UnifiedAddress},
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
wallet::{NoteId, ReceivedNote, WalletTransparentOutput},
|
||||
};
|
||||
|
||||
use super::{
|
||||
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
|
||||
DecryptedTransaction, NoteId, NullifierQuery, SaplingInputSource, ScannedBlock,
|
||||
SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
DecryptedTransaction, NullifierQuery, SaplingInputSource, ScannedBlock, SentTransaction,
|
||||
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
};
|
||||
|
||||
pub struct MockWalletDb {
|
||||
|
@ -1031,7 +1112,7 @@ pub mod testing {
|
|||
&self,
|
||||
_txid: &TxId,
|
||||
_index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
@ -1041,7 +1122,7 @@ pub mod testing {
|
|||
_target_value: Amount,
|
||||
_anchor_height: BlockHeight,
|
||||
_exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
@ -1209,7 +1290,7 @@ pub mod testing {
|
|||
#[allow(clippy::type_complexity)]
|
||||
fn put_blocks(
|
||||
&mut self,
|
||||
_blocks: Vec<ScannedBlock<sapling::Nullifier>>,
|
||||
_blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use crate::data_api::wallet::input_selection::InputSelectorError;
|
||||
use crate::data_api::PoolType;
|
||||
use crate::PoolType;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex};
|
||||
|
||||
use super::NoteId;
|
||||
use crate::wallet::NoteId;
|
||||
|
||||
/// Errors that can occur as a consequence of wallet operations.
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
consensus::{self, NetworkUpgrade},
|
||||
memo::MemoBytes,
|
||||
sapling::{
|
||||
self,
|
||||
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
|
||||
prover::{OutputProver, SpendProver},
|
||||
zip32::DiversifiableFullViewingKey,
|
||||
Node,
|
||||
},
|
||||
transaction::{
|
||||
builder::Builder,
|
||||
|
@ -24,15 +19,15 @@ use zcash_primitives::{
|
|||
use crate::{
|
||||
address::RecipientAddress,
|
||||
data_api::{
|
||||
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, PoolType, Recipient,
|
||||
SentTransaction, SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, SentTransaction,
|
||||
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{self, ChangeValue, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
wallet::{OvkPolicy, ReceivedSaplingNote},
|
||||
wallet::{OvkPolicy, Recipient, WalletNote},
|
||||
zip321::{self, Payment},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
pub mod input_selection;
|
||||
|
@ -40,7 +35,7 @@ use input_selection::{
|
|||
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
|
||||
};
|
||||
|
||||
use super::{NoteId, SaplingInputSource, ShieldedProtocol};
|
||||
use super::SaplingInputSource;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
|
@ -590,26 +585,26 @@ where
|
|||
if let Some(sapling_inputs) = proposal.sapling_inputs() {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
|
||||
for selected in sapling_inputs.notes() {
|
||||
let (note, scope, merkle_path) = select_key_for_note(
|
||||
sapling_tree,
|
||||
selected,
|
||||
&dfvk,
|
||||
sapling_inputs.anchor_height(),
|
||||
)?
|
||||
.ok_or_else(|| {
|
||||
Error::NoteMismatch(NoteId {
|
||||
txid: *selected.txid(),
|
||||
protocol: ShieldedProtocol::Sapling,
|
||||
output_index: selected.output_index(),
|
||||
})
|
||||
})?;
|
||||
match selected.note() {
|
||||
WalletNote::Sapling(note) => {
|
||||
let key = match selected.spending_key_scope() {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
|
||||
let key = match scope {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&sapling_inputs.anchor_height(),
|
||||
)?;
|
||||
|
||||
builder.add_sapling_spend(key, note, merkle_path)?;
|
||||
builder.add_sapling_spend(key, note.clone(), merkle_path)?;
|
||||
}
|
||||
WalletNote::Orchard(_) => {
|
||||
// FIXME: Implement this once `Proposal` has been refactored to
|
||||
// include Orchard notes.
|
||||
panic!("Orchard spends are not yet supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
@ -880,40 +875,3 @@ where
|
|||
&proposal,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn select_key_for_note<N, S: ShardStore<H = Node, CheckpointId = BlockHeight>>(
|
||||
commitment_tree: &mut ShardTree<
|
||||
S,
|
||||
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
>,
|
||||
selected: &ReceivedSaplingNote<N>,
|
||||
dfvk: &DiversifiableFullViewingKey,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Option<(sapling::Note, Scope, sapling::MerklePath)>, ShardTreeError<S::Error>> {
|
||||
// Attempt to reconstruct the note being spent using both the internal and external dfvks
|
||||
// corresponding to the unified spending key, checking against the witness we are using
|
||||
// to spend the note that we've used the correct key.
|
||||
let external_note = dfvk
|
||||
.diversified_address(selected.diversifier())
|
||||
.map(|addr| addr.create_note(selected.value().try_into().unwrap(), selected.rseed()));
|
||||
let internal_note = dfvk
|
||||
.diversified_change_address(selected.diversifier())
|
||||
.map(|addr| addr.create_note(selected.value().try_into().unwrap(), selected.rseed()));
|
||||
|
||||
let expected_root = commitment_tree.root_at_checkpoint_id(&anchor_height)?;
|
||||
let merkle_path = commitment_tree.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&anchor_height,
|
||||
)?;
|
||||
|
||||
Ok(external_note
|
||||
.filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu())))
|
||||
.map(|n| (n, Scope::External, merkle_path.clone()))
|
||||
.or_else(|| {
|
||||
internal_note
|
||||
.filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu())))
|
||||
.map(|n| (n, Scope::Internal, merkle_path))
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ use crate::{
|
|||
address::{RecipientAddress, UnifiedAddress},
|
||||
data_api::SaplingInputSource,
|
||||
fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
wallet::{ReceivedNote, WalletTransparentOutput},
|
||||
zip321::TransactionRequest,
|
||||
};
|
||||
|
||||
|
@ -263,15 +263,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
|||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SaplingInputs<NoteRef> {
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef>>,
|
||||
}
|
||||
|
||||
impl<NoteRef> SaplingInputs<NoteRef> {
|
||||
/// Constructs a [`SaplingInputs`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
|
||||
) -> Self {
|
||||
pub fn from_parts(anchor_height: BlockHeight, notes: NonEmpty<ReceivedNote<NoteRef>>) -> Self {
|
||||
Self {
|
||||
anchor_height,
|
||||
notes,
|
||||
|
@ -285,7 +282,7 @@ impl<NoteRef> SaplingInputs<NoteRef> {
|
|||
}
|
||||
|
||||
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedSaplingNote<NoteRef>> {
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef>> {
|
||||
&self.notes
|
||||
}
|
||||
}
|
||||
|
@ -534,7 +531,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let mut sapling_inputs: Vec<ReceivedSaplingNote<DbT::NoteRef>> = vec![];
|
||||
let mut sapling_inputs: Vec<ReceivedNote<DbT::NoteRef>> = vec![];
|
||||
let mut prior_available = NonNegativeAmount::ZERO;
|
||||
let mut amount_required = NonNegativeAmount::ZERO;
|
||||
let mut exclude: Vec<DbT::NoteRef> = vec![];
|
||||
|
@ -654,7 +651,7 @@ where
|
|||
target_height,
|
||||
&transparent_inputs,
|
||||
&Vec::<TxOut>::new(),
|
||||
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
|
||||
&Vec::<ReceivedNote<Infallible>>::new(),
|
||||
&Vec::<SaplingPayment>::new(),
|
||||
&self.dust_output_policy,
|
||||
);
|
||||
|
@ -670,7 +667,7 @@ where
|
|||
target_height,
|
||||
&transparent_inputs,
|
||||
&Vec::<TxOut>::new(),
|
||||
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
|
||||
&Vec::<ReceivedNote<Infallible>>::new(),
|
||||
&Vec::<SaplingPayment>::new(),
|
||||
&self.dust_output_policy,
|
||||
)?
|
||||
|
|
|
@ -23,8 +23,38 @@ pub mod zip321;
|
|||
#[cfg(feature = "unstable-serialization")]
|
||||
pub mod serialization;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType};
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate assert_matches;
|
||||
|
||||
/// A shielded transfer protocol supported by the wallet.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ShieldedProtocol {
|
||||
/// The Sapling protocol
|
||||
Sapling,
|
||||
/// The Orchard protocol
|
||||
Orchard,
|
||||
}
|
||||
|
||||
/// A value pool to which the wallet supports sending transaction outputs.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PoolType {
|
||||
/// The transparent value pool
|
||||
Transparent,
|
||||
/// A shielded value pool.
|
||||
Shielded(ShieldedProtocol),
|
||||
}
|
||||
|
||||
impl fmt::Display for PoolType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PoolType::Transparent => f.write_str("Transparent"),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,11 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
|||
use crate::{
|
||||
data_api::{
|
||||
wallet::input_selection::{Proposal, ProposalError, SaplingInputs},
|
||||
PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
|
||||
SaplingInputSource, TransparentInputSource,
|
||||
},
|
||||
fees::{ChangeValue, TransactionBalance},
|
||||
zip321::{TransactionRequest, Zip321Error},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
|
|
|
@ -19,11 +19,12 @@ use zcash_primitives::{
|
|||
zip32::{AccountId, Scope},
|
||||
};
|
||||
|
||||
use crate::data_api::{BlockMetadata, ScannedBlock, ShieldedProtocol};
|
||||
use crate::data_api::{BlockMetadata, ScannedBlock};
|
||||
use crate::{
|
||||
proto::compact_formats::CompactBlock,
|
||||
scan::{Batch, BatchRunner, Tasks},
|
||||
wallet::{WalletSaplingOutput, WalletSaplingSpend, WalletTx},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
/// A key that can be used to perform trial decryption and nullifier
|
||||
|
@ -257,7 +258,7 @@ pub fn scan_block<P: consensus::Parameters + Send + 'static, K: ScanningKey>(
|
|||
vks: &[(&AccountId, &K)],
|
||||
sapling_nullifiers: &[(AccountId, sapling::Nullifier)],
|
||||
prior_block_metadata: Option<&BlockMetadata>,
|
||||
) -> Result<ScannedBlock<K::Nf>, ScanError> {
|
||||
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
|
||||
scan_block_with_runner::<_, _, ()>(
|
||||
params,
|
||||
block,
|
||||
|
@ -340,7 +341,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
nullifiers: &[(AccountId, sapling::Nullifier)],
|
||||
prior_block_metadata: Option<&BlockMetadata>,
|
||||
mut batch_runner: Option<&mut TaggedBatchRunner<K::Scope, T>>,
|
||||
) -> Result<ScannedBlock<K::Nf>, ScanError> {
|
||||
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
|
||||
if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) {
|
||||
return Err(scan_error);
|
||||
}
|
||||
|
@ -440,7 +441,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
)?;
|
||||
|
||||
let compact_block_tx_count = block.vtx.len();
|
||||
let mut wtxs: Vec<WalletTx<K::Nf>> = vec![];
|
||||
let mut wtxs: Vec<WalletTx<K::Nf, K::Scope>> = vec![];
|
||||
let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len());
|
||||
let mut sapling_note_commitments: Vec<(sapling::Node, Retention<BlockHeight>)> = vec![];
|
||||
for (tx_idx, tx) in block.vtx.into_iter().enumerate() {
|
||||
|
@ -493,7 +494,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
u32::try_from(tx.actions.len()).expect("Orchard action count cannot exceed a u32");
|
||||
|
||||
// Check for incoming notes while incrementing tree and witnesses
|
||||
let mut shielded_outputs: Vec<WalletSaplingOutput<K::Nf>> = vec![];
|
||||
let mut shielded_outputs: Vec<WalletSaplingOutput<K::Nf, K::Scope>> = vec![];
|
||||
{
|
||||
let decoded = &tx
|
||||
.outputs
|
||||
|
@ -526,7 +527,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
"The batch runner and scan_block must use the same set of IVKs.",
|
||||
);
|
||||
|
||||
(d_note.note, a, (*nk).clone())
|
||||
(d_note.note, a, d_note.ivk_tag.1, (*nk).clone())
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
@ -536,13 +537,13 @@ pub(crate) fn scan_block_with_runner<
|
|||
.flat_map(|(a, k)| {
|
||||
k.to_sapling_keys()
|
||||
.into_iter()
|
||||
.map(move |(_, ivk, nk)| (**a, ivk, nk))
|
||||
.map(move |(scope, ivk, nk)| (**a, scope, ivk, nk))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let ivks = vks
|
||||
.iter()
|
||||
.map(|(_, ivk, _)| ivk)
|
||||
.map(|(_, _, ivk, _)| ivk)
|
||||
.map(PreparedIncomingViewingKey::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -550,8 +551,8 @@ pub(crate) fn scan_block_with_runner<
|
|||
.into_iter()
|
||||
.map(|v| {
|
||||
v.map(|((note, _), ivk_idx)| {
|
||||
let (account, _, nk) = &vks[ivk_idx];
|
||||
(note, *account, (*nk).clone())
|
||||
let (account, scope, _, nk) = &vks[ivk_idx];
|
||||
(note, *account, scope.clone(), (*nk).clone())
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
@ -572,7 +573,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
(false, false) => Retention::Ephemeral,
|
||||
};
|
||||
|
||||
if let Some((note, account, nk)) = dec_output {
|
||||
if let Some((note, account, scope, nk)) = dec_output {
|
||||
// A note is marked as "change" if the account that received it
|
||||
// also spent notes in the same transaction. This will catch,
|
||||
// for instance:
|
||||
|
@ -594,6 +595,7 @@ pub(crate) fn scan_block_with_runner<
|
|||
is_change,
|
||||
note_commitment_tree_position,
|
||||
nf,
|
||||
scope,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -643,6 +645,8 @@ pub(crate) fn scan_block_with_runner<
|
|||
wtxs,
|
||||
sapling_nullifier_map,
|
||||
sapling_note_commitments,
|
||||
vec![], // FIXME: collect the Orchard nullifiers
|
||||
vec![], // FIXME: collect the Orchard note commitments
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -15,17 +15,66 @@ use zcash_primitives::{
|
|||
},
|
||||
TxId,
|
||||
},
|
||||
zip32::AccountId,
|
||||
zip32::{AccountId, Scope},
|
||||
};
|
||||
|
||||
use crate::{address::UnifiedAddress, PoolType, ShieldedProtocol};
|
||||
|
||||
/// A unique identifier for a shielded transaction output
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NoteId {
|
||||
txid: TxId,
|
||||
protocol: ShieldedProtocol,
|
||||
output_index: u16,
|
||||
}
|
||||
|
||||
impl NoteId {
|
||||
/// Constructs a new `NoteId` from its parts.
|
||||
pub fn new(txid: TxId, protocol: ShieldedProtocol, output_index: u16) -> Self {
|
||||
Self {
|
||||
txid,
|
||||
protocol,
|
||||
output_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ID of the transaction containing this note.
|
||||
pub fn txid(&self) -> &TxId {
|
||||
&self.txid
|
||||
}
|
||||
|
||||
/// Returns the shielded protocol used by this note.
|
||||
pub fn protocol(&self) -> ShieldedProtocol {
|
||||
self.protocol
|
||||
}
|
||||
|
||||
/// Returns the index of this note within its transaction's corresponding list of
|
||||
/// shielded outputs.
|
||||
pub fn output_index(&self) -> u16 {
|
||||
self.output_index
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that represents the recipient of a transaction output: a recipient address (and, for
|
||||
/// unified addresses, the pool to which the payment is sent) in the case of an outgoing output, or an
|
||||
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
|
||||
/// output.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Recipient {
|
||||
Transparent(TransparentAddress),
|
||||
Sapling(sapling::PaymentAddress),
|
||||
Unified(UnifiedAddress, PoolType),
|
||||
InternalAccount(AccountId, PoolType),
|
||||
}
|
||||
|
||||
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`Transaction`]: zcash_primitives::transaction::Transaction
|
||||
pub struct WalletTx<N> {
|
||||
pub struct WalletTx<N, S> {
|
||||
pub txid: TxId,
|
||||
pub index: usize,
|
||||
pub sapling_spends: Vec<WalletSaplingSpend>,
|
||||
pub sapling_outputs: Vec<WalletSaplingOutput<N>>,
|
||||
pub sapling_outputs: Vec<WalletSaplingOutput<N, S>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -109,8 +158,17 @@ impl WalletSaplingSpend {
|
|||
|
||||
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
|
||||
///
|
||||
/// The type parameter `<N>` is used to specify the nullifier type, which may vary between
|
||||
/// `Sapling` and `Orchard`, and also may vary depending upon the type of key that was used to
|
||||
/// decrypt this output; incoming viewing keys do not have the capability to derive the nullifier
|
||||
/// for a note, and the `<N>` will be `()` in these cases.
|
||||
///
|
||||
/// The type parameter `<S>` is used to specify the type of the scope of the key used to recover
|
||||
/// this output; this will usually be [`zcash_primitives::zip32::Scope`] for received notes, and
|
||||
/// `()` for sent notes.
|
||||
///
|
||||
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
|
||||
pub struct WalletSaplingOutput<N> {
|
||||
pub struct WalletSaplingOutput<N, S> {
|
||||
index: usize,
|
||||
cmu: sapling::note::ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
|
@ -119,9 +177,10 @@ pub struct WalletSaplingOutput<N> {
|
|||
is_change: bool,
|
||||
note_commitment_tree_position: Position,
|
||||
nf: N,
|
||||
recipient_key_scope: S,
|
||||
}
|
||||
|
||||
impl<N> WalletSaplingOutput<N> {
|
||||
impl<N, S> WalletSaplingOutput<N, S> {
|
||||
/// Constructs a new `WalletSaplingOutput` value from its constituent parts.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_parts(
|
||||
|
@ -133,6 +192,7 @@ impl<N> WalletSaplingOutput<N> {
|
|||
is_change: bool,
|
||||
note_commitment_tree_position: Position,
|
||||
nf: N,
|
||||
recipient_key_scope: S,
|
||||
) -> Self {
|
||||
Self {
|
||||
index,
|
||||
|
@ -143,6 +203,7 @@ impl<N> WalletSaplingOutput<N> {
|
|||
is_change,
|
||||
note_commitment_tree_position,
|
||||
nf,
|
||||
recipient_key_scope,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,38 +231,58 @@ impl<N> WalletSaplingOutput<N> {
|
|||
pub fn nf(&self) -> &N {
|
||||
&self.nf
|
||||
}
|
||||
pub fn recipient_key_scope(&self) -> &S {
|
||||
&self.recipient_key_scope
|
||||
}
|
||||
}
|
||||
|
||||
/// An enumeration of supported shielded note types for use in [`ReceivedNote`]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WalletNote {
|
||||
Sapling(sapling::Note),
|
||||
Orchard(orchard::Note),
|
||||
}
|
||||
|
||||
impl WalletNote {
|
||||
pub fn value(&self) -> NonNegativeAmount {
|
||||
match self {
|
||||
WalletNote::Sapling(n) => n.value().try_into().expect(
|
||||
"Sapling notes must have values in the range of valid non-negative ZEC values.",
|
||||
),
|
||||
WalletNote::Orchard(n) => NonNegativeAmount::from_u64(n.value().inner()).expect(
|
||||
"Orchard notes must have values in the range of valid non-negative ZEC values.",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a note that is tracked by the wallet that is available for spending,
|
||||
/// with sufficient information for use in note selection.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReceivedSaplingNote<NoteRef> {
|
||||
pub struct ReceivedNote<NoteRef> {
|
||||
note_id: NoteRef,
|
||||
txid: TxId,
|
||||
output_index: u16,
|
||||
diversifier: sapling::Diversifier,
|
||||
note_value: NonNegativeAmount,
|
||||
rseed: sapling::Rseed,
|
||||
note: WalletNote,
|
||||
spending_key_scope: Scope,
|
||||
note_commitment_tree_position: Position,
|
||||
}
|
||||
|
||||
impl<NoteRef> ReceivedSaplingNote<NoteRef> {
|
||||
impl<NoteRef> ReceivedNote<NoteRef> {
|
||||
pub fn from_parts(
|
||||
note_id: NoteRef,
|
||||
txid: TxId,
|
||||
output_index: u16,
|
||||
diversifier: sapling::Diversifier,
|
||||
note_value: NonNegativeAmount,
|
||||
rseed: sapling::Rseed,
|
||||
note: WalletNote,
|
||||
spending_key_scope: Scope,
|
||||
note_commitment_tree_position: Position,
|
||||
) -> Self {
|
||||
ReceivedSaplingNote {
|
||||
ReceivedNote {
|
||||
note_id,
|
||||
txid,
|
||||
output_index,
|
||||
diversifier,
|
||||
note_value,
|
||||
rseed,
|
||||
note,
|
||||
spending_key_scope,
|
||||
note_commitment_tree_position,
|
||||
}
|
||||
}
|
||||
|
@ -209,34 +290,33 @@ impl<NoteRef> ReceivedSaplingNote<NoteRef> {
|
|||
pub fn internal_note_id(&self) -> &NoteRef {
|
||||
&self.note_id
|
||||
}
|
||||
|
||||
pub fn txid(&self) -> &TxId {
|
||||
&self.txid
|
||||
}
|
||||
pub fn output_index(&self) -> u16 {
|
||||
self.output_index
|
||||
}
|
||||
pub fn diversifier(&self) -> sapling::Diversifier {
|
||||
self.diversifier
|
||||
pub fn note(&self) -> &WalletNote {
|
||||
&self.note
|
||||
}
|
||||
pub fn value(&self) -> NonNegativeAmount {
|
||||
self.note_value
|
||||
self.note.value()
|
||||
}
|
||||
pub fn rseed(&self) -> sapling::Rseed {
|
||||
self.rseed
|
||||
pub fn spending_key_scope(&self) -> Scope {
|
||||
self.spending_key_scope
|
||||
}
|
||||
pub fn note_commitment_tree_position(&self) -> Position {
|
||||
self.note_commitment_tree_position
|
||||
}
|
||||
}
|
||||
|
||||
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedSaplingNote<NoteRef> {
|
||||
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef> {
|
||||
fn note_id(&self) -> &NoteRef {
|
||||
&self.note_id
|
||||
}
|
||||
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
self.note_value
|
||||
self.note.value()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ and this library adheres to Rust's notion of
|
|||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- `zcash_client_sqlite::error::SqliteClientError` has new error variant:
|
||||
- `zcash_client_sqlite::error::SqliteClientError` has new error variants:
|
||||
- `SqliteClientError::UnsupportedPoolType`
|
||||
- `SqliteClientError::BalanceError`
|
||||
|
||||
## [0.8.1] - 2023-10-18
|
||||
|
||||
|
|
|
@ -4,9 +4,13 @@ use std::error;
|
|||
use std::fmt;
|
||||
|
||||
use shardtree::error::ShardTreeError;
|
||||
use zcash_client_backend::data_api::PoolType;
|
||||
use zcash_client_backend::encoding::{Bech32DecodeError, TransparentCodecError};
|
||||
use zcash_primitives::{consensus::BlockHeight, zip32::AccountId};
|
||||
use zcash_client_backend::{
|
||||
encoding::{Bech32DecodeError, TransparentCodecError},
|
||||
PoolType,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight, transaction::components::amount::BalanceError, zip32::AccountId,
|
||||
};
|
||||
|
||||
use crate::wallet::commitment_tree;
|
||||
use crate::PRUNING_DEPTH;
|
||||
|
@ -102,6 +106,9 @@ pub enum SqliteClientError {
|
|||
|
||||
/// Unsupported pool type
|
||||
UnsupportedPoolType(PoolType),
|
||||
|
||||
/// An error occurred in computing wallet balance
|
||||
BalanceError(BalanceError),
|
||||
}
|
||||
|
||||
impl error::Error for SqliteClientError {
|
||||
|
@ -111,6 +118,7 @@ impl error::Error for SqliteClientError {
|
|||
SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e),
|
||||
SqliteClientError::DbError(e) => Some(e),
|
||||
SqliteClientError::Io(e) => Some(e),
|
||||
SqliteClientError::BalanceError(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +157,8 @@ impl fmt::Display for SqliteClientError {
|
|||
SqliteClientError::CommitmentTree(err) => write!(f, "An error occurred accessing or updating note commitment tree data: {}.", err),
|
||||
SqliteClientError::CacheMiss(height) => write!(f, "Requested height {} does not exist in the block cache.", height),
|
||||
SqliteClientError::ChainHeightUnknown => write!(f, "Chain height unknown; please call `update_chain_tip`"),
|
||||
SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t)
|
||||
SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t),
|
||||
SqliteClientError::BalanceError(e) => write!(f, "Balance error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -202,3 +211,9 @@ impl From<ShardTreeError<commitment_tree::Error>> for SqliteClientError {
|
|||
SqliteClientError::CommitmentTree(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BalanceError> for SqliteClientError {
|
||||
fn from(e: BalanceError) -> Self {
|
||||
SqliteClientError::BalanceError(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ use zcash_primitives::{
|
|||
components::amount::{Amount, NonNegativeAmount},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::{AccountId, DiversifierIndex},
|
||||
zip32::{AccountId, DiversifierIndex, Scope},
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
|
@ -64,14 +64,14 @@ use zcash_client_backend::{
|
|||
self,
|
||||
chain::{BlockSource, CommitmentTreeRoot},
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
|
||||
Recipient, SaplingInputSource, ScannedBlock, SentTransaction, ShieldedProtocol,
|
||||
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
AccountBirthday, BlockMetadata, DecryptedTransaction, NullifierQuery, SaplingInputSource,
|
||||
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary,
|
||||
WalletWrite, SAPLING_SHARD_HEIGHT,
|
||||
},
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
DecryptedOutput, TransferType,
|
||||
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
||||
|
@ -177,8 +177,8 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
|
|||
&self,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), txid, index)
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index)
|
||||
}
|
||||
|
||||
fn select_spendable_sapling_notes(
|
||||
|
@ -187,9 +187,10 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
|
|||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
wallet::sapling::select_spendable_sapling_notes(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
|
@ -439,7 +440,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
#[allow(clippy::type_complexity)]
|
||||
fn put_blocks(
|
||||
&mut self,
|
||||
blocks: Vec<ScannedBlock<sapling::Nullifier>>,
|
||||
blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.transactionally(|wdb| {
|
||||
let start_positions = blocks.first().map(|block| {
|
||||
|
@ -483,7 +484,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
for output in &tx.sapling_outputs {
|
||||
// Check whether this note was spent in a later block range that
|
||||
// we previously scanned.
|
||||
let spent_in = wallet::query_nullifier_map(
|
||||
let spent_in = wallet::query_nullifier_map::<_, Scope>(
|
||||
wdb.conn.0,
|
||||
ShieldedProtocol::Sapling,
|
||||
output.nf(),
|
||||
|
@ -508,7 +509,8 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
}));
|
||||
|
||||
last_scanned_height = Some(block.height());
|
||||
sapling_commitments.extend(block.into_sapling_commitments().into_iter().map(Some));
|
||||
let (block_sapling_commitments, _) = block.into_commitments();
|
||||
sapling_commitments.extend(block_sapling_commitments.into_iter().map(Some));
|
||||
}
|
||||
|
||||
// Prune the nullifier map of entries we no longer need.
|
||||
|
|
|
@ -708,7 +708,7 @@ impl<Cache> TestState<Cache> {
|
|||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
self.with_account_balance(account, min_confirmations, |balance| {
|
||||
balance.sapling_balance.spendable_value
|
||||
balance.sapling_balance().spendable_value()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -718,8 +718,8 @@ impl<Cache> TestState<Cache> {
|
|||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
self.with_account_balance(account, min_confirmations, |balance| {
|
||||
balance.sapling_balance.value_pending_spendability
|
||||
+ balance.sapling_balance.change_pending_confirmation
|
||||
balance.sapling_balance().value_pending_spendability()
|
||||
+ balance.sapling_balance().change_pending_confirmation()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -731,7 +731,7 @@ impl<Cache> TestState<Cache> {
|
|||
min_confirmations: u32,
|
||||
) -> NonNegativeAmount {
|
||||
self.with_account_balance(account, min_confirmations, |balance| {
|
||||
balance.sapling_balance.change_pending_confirmation
|
||||
balance.sapling_balance().change_pending_confirmation()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -75,28 +75,27 @@ use std::ops::RangeInclusive;
|
|||
use tracing::debug;
|
||||
use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary};
|
||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||
|
||||
use zcash_client_backend::data_api::{
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBirthday, NoteId, ShieldedProtocol, SAPLING_SHARD_HEIGHT,
|
||||
};
|
||||
use zcash_primitives::transaction::TransactionData;
|
||||
use zcash_primitives::zip32::Scope;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
|
||||
memo::{Memo, MemoBytes},
|
||||
merkle_tree::read_commitment_tree,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
transaction::{components::Amount, Transaction, TransactionData, TxId},
|
||||
zip32::{AccountId, DiversifierIndex},
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::{RecipientAddress, UnifiedAddress},
|
||||
data_api::{BlockMetadata, PoolType, Recipient, SentTransactionOutput},
|
||||
data_api::{
|
||||
scanning::{ScanPriority, ScanRange},
|
||||
AccountBirthday, BlockMetadata, SentTransactionOutput, SAPLING_SHARD_HEIGHT,
|
||||
},
|
||||
encoding::AddressCodec,
|
||||
keys::UnifiedFullViewingKey,
|
||||
wallet::WalletTx,
|
||||
wallet::{NoteId, Recipient, WalletTx},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
use crate::wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore};
|
||||
|
@ -137,6 +136,21 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scope_code(scope: Scope) -> i64 {
|
||||
match scope {
|
||||
Scope::External => 0i64,
|
||||
Scope::Internal => 1i64,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_scope(code: i64) -> Option<Scope> {
|
||||
match code {
|
||||
0i64 => Some(Scope::External),
|
||||
1i64 => Some(Scope::Internal),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn memo_repr(memo: Option<&MemoBytes>) -> Option<&[u8]> {
|
||||
memo.map(|m| {
|
||||
if m == &MemoBytes::empty() {
|
||||
|
@ -672,17 +686,14 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
}
|
||||
};
|
||||
|
||||
account_balances.entry(account).and_modify(|bal| {
|
||||
bal.sapling_balance.spendable_value = (bal.sapling_balance.spendable_value
|
||||
+ spendable_value)
|
||||
.expect("Spendable value cannot overflow");
|
||||
bal.sapling_balance.change_pending_confirmation =
|
||||
(bal.sapling_balance.change_pending_confirmation + change_pending_confirmation)
|
||||
.expect("Pending change value cannot overflow");
|
||||
bal.sapling_balance.value_pending_spendability =
|
||||
(bal.sapling_balance.value_pending_spendability + value_pending_spendability)
|
||||
.expect("Value pending spendability cannot overflow");
|
||||
});
|
||||
if let Some(balances) = account_balances.get_mut(&account) {
|
||||
balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
|
||||
bal.add_spendable_value(spendable_value)?;
|
||||
bal.add_pending_change_value(change_pending_confirmation)?;
|
||||
bal.add_pending_spendable_value(value_pending_spendability)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -712,9 +723,9 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value))
|
||||
})?;
|
||||
|
||||
account_balances.entry(account).and_modify(|bal| {
|
||||
bal.unshielded = (bal.unshielded + value).expect("Unshielded value cannot overflow")
|
||||
});
|
||||
if let Some(balances) = account_balances.get_mut(&account) {
|
||||
balances.add_unshielded_value(value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1513,9 +1524,9 @@ pub(crate) fn put_block(
|
|||
|
||||
/// Inserts information about a mined transaction that was observed to
|
||||
/// contain a note related to this wallet into the database.
|
||||
pub(crate) fn put_tx_meta<N>(
|
||||
pub(crate) fn put_tx_meta<N, S>(
|
||||
conn: &rusqlite::Connection,
|
||||
tx: &WalletTx<N>,
|
||||
tx: &WalletTx<N, S>,
|
||||
height: BlockHeight,
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
|
@ -1899,7 +1910,7 @@ pub(crate) fn insert_nullifier_map<N: AsRef<[u8]>>(
|
|||
|
||||
/// Returns the row of the `transactions` table corresponding to the transaction in which
|
||||
/// this nullifier is revealed, if any.
|
||||
pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
|
||||
pub(crate) fn query_nullifier_map<N: AsRef<[u8]>, S>(
|
||||
conn: &rusqlite::Transaction<'_>,
|
||||
spend_pool: ShieldedProtocol,
|
||||
nf: &N,
|
||||
|
@ -1937,7 +1948,7 @@ pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
|
|||
// change or explicit in-wallet recipient.
|
||||
put_tx_meta(
|
||||
conn,
|
||||
&WalletTx::<N> {
|
||||
&WalletTx::<N, S> {
|
||||
txid,
|
||||
index,
|
||||
sapling_spends: vec![],
|
||||
|
@ -2151,7 +2162,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.unwrap();
|
||||
let balance = summary.account_balances().get(&account_id).unwrap();
|
||||
assert_eq!(balance.unshielded, expected);
|
||||
assert_eq!(balance.unshielded(), expected);
|
||||
|
||||
// Check the older APIs for consistency.
|
||||
let max_height = st.wallet().chain_height().unwrap().unwrap() + 1 - min_confirmations;
|
||||
|
|
|
@ -248,6 +248,7 @@ mod tests {
|
|||
memo BLOB,
|
||||
spent INTEGER,
|
||||
commitment_tree_position INTEGER,
|
||||
recipient_key_scope INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||
FOREIGN KEY (account) REFERENCES accounts(account),
|
||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
||||
|
|
|
@ -5,6 +5,7 @@ mod addresses_table;
|
|||
mod initial_setup;
|
||||
mod nullifier_map;
|
||||
mod received_notes_nullable_nf;
|
||||
mod receiving_key_scopes;
|
||||
mod sapling_memo_consistency;
|
||||
mod sent_notes_to_internal;
|
||||
mod shardtree_support;
|
||||
|
@ -40,17 +41,17 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
// |
|
||||
// v_transactions_net
|
||||
// |
|
||||
// received_notes_nullable_nf
|
||||
// / | \
|
||||
// shardtree_support nullifier_map sapling_memo_consistency
|
||||
// | |
|
||||
// add_account_birthdays v_transactions_transparent_history
|
||||
// | |
|
||||
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
|
||||
// | |
|
||||
// wallet_summaries v_transactions_shielding_balance
|
||||
// |
|
||||
// v_transactions_note_uniqueness
|
||||
// received_notes_nullable_nf
|
||||
// / | \
|
||||
// shardtree_support nullifier_map sapling_memo_consistency
|
||||
// / \ |
|
||||
// add_account_birthdays receiving_key_scopes v_transactions_transparent_history
|
||||
// | |
|
||||
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
|
||||
// | |
|
||||
// wallet_summaries v_transactions_shielding_balance
|
||||
// |
|
||||
// v_transactions_note_uniqueness
|
||||
vec![
|
||||
Box::new(initial_setup::Migration {}),
|
||||
Box::new(utxos_table::Migration {}),
|
||||
|
@ -86,5 +87,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
|||
Box::new(v_tx_outputs_use_legacy_false::Migration),
|
||||
Box::new(v_transactions_shielding_balance::Migration),
|
||||
Box::new(v_transactions_note_uniqueness::Migration),
|
||||
Box::new(receiving_key_scopes::Migration {
|
||||
params: params.clone(),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
//! This migration adds decryption key scope to persisted information about received notes.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use rusqlite::{self, named_params};
|
||||
use schemer;
|
||||
use schemer_rusqlite::RusqliteMigration;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use zcash_client_backend::keys::UnifiedFullViewingKey;
|
||||
use zcash_primitives::{
|
||||
consensus::{self, sapling_zip212_enforcement, BlockHeight, BranchId},
|
||||
sapling::note_encryption::{
|
||||
try_sapling_note_decryption, PreparedIncomingViewingKey, Zip212Enforcement,
|
||||
},
|
||||
transaction::Transaction,
|
||||
zip32::Scope,
|
||||
};
|
||||
|
||||
use crate::wallet::{
|
||||
init::{migrations::shardtree_support, WalletMigrationError},
|
||||
scan_queue_extrema, scope_code,
|
||||
};
|
||||
|
||||
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xee89ed2b_c1c2_421e_9e98_c1e3e54a7fc2);
|
||||
|
||||
pub(super) struct Migration<P> {
|
||||
pub(super) params: P,
|
||||
}
|
||||
|
||||
impl<P> schemer::Migration for Migration<P> {
|
||||
fn id(&self) -> Uuid {
|
||||
MIGRATION_ID
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> HashSet<Uuid> {
|
||||
[shardtree_support::MIGRATION_ID].into_iter().collect()
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Add decryption key scope to persisted information about received notes."
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||
type Error = WalletMigrationError;
|
||||
|
||||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||
transaction.execute_batch(
|
||||
&format!(
|
||||
"ALTER TABLE sapling_received_notes ADD COLUMN recipient_key_scope INTEGER NOT NULL DEFAULT {};",
|
||||
scope_code(Scope::External)
|
||||
)
|
||||
)?;
|
||||
|
||||
// For all notes we have to determine whether they were actually sent to the internal key
|
||||
// or the external key for the account, so we trial-decrypt the original output with the
|
||||
// internal IVK and update the persisted scope value if necessary. We check all notes,
|
||||
// rather than just change notes, because shielding notes may not have been considered
|
||||
// change.
|
||||
let mut stmt_select_notes = transaction.prepare(
|
||||
"SELECT id_note, output_index, transactions.raw, transactions.block, transactions.expiry_height, accounts.ufvk
|
||||
FROM sapling_received_notes
|
||||
INNER JOIN accounts on accounts.account = sapling_received_notes.account
|
||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx"
|
||||
)?;
|
||||
|
||||
let mut rows = stmt_select_notes.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let note_id: i64 = row.get(0)?;
|
||||
let output_index: usize = row.get(1)?;
|
||||
let tx_data: Vec<u8> = row.get(2)?;
|
||||
|
||||
let tx = Transaction::read(&tx_data[..], BranchId::Canopy)
|
||||
.expect("Transaction must be valid");
|
||||
let output = tx
|
||||
.sapling_bundle()
|
||||
.and_then(|b| b.shielded_outputs().get(output_index))
|
||||
.unwrap_or_else(|| panic!("A Sapling output must exist at index {}", output_index));
|
||||
|
||||
let tx_height = row.get::<_, Option<u32>>(3)?.map(BlockHeight::from);
|
||||
let tx_expiry = row.get::<_, u32>(4)?;
|
||||
let zip212_height = tx_height.map_or_else(
|
||||
|| {
|
||||
if tx_expiry == 0 {
|
||||
scan_queue_extrema(transaction).map(|extrema| extrema.map(|r| *r.end()))
|
||||
} else {
|
||||
Ok(Some(BlockHeight::from(tx_expiry)))
|
||||
}
|
||||
},
|
||||
|h| Ok(Some(h)),
|
||||
)?;
|
||||
|
||||
let zip212_enforcement = zip212_height.map_or_else(
|
||||
|| {
|
||||
// If the transaction has not been mined and the expiry height is set to 0 (no
|
||||
// expiry) an no chain tip information is available, then we assume it can only
|
||||
// be mined under ZIP 212 enforcement rules, so we default to `On`
|
||||
Zip212Enforcement::On
|
||||
},
|
||||
|h| sapling_zip212_enforcement(&self.params, h),
|
||||
);
|
||||
|
||||
let ufvk_str: String = row.get(5)?;
|
||||
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
|
||||
.expect("Stored UFVKs must be valid");
|
||||
let dfvk = ufvk
|
||||
.sapling()
|
||||
.expect("UFVK must have a Sapling component to have received Sapling notes");
|
||||
|
||||
// We previously set the default to external scope, so we now verify whether the output
|
||||
// is decryptable using the intenally-scoped IVK and, if so, mark it as such.
|
||||
let pivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
|
||||
if try_sapling_note_decryption(&pivk, output, zip212_enforcement).is_some() {
|
||||
transaction.execute(
|
||||
"UPDATE sapling_received_notes SET recipient_key_scope = :scope
|
||||
WHERE id_note = :note_id",
|
||||
named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||
// TODO: something better than just panic?
|
||||
panic!("Cannot revert this migration.");
|
||||
}
|
||||
}
|
|
@ -8,9 +8,7 @@ use secrecy::{ExposeSecret, SecretVec};
|
|||
use uuid::Uuid;
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress,
|
||||
data_api::{PoolType, ShieldedProtocol},
|
||||
keys::UnifiedSpendingKey,
|
||||
address::RecipientAddress, keys::UnifiedSpendingKey, PoolType, ShieldedProtocol,
|
||||
};
|
||||
use zcash_primitives::{consensus, zip32::AccountId};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use rusqlite::{self, named_params};
|
|||
use schemer;
|
||||
use schemer_rusqlite::RusqliteMigration;
|
||||
use uuid::Uuid;
|
||||
use zcash_client_backend::data_api::{PoolType, ShieldedProtocol};
|
||||
use zcash_client_backend::{PoolType, ShieldedProtocol};
|
||||
|
||||
use super::add_transaction_views;
|
||||
use crate::wallet::{init::WalletMigrationError, pool_code};
|
||||
|
|
|
@ -6,24 +6,25 @@ use rusqlite::{named_params, params, types::Value, Connection, Row};
|
|||
use std::rc::Rc;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight,
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
sapling::{self, Diversifier, Note, Nullifier, Rseed},
|
||||
transaction::{
|
||||
components::{amount::NonNegativeAmount, Amount},
|
||||
TxId,
|
||||
},
|
||||
zip32::AccountId,
|
||||
zip32::{AccountId, Scope},
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
wallet::{ReceivedSaplingNote, WalletSaplingOutput},
|
||||
keys::UnifiedFullViewingKey,
|
||||
wallet::{ReceivedNote, WalletNote, WalletSaplingOutput},
|
||||
DecryptedOutput, TransferType,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, ReceivedNoteId};
|
||||
|
||||
use super::{memo_repr, wallet_birthday};
|
||||
use super::{memo_repr, parse_scope, scope_code, wallet_birthday};
|
||||
|
||||
/// This trait provides a generalization over shielded output representations.
|
||||
pub(crate) trait ReceivedSaplingOutput {
|
||||
|
@ -34,9 +35,10 @@ pub(crate) trait ReceivedSaplingOutput {
|
|||
fn is_change(&self) -> bool;
|
||||
fn nullifier(&self) -> Option<&sapling::Nullifier>;
|
||||
fn note_commitment_tree_position(&self) -> Option<Position>;
|
||||
fn recipient_key_scope(&self) -> Scope;
|
||||
}
|
||||
|
||||
impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier> {
|
||||
impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier, Scope> {
|
||||
fn index(&self) -> usize {
|
||||
self.index()
|
||||
}
|
||||
|
@ -58,6 +60,10 @@ impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier> {
|
|||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||
Some(WalletSaplingOutput::note_commitment_tree_position(self))
|
||||
}
|
||||
|
||||
fn recipient_key_scope(&self) -> Scope {
|
||||
*self.recipient_key_scope()
|
||||
}
|
||||
}
|
||||
|
||||
impl ReceivedSaplingOutput for DecryptedOutput<Note> {
|
||||
|
@ -82,9 +88,19 @@ impl ReceivedSaplingOutput for DecryptedOutput<Note> {
|
|||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||
None
|
||||
}
|
||||
fn recipient_key_scope(&self) -> Scope {
|
||||
if self.transfer_type == TransferType::WalletInternal {
|
||||
Scope::Internal
|
||||
} else {
|
||||
Scope::External
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, SqliteClientError> {
|
||||
fn to_spendable_note<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
row: &Row,
|
||||
) -> Result<ReceivedNote<ReceivedNoteId>, SqliteClientError> {
|
||||
let note_id = ReceivedNoteId(row.get(0)?);
|
||||
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
|
||||
let output_index = row.get(2)?;
|
||||
|
@ -124,13 +140,35 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, S
|
|||
SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string())
|
||||
})?);
|
||||
|
||||
Ok(ReceivedSaplingNote::from_parts(
|
||||
let ufvk_str: String = row.get(7)?;
|
||||
let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
|
||||
.map_err(SqliteClientError::CorruptedData)?;
|
||||
|
||||
let scope_code: i64 = row.get(8)?;
|
||||
let spending_key_scope = parse_scope(scope_code).ok_or_else(|| {
|
||||
SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code))
|
||||
})?;
|
||||
|
||||
let recipient = match spending_key_scope {
|
||||
Scope::Internal => ufvk
|
||||
.sapling()
|
||||
.and_then(|dfvk| dfvk.diversified_change_address(diversifier)),
|
||||
Scope::External => ufvk
|
||||
.sapling()
|
||||
.and_then(|dfvk| dfvk.diversified_address(diversifier)),
|
||||
}
|
||||
.ok_or_else(|| SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()))?;
|
||||
|
||||
Ok(ReceivedNote::from_parts(
|
||||
note_id,
|
||||
txid,
|
||||
output_index,
|
||||
diversifier,
|
||||
note_value,
|
||||
rseed,
|
||||
WalletNote::Sapling(sapling::Note::from_parts(
|
||||
recipient,
|
||||
note_value.into(),
|
||||
rseed,
|
||||
)),
|
||||
spending_key_scope,
|
||||
note_commitment_tree_position,
|
||||
))
|
||||
}
|
||||
|
@ -139,14 +177,17 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, S
|
|||
// (https://github.com/rust-lang/rust-clippy/issues/11308) means it fails to identify that the `result` temporary
|
||||
// is required in order to resolve the borrows involved in the `query_and_then` call.
|
||||
#[allow(clippy::let_and_return)]
|
||||
pub(crate) fn get_spendable_sapling_note(
|
||||
pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
|
||||
conn: &Connection,
|
||||
params: &P,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
) -> Result<Option<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
let mut stmt_select_note = conn.prepare_cached(
|
||||
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
|
||||
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
|
||||
accounts.ufvk, recipient_key_scope
|
||||
FROM sapling_received_notes
|
||||
INNER JOIN accounts on accounts.account = sapling_received_notes.account
|
||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
|
||||
WHERE txid = :txid
|
||||
AND output_index = :output_index
|
||||
|
@ -159,7 +200,7 @@ pub(crate) fn get_spendable_sapling_note(
|
|||
":txid": txid.as_ref(),
|
||||
":output_index": index,
|
||||
],
|
||||
to_spendable_note,
|
||||
|r| to_spendable_note(params, r),
|
||||
)?
|
||||
.next()
|
||||
.transpose();
|
||||
|
@ -182,8 +223,8 @@ fn unscanned_tip_exists(
|
|||
"SELECT EXISTS (
|
||||
SELECT 1 FROM v_sapling_shard_unscanned_ranges range
|
||||
WHERE range.block_range_start <= :anchor_height
|
||||
AND :anchor_height BETWEEN
|
||||
range.subtree_start_height
|
||||
AND :anchor_height BETWEEN
|
||||
range.subtree_start_height
|
||||
AND IFNULL(range.subtree_end_height, :anchor_height)
|
||||
)",
|
||||
named_params![":anchor_height": u32::from(anchor_height),],
|
||||
|
@ -191,13 +232,14 @@ fn unscanned_tip_exists(
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn select_spendable_sapling_notes(
|
||||
pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
|
||||
conn: &Connection,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[ReceivedNoteId],
|
||||
) -> Result<Vec<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
) -> Result<Vec<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
let birthday_height = match wallet_birthday(conn)? {
|
||||
Some(birthday) => birthday,
|
||||
None => {
|
||||
|
@ -229,13 +271,16 @@ pub(crate) fn select_spendable_sapling_notes(
|
|||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let mut stmt_select_notes = conn.prepare_cached(
|
||||
"WITH eligible AS (
|
||||
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
|
||||
SELECT
|
||||
id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
|
||||
SUM(value)
|
||||
OVER (PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||
OVER (PARTITION BY sapling_received_notes.account, spent ORDER BY id_note) AS so_far,
|
||||
accounts.ufvk as ufvk, recipient_key_scope
|
||||
FROM sapling_received_notes
|
||||
INNER JOIN accounts on accounts.account = sapling_received_notes.account
|
||||
INNER JOIN transactions
|
||||
ON transactions.id_tx = sapling_received_notes.tx
|
||||
WHERE account = :account
|
||||
WHERE sapling_received_notes.account = :account
|
||||
AND commitment_tree_position IS NOT NULL
|
||||
AND spent IS NULL
|
||||
AND transactions.block <= :anchor_height
|
||||
|
@ -251,10 +296,10 @@ pub(crate) fn select_spendable_sapling_notes(
|
|||
AND unscanned.block_range_end > :wallet_birthday
|
||||
)
|
||||
)
|
||||
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
|
||||
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
|
||||
FROM eligible WHERE so_far < :target_value
|
||||
UNION
|
||||
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
|
||||
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
|
||||
FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)",
|
||||
)?;
|
||||
|
||||
|
@ -269,7 +314,7 @@ pub(crate) fn select_spendable_sapling_notes(
|
|||
":exclude": &excluded_ptr,
|
||||
":wallet_birthday": u32::from(birthday_height)
|
||||
],
|
||||
to_spendable_note,
|
||||
|r| to_spendable_note(params, r),
|
||||
)?;
|
||||
|
||||
notes.collect::<Result<_, _>>()
|
||||
|
@ -360,7 +405,9 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
|||
) -> Result<(), 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, spent, commitment_tree_position)
|
||||
(tx, output_index, account, diversifier, value, rcm, memo, nf,
|
||||
is_change, spent, commitment_tree_position,
|
||||
recipient_key_scope)
|
||||
VALUES (
|
||||
:tx,
|
||||
:output_index,
|
||||
|
@ -372,7 +419,8 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
|||
:nf,
|
||||
:is_change,
|
||||
:spent,
|
||||
:commitment_tree_position
|
||||
:commitment_tree_position,
|
||||
:recipient_key_scope
|
||||
)
|
||||
ON CONFLICT (tx, output_index) DO UPDATE
|
||||
SET account = :account,
|
||||
|
@ -383,7 +431,8 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
|||
memo = IFNULL(:memo, memo),
|
||||
is_change = IFNULL(:is_change, is_change),
|
||||
spent = IFNULL(:spent, spent),
|
||||
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position)",
|
||||
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
|
||||
recipient_key_scope = :recipient_key_scope",
|
||||
)?;
|
||||
|
||||
let rcm = output.note().rcm().to_repr();
|
||||
|
@ -402,6 +451,7 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
|||
":is_change": output.is_change(),
|
||||
":spent": spent_in,
|
||||
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
|
||||
":recipient_key_scope": scope_code(output.recipient_key_scope()),
|
||||
];
|
||||
|
||||
stmt_upsert_received_note
|
||||
|
@ -416,6 +466,7 @@ pub(crate) mod tests {
|
|||
use std::{convert::Infallible, num::NonZeroU32};
|
||||
|
||||
use incrementalmerkletree::Hashable;
|
||||
use rusqlite::params;
|
||||
use secrecy::Secret;
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
|
||||
|
@ -447,22 +498,22 @@ pub(crate) mod tests {
|
|||
chain::CommitmentTreeRoot,
|
||||
error::Error,
|
||||
wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError},
|
||||
AccountBirthday, Ratio, ShieldedProtocol, WalletCommitmentTrees, WalletRead,
|
||||
WalletWrite,
|
||||
AccountBirthday, Ratio, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{fixed, standard, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
wallet::OvkPolicy,
|
||||
zip321::{self, Payment, TransactionRequest},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::SqliteClientError,
|
||||
testing::{input_selector, AddressType, BlockCache, TestBuilder, TestState},
|
||||
wallet::{
|
||||
block_max_scanned, commitment_tree, sapling::select_spendable_sapling_notes,
|
||||
scanning::tests::test_with_canopy_birthday,
|
||||
block_max_scanned, commitment_tree, parse_scope,
|
||||
sapling::select_spendable_sapling_notes, scanning::tests::test_with_canopy_birthday,
|
||||
},
|
||||
AccountId, NoteId, ReceivedNoteId,
|
||||
};
|
||||
|
@ -1149,6 +1200,15 @@ pub(crate) mod tests {
|
|||
NonNegativeAmount::ZERO
|
||||
);
|
||||
|
||||
let change_note_scope = st.wallet().conn.query_row(
|
||||
"SELECT recipient_key_scope
|
||||
FROM sapling_received_notes
|
||||
WHERE value = ?",
|
||||
params![u64::from(value)],
|
||||
|row| Ok(parse_scope(row.get(0)?)),
|
||||
);
|
||||
assert_matches!(change_note_scope, Ok(Some(Scope::Internal)));
|
||||
|
||||
// TODO: This test was originally written to use the pre-zip-313 fee rule
|
||||
// and has not yet been updated.
|
||||
#[allow(deprecated)]
|
||||
|
@ -1518,6 +1578,7 @@ pub(crate) mod tests {
|
|||
// Verify that the received note is not considered spendable
|
||||
let spendable = select_spendable_sapling_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
AccountId::ZERO,
|
||||
Amount::const_from_i64(300000),
|
||||
received_tx_height + 10,
|
||||
|
@ -1533,6 +1594,7 @@ pub(crate) mod tests {
|
|||
// Verify that the received note is now considered spendable
|
||||
let spendable = select_spendable_sapling_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
AccountId::ZERO,
|
||||
Amount::const_from_i64(300000),
|
||||
received_tx_height + 10,
|
||||
|
@ -1586,6 +1648,7 @@ pub(crate) mod tests {
|
|||
// Verify that our note is considered spendable
|
||||
let spendable = select_spendable_sapling_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
account,
|
||||
Amount::const_from_i64(300000),
|
||||
birthday.height() + 5,
|
||||
|
|
|
@ -95,6 +95,7 @@ and this library adheres to Rust's notion of
|
|||
- `impl From<NonNegativeAmount> for zcash_primitives::sapling::value::NoteValue`
|
||||
- `impl Sum<NonNegativeAmount> for Option<NonNegativeAmount>`
|
||||
- `impl<'a> Sum<&'a NonNegativeAmount> for Option<NonNegativeAmount>`
|
||||
- `impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount`
|
||||
- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
|
||||
- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
|
||||
- `impl From<TxId> for [u8; 32]`
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::error;
|
||||
use std::iter::Sum;
|
||||
use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
|
||||
|
||||
|
@ -322,6 +323,14 @@ impl From<NonNegativeAmount> for sapling::value::NoteValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: sapling::value::NoteValue) -> Result<Self, Self::Error> {
|
||||
Self::from_u64(value.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Amount> for NonNegativeAmount {
|
||||
type Error = ();
|
||||
|
||||
|
@ -394,6 +403,8 @@ pub enum BalanceError {
|
|||
Underflow,
|
||||
}
|
||||
|
||||
impl error::Error for BalanceError {}
|
||||
|
||||
impl std::fmt::Display for BalanceError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self {
|
||||
|
|
Loading…
Reference in New Issue