zcash_client_backend: Add serialization & parsing for protobuf Proposal representation.
This commit is contained in:
parent
572563338b
commit
33169719ce
|
@ -15,6 +15,7 @@ and this library adheres to Rust's notion of
|
|||
- `ScannedBlock::sapling_tree_size`.
|
||||
- `ScannedBlock::orchard_tree_size`.
|
||||
- `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
|
||||
|
@ -23,6 +24,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`:
|
||||
|
@ -42,11 +59,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
|
||||
|
|
|
@ -17,7 +17,10 @@ use zcash_primitives::{
|
|||
memo::{Memo, MemoBytes},
|
||||
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
|
||||
transaction::{
|
||||
components::amount::{Amount, NonNegativeAmount},
|
||||
components::{
|
||||
amount::{Amount, NonNegativeAmount},
|
||||
OutPoint,
|
||||
},
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::AccountId,
|
||||
|
@ -31,9 +34,6 @@ use crate::{
|
|||
wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx},
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use zcash_primitives::transaction::components::OutPoint;
|
||||
|
||||
use self::chain::CommitmentTreeRoot;
|
||||
use self::scanning::ScanRange;
|
||||
|
||||
|
@ -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<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
|
||||
|
||||
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
|
||||
/// if possible.
|
||||
fn select_spendable_sapling_notes(
|
||||
|
@ -235,11 +243,17 @@ pub trait SaplingInputSource {
|
|||
|
||||
/// A trait representing the capability to query a data store for unspent transparent UTXOs
|
||||
/// belonging to a wallet.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
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<Option<WalletTransparentOutput>, 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(
|
||||
|
@ -1008,6 +1022,14 @@ pub mod testing {
|
|||
type Error = ();
|
||||
type NoteRef = u32;
|
||||
|
||||
fn get_spendable_sapling_note(
|
||||
&self,
|
||||
_txid: &TxId,
|
||||
_index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn select_spendable_sapling_notes(
|
||||
&self,
|
||||
_account: AccountId,
|
||||
|
@ -1023,6 +1045,13 @@ pub mod testing {
|
|||
impl TransparentInputSource for MockWalletDb {
|
||||
type Error = ();
|
||||
|
||||
fn get_unspent_transparent_output(
|
||||
&self,
|
||||
_outpoint: &OutPoint,
|
||||
) -> Result<Option<WalletTransparentOutput>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_unspent_transparent_outputs(
|
||||
&self,
|
||||
_address: &TransparentAddress,
|
||||
|
|
|
@ -79,6 +79,7 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
}
|
||||
|
||||
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Proposal<FeeRuleT, NoteRef> {
|
||||
transaction_request: TransactionRequest,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
|
@ -90,6 +91,45 @@ pub struct Proposal<FeeRuleT, NoteRef> {
|
|||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a [`Proposal`] from its constituent parts.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_parts(
|
||||
transaction_request: TransactionRequest,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
sapling_inputs: Option<SaplingInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
) -> Result<Self, ()> {
|
||||
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<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
|||
}
|
||||
|
||||
/// The Sapling inputs to a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SaplingInputs<NoteRef> {
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
|
||||
}
|
||||
|
||||
impl<NoteRef> SaplingInputs<NoteRef> {
|
||||
/// Constructs a [`SaplingInputs`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
|
||||
) -> 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 {
|
||||
|
|
|
@ -54,6 +54,9 @@ impl ChangeValue {
|
|||
pub struct TransactionBalance {
|
||||
proposed_change: Vec<ChangeValue>,
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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::{self, note::ExtractedNoteCommitment, Node, Nullifier, NOTE_COMMITMENT_TREE_DEPTH},
|
||||
transaction::TxId,
|
||||
transaction::{
|
||||
components::{amount::NonNegativeAmount, 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)]
|
||||
|
@ -188,3 +204,223 @@ impl service::TreeState {
|
|||
read_commitment_tree::<Node, _, NOTE_COMMITMENT_TREE_DEPTH>(&sapling_tree_bytes[..])
|
||||
}
|
||||
}
|
||||
|
||||
pub const PROPOSAL_SER_V1: u32 = 1;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProposalError<DbError> {
|
||||
Zip321(Zip321Error),
|
||||
TxIdInvalid(Vec<u8>),
|
||||
InputRetrieval(DbError),
|
||||
InputNotFound(TxId, PoolType, u32),
|
||||
BalanceInvalid,
|
||||
MemoInvalid(memo::Error),
|
||||
VersionInvalid(u32),
|
||||
ZeroMinConf,
|
||||
FeeRuleNotSpecified,
|
||||
}
|
||||
|
||||
impl<E> From<Zip321Error> for ProposalError<E> {
|
||||
fn from(value: Zip321Error) -> Self {
|
||||
Self::Zip321(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl proposal::ProposedInput {
|
||||
pub fn parse_txid(&self) -> Result<TxId, Vec<u8>> {
|
||||
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<P: Parameters, NoteRef>(
|
||||
params: &P,
|
||||
value: &Proposal<StandardFeeRule, NoteRef>,
|
||||
) -> Option<Self> {
|
||||
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<P: consensus::Parameters, DbT, DbError>(
|
||||
&self,
|
||||
params: &P,
|
||||
wallet_db: &DbT,
|
||||
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalError<DbError>>
|
||||
where
|
||||
DbT: TransparentInputSource<Error = DbError> + SaplingInputSource<Error = DbError>,
|
||||
{
|
||||
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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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::<Result<Vec<_>, _>>()
|
||||
.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<ChangeValue, ProposalError<_>> {
|
||||
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::<Result<Vec<_>, _>>()?,
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ pub struct WalletTx<N> {
|
|||
pub sapling_outputs: Vec<WalletSaplingOutput<N>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WalletTransparentOutput {
|
||||
outpoint: OutPoint,
|
||||
txout: TxOut,
|
||||
|
@ -175,7 +175,7 @@ impl<N> WalletSaplingOutput<N> {
|
|||
|
||||
/// 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<NoteRef> {
|
||||
note_id: NoteRef,
|
||||
txid: TxId,
|
||||
|
|
|
@ -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<MemoBytes, Zip321Error> {
|
|||
}
|
||||
|
||||
/// 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<Payment>,
|
||||
}
|
||||
|
@ -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<NonNegativeAmount, ()> {
|
||||
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<P: consensus::Parameters>(&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<RecipientAddress>),
|
||||
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,
|
||||
|
|
|
@ -173,6 +173,14 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
|
|||
type Error = SqliteClientError;
|
||||
type NoteRef = ReceivedNoteId;
|
||||
|
||||
fn get_spendable_sapling_note(
|
||||
&self,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, 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<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> TransparentInput
|
|||
{
|
||||
type Error = SqliteClientError;
|
||||
|
||||
fn get_unspent_transparent_output(
|
||||
&self,
|
||||
_outpoint: &OutPoint,
|
||||
) -> Result<Option<WalletTransparentOutput>, 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,
|
||||
|
|
|
@ -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<Cache> TestState<Cache> {
|
|||
>,
|
||||
> {
|
||||
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<Cache> TestState<Cache> {
|
|||
amount,
|
||||
memo,
|
||||
change_memo,
|
||||
)
|
||||
);
|
||||
|
||||
if let Ok(proposal) = &result {
|
||||
check_proposal_serialization_roundtrip(self.wallet(), proposal);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Invokes [`propose_shielding`] with the given arguments.
|
||||
|
@ -1061,3 +1070,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<rusqlite::Connection, Network>,
|
||||
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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::{
|
||||
|
@ -1287,6 +1288,65 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn to_unspent_transparent_output(row: &Row) -> Result<WalletTransparentOutput, SqliteClientError> {
|
||||
let txid: Vec<u8> = 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<Option<WalletTransparentOutput>, 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<Option<WalletTransparentOutput>, 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`.
|
||||
|
@ -1303,7 +1363,7 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
|||
.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
|
||||
|
@ -1317,45 +1377,18 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
|||
let addr_str = address.encode(params);
|
||||
|
||||
let mut utxos = Vec::<WalletTransparentOutput>::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<OutPoint> = exclude.iter().cloned().collect();
|
||||
while let Some(row) = rows.next()? {
|
||||
let txid: Vec<u8> = 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -135,6 +135,38 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, 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<Option<ReceivedSaplingNote<ReceivedNoteId>>, 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
|
||||
|
|
|
@ -82,6 +82,9 @@ 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 {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
|
||||
- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
|
||||
- `impl From<TxId> for [u8; 32]`
|
||||
|
||||
### Changed
|
||||
- `zcash_primitives::sapling`:
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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]),
|
||||
|
|
Loading…
Reference in New Issue