Merge pull request #891 from nuttycom/proposal-ffi
Add protobuf representation for transaction proposals
This commit is contained in:
commit
236cd569ee
|
@ -1,2 +1,3 @@
|
|||
zcash_client_backend/src/proto/compact_formats.rs linguist-generated=true
|
||||
zcash_client_backend/src/proto/service.rs linguist-generated=true
|
||||
zcash_client_backend/src/proto/proposal.rs linguist-generated=true
|
||||
|
|
|
@ -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::zip321::parse::Param::name`
|
||||
- `zcash_client_backend::proto::`
|
||||
- `PROPOSAL_SER_V1`
|
||||
- `ProposalError`
|
||||
- `proposal` module, for parsing and serializing transaction proposals.
|
||||
- `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
|
||||
|
|
|
@ -4,6 +4,9 @@ use std::io;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
const COMPACT_FORMATS_PROTO: &str = "proto/compact_formats.proto";
|
||||
|
||||
const PROPOSAL_PROTO: &str = "proto/proposal.proto";
|
||||
|
||||
const SERVICE_PROTO: &str = "proto/service.proto";
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
|
@ -71,6 +74,15 @@ fn build() -> io::Result<()> {
|
|||
)
|
||||
.compile(&[SERVICE_PROTO], &["proto/"])?;
|
||||
|
||||
// Build the proposal types.
|
||||
tonic_build::compile_protos(PROPOSAL_PROTO)?;
|
||||
|
||||
// Copy the generated types into the source tree so changes can be committed.
|
||||
fs::copy(
|
||||
out.join("cash.z.wallet.sdk.ffi.rs"),
|
||||
"src/proto/proposal.rs",
|
||||
)?;
|
||||
|
||||
// Copy the generated types into the source tree so changes can be committed. The
|
||||
// file has the same name as for the compact format types because they have the
|
||||
// same package, but we've set things up so this only contains the service types.
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2023 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.ffi;
|
||||
|
||||
// A data structure that describes the inputs to be consumed and outputs to
|
||||
// be produced in a proposed transaction.
|
||||
message Proposal {
|
||||
uint32 protoVersion = 1;
|
||||
// ZIP 321 serialized transaction request
|
||||
string transactionRequest = 2;
|
||||
// The transparent UTXOs to use as inputs to the transaction.
|
||||
repeated ProposedInput transparentInputs = 3;
|
||||
// The Sapling input notes and anchor height to be used in creating the transaction.
|
||||
SaplingInputs saplingInputs = 4;
|
||||
// The total value, fee amount, and change outputs of the proposed
|
||||
// transaction
|
||||
TransactionBalance balance = 5;
|
||||
// The fee rule used in constructing this proposal
|
||||
FeeRule feeRule = 6;
|
||||
// The target height for which the proposal was constructed
|
||||
//
|
||||
// The chain must contain at least this many blocks in order for the proposal to
|
||||
// be executed.
|
||||
uint32 minTargetHeight = 7;
|
||||
// A flag indicating whether the proposal is for a shielding transaction,
|
||||
// used for determining which OVK to select for wallet-internal outputs.
|
||||
bool isShielding = 8;
|
||||
}
|
||||
|
||||
message SaplingInputs {
|
||||
// The Sapling anchor height to be used in creating the transaction
|
||||
uint32 anchorHeight = 1;
|
||||
// The unique identifier and amount for each proposed Sapling input
|
||||
repeated ProposedInput inputs = 2;
|
||||
}
|
||||
|
||||
// The unique identifier and amount for each proposed input.
|
||||
message ProposedInput {
|
||||
bytes txid = 1;
|
||||
uint32 index = 2;
|
||||
uint64 value = 3;
|
||||
}
|
||||
|
||||
// The fee rule used in constructing a Proposal
|
||||
enum FeeRule {
|
||||
// Protobuf requires that enums have a zero discriminant as the default
|
||||
// value. However, we need to require that a known fee rule is selected,
|
||||
// and we do not want to fall back to any default, so sending the
|
||||
// FeeRuleNotSpecified value will be treated as an error.
|
||||
FeeRuleNotSpecified = 0;
|
||||
// 10000 ZAT
|
||||
PreZip313 = 1;
|
||||
// 1000 ZAT
|
||||
Zip313 = 2;
|
||||
// MAX(10000, 5000 * logical_actions) ZAT
|
||||
Zip317 = 3;
|
||||
}
|
||||
|
||||
// The proposed change outputs and fee amount.
|
||||
message TransactionBalance {
|
||||
repeated ChangeValue proposedChange = 1;
|
||||
uint64 feeRequired = 2;
|
||||
}
|
||||
|
||||
// An enumeration of change value types.
|
||||
message ChangeValue {
|
||||
oneof value {
|
||||
SaplingChange saplingValue = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// An object wrapper for memo bytes, to facilitate representing the
|
||||
// `change_memo == None` case.
|
||||
message MemoBytes {
|
||||
bytes value = 1;
|
||||
}
|
||||
|
||||
// The amount and memo for a proposed Sapling change output.
|
||||
message SaplingChange {
|
||||
uint64 amount = 1;
|
||||
MemoBytes memo = 2;
|
||||
}
|
||||
|
|
@ -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,17 @@ pub trait SaplingInputSource {
|
|||
/// or a UUID.
|
||||
type NoteRef: Copy + Debug + Eq + Ord;
|
||||
|
||||
/// Fetches a spendable Sapling note by indexing into the specified transaction's
|
||||
/// [`sapling::Bundle::shielded_outputs`].
|
||||
///
|
||||
/// Returns `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 +246,19 @@ 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;
|
||||
|
||||
/// Fetches a spendable transparent output.
|
||||
///
|
||||
/// Returns `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 +1027,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 +1050,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,
|
||||
|
|
|
@ -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::{
|
||||
|
@ -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>,
|
||||
|
@ -89,7 +90,119 @@ pub struct Proposal<FeeRuleT, NoteRef> {
|
|||
is_shielding: bool,
|
||||
}
|
||||
|
||||
/// Errors that can occur in construction of a [`Proposal`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalError {
|
||||
/// 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,
|
||||
},
|
||||
/// 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.
|
||||
///
|
||||
/// This operation validates the proposal for balance consistency and agreement between
|
||||
/// the `is_shielding` flag and the structure of the proposal.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub 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, ProposalError> {
|
||||
let transparent_input_total = transparent_inputs
|
||||
.iter()
|
||||
.map(|out| out.txout().value)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(ProposalError::Overflow)
|
||||
})?;
|
||||
let sapling_input_total = sapling_inputs
|
||||
.iter()
|
||||
.flat_map(|s_in| s_in.notes().iter())
|
||||
.map(|out| out.value())
|
||||
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
let input_total =
|
||||
(transparent_input_total + sapling_input_total).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
.map_err(|_| ProposalError::RequestTotalInvalid)?;
|
||||
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
if is_shielding
|
||||
&& (transparent_input_total == NonNegativeAmount::ZERO
|
||||
|| sapling_input_total > NonNegativeAmount::ZERO
|
||||
|| request_total > NonNegativeAmount::ZERO)
|
||||
{
|
||||
return Err(ProposalError::ShieldingInvalid);
|
||||
}
|
||||
|
||||
if input_total == output_total {
|
||||
Ok(Self {
|
||||
transaction_request,
|
||||
transparent_inputs,
|
||||
sapling_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
is_shielding,
|
||||
})
|
||||
} else {
|
||||
Err(ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the transaction request that describes the payments to be made.
|
||||
pub fn transaction_request(&self) -> &TransactionRequest {
|
||||
&self.transaction_request
|
||||
|
@ -147,12 +260,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,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,48 @@
|
|||
//! Generated code for handling light client protobuf structs.
|
||||
|
||||
use std::io;
|
||||
use std::{
|
||||
array::TryFromSliceError,
|
||||
fmt::{self, Display},
|
||||
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, ProposalError, SaplingInputs},
|
||||
PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
|
||||
},
|
||||
fees::{ChangeValue, TransactionBalance},
|
||||
zip321::{TransactionRequest, Zip321Error},
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(unknown_lints)]
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
pub mod compact_formats;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(unknown_lints)]
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
pub mod proposal;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(unknown_lints)]
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
@ -183,3 +208,305 @@ impl service::TreeState {
|
|||
read_commitment_tree::<Node, _, NOTE_COMMITMENT_TREE_DEPTH>(&sapling_tree_bytes[..])
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 retrieve an 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),
|
||||
/// The proposal did not correctly specify a standard fee rule.
|
||||
FeeRuleNotSpecified,
|
||||
/// The proposal violated balance or structural constraints.
|
||||
ProposalInvalid(ProposalError),
|
||||
/// An inputs field for the given protocol was present, but contained no input note references.
|
||||
EmptyShieldedInputs(ShieldedProtocol),
|
||||
}
|
||||
|
||||
impl<E> From<Zip321Error> for ProposalDecodingError<E> {
|
||||
fn from(value: Zip321Error) -> Self {
|
||||
Self::Zip321(value)
|
||||
}
|
||||
}
|
||||
|
||||
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 input: {}",
|
||||
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::EmptyShieldedInputs(protocol) => write!(
|
||||
f,
|
||||
"An inputs field was present for {:?}, but contained no note references.",
|
||||
protocol
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()?))
|
||||
}
|
||||
}
|
||||
|
||||
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(|change| match change {
|
||||
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>, ProposalDecodingError<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(ProposalDecodingError::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(ProposalDecodingError::TxIdInvalid)?;
|
||||
let outpoint = OutPoint::new(txid.into(), t_in.index);
|
||||
|
||||
wallet_db
|
||||
.get_unspent_transparent_output(&outpoint)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)?
|
||||
.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
t_in.index,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let sapling_inputs = self.sapling_inputs.as_ref().map(|s_in| {
|
||||
s_in.inputs
|
||||
.iter()
|
||||
.map(|s_in| {
|
||||
let txid = s_in
|
||||
.parse_txid()
|
||||
.map_err(ProposalDecodingError::TxIdInvalid)?;
|
||||
|
||||
wallet_db
|
||||
.get_spendable_sapling_note(&txid, s_in.index)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
s_in.index,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.and_then(|notes| {
|
||||
NonEmpty::from_vec(notes)
|
||||
.map(|notes| {
|
||||
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
|
||||
})
|
||||
.ok_or({
|
||||
ProposalDecodingError::EmptyShieldedInputs(
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let proto_balance = self
|
||||
.balance
|
||||
.as_ref()
|
||||
.ok_or(ProposalDecodingError::BalanceInvalid)?;
|
||||
let balance = TransactionBalance::new(
|
||||
proto_balance
|
||||
.proposed_change
|
||||
.iter()
|
||||
.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) => {
|
||||
Ok(ChangeValue::sapling(
|
||||
NonNegativeAmount::from_u64(sc.amount).map_err(
|
||||
|_| ProposalDecodingError::BalanceInvalid,
|
||||
)?,
|
||||
sc.memo
|
||||
.as_ref()
|
||||
.map(|bytes| {
|
||||
MemoBytes::from_bytes(&bytes.value).map_err(
|
||||
ProposalDecodingError::MemoInvalid,
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
NonNegativeAmount::from_u64(proto_balance.fee_required)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
|
||||
)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
|
||||
|
||||
Proposal::from_parts(
|
||||
transaction_request,
|
||||
transparent_inputs,
|
||||
sapling_inputs.transpose()?,
|
||||
balance,
|
||||
fee_rule,
|
||||
self.min_target_height.into(),
|
||||
self.is_shielding,
|
||||
)
|
||||
.map_err(ProposalDecodingError::ProposalInvalid)
|
||||
}
|
||||
other => Err(ProposalDecodingError::VersionInvalid(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/// A data structure that describes the inputs to be consumed and outputs to
|
||||
/// be produced in a proposed transaction.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Proposal {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub proto_version: u32,
|
||||
/// ZIP 321 serialized transaction request
|
||||
#[prost(string, tag = "2")]
|
||||
pub transaction_request: ::prost::alloc::string::String,
|
||||
/// The transparent UTXOs to use as inputs to the transaction.
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub transparent_inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
/// The Sapling input notes and anchor height to be used in creating the transaction.
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub sapling_inputs: ::core::option::Option<SaplingInputs>,
|
||||
/// The total value, fee amount, and change outputs of the proposed
|
||||
/// transaction
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub balance: ::core::option::Option<TransactionBalance>,
|
||||
/// The fee rule used in constructing this proposal
|
||||
#[prost(enumeration = "FeeRule", tag = "6")]
|
||||
pub fee_rule: i32,
|
||||
/// The target height for which the proposal was constructed
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
#[prost(uint32, tag = "7")]
|
||||
pub min_target_height: u32,
|
||||
/// A flag indicating whether the proposal is for a shielding transaction,
|
||||
/// used for determining which OVK to select for wallet-internal outputs.
|
||||
#[prost(bool, tag = "8")]
|
||||
pub is_shielding: bool,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SaplingInputs {
|
||||
/// Returns the Sapling anchor height to be used in creating the transaction.
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub anchor_height: u32,
|
||||
/// The unique identifier and amount for each proposed Sapling input
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
}
|
||||
/// The unique identifier and amount for each proposed input.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProposedInput {
|
||||
#[prost(bytes = "vec", tag = "1")]
|
||||
pub txid: ::prost::alloc::vec::Vec<u8>,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub index: u32,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub value: u64,
|
||||
}
|
||||
/// The proposed change outputs and fee amount.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TransactionBalance {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub proposed_change: ::prost::alloc::vec::Vec<ChangeValue>,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub fee_required: u64,
|
||||
}
|
||||
/// An enumeration of change value types.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ChangeValue {
|
||||
#[prost(oneof = "change_value::Value", tags = "1")]
|
||||
pub value: ::core::option::Option<change_value::Value>,
|
||||
}
|
||||
/// Nested message and enum types in `ChangeValue`.
|
||||
pub mod change_value {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Value {
|
||||
#[prost(message, tag = "1")]
|
||||
SaplingValue(super::SaplingChange),
|
||||
}
|
||||
}
|
||||
/// An object wrapper for memo bytes, to facilitate representing the
|
||||
/// `change_memo == None` case.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct MemoBytes {
|
||||
#[prost(bytes = "vec", tag = "1")]
|
||||
pub value: ::prost::alloc::vec::Vec<u8>,
|
||||
}
|
||||
/// The amount and memo for a proposed Sapling change output.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SaplingChange {
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub amount: u64,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub memo: ::core::option::Option<MemoBytes>,
|
||||
}
|
||||
/// The fee rule used in constructing a Proposal
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum FeeRule {
|
||||
/// Protobuf requires that enums have a zero descriminant as the default
|
||||
/// value. However, we need to require that a known fee rule is selected,
|
||||
/// and we do not want to fall back to any default, so sending the
|
||||
/// FeeRuleNotSpecified value will be treated as an error.
|
||||
NotSpecified = 0,
|
||||
/// 10000 ZAT
|
||||
PreZip313 = 1,
|
||||
/// 1000 ZAT
|
||||
Zip313 = 2,
|
||||
/// MAX(10000, 5000 * logical_actions) ZAT
|
||||
Zip317 = 3,
|
||||
}
|
||||
impl FeeRule {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
FeeRule::NotSpecified => "FeeRuleNotSpecified",
|
||||
FeeRule::PreZip313 => "PreZip313",
|
||||
FeeRule::Zip313 => "Zip313",
|
||||
FeeRule::Zip317 => "Zip317",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"FeeRuleNotSpecified" => Some(Self::NotSpecified),
|
||||
"PreZip313" => Some(Self::PreZip313),
|
||||
"Zip313" => Some(Self::Zip313),
|
||||
"Zip317" => Some(Self::Zip317),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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::{
|
||||
|
@ -24,7 +27,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),
|
||||
|
@ -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 encoding 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
|
||||
|
@ -63,7 +111,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 +169,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 +200,21 @@ impl TransactionRequest {
|
|||
&self.payments[..]
|
||||
}
|
||||
|
||||
/// Returns the total value of payments to be made.
|
||||
///
|
||||
/// Returns `Err` in the case of overflow, or if the value is
|
||||
/// outside the range `0..=MAX_MONEY` zatoshis.
|
||||
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 +479,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),
|
||||
|
@ -426,8 +489,22 @@ 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)]
|
||||
#[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,13 @@ 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> {
|
||||
wallet::get_unspent_transparent_output(self.conn.borrow(), outpoint)
|
||||
}
|
||||
|
||||
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,17 @@ pub(crate) fn input_selector(
|
|||
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
|
||||
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>,
|
||||
) {
|
||||
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 Clippy
|
||||
// (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(
|
||||
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