zcash_client_backend: add `Display` and `Error` impls for proposal parsing errors.
This commit is contained in:
parent
aeb405ef5d
commit
7aab6fd7a7
|
@ -81,7 +81,7 @@ where
|
|||
FE: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
match self {
|
||||
Error::DataSource(e) => {
|
||||
write!(
|
||||
f,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Types related to the process of selecting inputs to be spent given a transaction request.
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::fmt::{self, Debug, Display};
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use zcash_primitives::{
|
||||
|
@ -90,18 +90,58 @@ pub struct Proposal<FeeRuleT, NoteRef> {
|
|||
is_shielding: bool,
|
||||
}
|
||||
|
||||
/// Errors that
|
||||
/// Errors that can occur in construction of a [`Proposal`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalError {
|
||||
RequestInvalid,
|
||||
/// The total output value of the transaction request is not a valid Zcash amount.
|
||||
RequestTotalInvalid,
|
||||
/// The total of transaction inputs overflows the valid range of Zcash values.
|
||||
Overflow,
|
||||
/// The input total and output total of the payment request are not equal to one another. The
|
||||
/// sum of transaction outputs, change, and fees is required to be exactly equal to the value
|
||||
/// of provided inputs.
|
||||
BalanceError {
|
||||
input_total: NonNegativeAmount,
|
||||
output_total: NonNegativeAmount,
|
||||
},
|
||||
ShieldingFlagInvalid,
|
||||
/// The `is_shielding` flag may only be set to `true` under the following conditions:
|
||||
/// * The total of transparent inputs is nonzero
|
||||
/// * There exist no Sapling inputs
|
||||
/// * There provided transaction request is empty; i.e. the only output values specified
|
||||
/// are change and fee amounts.
|
||||
ShieldingInvalid,
|
||||
}
|
||||
|
||||
impl Display for ProposalError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProposalError::RequestTotalInvalid => write!(
|
||||
f,
|
||||
"The total requested output value is not a valid Zcash amount."
|
||||
),
|
||||
ProposalError::Overflow => write!(
|
||||
f,
|
||||
"The total of transaction inputs overflows the valid range of Zcash values."
|
||||
),
|
||||
ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
} => write!(
|
||||
f,
|
||||
"Balance error: the output total {} was not equal to the input total {}",
|
||||
u64::from(*output_total),
|
||||
u64::from(*input_total)
|
||||
),
|
||||
ProposalError::ShieldingInvalid => write!(
|
||||
f,
|
||||
"The proposal violates the rules for a shielding transaction."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProposalError {}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a validated [`Proposal`] from its constituent parts.
|
||||
///
|
||||
|
@ -135,7 +175,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
.map_err(|_| ProposalError::RequestInvalid)?;
|
||||
.map_err(|_| ProposalError::RequestTotalInvalid)?;
|
||||
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
if is_shielding
|
||||
|
@ -143,7 +183,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
|| sapling_input_total > NonNegativeAmount::ZERO
|
||||
|| request_total > NonNegativeAmount::ZERO)
|
||||
{
|
||||
return Err(ProposalError::ShieldingFlagInvalid);
|
||||
return Err(ProposalError::ShieldingInvalid);
|
||||
}
|
||||
|
||||
if input_total == output_total {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
//! Generated code for handling light client protobuf structs.
|
||||
|
||||
use std::{array::TryFromSliceError, io};
|
||||
use std::{
|
||||
array::TryFromSliceError,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
};
|
||||
|
||||
use incrementalmerkletree::frontier::CommitmentTree;
|
||||
|
||||
|
@ -205,20 +209,34 @@ impl service::TreeState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Constant for the V1 proposal serialization version.
|
||||
pub const PROPOSAL_SER_V1: u32 = 1;
|
||||
|
||||
/// Errors that can occur in the process of decoding a [`Proposal`] from its protobuf
|
||||
/// representation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalDecodingError<DbError> {
|
||||
/// The ZIP 321 transaction request URI was invalid.
|
||||
Zip321(Zip321Error),
|
||||
/// A transaction identifier string did not decode to a valid transaction ID.
|
||||
TxIdInvalid(TryFromSliceError),
|
||||
/// A failure occurred trying to retrievean unspent note or UTXO from the wallet database.
|
||||
InputRetrieval(DbError),
|
||||
/// The unspent note or UTXO corresponding to a proposal input was not found in the wallet
|
||||
/// database.
|
||||
InputNotFound(TxId, PoolType, u32),
|
||||
/// The transaction balance, or a component thereof, failed to decode correctly.
|
||||
BalanceInvalid,
|
||||
/// Failed to decode a ZIP-302 compliant memo from the provided memo bytes.
|
||||
MemoInvalid(memo::Error),
|
||||
/// The serialization version returned by the protobuf was not recognized.
|
||||
VersionInvalid(u32),
|
||||
ZeroMinConf,
|
||||
/// The proposal did not correctly specify a standard fee rule.
|
||||
FeeRuleNotSpecified,
|
||||
/// The proposal violated balance or structural constraints.
|
||||
ProposalInvalid(ProposalError),
|
||||
/// A `sapling_inputs` field was present, but contained no input note references.
|
||||
EmptySaplingInputs,
|
||||
}
|
||||
|
||||
impl<E> From<Zip321Error> for ProposalDecodingError<E> {
|
||||
|
@ -227,6 +245,55 @@ impl<E> From<Zip321Error> for ProposalDecodingError<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: Display> Display for ProposalDecodingError<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err),
|
||||
ProposalDecodingError::TxIdInvalid(err) => {
|
||||
write!(f, "Invalid transaction id: {:?}", err)
|
||||
}
|
||||
ProposalDecodingError::InputRetrieval(err) => write!(
|
||||
f,
|
||||
"An error occurred retrieving a transaction intput: {}",
|
||||
err
|
||||
),
|
||||
ProposalDecodingError::InputNotFound(txid, pool, idx) => write!(
|
||||
f,
|
||||
"No {} input found for txid {}, index {}",
|
||||
pool, txid, idx
|
||||
),
|
||||
ProposalDecodingError::BalanceInvalid => {
|
||||
write!(f, "An error occurred decoding the proposal balance.")
|
||||
}
|
||||
ProposalDecodingError::MemoInvalid(err) => {
|
||||
write!(f, "An error occurred decoding a proposed memo: {}", err)
|
||||
}
|
||||
ProposalDecodingError::VersionInvalid(v) => {
|
||||
write!(f, "Unrecognized proposal version {}", v)
|
||||
}
|
||||
ProposalDecodingError::FeeRuleNotSpecified => {
|
||||
write!(f, "Proposal did not specify a known fee rule.")
|
||||
}
|
||||
ProposalDecodingError::ProposalInvalid(err) => write!(f, "{}", err),
|
||||
ProposalDecodingError::EmptySaplingInputs => write!(
|
||||
f,
|
||||
"A `sapling_inputs` field was present, but contained no Sapling note references."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError<E> {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
ProposalDecodingError::Zip321(e) => Some(e),
|
||||
ProposalDecodingError::InputRetrieval(e) => Some(e),
|
||||
ProposalDecodingError::MemoInvalid(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl proposal::ProposedInput {
|
||||
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
|
||||
Ok(TxId::from_bytes(self.txid[..].try_into()?))
|
||||
|
@ -272,7 +339,7 @@ impl proposal::Proposal {
|
|||
.balance()
|
||||
.proposed_change()
|
||||
.iter()
|
||||
.map(|cv| match cv {
|
||||
.map(|change| match change {
|
||||
ChangeValue::Sapling { value, memo } => proposal::ChangeValue {
|
||||
value: Some(proposal::change_value::Value::SaplingValue(
|
||||
proposal::SaplingChange {
|
||||
|
@ -342,7 +409,7 @@ impl proposal::Proposal {
|
|||
wallet_db
|
||||
.get_unspent_transparent_output(&outpoint)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)?
|
||||
.ok_or_else(|| {
|
||||
.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
|
@ -352,7 +419,7 @@ impl proposal::Proposal {
|
|||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let sapling_inputs = self.sapling_inputs.as_ref().and_then(|s_in| {
|
||||
let sapling_inputs = self.sapling_inputs.as_ref().map(|s_in| {
|
||||
s_in.inputs
|
||||
.iter()
|
||||
.map(|s_in| {
|
||||
|
@ -364,7 +431,7 @@ impl proposal::Proposal {
|
|||
.get_spendable_sapling_note(&txid, s_in.index)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or_else(|| {
|
||||
opt.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
|
@ -374,12 +441,13 @@ impl proposal::Proposal {
|
|||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|notes| {
|
||||
NonEmpty::from_vec(notes).map(|notes| {
|
||||
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
|
||||
})
|
||||
.and_then(|notes| {
|
||||
NonEmpty::from_vec(notes)
|
||||
.map(|notes| {
|
||||
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
|
||||
})
|
||||
.ok_or(ProposalDecodingError::EmptySaplingInputs)
|
||||
})
|
||||
.transpose()
|
||||
});
|
||||
|
||||
let proto_balance = self
|
||||
|
@ -390,8 +458,11 @@ impl proposal::Proposal {
|
|||
proto_balance
|
||||
.proposed_change
|
||||
.iter()
|
||||
.filter_map(|cv| {
|
||||
cv.value.as_ref().map(
|
||||
.filter_map(|change| {
|
||||
// An empty `value` field can be treated as though the whole
|
||||
// `ChangeValue` was absent; this optionality is an artifact of
|
||||
// protobuf encoding.
|
||||
change.value.as_ref().map(
|
||||
|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
|
||||
match cv {
|
||||
proposal::change_value::Value::SaplingValue(sc) => {
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
//!
|
||||
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
|
||||
use core::fmt::Debug;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
|
||||
use nom::{
|
||||
|
@ -45,6 +48,51 @@ pub enum Zip321Error {
|
|||
ParseError(String),
|
||||
}
|
||||
|
||||
impl Display for Zip321Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Zip321Error::InvalidBase64(err) => {
|
||||
write!(f, "Memo value was not correctly base64 encoded: {:?}", err)
|
||||
}
|
||||
Zip321Error::MemoBytesError(err) => write!(
|
||||
f,
|
||||
"Memo exceeded maximum length or violated utf-8 encodiding restrictions: {:?}.",
|
||||
err
|
||||
),
|
||||
Zip321Error::TooManyPayments(n) => write!(
|
||||
f,
|
||||
"Cannot create a Zcash transaction containing {} payments",
|
||||
n
|
||||
),
|
||||
Zip321Error::DuplicateParameter(param, idx) => write!(
|
||||
f,
|
||||
"There is a duplicate {} parameter at index {}",
|
||||
param.name(),
|
||||
idx
|
||||
),
|
||||
Zip321Error::TransparentMemo(idx) => write!(
|
||||
f,
|
||||
"Payment {} is invalid: cannot send a memo to a transparent recipient address.",
|
||||
idx
|
||||
),
|
||||
Zip321Error::RecipientMissing(idx) => {
|
||||
write!(f, "Payment {} is missing its recipient address.", idx)
|
||||
}
|
||||
Zip321Error::ParseError(s) => write!(f, "Parse failure: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Zip321Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Zip321Error::InvalidBase64(err) => Some(err),
|
||||
Zip321Error::MemoBytesError(err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string.
|
||||
///
|
||||
/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes
|
||||
|
@ -441,6 +489,20 @@ mod parse {
|
|||
Other(String, String),
|
||||
}
|
||||
|
||||
impl Param {
|
||||
/// Returns the name of the parameter from which this value was parsed.
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
Param::Addr(_) => "address".to_owned(),
|
||||
Param::Amount(_) => "amount".to_owned(),
|
||||
Param::Memo(_) => "memo".to_owned(),
|
||||
Param::Label(_) => "label".to_owned(),
|
||||
Param::Message(_) => "message".to_owned(),
|
||||
Param::Other(name, _) => name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Param`] value with its associated index.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexedParam {
|
||||
|
|
|
@ -1071,6 +1071,8 @@ pub(crate) fn input_selector(
|
|||
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to
|
||||
// the same proposal value.
|
||||
pub(crate) fn check_proposal_serialization_roundtrip(
|
||||
db_data: &WalletDb<rusqlite::Connection, Network>,
|
||||
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
|
||||
|
|
Loading…
Reference in New Issue