Merge pull request #1060 from nuttycom/wallet/generalize_proposals
Add Orchard support to fees & transaction proposals.
This commit is contained in:
commit
c3a630bac3
|
@ -3056,6 +3056,7 @@ dependencies = [
|
|||
"incrementalmerkletree",
|
||||
"jubjub",
|
||||
"maybe-rayon",
|
||||
"orchard",
|
||||
"proptest",
|
||||
"prost",
|
||||
"rand_core",
|
||||
|
|
|
@ -22,12 +22,13 @@ and this library adheres to Rust's notion of
|
|||
add_unshielded_value
|
||||
}`
|
||||
- `wallet::propose_standard_transfer_to_address`
|
||||
- `wallet::input_selection::Proposal::from_parts`
|
||||
- `wallet::input_selection::SaplingInputs`
|
||||
- `wallet::input_selection::Proposal::{from_parts, shielded_inputs}`
|
||||
- `wallet::input_selection::ShieldedInputs`
|
||||
- `wallet::input_selection::ShieldingSelector` has been
|
||||
factored out from the `InputSelector` trait to separate out transparent
|
||||
functionality and move it behind the `transparent-inputs` feature flag.
|
||||
- `zcash_client_backend::fees::{standard, sapling}`
|
||||
- `zcash_client_backend::fees::{standard, orchard, sapling}`
|
||||
- `zcash_client_backend::fees::ChangeValue::{new, orchard}`
|
||||
- `zcash_client_backend::wallet`:
|
||||
- `Note`
|
||||
- `ReceivedNote`
|
||||
|
@ -124,26 +125,34 @@ and this library adheres to Rust's notion of
|
|||
argument, instead taking explicit `target_height` and `anchor_height`
|
||||
arguments. This helps to minimize the set of capabilities that the
|
||||
`data_api::InputSource` must expose.
|
||||
- `WalletRead::get_checkpoint_depth` has been removed without replacement. This
|
||||
- Changes to the `WalletRead` trait:
|
||||
- Added `get_orchard_nullifiers` (under the `orchard` feature flag.)
|
||||
- `get_checkpoint_depth` has been removed without replacement. This
|
||||
is no longer needed given the change to use the stored anchor height for transaction
|
||||
proposal execution.
|
||||
- `is_valid_account_extfvk` has been removed; it was unused in
|
||||
the ECC mobile wallet SDKs and has been superseded by `get_account_for_ufvk`.
|
||||
- `get_spendable_sapling_notes`, `select_spendable_sapling_notes`, and
|
||||
`get_unspent_transparent_outputs` have been removed; use
|
||||
`data_api::InputSource` instead.
|
||||
- `wallet::{propose_shielding, shield_transparent_funds}` now takes their
|
||||
`min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit
|
||||
implmentations to enable zero-conf shielding.
|
||||
- `wallet::create_proposed_transaction` now forces implementations to ignore
|
||||
the database identifiers for its contained notes by universally quantifying
|
||||
the `NoteRef` type parameter.
|
||||
- `wallet::input_selection::Proposal::sapling_inputs` now returns type
|
||||
`Option<&SaplingInputs>`.
|
||||
- Arguments to `wallet::input_selection::Proposal::from_parts` have changed.
|
||||
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in
|
||||
favor of storing this value in `SaplingInputs`.
|
||||
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
|
||||
for its `InputSource` associated type.
|
||||
|
||||
- `zcash_client_backend::fees`:
|
||||
- `ChangeValue::Sapling` is now a structured variant. In addition to the
|
||||
existing change value, it now also carries an optional memo to be associated
|
||||
with the change output.
|
||||
- `ChangeStrategy::compute_balance` arguments have changed.
|
||||
- `ChangeValue` is now a struct. In addition to the existing change value, it
|
||||
now also provides the output pool to which change should be sent and an
|
||||
optional memo to be associated with the change output.
|
||||
- `ChangeError` has a new `BundleError` variant.
|
||||
- `fixed::SingleOutputChangeStrategy::new` and
|
||||
`zip317::SingleOutputChangeStrategy::new` each now accept an additional
|
||||
`change_memo` argument.
|
||||
|
@ -157,7 +166,6 @@ and this library adheres to Rust's notion of
|
|||
- `error::Error::InsufficientFunds.{available, required}`
|
||||
- `wallet::input_selection::InputSelectorError::InsufficientFunds.{available, required}`
|
||||
- `zcash_client_backend::fees`:
|
||||
- `ChangeValue::Sapling.value`
|
||||
- `ChangeError::InsufficientFunds.{available, required}`
|
||||
- `zcash_client_backend::zip321::Payment.amount`
|
||||
- The following methods now take `NonNegativeAmount` instead of `Amount`:
|
||||
|
@ -199,14 +207,9 @@ and this library adheres to Rust's notion of
|
|||
### Removed
|
||||
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
|
||||
`zcash_client_backend::ReceivedNote`.
|
||||
- `zcash_client_backend::data_api::WalletRead::is_valid_account_extfvk` has been
|
||||
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by
|
||||
`get_account_for_ufvk`.
|
||||
- `zcash_client_backend::data_api::WalletRead::{
|
||||
get_spendable_sapling_notes
|
||||
select_spendable_sapling_notes,
|
||||
get_unspent_transparent_outputs,
|
||||
}` - use `data_api::InputSource` instead.
|
||||
- `zcash_client_backend::wallet::input_selection::Proposal::sapling_inputs` has
|
||||
been replaced by `Proposal::shielded_inputs`
|
||||
- `zcash_client_backend::data_api`
|
||||
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
|
||||
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
|
||||
replaced by `into_commitments` which returns both Sapling and Orchard note commitments
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
This library contains Rust structs and traits for creating shielded Zcash light
|
||||
clients.
|
||||
|
||||
## Building
|
||||
|
||||
Note that in order to (re)build the GRPC interface, you will need `protoc` on
|
||||
your `$PATH`. This is not required unless you make changes to any of the files
|
||||
in `./proto/`.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
|
|
@ -11,11 +11,13 @@ 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
|
||||
// The anchor height to be used in creating the transaction, if any.
|
||||
// Setting the anchor height to zero will disallow the use of any shielded
|
||||
// inputs.
|
||||
uint32 anchorHeight = 3;
|
||||
// The inputs to be used in creating the transaction.
|
||||
repeated ProposedInput inputs = 4;
|
||||
// The total value, fee value, and change outputs of the proposed
|
||||
// transaction
|
||||
TransactionBalance balance = 5;
|
||||
// The fee rule used in constructing this proposal
|
||||
|
@ -30,18 +32,26 @@ message Proposal {
|
|||
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;
|
||||
enum ValuePool {
|
||||
// Protobuf requires that enums have a zero discriminant as the default
|
||||
// value. However, we need to require that a known value pool is selected,
|
||||
// and we do not want to fall back to any default, so sending the
|
||||
// PoolNotSpecified value will be treated as an error.
|
||||
PoolNotSpecified = 0;
|
||||
// The transparent value pool (P2SH is not distinguished from P2PKH)
|
||||
Transparent = 1;
|
||||
// The Sapling value pool
|
||||
Sapling = 2;
|
||||
// The Orchard value pool
|
||||
Orchard = 3;
|
||||
}
|
||||
|
||||
// The unique identifier and amount for each proposed input.
|
||||
// The unique identifier and value for each proposed input.
|
||||
message ProposedInput {
|
||||
bytes txid = 1;
|
||||
uint32 index = 2;
|
||||
uint64 value = 3;
|
||||
ValuePool valuePool = 2;
|
||||
uint32 index = 3;
|
||||
uint64 value = 4;
|
||||
}
|
||||
|
||||
// The fee rule used in constructing a Proposal
|
||||
|
@ -59,17 +69,18 @@ enum FeeRule {
|
|||
Zip317 = 3;
|
||||
}
|
||||
|
||||
// The proposed change outputs and fee amount.
|
||||
// The proposed change outputs and fee value.
|
||||
message TransactionBalance {
|
||||
repeated ChangeValue proposedChange = 1;
|
||||
uint64 feeRequired = 2;
|
||||
}
|
||||
|
||||
// An enumeration of change value types.
|
||||
// A proposed change output. If the transparent value pool is selected,
|
||||
// the `memo` field must be null.
|
||||
message ChangeValue {
|
||||
oneof value {
|
||||
SaplingChange saplingValue = 1;
|
||||
}
|
||||
uint64 value = 1;
|
||||
ValuePool valuePool = 2;
|
||||
MemoBytes memo = 3;
|
||||
}
|
||||
|
||||
// An object wrapper for memo bytes, to facilitate representing the
|
||||
|
@ -77,10 +88,3 @@ message ChangeValue {
|
|||
message MemoBytes {
|
||||
bytes value = 1;
|
||||
}
|
||||
|
||||
// The amount and memo for a proposed Sapling change output.
|
||||
message SaplingChange {
|
||||
uint64 amount = 1;
|
||||
MemoBytes memo = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ use crate::{
|
|||
decrypt::DecryptedOutput,
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::service::TreeState,
|
||||
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
|
||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
|
@ -360,7 +360,7 @@ pub trait InputSource {
|
|||
txid: &TxId,
|
||||
protocol: ShieldedProtocol,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error>;
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
|
||||
|
||||
/// Returns a list of spendable notes sufficient to cover the specified target value, if
|
||||
/// possible. Only spendable notes corresponding to the specified shielded protocol will
|
||||
|
@ -372,7 +372,7 @@ pub trait InputSource {
|
|||
sources: &[ShieldedProtocol],
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error>;
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
|
||||
|
||||
/// Fetches a spendable transparent output.
|
||||
///
|
||||
|
@ -526,14 +526,23 @@ pub trait WalletRead {
|
|||
/// Returns a transaction.
|
||||
fn get_transaction(&self, txid: TxId) -> Result<Transaction, Self::Error>;
|
||||
|
||||
/// Returns the nullifiers for notes that the wallet is tracking, along with their associated
|
||||
/// account IDs, that are either unspent or have not yet been confirmed as spent (in that a
|
||||
/// spending transaction known to the wallet has not yet been included in a block).
|
||||
/// Returns the nullifiers for Sapling notes that the wallet is tracking, along with their
|
||||
/// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
|
||||
/// that a spending transaction known to the wallet has not yet been included in a block).
|
||||
fn get_sapling_nullifiers(
|
||||
&self,
|
||||
query: NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error>;
|
||||
|
||||
/// Returns the nullifiers for Orchard notes that the wallet is tracking, along with their
|
||||
/// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
|
||||
/// that a spending transaction known to the wallet has not yet been included in a block).
|
||||
#[cfg(feature = "orchard")]
|
||||
fn get_orchard_nullifiers(
|
||||
&self,
|
||||
query: NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error>;
|
||||
|
||||
/// Returns the set of all transparent receivers associated with the given account.
|
||||
///
|
||||
/// The set contains all transparent receivers that are known to have been derived
|
||||
|
@ -1096,7 +1105,7 @@ pub mod testing {
|
|||
use crate::{
|
||||
address::{AddressMetadata, UnifiedAddress},
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
wallet::{NoteId, ReceivedNote, WalletTransparentOutput},
|
||||
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
|
@ -1133,7 +1142,7 @@ pub mod testing {
|
|||
_txid: &TxId,
|
||||
_protocol: ShieldedProtocol,
|
||||
_index: u32,
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
@ -1144,7 +1153,7 @@ pub mod testing {
|
|||
_sources: &[ShieldedProtocol],
|
||||
_anchor_height: BlockHeight,
|
||||
_exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
@ -1251,6 +1260,14 @@ pub mod testing {
|
|||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn get_orchard_nullifiers(
|
||||
&self,
|
||||
_query: NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn get_transparent_receivers(
|
||||
&self,
|
||||
_account: AccountId,
|
||||
|
|
|
@ -24,7 +24,7 @@ use crate::{
|
|||
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{self, ChangeValue, DustOutputPolicy},
|
||||
fees::{self, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
wallet::{Note, OvkPolicy, Recipient},
|
||||
zip321::{self, Payment},
|
||||
|
@ -573,7 +573,7 @@ where
|
|||
Some(dfvk.to_ovk(Scope::Internal))
|
||||
};
|
||||
|
||||
let (sapling_anchor, sapling_inputs) = proposal.sapling_inputs().map_or_else(
|
||||
let (sapling_anchor, sapling_inputs) = proposal.shielded_inputs().map_or_else(
|
||||
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
|
||||
|
@ -718,13 +718,15 @@ where
|
|||
}
|
||||
|
||||
for change_value in proposal.balance().proposed_change() {
|
||||
match change_value {
|
||||
ChangeValue::Sapling { value, memo } => {
|
||||
let memo = memo.as_ref().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
let memo = change_value
|
||||
.memo()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
match change_value.output_pool() {
|
||||
ShieldedProtocol::Sapling => {
|
||||
builder.add_sapling_output(
|
||||
internal_ovk(),
|
||||
dfvk.change_address().1,
|
||||
*value,
|
||||
change_value.value(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((
|
||||
|
@ -732,10 +734,19 @@ where
|
|||
account,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
*value,
|
||||
change_value.value(),
|
||||
Some(memo),
|
||||
))
|
||||
}
|
||||
ShieldedProtocol::Orchard => {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
return Err(Error::UnsupportedPoolType(PoolType::Shielded(
|
||||
ShieldedProtocol::Orchard,
|
||||
)));
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
unimplemented!("FIXME: implement Orchard change output creation.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,19 @@ use crate::{
|
|||
address::{Address, UnifiedAddress},
|
||||
data_api::InputSource,
|
||||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
|
||||
wallet::{ReceivedNote, WalletTransparentOutput},
|
||||
wallet::{Note, ReceivedNote, WalletTransparentOutput},
|
||||
zip321::TransactionRequest,
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "transparent-inputs", feature = "orchard"))]
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
std::collections::BTreeSet, std::convert::Infallible,
|
||||
zcash_primitives::transaction::components::OutPoint,
|
||||
};
|
||||
use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use crate::fees::orchard as orchard_fees;
|
||||
|
||||
/// The type of errors that may be produced in input selection.
|
||||
pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
||||
|
@ -83,7 +86,7 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
pub struct Proposal<FeeRuleT, NoteRef> {
|
||||
transaction_request: TransactionRequest,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
sapling_inputs: Option<SaplingInputs<NoteRef>>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
|
@ -151,7 +154,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
pub fn from_parts(
|
||||
transaction_request: TransactionRequest,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
sapling_inputs: Option<SaplingInputs<NoteRef>>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
|
@ -163,14 +166,14 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(ProposalError::Overflow)
|
||||
})?;
|
||||
let sapling_input_total = sapling_inputs
|
||||
let shielded_input_total = shielded_inputs
|
||||
.iter()
|
||||
.flat_map(|s_in| s_in.notes().iter())
|
||||
.map(|out| out.value())
|
||||
.map(|out| out.note().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)?;
|
||||
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
|
@ -179,7 +182,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
|
||||
if is_shielding
|
||||
&& (transparent_input_total == NonNegativeAmount::ZERO
|
||||
|| sapling_input_total > NonNegativeAmount::ZERO
|
||||
|| shielded_input_total > NonNegativeAmount::ZERO
|
||||
|| request_total > NonNegativeAmount::ZERO)
|
||||
{
|
||||
return Err(ProposalError::ShieldingInvalid);
|
||||
|
@ -189,7 +192,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
Ok(Self {
|
||||
transaction_request,
|
||||
transparent_inputs,
|
||||
sapling_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
|
@ -212,8 +215,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
&self.transparent_inputs
|
||||
}
|
||||
/// Returns the Sapling inputs that have been selected to fund the transaction.
|
||||
pub fn sapling_inputs(&self) -> Option<&SaplingInputs<NoteRef>> {
|
||||
self.sapling_inputs.as_ref()
|
||||
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
|
||||
self.shielded_inputs.as_ref()
|
||||
}
|
||||
/// Returns the change outputs to be added to the transaction and the fee to be paid.
|
||||
pub fn balance(&self) -> &TransactionBalance {
|
||||
|
@ -244,12 +247,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
|||
.field("transaction_request", &self.transaction_request)
|
||||
.field("transparent_inputs", &self.transparent_inputs)
|
||||
.field(
|
||||
"sapling_inputs",
|
||||
&self.sapling_inputs().map(|i| i.notes.len()),
|
||||
"shielded_inputs",
|
||||
&self.shielded_inputs().map(|i| i.notes.len()),
|
||||
)
|
||||
.field(
|
||||
"sapling_anchor_height",
|
||||
&self.sapling_inputs().map(|i| i.anchor_height),
|
||||
"anchor_height",
|
||||
&self.shielded_inputs().map(|i| i.anchor_height),
|
||||
)
|
||||
.field("balance", &self.balance)
|
||||
//.field("fee_rule", &self.fee_rule)
|
||||
|
@ -261,14 +264,17 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
|||
|
||||
/// The Sapling inputs to a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct SaplingInputs<NoteRef> {
|
||||
pub struct ShieldedInputs<NoteRef> {
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef>>,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
}
|
||||
|
||||
impl<NoteRef> SaplingInputs<NoteRef> {
|
||||
/// Constructs a [`SaplingInputs`] from its constituent parts.
|
||||
pub fn from_parts(anchor_height: BlockHeight, notes: NonEmpty<ReceivedNote<NoteRef>>) -> Self {
|
||||
impl<NoteRef> ShieldedInputs<NoteRef> {
|
||||
/// Constructs a [`ShieldedInputs`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
anchor_height,
|
||||
notes,
|
||||
|
@ -282,7 +288,7 @@ impl<NoteRef> SaplingInputs<NoteRef> {
|
|||
}
|
||||
|
||||
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef>> {
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
|
||||
&self.notes
|
||||
}
|
||||
}
|
||||
|
@ -447,6 +453,23 @@ impl sapling::OutputView for SaplingPayment {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct OrchardPayment(NonNegativeAmount);
|
||||
|
||||
// TODO: introduce this method when it is needed for testing.
|
||||
// #[cfg(test)]
|
||||
// impl OrchardPayment {
|
||||
// pub(crate) fn new(amount: NonNegativeAmount) -> Self {
|
||||
// OrchardPayment(amount)
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
impl orchard_fees::OutputView for OrchardPayment {
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`InputSelector`] implementation that uses a greedy strategy to select between available
|
||||
/// notes.
|
||||
///
|
||||
|
@ -499,6 +522,7 @@ where
|
|||
{
|
||||
let mut transparent_outputs = vec![];
|
||||
let mut sapling_outputs = vec![];
|
||||
let mut orchard_outputs = vec![];
|
||||
for payment in transaction_request.payments() {
|
||||
let mut push_transparent = |taddr: TransparentAddress| {
|
||||
transparent_outputs.push(TxOut {
|
||||
|
@ -509,6 +533,9 @@ where
|
|||
let mut push_sapling = || {
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
};
|
||||
let mut push_orchard = || {
|
||||
orchard_outputs.push(OrchardPayment(payment.amount));
|
||||
};
|
||||
|
||||
match &payment.recipient_address {
|
||||
Address::Transparent(addr) => {
|
||||
|
@ -518,7 +545,14 @@ where
|
|||
push_sapling();
|
||||
}
|
||||
Address::Unified(addr) => {
|
||||
if addr.sapling().is_some() {
|
||||
#[cfg(feature = "orchard")]
|
||||
let has_orchard = addr.orchard().is_some();
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let has_orchard = false;
|
||||
|
||||
if has_orchard {
|
||||
push_orchard();
|
||||
} else if addr.sapling().is_some() {
|
||||
push_sapling();
|
||||
} else if let Some(addr) = addr.transparent() {
|
||||
push_transparent(*addr);
|
||||
|
@ -531,7 +565,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let mut sapling_inputs: Vec<ReceivedNote<DbT::NoteRef>> = vec![];
|
||||
let mut shielded_inputs: Vec<ReceivedNote<DbT::NoteRef, Note>> = vec![];
|
||||
let mut prior_available = NonNegativeAmount::ZERO;
|
||||
let mut amount_required = NonNegativeAmount::ZERO;
|
||||
let mut exclude: Vec<DbT::NoteRef> = vec![];
|
||||
|
@ -544,8 +578,34 @@ where
|
|||
target_height,
|
||||
&Vec::<WalletTransparentOutput>::new(),
|
||||
&transparent_outputs,
|
||||
&sapling_inputs,
|
||||
&sapling_outputs,
|
||||
&(
|
||||
::sapling::builder::BundleType::DEFAULT,
|
||||
&shielded_inputs
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
i.clone().traverse_opt(|wn| match wn {
|
||||
Note::Sapling(n) => Some(n),
|
||||
#[cfg(feature = "orchard")]
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()[..],
|
||||
&sapling_outputs[..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
::orchard::builder::BundleType::DEFAULT,
|
||||
&shielded_inputs
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
i.clone().traverse_opt(|wn| match wn {
|
||||
Note::Orchard(n) => Some(n),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()[..],
|
||||
&orchard_outputs[..],
|
||||
),
|
||||
&self.dust_output_policy,
|
||||
);
|
||||
|
||||
|
@ -554,8 +614,8 @@ where
|
|||
return Ok(Proposal {
|
||||
transaction_request,
|
||||
transparent_inputs: vec![],
|
||||
sapling_inputs: NonEmpty::from_vec(sapling_inputs).map(|notes| {
|
||||
SaplingInputs {
|
||||
shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
|
||||
ShieldedInputs {
|
||||
anchor_height,
|
||||
notes,
|
||||
}
|
||||
|
@ -575,19 +635,19 @@ where
|
|||
Err(other) => return Err(other.into()),
|
||||
}
|
||||
|
||||
sapling_inputs = wallet_db
|
||||
shielded_inputs = wallet_db
|
||||
.select_spendable_notes(
|
||||
account,
|
||||
amount_required.into(),
|
||||
&[ShieldedProtocol::Sapling],
|
||||
&[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard],
|
||||
anchor_height,
|
||||
&exclude,
|
||||
)
|
||||
.map_err(InputSelectorError::DataSource)?;
|
||||
|
||||
let new_available = sapling_inputs
|
||||
let new_available = shielded_inputs
|
||||
.iter()
|
||||
.map(|n| n.value())
|
||||
.map(|n| n.note().value())
|
||||
.sum::<Option<NonNegativeAmount>>()
|
||||
.ok_or(BalanceError::Overflow)?;
|
||||
|
||||
|
@ -652,8 +712,17 @@ where
|
|||
target_height,
|
||||
&transparent_inputs,
|
||||
&Vec::<TxOut>::new(),
|
||||
&Vec::<ReceivedNote<Infallible>>::new(),
|
||||
&Vec::<SaplingPayment>::new(),
|
||||
&(
|
||||
::sapling::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&self.dust_output_policy,
|
||||
);
|
||||
|
||||
|
@ -668,8 +737,17 @@ where
|
|||
target_height,
|
||||
&transparent_inputs,
|
||||
&Vec::<TxOut>::new(),
|
||||
&Vec::<ReceivedNote<Infallible>>::new(),
|
||||
&Vec::<SaplingPayment>::new(),
|
||||
&(
|
||||
::sapling::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&self.dust_output_policy,
|
||||
)?
|
||||
}
|
||||
|
@ -682,7 +760,7 @@ where
|
|||
Ok(Proposal {
|
||||
transaction_request: TransactionRequest::empty(),
|
||||
transparent_inputs,
|
||||
sapling_inputs: None,
|
||||
shielded_inputs: None,
|
||||
balance,
|
||||
fee_rule: (*self.change_strategy.fee_rule()).clone(),
|
||||
min_target_height: target_height,
|
||||
|
|
|
@ -12,38 +12,70 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
|
||||
pub(crate) mod common;
|
||||
pub mod fixed;
|
||||
#[cfg(feature = "orchard")]
|
||||
pub mod orchard;
|
||||
pub mod sapling;
|
||||
pub mod standard;
|
||||
pub mod zip317;
|
||||
|
||||
/// A proposed change amount and output pool.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ChangeValue {
|
||||
Sapling {
|
||||
pub struct ChangeValue {
|
||||
output_pool: ShieldedProtocol,
|
||||
value: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ChangeValue {
|
||||
/// Constructs a new change value from its constituent parts.
|
||||
pub fn new(
|
||||
output_pool: ShieldedProtocol,
|
||||
value: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output_pool,
|
||||
value,
|
||||
memo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new change value that will be created as a Sapling output.
|
||||
pub fn sapling(value: NonNegativeAmount, memo: Option<MemoBytes>) -> Self {
|
||||
Self::Sapling { value, memo }
|
||||
Self {
|
||||
output_pool: ShieldedProtocol::Sapling,
|
||||
value,
|
||||
memo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new change value that will be created as an Orchard output.
|
||||
#[cfg(feature = "orchard")]
|
||||
pub fn orchard(value: NonNegativeAmount, memo: Option<MemoBytes>) -> Self {
|
||||
Self {
|
||||
output_pool: ShieldedProtocol::Orchard,
|
||||
value,
|
||||
memo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pool to which the change output should be sent.
|
||||
pub fn output_pool(&self) -> ShieldedProtocol {
|
||||
self.output_pool
|
||||
}
|
||||
|
||||
/// Returns the value of the change output to be created, in zatoshis.
|
||||
pub fn value(&self) -> NonNegativeAmount {
|
||||
match self {
|
||||
ChangeValue::Sapling { value, .. } => *value,
|
||||
}
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Returns the memo to be associated with the change output.
|
||||
pub fn memo(&self) -> Option<&MemoBytes> {
|
||||
match self {
|
||||
ChangeValue::Sapling { memo, .. } => memo.as_ref(),
|
||||
}
|
||||
self.memo.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,6 +152,8 @@ pub enum ChangeError<E, NoteRefT> {
|
|||
},
|
||||
/// An error occurred that was specific to the change selection strategy in use.
|
||||
StrategyError(E),
|
||||
/// The proposed bundle structure would violate bundle type construction rules.
|
||||
BundleError(&'static str),
|
||||
}
|
||||
|
||||
impl<E, NoteRefT> ChangeError<E, NoteRefT> {
|
||||
|
@ -140,6 +174,7 @@ impl<E, NoteRefT> ChangeError<E, NoteRefT> {
|
|||
sapling,
|
||||
},
|
||||
ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)),
|
||||
ChangeError::BundleError(e) => ChangeError::BundleError(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +202,13 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
|
|||
ChangeError::StrategyError(err) => {
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
ChangeError::BundleError(err) => {
|
||||
write!(
|
||||
f,
|
||||
"The proposed transaction structure violates bundle type constrints: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -252,8 +294,8 @@ pub trait ChangeStrategy {
|
|||
target_height: BlockHeight,
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||
sapling_outputs: &[impl sapling::OutputView],
|
||||
sapling: &impl sapling::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,15 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use super::{sapling, ChangeError, ChangeValue, DustAction, DustOutputPolicy, TransactionBalance};
|
||||
use crate::ShieldedProtocol;
|
||||
|
||||
use super::{
|
||||
sapling as sapling_fees, ChangeError, ChangeValue, DustAction, DustOutputPolicy,
|
||||
TransactionBalance,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use super::orchard as orchard_fees;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn single_change_output_balance<
|
||||
|
@ -21,8 +29,8 @@ pub(crate) fn single_change_output_balance<
|
|||
target_height: BlockHeight,
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||
sapling_outputs: &[impl sapling::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
default_dust_threshold: NonNegativeAmount,
|
||||
change_memo: Option<MemoBytes>,
|
||||
|
@ -43,47 +51,98 @@ where
|
|||
.map(|t_out| t_out.value())
|
||||
.sum::<Option<_>>()
|
||||
.ok_or_else(overflow)?;
|
||||
let sapling_in = sapling_inputs
|
||||
let sapling_in = sapling
|
||||
.inputs()
|
||||
.iter()
|
||||
.map(|s_in| s_in.value())
|
||||
.map(sapling_fees::InputView::<NoteRefT>::value)
|
||||
.sum::<Option<_>>()
|
||||
.ok_or_else(overflow)?;
|
||||
let sapling_out = sapling_outputs
|
||||
let sapling_out = sapling
|
||||
.outputs()
|
||||
.iter()
|
||||
.map(|s_out| s_out.value())
|
||||
.map(sapling_fees::OutputView::value)
|
||||
.sum::<Option<_>>()
|
||||
.ok_or_else(overflow)?;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_in = orchard
|
||||
.inputs()
|
||||
.iter()
|
||||
.map(orchard_fees::InputView::<NoteRefT>::value)
|
||||
.sum::<Option<_>>()
|
||||
.ok_or_else(overflow)?;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_in = NonNegativeAmount::ZERO;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_out = orchard
|
||||
.outputs()
|
||||
.iter()
|
||||
.map(orchard_fees::OutputView::value)
|
||||
.sum::<Option<_>>()
|
||||
.ok_or_else(overflow)?;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_out = NonNegativeAmount::ZERO;
|
||||
|
||||
// TODO: implement a less naive strategy for selecting the pool to which change will be sent.
|
||||
#[cfg(feature = "orchard")]
|
||||
let (change_pool, sapling_change, orchard_change) =
|
||||
if orchard_in.is_positive() || orchard_out.is_positive() {
|
||||
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
|
||||
(ShieldedProtocol::Orchard, 0, 1)
|
||||
} else if sapling_in.is_positive() || sapling_out.is_positive() {
|
||||
// Otherwise, send change to Sapling if we're spending any Sapling inputs or creating any
|
||||
// Sapling outputs, so that we avoid pool-crossing.
|
||||
(ShieldedProtocol::Sapling, 1, 0)
|
||||
} else {
|
||||
// For all other transactions, send change to Sapling.
|
||||
// FIXME: Change this to Orchard once Orchard outputs are enabled.
|
||||
(ShieldedProtocol::Sapling, 1, 0)
|
||||
};
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_num_actions = orchard
|
||||
.bundle_type()
|
||||
.num_actions(
|
||||
orchard.inputs().len(),
|
||||
orchard.outputs().len() + orchard_change,
|
||||
)
|
||||
.map_err(ChangeError::BundleError)?;
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_num_actions = 0;
|
||||
|
||||
let fee_amount = fee_rule
|
||||
.fee_required(
|
||||
params,
|
||||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs.len(),
|
||||
if sapling_inputs.is_empty() {
|
||||
sapling_outputs.len() + 1
|
||||
} else {
|
||||
std::cmp::max(sapling_outputs.len() + 1, 2)
|
||||
},
|
||||
//Orchard is not yet supported in zcash_client_backend
|
||||
0,
|
||||
sapling
|
||||
.bundle_type()
|
||||
.num_spends(sapling.inputs().len())
|
||||
.map_err(ChangeError::BundleError)?,
|
||||
sapling
|
||||
.bundle_type()
|
||||
.num_outputs(
|
||||
sapling.inputs().len(),
|
||||
sapling.outputs().len() + sapling_change,
|
||||
)
|
||||
.map_err(ChangeError::BundleError)?,
|
||||
orchard_num_actions,
|
||||
)
|
||||
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
|
||||
|
||||
let total_in = (t_in + sapling_in).ok_or_else(overflow)?;
|
||||
|
||||
let total_out = [t_out, sapling_out, fee_amount]
|
||||
.iter()
|
||||
.sum::<Option<NonNegativeAmount>>()
|
||||
.ok_or_else(overflow)?;
|
||||
let total_in = (t_in + sapling_in + orchard_in).ok_or_else(overflow)?;
|
||||
let total_out = (t_out + sapling_out + orchard_out + fee_amount).ok_or_else(overflow)?;
|
||||
|
||||
let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds {
|
||||
available: total_in,
|
||||
required: total_out,
|
||||
})?;
|
||||
|
||||
if proposed_change == NonNegativeAmount::ZERO {
|
||||
if proposed_change.is_zero() {
|
||||
TransactionBalance::new(vec![], fee_amount).map_err(|_| overflow())
|
||||
} else {
|
||||
let dust_threshold = dust_output_policy
|
||||
|
@ -101,7 +160,7 @@ where
|
|||
})
|
||||
}
|
||||
DustAction::AllowDustChange => TransactionBalance::new(
|
||||
vec![ChangeValue::sapling(proposed_change, change_memo)],
|
||||
vec![ChangeValue::new(change_pool, proposed_change, change_memo)],
|
||||
fee_amount,
|
||||
)
|
||||
.map_err(|_| overflow()),
|
||||
|
@ -113,7 +172,7 @@ where
|
|||
}
|
||||
} else {
|
||||
TransactionBalance::new(
|
||||
vec![ChangeValue::sapling(proposed_change, change_memo)],
|
||||
vec![ChangeValue::new(change_pool, proposed_change, change_memo)],
|
||||
fee_amount,
|
||||
)
|
||||
.map_err(|_| overflow())
|
||||
|
|
|
@ -10,10 +10,13 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||
TransactionBalance,
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
DustOutputPolicy, TransactionBalance,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use super::orchard as orchard_fees;
|
||||
|
||||
/// A change strategy that and proposes change as a single output to the most current supported
|
||||
/// shielded pool and delegates fee calculation to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
|
@ -46,8 +49,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height: BlockHeight,
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||
sapling_outputs: &[impl sapling::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
single_change_output_balance(
|
||||
|
@ -56,8 +59,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs,
|
||||
sapling_outputs,
|
||||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
self.fee_rule().fixed_fee(),
|
||||
self.change_memo.clone(),
|
||||
|
@ -67,6 +71,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "orchard")]
|
||||
use std::convert::Infallible;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{Network, NetworkUpgrade, Parameters},
|
||||
transaction::{
|
||||
|
@ -98,13 +105,22 @@ mod tests {
|
|||
.unwrap(),
|
||||
&Vec::<TestTransparentInput>::new(),
|
||||
&Vec::<TxOut>::new(),
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[TestSaplingInput {
|
||||
note_id: 0,
|
||||
value: NonNegativeAmount::const_from_u64(60000),
|
||||
}],
|
||||
}][..],
|
||||
&[SaplingPayment::new(NonNegativeAmount::const_from_u64(
|
||||
40000,
|
||||
))],
|
||||
))][..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&[] as &[Infallible],
|
||||
&[] as &[Infallible],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -130,6 +146,8 @@ mod tests {
|
|||
.unwrap(),
|
||||
&Vec::<TestTransparentInput>::new(),
|
||||
&Vec::<TxOut>::new(),
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[
|
||||
TestSaplingInput {
|
||||
note_id: 0,
|
||||
|
@ -140,10 +158,17 @@ mod tests {
|
|||
note_id: 0,
|
||||
value: NonNegativeAmount::const_from_u64(10100),
|
||||
},
|
||||
],
|
||||
][..],
|
||||
&[SaplingPayment::new(NonNegativeAmount::const_from_u64(
|
||||
40000,
|
||||
))],
|
||||
))][..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&[] as &[Infallible],
|
||||
&[] as &[Infallible],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
//! Types related to computation of fees and change related to the Orchard components
|
||||
//! of a transaction.
|
||||
|
||||
use std::convert::Infallible;
|
||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||
|
||||
use orchard::builder::BundleType;
|
||||
|
||||
/// A trait that provides a minimized view of Orchard bundle configuration
|
||||
/// suitable for use in fee and change calculation.
|
||||
pub trait BundleView<NoteRef> {
|
||||
/// The type of inputs to the bundle.
|
||||
type In: InputView<NoteRef>;
|
||||
/// The type of inputs of the bundle.
|
||||
type Out: OutputView;
|
||||
|
||||
/// Returns the type of the bundle
|
||||
fn bundle_type(&self) -> BundleType;
|
||||
/// Returns the inputs to the bundle.
|
||||
fn inputs(&self) -> &[Self::In];
|
||||
/// Returns the outputs of the bundle.
|
||||
fn outputs(&self) -> &[Self::Out];
|
||||
}
|
||||
|
||||
impl<'a, NoteRef, In: InputView<NoteRef>, Out: OutputView> BundleView<NoteRef>
|
||||
for (BundleType, &'a [In], &'a [Out])
|
||||
{
|
||||
type In = In;
|
||||
type Out = Out;
|
||||
|
||||
fn bundle_type(&self) -> BundleType {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn inputs(&self) -> &[In] {
|
||||
self.1
|
||||
}
|
||||
|
||||
fn outputs(&self) -> &[Out] {
|
||||
self.2
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides a minimized view of an Orchard input suitable for use in fee and change
|
||||
/// calculation.
|
||||
pub trait InputView<NoteRef> {
|
||||
/// An identifier for the input being spent.
|
||||
fn note_id(&self) -> &NoteRef;
|
||||
/// The value of the input being spent.
|
||||
fn value(&self) -> NonNegativeAmount;
|
||||
}
|
||||
|
||||
impl<N> InputView<N> for Infallible {
|
||||
fn note_id(&self) -> &N {
|
||||
unreachable!()
|
||||
}
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides a minimized view of a Orchard output suitable for use in fee and change
|
||||
/// calculation.
|
||||
pub trait OutputView {
|
||||
/// The value of the output being produced.
|
||||
fn value(&self) -> NonNegativeAmount;
|
||||
}
|
||||
|
||||
impl OutputView for Infallible {
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
|
@ -3,9 +3,44 @@
|
|||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use sapling::builder::{OutputInfo, SpendInfo};
|
||||
use sapling::builder::{BundleType, OutputInfo, SpendInfo};
|
||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||
|
||||
/// A trait that provides a minimized view of Sapling bundle configuration
|
||||
/// suitable for use in fee and change calculation.
|
||||
pub trait BundleView<NoteRef> {
|
||||
/// The type of inputs to the bundle.
|
||||
type In: InputView<NoteRef>;
|
||||
/// The type of inputs of the bundle.
|
||||
type Out: OutputView;
|
||||
|
||||
/// Returns the type of the bundle
|
||||
fn bundle_type(&self) -> BundleType;
|
||||
/// Returns the inputs to the bundle.
|
||||
fn inputs(&self) -> &[Self::In];
|
||||
/// Returns the outputs of the bundle.
|
||||
fn outputs(&self) -> &[Self::Out];
|
||||
}
|
||||
|
||||
impl<'a, NoteRef, In: InputView<NoteRef>, Out: OutputView> BundleView<NoteRef>
|
||||
for (BundleType, &'a [In], &'a [Out])
|
||||
{
|
||||
type In = In;
|
||||
type Out = Out;
|
||||
|
||||
fn bundle_type(&self) -> BundleType {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn inputs(&self) -> &[In] {
|
||||
self.1
|
||||
}
|
||||
|
||||
fn outputs(&self) -> &[Out] {
|
||||
self.2
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that provides a minimized view of a Sapling input suitable for use in
|
||||
/// fee and change calculation.
|
||||
pub trait InputView<NoteRef> {
|
||||
|
|
|
@ -15,9 +15,13 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
fixed, sapling, zip317, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance,
|
||||
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||
TransactionBalance,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use super::orchard as orchard_fees;
|
||||
|
||||
/// A change strategy that proposes change as a single output to the most current supported
|
||||
/// shielded pool and delegates fee calculation to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
|
@ -50,8 +54,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height: BlockHeight,
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||
sapling_outputs: &[impl sapling::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
#[allow(deprecated)]
|
||||
|
@ -65,8 +69,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs,
|
||||
sapling_outputs,
|
||||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
)
|
||||
.map_err(|e| e.map(Zip317FeeError::Balance)),
|
||||
|
@ -79,8 +84,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs,
|
||||
sapling_outputs,
|
||||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
)
|
||||
.map_err(|e| e.map(Zip317FeeError::Balance)),
|
||||
|
@ -93,8 +99,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs,
|
||||
sapling_outputs,
|
||||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
),
|
||||
}
|
||||
|
|
|
@ -14,10 +14,13 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||
TransactionBalance,
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
DustOutputPolicy, TransactionBalance,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use super::orchard as orchard_fees;
|
||||
|
||||
/// A change strategy that and proposes change as a single output to the most current supported
|
||||
/// shielded pool and delegates fee calculation to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
|
@ -50,8 +53,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height: BlockHeight,
|
||||
transparent_inputs: &[impl transparent::InputView],
|
||||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling_inputs: &[impl sapling::InputView<NoteRefT>],
|
||||
sapling_outputs: &[impl sapling::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
let mut transparent_dust: Vec<_> = transparent_inputs
|
||||
|
@ -67,11 +70,12 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let mut sapling_dust: Vec<_> = sapling_inputs
|
||||
let mut sapling_dust: Vec<_> = sapling
|
||||
.inputs()
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
if i.value() < self.fee_rule.marginal_fee() {
|
||||
Some(i.note_id().clone())
|
||||
if sapling_fees::InputView::<NoteRefT>::value(i) < self.fee_rule.marginal_fee() {
|
||||
Some(sapling_fees::InputView::<NoteRefT>::note_id(i).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -88,8 +92,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
// We add one to the sapling outputs for the (single) change output Note that this
|
||||
// means that wallet-internal shielding transactions are an opportunity to spend a dust
|
||||
// note.
|
||||
let s_non_dust = sapling_inputs.len() - sapling_dust.len();
|
||||
let s_allowed_dust = (sapling_outputs.len() + 1).saturating_sub(s_non_dust);
|
||||
let s_non_dust = sapling.inputs().len() - sapling_dust.len();
|
||||
let s_allowed_dust = (sapling.outputs().len() + 1).saturating_sub(s_non_dust);
|
||||
|
||||
let available_grace_inputs = self
|
||||
.fee_rule
|
||||
|
@ -135,8 +139,9 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
target_height,
|
||||
transparent_inputs,
|
||||
transparent_outputs,
|
||||
sapling_inputs,
|
||||
sapling_outputs,
|
||||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
self.fee_rule.marginal_fee(),
|
||||
self.change_memo.clone(),
|
||||
|
@ -147,6 +152,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::convert::Infallible;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{Network, NetworkUpgrade, Parameters},
|
||||
legacy::Script,
|
||||
|
@ -177,13 +184,22 @@ mod tests {
|
|||
.unwrap(),
|
||||
&Vec::<TestTransparentInput>::new(),
|
||||
&Vec::<TxOut>::new(),
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[TestSaplingInput {
|
||||
note_id: 0,
|
||||
value: NonNegativeAmount::const_from_u64(55000),
|
||||
}],
|
||||
}][..],
|
||||
&[SaplingPayment::new(NonNegativeAmount::const_from_u64(
|
||||
40000,
|
||||
))],
|
||||
))][..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -210,11 +226,20 @@ mod tests {
|
|||
value: NonNegativeAmount::const_from_u64(40000),
|
||||
script_pubkey: Script(vec![]),
|
||||
}],
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[TestSaplingInput {
|
||||
note_id: 0,
|
||||
value: NonNegativeAmount::const_from_u64(55000),
|
||||
}],
|
||||
&Vec::<SaplingPayment>::new(),
|
||||
}][..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -237,6 +262,8 @@ mod tests {
|
|||
.unwrap(),
|
||||
&Vec::<TestTransparentInput>::new(),
|
||||
&Vec::<TxOut>::new(),
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[
|
||||
TestSaplingInput {
|
||||
note_id: 0,
|
||||
|
@ -246,10 +273,17 @@ mod tests {
|
|||
note_id: 1,
|
||||
value: NonNegativeAmount::const_from_u64(1000),
|
||||
},
|
||||
],
|
||||
][..],
|
||||
&[SaplingPayment::new(NonNegativeAmount::const_from_u64(
|
||||
40000,
|
||||
))],
|
||||
))][..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -272,6 +306,8 @@ mod tests {
|
|||
.unwrap(),
|
||||
&Vec::<TestTransparentInput>::new(),
|
||||
&Vec::<TxOut>::new(),
|
||||
&(
|
||||
sapling::builder::BundleType::DEFAULT,
|
||||
&[
|
||||
TestSaplingInput {
|
||||
note_id: 0,
|
||||
|
@ -285,10 +321,17 @@ mod tests {
|
|||
note_id: 2,
|
||||
value: NonNegativeAmount::const_from_u64(1000),
|
||||
},
|
||||
],
|
||||
][..],
|
||||
&[SaplingPayment::new(NonNegativeAmount::const_from_u64(
|
||||
40000,
|
||||
))],
|
||||
))][..],
|
||||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&(
|
||||
orchard::builder::BundleType::DEFAULT,
|
||||
&Vec::<Infallible>::new()[..],
|
||||
&Vec::<Infallible>::new()[..],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
|||
|
||||
use crate::{
|
||||
data_api::{
|
||||
wallet::input_selection::{Proposal, ProposalError, SaplingInputs},
|
||||
wallet::input_selection::{Proposal, ProposalError, ShieldedInputs},
|
||||
InputSource,
|
||||
},
|
||||
fees::{ChangeValue, TransactionBalance},
|
||||
|
@ -221,6 +221,8 @@ pub enum ProposalDecodingError<DbError> {
|
|||
Zip321(Zip321Error),
|
||||
/// A transaction identifier string did not decode to a valid transaction ID.
|
||||
TxIdInvalid(TryFromSliceError),
|
||||
/// An invalid value pool identifier was encountered.
|
||||
ValuePoolInvalid(i32),
|
||||
/// 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
|
||||
|
@ -238,6 +240,8 @@ pub enum ProposalDecodingError<DbError> {
|
|||
ProposalInvalid(ProposalError),
|
||||
/// An inputs field for the given protocol was present, but contained no input note references.
|
||||
EmptyShieldedInputs(ShieldedProtocol),
|
||||
/// Change outputs to the specified pool are not supported.
|
||||
InvalidChangeRecipient(PoolType),
|
||||
}
|
||||
|
||||
impl<E> From<Zip321Error> for ProposalDecodingError<E> {
|
||||
|
@ -253,6 +257,9 @@ impl<E: Display> Display for ProposalDecodingError<E> {
|
|||
ProposalDecodingError::TxIdInvalid(err) => {
|
||||
write!(f, "Invalid transaction id: {:?}", err)
|
||||
}
|
||||
ProposalDecodingError::ValuePoolInvalid(id) => {
|
||||
write!(f, "Invalid value pool identifier: {:?}", id)
|
||||
}
|
||||
ProposalDecodingError::InputRetrieval(err) => write!(
|
||||
f,
|
||||
"An error occurred retrieving a transaction input: {}",
|
||||
|
@ -281,6 +288,11 @@ impl<E: Display> Display for ProposalDecodingError<E> {
|
|||
"An inputs field was present for {:?}, but contained no note references.",
|
||||
protocol
|
||||
),
|
||||
ProposalDecodingError::InvalidChangeRecipient(pool_type) => write!(
|
||||
f,
|
||||
"Change outputs to the {} pool are not supported.",
|
||||
pool_type
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,10 +308,38 @@ impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError
|
|||
}
|
||||
}
|
||||
|
||||
fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
|
||||
match proposal::ValuePool::try_from(pool_id) {
|
||||
Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent),
|
||||
Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)),
|
||||
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
|
||||
_ => Err(ProposalDecodingError::ValuePoolInvalid(pool_id)),
|
||||
}
|
||||
}
|
||||
|
||||
impl proposal::ProposedInput {
|
||||
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
|
||||
Ok(TxId::from_bytes(self.txid[..].try_into()?))
|
||||
}
|
||||
|
||||
pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
|
||||
pool_type(self.value_pool)
|
||||
}
|
||||
}
|
||||
|
||||
impl proposal::ChangeValue {
|
||||
pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
|
||||
pool_type(self.value_pool)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShieldedProtocol> for proposal::ValuePool {
|
||||
fn from(value: ShieldedProtocol) -> Self {
|
||||
match value {
|
||||
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
|
||||
ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl proposal::Proposal {
|
||||
|
@ -311,47 +351,40 @@ impl proposal::Proposal {
|
|||
) -> Option<Self> {
|
||||
let transaction_request = value.transaction_request().to_uri(params)?;
|
||||
|
||||
let transparent_inputs = value
|
||||
let anchor_height = value
|
||||
.shielded_inputs()
|
||||
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
|
||||
|
||||
let inputs = value
|
||||
.transparent_inputs()
|
||||
.iter()
|
||||
.map(|utxo| proposal::ProposedInput {
|
||||
txid: utxo.outpoint().hash().to_vec(),
|
||||
value_pool: proposal::ValuePool::Transparent.into(),
|
||||
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 {
|
||||
.chain(value.shielded_inputs().iter().flat_map(|s_in| {
|
||||
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
|
||||
txid: rec_note.txid().as_ref().to_vec(),
|
||||
value_pool: proposal::ValuePool::from(rec_note.note().protocol()).into(),
|
||||
index: rec_note.output_index().into(),
|
||||
value: rec_note.value().into(),
|
||||
value: rec_note.note().value().into(),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}))
|
||||
.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 {
|
||||
.map(|change| proposal::ChangeValue {
|
||||
value: change.value().into(),
|
||||
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
|
||||
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
|
||||
value: memo_bytes.as_slice().to_vec(),
|
||||
}),
|
||||
},
|
||||
)),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
fee_required: value.balance().fee_required().into(),
|
||||
|
@ -361,8 +394,8 @@ impl proposal::Proposal {
|
|||
Some(proposal::Proposal {
|
||||
proto_version: PROPOSAL_SER_V1,
|
||||
transaction_request,
|
||||
transparent_inputs,
|
||||
sapling_inputs,
|
||||
anchor_height,
|
||||
inputs,
|
||||
balance,
|
||||
fee_rule: match value.fee_rule() {
|
||||
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
|
||||
|
@ -399,24 +432,27 @@ impl proposal::Proposal {
|
|||
|
||||
let transaction_request =
|
||||
TransactionRequest::from_uri(params, &self.transaction_request)?;
|
||||
let transparent_inputs = self
|
||||
.transparent_inputs
|
||||
.iter()
|
||||
.map(|t_in| {
|
||||
let txid = t_in
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_inputs = vec![];
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut transparent_inputs = vec![];
|
||||
|
||||
let mut received_notes = vec![];
|
||||
for input in self.inputs.iter() {
|
||||
let txid = input
|
||||
.parse_txid()
|
||||
.map_err(ProposalDecodingError::TxIdInvalid)?;
|
||||
|
||||
match input.pool_type()? {
|
||||
PoolType::Transparent => {
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
return Err(ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
t_in.index,
|
||||
));
|
||||
return Err(ProposalDecodingError::ValuePoolInvalid(1));
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let outpoint = OutPoint::new(txid.into(), t_in.index);
|
||||
let outpoint = OutPoint::new(txid.into(), input.index);
|
||||
transparent_inputs.push(
|
||||
wallet_db
|
||||
.get_unspent_transparent_output(&outpoint)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)?
|
||||
|
@ -424,47 +460,31 @@ impl proposal::Proposal {
|
|||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
t_in.index,
|
||||
input.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)?;
|
||||
|
||||
}
|
||||
PoolType::Shielded(protocol) => received_notes.push(
|
||||
wallet_db
|
||||
.get_spendable_note(&txid, ShieldedProtocol::Sapling, s_in.index)
|
||||
.get_spendable_note(&txid, protocol, input.index)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
s_in.index,
|
||||
PoolType::Shielded(protocol),
|
||||
input.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 shielded_inputs = NonEmpty::from_vec(received_notes)
|
||||
.map(|notes| ShieldedInputs::from_parts(self.anchor_height.into(), notes));
|
||||
|
||||
let proto_balance = self
|
||||
.balance
|
||||
|
@ -474,31 +494,23 @@ impl proposal::Proposal {
|
|||
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) => {
|
||||
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
|
||||
match cv.pool_type()? {
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => {
|
||||
Ok(ChangeValue::sapling(
|
||||
NonNegativeAmount::from_u64(sc.amount).map_err(
|
||||
|_| ProposalDecodingError::BalanceInvalid,
|
||||
)?,
|
||||
sc.memo
|
||||
NonNegativeAmount::from_u64(cv.value)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
|
||||
cv.memo
|
||||
.as_ref()
|
||||
.map(|bytes| {
|
||||
MemoBytes::from_bytes(&bytes.value).map_err(
|
||||
ProposalDecodingError::MemoInvalid,
|
||||
)
|
||||
MemoBytes::from_bytes(&bytes.value)
|
||||
.map_err(ProposalDecodingError::MemoInvalid)
|
||||
})
|
||||
.transpose()?,
|
||||
))
|
||||
}
|
||||
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
NonNegativeAmount::from_u64(proto_balance.fee_required)
|
||||
|
@ -509,7 +521,7 @@ impl proposal::Proposal {
|
|||
Proposal::from_parts(
|
||||
transaction_request,
|
||||
transparent_inputs,
|
||||
sapling_inputs.transpose()?,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
self.min_target_height.into(),
|
||||
|
|
|
@ -8,13 +8,15 @@ pub struct Proposal {
|
|||
/// 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
|
||||
/// The anchor height to be used in creating the transaction, if any.
|
||||
/// Setting the anchor height to zero will disallow the use of any shielded
|
||||
/// inputs.
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub anchor_height: u32,
|
||||
/// The inputs to be used in creating the transaction.
|
||||
#[prost(message, repeated, tag = "4")]
|
||||
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
/// The total value, fee value, and change outputs of the proposed
|
||||
/// transaction
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub balance: ::core::option::Option<TransactionBalance>,
|
||||
|
@ -32,28 +34,20 @@ pub struct Proposal {
|
|||
#[prost(bool, tag = "8")]
|
||||
pub is_shielding: bool,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SaplingInputs {
|
||||
/// 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.
|
||||
/// The unique identifier and value 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")]
|
||||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
pub value_pool: i32,
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub index: u32,
|
||||
#[prost(uint64, tag = "3")]
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub value: u64,
|
||||
}
|
||||
/// The proposed change outputs and fee amount.
|
||||
/// The proposed change outputs and fee value.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TransactionBalance {
|
||||
|
@ -62,21 +56,17 @@ pub struct TransactionBalance {
|
|||
#[prost(uint64, tag = "2")]
|
||||
pub fee_required: u64,
|
||||
}
|
||||
/// An enumeration of change value types.
|
||||
/// A proposed change output. If the transparent value pool is selected,
|
||||
/// the `memo` field must be null.
|
||||
#[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),
|
||||
}
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub value: u64,
|
||||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
pub value_pool: i32,
|
||||
#[prost(message, optional, tag = "3")]
|
||||
pub memo: ::core::option::Option<MemoBytes>,
|
||||
}
|
||||
/// An object wrapper for memo bytes, to facilitate representing the
|
||||
/// `change_memo == None` case.
|
||||
|
@ -86,14 +76,44 @@ 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>,
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum ValuePool {
|
||||
/// Protobuf requires that enums have a zero discriminant as the default
|
||||
/// value. However, we need to require that a known value pool is selected,
|
||||
/// and we do not want to fall back to any default, so sending the
|
||||
/// PoolNotSpecified value will be treated as an error.
|
||||
PoolNotSpecified = 0,
|
||||
/// The transparent value pool (P2SH is not distinguished from P2PKH)
|
||||
Transparent = 1,
|
||||
/// The Sapling value pool
|
||||
Sapling = 2,
|
||||
/// The Orchard value pool
|
||||
Orchard = 3,
|
||||
}
|
||||
impl ValuePool {
|
||||
/// 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 {
|
||||
ValuePool::PoolNotSpecified => "PoolNotSpecified",
|
||||
ValuePool::Transparent => "Transparent",
|
||||
ValuePool::Sapling => "Sapling",
|
||||
ValuePool::Orchard => "Orchard",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"PoolNotSpecified" => Some(Self::PoolNotSpecified),
|
||||
"Transparent" => Some(Self::Transparent),
|
||||
"Sapling" => Some(Self::Sapling),
|
||||
"Orchard" => Some(Self::Orchard),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The fee rule used in constructing a Proposal
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
|
|
|
@ -19,6 +19,9 @@ use zcash_primitives::{
|
|||
|
||||
use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, ShieldedProtocol};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use crate::fees::orchard as orchard_fees;
|
||||
|
||||
/// A unique identifier for a shielded transaction output
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NoteId {
|
||||
|
@ -255,26 +258,35 @@ impl Note {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the shielded protocol used by this note.
|
||||
pub fn protocol(&self) -> ShieldedProtocol {
|
||||
match self {
|
||||
Note::Sapling(_) => ShieldedProtocol::Sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(_) => ShieldedProtocol::Orchard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a note that is tracked by the wallet that is available for spending,
|
||||
/// with sufficient information for use in note selection.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReceivedNote<NoteRef> {
|
||||
pub struct ReceivedNote<NoteRef, NoteT> {
|
||||
note_id: NoteRef,
|
||||
txid: TxId,
|
||||
output_index: u16,
|
||||
note: Note,
|
||||
note: NoteT,
|
||||
spending_key_scope: Scope,
|
||||
note_commitment_tree_position: Position,
|
||||
}
|
||||
|
||||
impl<NoteRef> ReceivedNote<NoteRef> {
|
||||
impl<NoteRef, NoteT> ReceivedNote<NoteRef, NoteT> {
|
||||
pub fn from_parts(
|
||||
note_id: NoteRef,
|
||||
txid: TxId,
|
||||
output_index: u16,
|
||||
note: Note,
|
||||
note: NoteT,
|
||||
spending_key_scope: Scope,
|
||||
note_commitment_tree_position: Position,
|
||||
) -> Self {
|
||||
|
@ -297,27 +309,62 @@ impl<NoteRef> ReceivedNote<NoteRef> {
|
|||
pub fn output_index(&self) -> u16 {
|
||||
self.output_index
|
||||
}
|
||||
pub fn note(&self) -> &Note {
|
||||
pub fn note(&self) -> &NoteT {
|
||||
&self.note
|
||||
}
|
||||
pub fn value(&self) -> NonNegativeAmount {
|
||||
self.note.value()
|
||||
}
|
||||
pub fn spending_key_scope(&self) -> Scope {
|
||||
self.spending_key_scope
|
||||
}
|
||||
pub fn note_commitment_tree_position(&self) -> Position {
|
||||
self.note_commitment_tree_position
|
||||
}
|
||||
|
||||
/// Applies the given function to the `note` field of this ReceivedNote and returns
|
||||
/// `None` if that function returns `None`, or otherwise a `Some` containing
|
||||
/// a `ReceivedNote` with its `note` field swapped out for the result of the function.
|
||||
///
|
||||
/// The name `traverse` refers to the general operation that has the Haskell type
|
||||
/// `Applicative f => (a -> f b) -> t a -> f (t b)`, that this method specializes
|
||||
/// with `ReceivedNote<NoteRef, _>` for `t` and `Option<_>` for `f`.
|
||||
pub fn traverse_opt<B>(
|
||||
self,
|
||||
f: impl FnOnce(NoteT) -> Option<B>,
|
||||
) -> Option<ReceivedNote<NoteRef, B>> {
|
||||
f(self.note).map(|n0| ReceivedNote {
|
||||
note_id: self.note_id,
|
||||
txid: self.txid,
|
||||
output_index: self.output_index,
|
||||
note: n0,
|
||||
spending_key_scope: self.spending_key_scope,
|
||||
note_commitment_tree_position: self.note_commitment_tree_position,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef> {
|
||||
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef, sapling::Note> {
|
||||
fn note_id(&self) -> &NoteRef {
|
||||
&self.note_id
|
||||
}
|
||||
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
self.note.value()
|
||||
self.note
|
||||
.value()
|
||||
.try_into()
|
||||
.expect("Sapling note values are indirectly checked by consensus.")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard::Note> {
|
||||
fn note_id(&self) -> &NoteRef {
|
||||
&self.note_id
|
||||
}
|
||||
|
||||
fn value(&self) -> NonNegativeAmount {
|
||||
self.note
|
||||
.value()
|
||||
.try_into()
|
||||
.expect("Orchard note values are indirectly checked by consensus.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ jubjub.workspace = true
|
|||
secrecy.workspace = true
|
||||
|
||||
# - Shielded protocols
|
||||
orchard.workspace = true
|
||||
sapling.workspace = true
|
||||
|
||||
# - Note commitment trees
|
||||
|
|
|
@ -69,7 +69,7 @@ use zcash_client_backend::{
|
|||
},
|
||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||
};
|
||||
|
||||
|
@ -172,7 +172,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
txid: &TxId,
|
||||
_protocol: ShieldedProtocol,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index)
|
||||
}
|
||||
|
||||
|
@ -183,7 +183,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
_sources: &[ShieldedProtocol],
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[Self::NoteRef],
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
|
||||
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
wallet::sapling::select_spendable_sapling_notes(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
|
@ -364,6 +364,14 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
|
|||
"The wallet must be compiled with the transparent-inputs feature to use this method."
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn get_orchard_nullifiers(
|
||||
&self,
|
||||
_query: NullifierQuery,
|
||||
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
|
||||
|
|
|
@ -100,7 +100,7 @@ impl ReceivedSaplingOutput for DecryptedOutput<sapling::Note> {
|
|||
fn to_spendable_note<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
row: &Row,
|
||||
) -> Result<ReceivedNote<ReceivedNoteId>, SqliteClientError> {
|
||||
) -> Result<ReceivedNote<ReceivedNoteId, Note>, SqliteClientError> {
|
||||
let note_id = ReceivedNoteId(row.get(0)?);
|
||||
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
|
||||
let output_index = row.get(2)?;
|
||||
|
@ -182,7 +182,7 @@ pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
|
|||
params: &P,
|
||||
txid: &TxId,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
||||
let mut stmt_select_note = conn.prepare_cached(
|
||||
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
|
||||
accounts.ufvk, recipient_key_scope
|
||||
|
@ -239,7 +239,7 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
|
|||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
exclude: &[ReceivedNoteId],
|
||||
) -> Result<Vec<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
|
||||
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
||||
let birthday_height = match wallet_birthday(conn)? {
|
||||
Some(birthday) => birthday,
|
||||
None => {
|
||||
|
|
|
@ -41,12 +41,16 @@ and this library adheres to Rust's notion of
|
|||
- `NonNegativeAmount::const_from_u64`
|
||||
- `NonNegativeAmount::from_nonnegative_i64_le_bytes`
|
||||
- `NonNegativeAmount::to_i64_le_bytes`
|
||||
- `NonNegativeAmount::is_zero`
|
||||
- `NonNegativeAmount::is_positive`
|
||||
- `impl From<&NonNegativeAmount> for Amount`
|
||||
- `impl From<NonNegativeAmount> for u64`
|
||||
- `impl From<NonNegativeAmount> for zcash_primitives::sapling::value::NoteValue`
|
||||
- `impl From<NonNegativeAmount> for orchard::::NoteValue`
|
||||
- `impl Sum<NonNegativeAmount> for Option<NonNegativeAmount>`
|
||||
- `impl<'a> Sum<&'a NonNegativeAmount> for Option<NonNegativeAmount>`
|
||||
- `impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount`
|
||||
- `impl TryFrom<orchard::NoteValue> for NonNegativeAmount`
|
||||
- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
|
||||
- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
|
||||
- `impl From<TxId> for [u8; 32]`
|
||||
|
|
|
@ -297,6 +297,16 @@ impl NonNegativeAmount {
|
|||
pub fn to_i64_le_bytes(self) -> [u8; 8] {
|
||||
self.0.to_i64_le_bytes()
|
||||
}
|
||||
|
||||
/// Returns whether or not this `NonNegativeAmount` is the zero value.
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self == &NonNegativeAmount::ZERO
|
||||
}
|
||||
|
||||
/// Returns whether or not this `NonNegativeAmount` is positive.
|
||||
pub fn is_positive(&self) -> bool {
|
||||
self > &NonNegativeAmount::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NonNegativeAmount> for Amount {
|
||||
|
@ -331,6 +341,20 @@ impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<NonNegativeAmount> for orchard::NoteValue {
|
||||
fn from(n: NonNegativeAmount) -> Self {
|
||||
orchard::NoteValue::from_raw(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<orchard::NoteValue> for NonNegativeAmount {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: orchard::NoteValue) -> Result<Self, Self::Error> {
|
||||
Self::from_u64(value.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Amount> for NonNegativeAmount {
|
||||
type Error = ();
|
||||
|
||||
|
|
Loading…
Reference in New Issue