diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md
index 35497ef653..f1afad0aec 100644
--- a/zcash_client_backend/CHANGELOG.md
+++ b/zcash_client_backend/CHANGELOG.md
@@ -12,6 +12,7 @@ and this library adheres to Rust's notion of
- `TransparentInputSource`
- `SaplingInputSource`
- `wallet::propose_standard_transfer_to_address`
+ - `wallet::input_selection::Proposal::from_parts`
- `wallet::input_selection::SaplingInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
@@ -20,6 +21,22 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::wallet`:
- `ReceivedSaplingNote::from_parts`
- `ReceivedSaplingNote::{txid, output_index, diversifier, rseed, note_commitment_tree_position}`
+- `zcash_client_backend::zip321::TransactionRequest::total`
+- `zcash_client_backend::proto::`
+ - `PROPOSAL_SER_V1`
+ - `ProposalError`
+ - `proposal::Proposal::{from_standard_proposal, try_into_standard_proposal}`
+ - `proposal::ProposedInput::parse_txid`
+- `impl Clone for zcash_client_backend::{
+ zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
+ wallet::{ReceivedSaplingNote, WalletTransparentOutput},
+ wallet::input_selection::{Proposal, SaplingInputs},
+ }`
+- `impl {PartialEq, Eq} for zcash_client_backend::{
+ zip321::{Zip321Error, parse::Param, parse::IndexedParam},
+ wallet::{ReceivedSaplingNote, WalletTransparentOutput},
+ wallet::input_selection::{Proposal, SaplingInputs},
+ }`
### Changed
- `zcash_client_backend::data_api`:
@@ -33,11 +50,11 @@ and this library adheres to Rust's notion of
backend-specific note identifier. The related `NoteRef` type parameter has
been removed from `error::Error`.
- A new variant `UnsupportedPoolType` has been added.
- - `wallet::shield_transparent_funds` no longer
- takes a `memo` argument; instead, memos to be associated with the shielded
- outputs should be specified in the construction of the value of the
- `input_selector` argument, which is used to construct the proposed shielded
- values as internal "change" outputs.
+ - `wallet::shield_transparent_funds` no longer takes a `memo` argument;
+ instead, memos to be associated with the shielded outputs should be
+ specified in the construction of the value of the `input_selector`
+ argument, which is used to construct the proposed shielded values as
+ internal "change" outputs.
- `wallet::create_proposed_transaction` no longer takes a
`change_memo` argument; instead, change memos are represented in the
individual values of the `proposed_change` field of the `Proposal`'s
diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs
index c623de26d3..7b79e8eaa6 100644
--- a/zcash_client_backend/src/data_api.rs
+++ b/zcash_client_backend/src/data_api.rs
@@ -222,6 +222,14 @@ pub trait SaplingInputSource {
/// or a UUID.
type NoteRef: Copy + Debug + Eq + Ord;
+ /// Returns a received Sapling note, or Ok(None) if the note is not known to belong to the
+ /// wallet or if the note is not spendable.
+ fn get_spendable_sapling_note(
+ &self,
+ txid: &TxId,
+ index: u32,
+ ) -> Result>, Self::Error>;
+
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
/// if possible.
fn select_spendable_sapling_notes(
@@ -240,6 +248,13 @@ pub trait TransparentInputSource {
/// The type of errors produced by a wallet backend.
type Error;
+ /// Returns a received transparent UTXO, or Ok(None) if the UTXO is not known to belong to the
+ /// wallet or is not spendable.
+ fn get_unspent_transparent_output(
+ &self,
+ outpoint: &OutPoint,
+ ) -> Result , Self::Error>;
+
/// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and
/// including `max_height`.
fn get_unspent_transparent_outputs(
@@ -979,6 +994,14 @@ pub mod testing {
type Error = ();
type NoteRef = u32;
+ fn get_spendable_sapling_note(
+ &self,
+ _txid: &TxId,
+ _index: u32,
+ ) -> Result >, Self::Error> {
+ Ok(None)
+ }
+
fn select_spendable_sapling_notes(
&self,
_account: AccountId,
@@ -994,6 +1017,13 @@ pub mod testing {
impl TransparentInputSource for MockWalletDb {
type Error = ();
+ fn get_unspent_transparent_output(
+ &self,
+ _outpoint: &OutPoint,
+ ) -> Result , Self::Error> {
+ Ok(None)
+ }
+
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,
diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs
index ccba6de006..1e675f3135 100644
--- a/zcash_client_backend/src/data_api/wallet/input_selection.rs
+++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs
@@ -79,6 +79,7 @@ impl fmt::Display for InputSelectorError {
transaction_request: TransactionRequest,
transparent_inputs: Vec,
@@ -90,6 +91,45 @@ pub struct Proposal {
}
impl Proposal {
+ /// Constructs a [`Proposal`] from its constituent parts.
+ #[allow(clippy::too_many_arguments)]
+ pub(crate) fn from_parts(
+ transaction_request: TransactionRequest,
+ transparent_inputs: Vec,
+ sapling_inputs: Option>,
+ balance: TransactionBalance,
+ fee_rule: FeeRuleT,
+ min_target_height: BlockHeight,
+ is_shielding: bool,
+ ) -> Result {
+ let transparent_total = transparent_inputs
+ .iter()
+ .map(|out| out.txout().value)
+ .fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))?;
+ let sapling_total = sapling_inputs
+ .iter()
+ .flat_map(|s_in| s_in.notes().iter())
+ .map(|out| out.value())
+ .fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))?;
+ let input_total = (transparent_total + sapling_total).ok_or(())?;
+
+ let output_total = (transaction_request.total()? + balance.total()).ok_or(())?;
+
+ if input_total == output_total {
+ Ok(Self {
+ transaction_request,
+ transparent_inputs,
+ sapling_inputs,
+ balance,
+ fee_rule,
+ min_target_height,
+ is_shielding,
+ })
+ } else {
+ Err(())
+ }
+ }
+
/// Returns the transaction request that describes the payments to be made.
pub fn transaction_request(&self) -> &TransactionRequest {
&self.transaction_request
@@ -147,12 +187,24 @@ impl Debug for Proposal {
}
/// The Sapling inputs to a proposed transaction.
+#[derive(Clone, PartialEq, Eq)]
pub struct SaplingInputs {
anchor_height: BlockHeight,
notes: NonEmpty>,
}
impl SaplingInputs {
+ /// Constructs a [`SaplingInputs`] from its constituent parts.
+ pub fn from_parts(
+ anchor_height: BlockHeight,
+ notes: NonEmpty>,
+ ) -> Self {
+ Self {
+ anchor_height,
+ notes,
+ }
+ }
+
/// Returns the anchor height for Sapling inputs that should be used when constructing the
/// proposed transaction.
pub fn anchor_height(&self) -> BlockHeight {
diff --git a/zcash_client_backend/src/fees.rs b/zcash_client_backend/src/fees.rs
index a10a119e33..cf52956c72 100644
--- a/zcash_client_backend/src/fees.rs
+++ b/zcash_client_backend/src/fees.rs
@@ -54,6 +54,9 @@ impl ChangeValue {
pub struct TransactionBalance {
proposed_change: Vec,
fee_required: NonNegativeAmount,
+
+ // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we
+ // cache the resulting value.
total: NonNegativeAmount,
}
diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs
index a440750fb0..445a3c4ba3 100644
--- a/zcash_client_backend/src/proto.rs
+++ b/zcash_client_backend/src/proto.rs
@@ -3,16 +3,32 @@
use std::io;
use incrementalmerkletree::frontier::CommitmentTree;
+
+use nonempty::NonEmpty;
use zcash_primitives::{
block::{BlockHash, BlockHeader},
- consensus::BlockHeight,
+ consensus::{self, BlockHeight, Parameters},
+ memo::{self, MemoBytes},
merkle_tree::read_commitment_tree,
sapling::{note::ExtractedNoteCommitment, Node, Nullifier, NOTE_COMMITMENT_TREE_DEPTH},
- transaction::{components::sapling, TxId},
+ transaction::{
+ components::{amount::NonNegativeAmount, sapling, OutPoint},
+ fees::StandardFeeRule,
+ TxId,
+ },
};
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
+use crate::{
+ data_api::{
+ wallet::input_selection::{Proposal, SaplingInputs},
+ PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
+ },
+ fees::{ChangeValue, TransactionBalance},
+ zip321::{TransactionRequest, Zip321Error},
+};
+
#[rustfmt::skip]
#[allow(unknown_lints)]
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -182,3 +198,223 @@ impl service::TreeState {
read_commitment_tree::(&sapling_tree_bytes[..])
}
}
+
+pub const PROPOSAL_SER_V1: u32 = 1;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProposalError {
+ Zip321(Zip321Error),
+ TxIdInvalid(Vec),
+ InputRetrieval(DbError),
+ InputNotFound(TxId, PoolType, u32),
+ BalanceInvalid,
+ MemoInvalid(memo::Error),
+ VersionInvalid(u32),
+ ZeroMinConf,
+ FeeRuleNotSpecified,
+}
+
+impl From for ProposalError {
+ fn from(value: Zip321Error) -> Self {
+ Self::Zip321(value)
+ }
+}
+
+impl proposal::ProposedInput {
+ pub fn parse_txid(&self) -> Result> {
+ Ok(TxId::from_bytes(self.txid.clone().try_into()?))
+ }
+}
+
+impl proposal::Proposal {
+ /// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
+ /// representation.
+ pub fn from_standard_proposal(
+ params: &P,
+ value: &Proposal,
+ ) -> Option {
+ let transaction_request = value.transaction_request().to_uri(params)?;
+
+ let transparent_inputs = value
+ .transparent_inputs()
+ .iter()
+ .map(|utxo| proposal::ProposedInput {
+ txid: utxo.outpoint().hash().to_vec(),
+ index: utxo.outpoint().n(),
+ value: utxo.txout().value.into(),
+ })
+ .collect();
+
+ let sapling_inputs = value
+ .sapling_inputs()
+ .map(|sapling_inputs| proposal::SaplingInputs {
+ anchor_height: sapling_inputs.anchor_height().into(),
+ inputs: sapling_inputs
+ .notes()
+ .iter()
+ .map(|rec_note| proposal::ProposedInput {
+ txid: rec_note.txid().as_ref().to_vec(),
+ index: rec_note.output_index().into(),
+ value: rec_note.value().into(),
+ })
+ .collect(),
+ });
+
+ let balance = Some(proposal::TransactionBalance {
+ proposed_change: value
+ .balance()
+ .proposed_change()
+ .iter()
+ .map(|cv| match cv {
+ ChangeValue::Sapling { value, memo } => proposal::ChangeValue {
+ value: Some(proposal::change_value::Value::SaplingValue(
+ proposal::SaplingChange {
+ amount: (*value).into(),
+ memo: memo.as_ref().map(|memo_bytes| proposal::MemoBytes {
+ value: memo_bytes.as_slice().to_vec(),
+ }),
+ },
+ )),
+ },
+ })
+ .collect(),
+ fee_required: value.balance().fee_required().into(),
+ });
+
+ #[allow(deprecated)]
+ Some(proposal::Proposal {
+ proto_version: PROPOSAL_SER_V1,
+ transaction_request,
+ transparent_inputs,
+ sapling_inputs,
+ balance,
+ fee_rule: match value.fee_rule() {
+ StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
+ StandardFeeRule::Zip313 => proposal::FeeRule::Zip313,
+ StandardFeeRule::Zip317 => proposal::FeeRule::Zip317,
+ }
+ .into(),
+ min_target_height: value.min_target_height().into(),
+ is_shielding: value.is_shielding(),
+ })
+ }
+
+ /// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
+ /// protobuf representation.
+ pub fn try_into_standard_proposal(
+ &self,
+ params: &P,
+ wallet_db: &DbT,
+ ) -> Result, ProposalError>
+ where
+ DbT: TransparentInputSource + SaplingInputSource,
+ {
+ match self.proto_version {
+ PROPOSAL_SER_V1 => {
+ #[allow(deprecated)]
+ let fee_rule = match self.fee_rule() {
+ proposal::FeeRule::PreZip313 => StandardFeeRule::PreZip313,
+ proposal::FeeRule::Zip313 => StandardFeeRule::Zip313,
+ proposal::FeeRule::Zip317 => StandardFeeRule::Zip317,
+ proposal::FeeRule::NotSpecified => {
+ return Err(ProposalError::FeeRuleNotSpecified);
+ }
+ };
+
+ let transaction_request =
+ TransactionRequest::from_uri(params, &self.transaction_request)?;
+ let transparent_inputs = self
+ .transparent_inputs
+ .iter()
+ .map(|t_in| {
+ let txid = t_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
+ let outpoint = OutPoint::new(txid.into(), t_in.index);
+
+ wallet_db
+ .get_unspent_transparent_output(&outpoint)
+ .map_err(ProposalError::InputRetrieval)?
+ .ok_or_else(|| {
+ ProposalError::InputNotFound(
+ txid,
+ PoolType::Transparent,
+ t_in.index,
+ )
+ })
+ })
+ .collect::, _>>()?;
+
+ let sapling_inputs = self.sapling_inputs.as_ref().and_then(|s_in| {
+ s_in.inputs
+ .iter()
+ .map(|s_in| {
+ let txid = s_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
+
+ wallet_db
+ .get_spendable_sapling_note(&txid, s_in.index)
+ .map_err(ProposalError::InputRetrieval)
+ .and_then(|opt| {
+ opt.ok_or_else(|| {
+ ProposalError::InputNotFound(
+ txid,
+ PoolType::Shielded(ShieldedProtocol::Sapling),
+ s_in.index,
+ )
+ })
+ })
+ })
+ .collect::, _>>()
+ .map(|notes| {
+ NonEmpty::from_vec(notes).map(|notes| {
+ SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
+ })
+ })
+ .transpose()
+ });
+
+ let balance = self.balance.as_ref().ok_or(ProposalError::BalanceInvalid)?;
+ let balance = TransactionBalance::new(
+ balance
+ .proposed_change
+ .iter()
+ .filter_map(|cv| {
+ cv.value
+ .as_ref()
+ .map(|cv| -> Result> {
+ match cv {
+ proposal::change_value::Value::SaplingValue(sc) => {
+ Ok(ChangeValue::sapling(
+ NonNegativeAmount::from_u64(sc.amount)
+ .map_err(|_| ProposalError::BalanceInvalid)?,
+ sc.memo
+ .as_ref()
+ .map(|bytes| {
+ MemoBytes::from_bytes(&bytes.value)
+ .map_err(ProposalError::MemoInvalid)
+ })
+ .transpose()?,
+ ))
+ }
+ }
+ })
+ })
+ .collect::, _>>()?,
+ NonNegativeAmount::from_u64(balance.fee_required)
+ .map_err(|_| ProposalError::BalanceInvalid)?,
+ )
+ .map_err(|_| ProposalError::BalanceInvalid)?;
+
+ Proposal::from_parts(
+ transaction_request,
+ transparent_inputs,
+ sapling_inputs.transpose()?,
+ balance,
+ fee_rule,
+ self.min_target_height.into(),
+ self.is_shielding,
+ )
+ .map_err(|_| ProposalError::BalanceInvalid)
+ }
+ other => Err(ProposalError::VersionInvalid(other)),
+ }
+ }
+}
diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs
index 63863f2e07..35b32fffca 100644
--- a/zcash_client_backend/src/wallet.rs
+++ b/zcash_client_backend/src/wallet.rs
@@ -29,7 +29,7 @@ pub struct WalletTx {
pub sapling_outputs: Vec>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletTransparentOutput {
outpoint: OutPoint,
txout: TxOut,
@@ -175,7 +175,7 @@ impl WalletSaplingOutput {
/// 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)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedSaplingNote {
note_id: NoteRef,
txid: TxId,
diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs
index 28523f677a..06d18e8d24 100644
--- a/zcash_client_backend/src/zip321.rs
+++ b/zcash_client_backend/src/zip321.rs
@@ -24,7 +24,7 @@ use std::cmp::Ordering;
use crate::address::RecipientAddress;
/// Errors that may be produced in decoding of payment requests.
-#[derive(Debug)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Zip321Error {
/// A memo field in the ZIP 321 URI was not properly base-64 encoded
InvalidBase64(base64::DecodeError),
@@ -63,7 +63,7 @@ pub fn memo_from_base64(s: &str) -> Result {
}
/// A single payment being requested.
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Payment {
/// The payment address to which the payment should be sent.
pub recipient_address: RecipientAddress,
@@ -121,7 +121,7 @@ impl Payment {
/// When constructing a transaction in response to such a request,
/// a separate output should be added to the transaction for each
/// payment value in the request.
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionRequest {
payments: Vec,
}
@@ -152,6 +152,21 @@ impl TransactionRequest {
&self.payments[..]
}
+ /// Returns the total value of payments to be made.
+ ///
+ /// Returns `Err` in the case of overflow, if payment values are negative, or the value is
+ /// outside the valid range of Zcash values. .
+ pub fn total(&self) -> Result {
+ if self.payments.is_empty() {
+ Ok(NonNegativeAmount::ZERO)
+ } else {
+ self.payments
+ .iter()
+ .map(|p| p.amount)
+ .fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
+ }
+ }
+
/// A utility for use in tests to help check round-trip serialization properties.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize(&mut self, params: &P) {
@@ -416,7 +431,7 @@ mod parse {
/// A data type that defines the possible parameter types which may occur within a
/// ZIP 321 URI.
- #[derive(Debug, PartialEq, Eq)]
+ #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Param {
Addr(Box),
Amount(NonNegativeAmount),
@@ -427,7 +442,7 @@ mod parse {
}
/// A [`Param`] value with its associated index.
- #[derive(Debug)]
+ #[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedParam {
pub param: Param,
pub payment_index: usize,
diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs
index 047c1d6ea1..85d0500435 100644
--- a/zcash_client_sqlite/src/lib.rs
+++ b/zcash_client_sqlite/src/lib.rs
@@ -173,6 +173,14 @@ impl, P: consensus::Parameters> SaplingInputSour
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
+ fn get_spendable_sapling_note(
+ &self,
+ txid: &TxId,
+ index: u32,
+ ) -> Result>, Self::Error> {
+ wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), txid, index)
+ }
+
fn select_spendable_sapling_notes(
&self,
account: AccountId,
@@ -196,6 +204,19 @@ impl, P: consensus::Parameters> TransparentInput
{
type Error = SqliteClientError;
+ fn get_unspent_transparent_output(
+ &self,
+ _outpoint: &OutPoint,
+ ) -> Result, Self::Error> {
+ #[cfg(feature = "transparent-inputs")]
+ return wallet::get_unspent_transparent_output(self.conn.borrow(), _outpoint);
+
+ #[cfg(not(feature = "transparent-inputs"))]
+ panic!(
+ "The wallet must be compiled with the transparent-inputs feature to use this method."
+ );
+ }
+
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs
index aa624c5425..57927e2ad5 100644
--- a/zcash_client_sqlite/src/testing.rs
+++ b/zcash_client_sqlite/src/testing.rs
@@ -14,7 +14,6 @@ use tempfile::NamedTempFile;
#[cfg(feature = "unstable")]
use tempfile::TempDir;
-use zcash_client_backend::fees::{standard, DustOutputPolicy};
#[allow(deprecated)]
use zcash_client_backend::{
address::RecipientAddress,
@@ -37,6 +36,10 @@ use zcash_client_backend::{
wallet::OvkPolicy,
zip321,
};
+use zcash_client_backend::{
+ fees::{standard, DustOutputPolicy},
+ proto::proposal,
+};
use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
@@ -555,7 +558,7 @@ impl TestState {
>,
> {
let params = self.network();
- propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
+ let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
&mut self.db_data,
¶ms,
fee_rule,
@@ -565,7 +568,13 @@ impl TestState {
amount,
memo,
change_memo,
- )
+ );
+
+ if let Ok(proposal) = &result {
+ check_proposal_serialization_roundtrip(self.wallet(), proposal);
+ }
+
+ result
}
/// Invokes [`propose_shielding`] with the given arguments.
@@ -1051,3 +1060,15 @@ pub(crate) fn input_selector(
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
}
+
+pub(crate) fn check_proposal_serialization_roundtrip(
+ db_data: &WalletDb,
+ proposal: &Proposal,
+) {
+ let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
+ assert_matches!(proposal_proto, Some(_));
+ let deserialized_proposal = proposal_proto
+ .unwrap()
+ .try_into_standard_proposal(&db_data.params, db_data);
+ assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
+}
diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs
index eaa45485f1..c30450d318 100644
--- a/zcash_client_sqlite/src/wallet.rs
+++ b/zcash_client_sqlite/src/wallet.rs
@@ -110,6 +110,7 @@ use self::scanning::{parse_priority_code, replace_queue_entries};
#[cfg(feature = "transparent-inputs")]
use {
crate::UtxoId,
+ rusqlite::Row,
std::collections::BTreeSet,
zcash_client_backend::{address::AddressMetadata, wallet::WalletTransparentOutput},
zcash_primitives::{
@@ -1267,6 +1268,65 @@ pub(crate) fn truncate_to_height(
Ok(())
}
+#[cfg(feature = "transparent-inputs")]
+fn to_unspent_transparent_output(row: &Row) -> Result {
+ let txid: Vec = row.get(0)?;
+ let mut txid_bytes = [0u8; 32];
+ txid_bytes.copy_from_slice(&txid);
+
+ let index: u32 = row.get(1)?;
+ let script_pubkey = Script(row.get(2)?);
+ let raw_value: i64 = row.get(3)?;
+ let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| {
+ SqliteClientError::CorruptedData(format!("Invalid UTXO value: {}", raw_value))
+ })?;
+ let height: u32 = row.get(4)?;
+
+ let outpoint = OutPoint::new(txid_bytes, index);
+ WalletTransparentOutput::from_parts(
+ outpoint,
+ TxOut {
+ value,
+ script_pubkey,
+ },
+ BlockHeight::from(height),
+ )
+ .ok_or_else(|| {
+ SqliteClientError::CorruptedData(
+ "Txout script_pubkey value did not correspond to a P2PKH or P2SH address".to_string(),
+ )
+ })
+}
+
+#[cfg(feature = "transparent-inputs")]
+pub(crate) fn get_unspent_transparent_output(
+ conn: &rusqlite::Connection,
+ outpoint: &OutPoint,
+) -> Result, SqliteClientError> {
+ let mut stmt_select_utxo = conn.prepare_cached(
+ "SELECT u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height
+ FROM utxos u
+ LEFT OUTER JOIN transactions tx
+ ON tx.id_tx = u.spent_in_tx
+ WHERE u.prevout_txid = :txid
+ AND u.prevout_idx = :output_index
+ AND tx.block IS NULL",
+ )?;
+
+ let result: Result , SqliteClientError> = stmt_select_utxo
+ .query_and_then(
+ named_params![
+ ":txid": outpoint.hash(),
+ ":output_index": outpoint.n()
+ ],
+ to_unspent_transparent_output,
+ )?
+ .next()
+ .transpose();
+
+ result
+}
+
/// Returns unspent transparent outputs that have been received by this wallet at the given
/// transparent address, such that the block that included the transaction was mined at a
/// height less than or equal to the provided `max_height`.
@@ -1283,7 +1343,7 @@ pub(crate) fn get_unspent_transparent_outputs(
.unwrap_or(max_height)
.saturating_sub(PRUNING_DEPTH);
- let mut stmt_blocks = conn.prepare(
+ let mut stmt_utxos = conn.prepare(
"SELECT u.prevout_txid, u.prevout_idx, u.script,
u.value_zat, u.height
FROM utxos u
@@ -1297,45 +1357,18 @@ pub(crate) fn get_unspent_transparent_outputs(
let addr_str = address.encode(params);
let mut utxos = Vec::::new();
- let mut rows = stmt_blocks.query(named_params![
+ let mut rows = stmt_utxos.query(named_params![
":address": addr_str,
":max_height": u32::from(max_height),
":stable_height": u32::from(stable_height),
])?;
let excluded: BTreeSet = exclude.iter().cloned().collect();
while let Some(row) = rows.next()? {
- let txid: Vec = row.get(0)?;
- let mut txid_bytes = [0u8; 32];
- txid_bytes.copy_from_slice(&txid);
-
- let index: u32 = row.get(1)?;
- let script_pubkey = Script(row.get(2)?);
- let value_raw: i64 = row.get(3)?;
- let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
- SqliteClientError::CorruptedData(format!("Negative utxo value: {}", value_raw))
- })?;
- let height: u32 = row.get(4)?;
-
- let outpoint = OutPoint::new(txid_bytes, index);
- if excluded.contains(&outpoint) {
+ let output = to_unspent_transparent_output(row)?;
+ if excluded.contains(output.outpoint()) {
continue;
}
- let output = WalletTransparentOutput::from_parts(
- outpoint,
- TxOut {
- value,
- script_pubkey,
- },
- BlockHeight::from(height),
- )
- .ok_or_else(|| {
- SqliteClientError::CorruptedData(
- "Txout script_pubkey value did not correspond to a P2PKH or P2SH address"
- .to_string(),
- )
- })?;
-
utxos.push(output);
}
diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs
index 970e9e33dc..ee53c1ba2a 100644
--- a/zcash_client_sqlite/src/wallet/sapling.rs
+++ b/zcash_client_sqlite/src/wallet/sapling.rs
@@ -135,6 +135,38 @@ fn to_spendable_note(row: &Row) -> Result, S
))
}
+// The `clippy::let_and_return` lint is explicitly allowed here because a bug in the Rust compiler
+// (https://github.com/rust-lang/rust/issues/114633) 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(
+ conn: &Connection,
+ txid: &TxId,
+ index: u32,
+) -> Result>, SqliteClientError> {
+ let mut stmt_select_note = conn.prepare_cached(
+ "SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
+ FROM sapling_received_notes
+ INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
+ WHERE txid = :txid
+ AND output_index = :output_index
+ AND spent IS NULL",
+ )?;
+
+ let result = stmt_select_note
+ .query_and_then(
+ named_params![
+ ":txid": txid.as_ref(),
+ ":output_index": index,
+ ],
+ to_spendable_note,
+ )?
+ .next()
+ .transpose();
+
+ result
+}
+
/// Utility method for determining whether we have any spendable notes
///
/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to
diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md
index 52c64dc4d7..2a0a0823ad 100644
--- a/zcash_primitives/CHANGELOG.md
+++ b/zcash_primitives/CHANGELOG.md
@@ -60,6 +60,9 @@ and this library adheres to Rust's notion of
- `impl From for zcash_primitives::sapling::value::NoteValue`
- `impl Sum for Option`
- `impl<'a> Sum<&'a NonNegativeAmount> for Option`
+- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
+- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
+- `impl From for [u8; 32]`
### Changed
- `zcash_primitives::sapling`:
diff --git a/zcash_primitives/src/memo.rs b/zcash_primitives/src/memo.rs
index 8143eec69d..c6e97b66f7 100644
--- a/zcash_primitives/src/memo.rs
+++ b/zcash_primitives/src/memo.rs
@@ -28,7 +28,7 @@ where
}
/// Errors that may result from attempting to construct an invalid memo.
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
InvalidUtf8(std::str::Utf8Error),
TooLong(usize),
diff --git a/zcash_primitives/src/sapling/note.rs b/zcash_primitives/src/sapling/note.rs
index 2e5b21bc6b..f92521a8ec 100644
--- a/zcash_primitives/src/sapling/note.rs
+++ b/zcash_primitives/src/sapling/note.rs
@@ -16,7 +16,7 @@ pub(super) mod nullifier;
/// Before ZIP 212, the note commitment trapdoor `rcm` must be a scalar value.
/// After ZIP 212, the note randomness `rseed` is a 32-byte sequence, used to derive
/// both the note commitment trapdoor `rcm` and the ephemeral private key `esk`.
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Rseed {
BeforeZip212(jubjub::Fr),
AfterZip212([u8; 32]),