zcash_client_backend: Add Orchard support to transaction proposals.

This commit is contained in:
Kris Nuttycombe 2023-12-04 14:45:22 -07:00
parent 56f2ac573c
commit adc75566a0
13 changed files with 393 additions and 247 deletions

1
Cargo.lock generated
View File

@ -3056,6 +3056,7 @@ dependencies = [
"incrementalmerkletree", "incrementalmerkletree",
"jubjub", "jubjub",
"maybe-rayon", "maybe-rayon",
"orchard",
"proptest", "proptest",
"prost", "prost",
"rand_core", "rand_core",

View File

@ -21,9 +21,10 @@ and this library adheres to Rust's notion of
with_orchard_balance_mut, with_orchard_balance_mut,
add_unshielded_value add_unshielded_value
}` }`
- `WalletRead::get_orchard_nullifiers`
- `wallet::propose_standard_transfer_to_address` - `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::Proposal::from_parts` - `wallet::input_selection::Proposal::{from_parts, shielded_inputs}`
- `wallet::input_selection::SaplingInputs` - `wallet::input_selection::ShieldedInputs`
- `wallet::input_selection::ShieldingSelector` has been - `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag. functionality and move it behind the `transparent-inputs` feature flag.
@ -134,8 +135,7 @@ and this library adheres to Rust's notion of
- `wallet::create_proposed_transaction` now forces implementations to ignore - `wallet::create_proposed_transaction` now forces implementations to ignore
the database identifiers for its contained notes by universally quantifying the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter. the `NoteRef` type parameter.
- `wallet::input_selection::Proposal::sapling_inputs` now returns type - Arguments to `wallet::input_selection::Proposal::from_parts` have changed.
`Option<&SaplingInputs>`.
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in - `wallet::input_selection::Proposal::min_anchor_height` has been removed in
favor of storing this value in `SaplingInputs`. favor of storing this value in `SaplingInputs`.
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements - `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
@ -146,9 +146,8 @@ and this library adheres to Rust's notion of
- `ChangeValue` is now a struct. In addition to the existing change value, it - `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 now also provides the output pool to which change should be sent and an
optional memo to be associated with the change output. optional memo to be associated with the change output.
- `fixed::SingleOutputChangeStrategy::new` and - `fixed::SingleOutputChangeStrategy::new` and `zip317::SingleOutputChangeStrategy::new`
`zip317::SingleOutputChangeStrategy::new` each now accept an additional each now accept an additional `change_memo` argument
`change_memo` argument.
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- The fields of `ReceivedSaplingNote` are now private. Use - The fields of `ReceivedSaplingNote` are now private. Use
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods `ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
@ -200,9 +199,11 @@ and this library adheres to Rust's notion of
### Removed ### Removed
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by - `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
`zcash_client_backend::ReceivedNote`. `zcash_client_backend::ReceivedNote`.
- `zcash_client_backend::data_api::WalletRead::is_valid_account_extfvk` has been - `zcash_client_backend::data_api`
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by - `WalletRead::is_valid_account_extfvk` has been removed; it was unused in
`get_account_for_ufvk`. the ECC mobile wallet SDKs and has been superseded by `get_account_for_ufvk`.
- `wallet::input_selection::Proposal::sapling_inputs` has been replaced
by `Proposal::shielded_inputs`
- `zcash_client_backend::data_api::WalletRead::{ - `zcash_client_backend::data_api::WalletRead::{
get_spendable_sapling_notes get_spendable_sapling_notes
select_spendable_sapling_notes, select_spendable_sapling_notes,

View File

@ -3,6 +3,12 @@
This library contains Rust structs and traits for creating shielded Zcash light This library contains Rust structs and traits for creating shielded Zcash light
clients. 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 ## License
Licensed under either of Licensed under either of

View File

@ -11,11 +11,13 @@ message Proposal {
uint32 protoVersion = 1; uint32 protoVersion = 1;
// ZIP 321 serialized transaction request // ZIP 321 serialized transaction request
string transactionRequest = 2; string transactionRequest = 2;
// The transparent UTXOs to use as inputs to the transaction. // The anchor height to be used in creating the transaction, if any.
repeated ProposedInput transparentInputs = 3; // Setting the anchor height to zero will disallow the use of any shielded
// The Sapling input notes and anchor height to be used in creating the transaction. // inputs.
SaplingInputs saplingInputs = 4; uint32 anchorHeight = 3;
// The total value, fee amount, and change outputs of the proposed // 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 // transaction
TransactionBalance balance = 5; TransactionBalance balance = 5;
// The fee rule used in constructing this proposal // The fee rule used in constructing this proposal
@ -30,18 +32,26 @@ message Proposal {
bool isShielding = 8; bool isShielding = 8;
} }
message SaplingInputs { enum ValuePool {
// The Sapling anchor height to be used in creating the transaction // Protobuf requires that enums have a zero discriminant as the default
uint32 anchorHeight = 1; // value. However, we need to require that a known value pool is selected,
// The unique identifier and amount for each proposed Sapling input // and we do not want to fall back to any default, so sending the
repeated ProposedInput inputs = 2; // 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 { message ProposedInput {
bytes txid = 1; bytes txid = 1;
uint32 index = 2; ValuePool valuePool = 2;
uint64 value = 3; uint32 index = 3;
uint64 value = 4;
} }
// The fee rule used in constructing a Proposal // The fee rule used in constructing a Proposal
@ -59,17 +69,18 @@ enum FeeRule {
Zip317 = 3; Zip317 = 3;
} }
// The proposed change outputs and fee amount. // The proposed change outputs and fee value.
message TransactionBalance { message TransactionBalance {
repeated ChangeValue proposedChange = 1; repeated ChangeValue proposedChange = 1;
uint64 feeRequired = 2; 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 { message ChangeValue {
oneof value { uint64 value = 1;
SaplingChange saplingValue = 1; ValuePool valuePool = 2;
} MemoBytes memo = 3;
} }
// An object wrapper for memo bytes, to facilitate representing the // An object wrapper for memo bytes, to facilitate representing the
@ -77,10 +88,3 @@ message ChangeValue {
message MemoBytes { message MemoBytes {
bytes value = 1; bytes value = 1;
} }
// The amount and memo for a proposed Sapling change output.
message SaplingChange {
uint64 amount = 1;
MemoBytes memo = 2;
}

View File

@ -28,7 +28,7 @@ use crate::{
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::service::TreeState, proto::service::TreeState,
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
ShieldedProtocol, ShieldedProtocol,
}; };
@ -360,7 +360,7 @@ pub trait InputSource {
txid: &TxId, txid: &TxId,
protocol: ShieldedProtocol, protocol: ShieldedProtocol,
index: u32, 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 /// 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 /// possible. Only spendable notes corresponding to the specified shielded protocol will
@ -372,7 +372,7 @@ pub trait InputSource {
sources: &[ShieldedProtocol], sources: &[ShieldedProtocol],
anchor_height: BlockHeight, anchor_height: BlockHeight,
exclude: &[Self::NoteRef], exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error>; ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
/// Fetches a spendable transparent output. /// Fetches a spendable transparent output.
/// ///
@ -526,14 +526,23 @@ pub trait WalletRead {
/// Returns a transaction. /// Returns a transaction.
fn get_transaction(&self, txid: TxId) -> Result<Transaction, Self::Error>; fn get_transaction(&self, txid: TxId) -> Result<Transaction, Self::Error>;
/// Returns the nullifiers for notes that the wallet is tracking, along with their associated /// Returns the nullifiers for Sapling notes that the wallet is tracking, along with their
/// account IDs, that are either unspent or have not yet been confirmed as spent (in that a /// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
/// spending transaction known to the wallet has not yet been included in a block). /// that a spending transaction known to the wallet has not yet been included in a block).
fn get_sapling_nullifiers( fn get_sapling_nullifiers(
&self, &self,
query: NullifierQuery, query: NullifierQuery,
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error>; ) -> 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. /// 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 /// The set contains all transparent receivers that are known to have been derived
@ -1096,7 +1105,7 @@ pub mod testing {
use crate::{ use crate::{
address::{AddressMetadata, UnifiedAddress}, address::{AddressMetadata, UnifiedAddress},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{NoteId, ReceivedNote, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol, ShieldedProtocol,
}; };
@ -1133,7 +1142,7 @@ pub mod testing {
_txid: &TxId, _txid: &TxId,
_protocol: ShieldedProtocol, _protocol: ShieldedProtocol,
_index: u32, _index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> { ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(None) Ok(None)
} }
@ -1144,7 +1153,7 @@ pub mod testing {
_sources: &[ShieldedProtocol], _sources: &[ShieldedProtocol],
_anchor_height: BlockHeight, _anchor_height: BlockHeight,
_exclude: &[Self::NoteRef], _exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> { ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(Vec::new()) Ok(Vec::new())
} }
} }
@ -1251,6 +1260,14 @@ pub mod testing {
Ok(Vec::new()) 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( fn get_transparent_receivers(
&self, &self,
_account: AccountId, _account: AccountId,

View File

@ -573,7 +573,7 @@ where
Some(dfvk.to_ovk(Scope::Internal)) 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![])), || Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| { |inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| { wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {

View File

@ -21,16 +21,19 @@ use crate::{
address::{Address, UnifiedAddress}, address::{Address, UnifiedAddress},
data_api::InputSource, data_api::InputSource,
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance}, fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
wallet::{ReceivedNote, WalletTransparentOutput}, wallet::{Note, ReceivedNote, WalletTransparentOutput},
zip321::TransactionRequest, zip321::TransactionRequest,
ShieldedProtocol, ShieldedProtocol,
}; };
#[cfg(any(feature = "transparent-inputs", feature = "orchard"))]
use std::convert::Infallible;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint}; use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
use {crate::fees::orchard as orchard_fees, std::convert::Infallible}; use crate::fees::orchard as orchard_fees;
/// The type of errors that may be produced in input selection. /// The type of errors that may be produced in input selection.
pub enum InputSelectorError<DbErrT, SelectorErrT> { 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> { pub struct Proposal<FeeRuleT, NoteRef> {
transaction_request: TransactionRequest, transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>, transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>, shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance, balance: TransactionBalance,
fee_rule: FeeRuleT, fee_rule: FeeRuleT,
min_target_height: BlockHeight, min_target_height: BlockHeight,
@ -151,7 +154,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
pub fn from_parts( pub fn from_parts(
transaction_request: TransactionRequest, transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>, transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>, shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance, balance: TransactionBalance,
fee_rule: FeeRuleT, fee_rule: FeeRuleT,
min_target_height: BlockHeight, min_target_height: BlockHeight,
@ -163,14 +166,14 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| { .fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
(acc? + a).ok_or(ProposalError::Overflow) (acc? + a).ok_or(ProposalError::Overflow)
})?; })?;
let sapling_input_total = sapling_inputs let shielded_input_total = shielded_inputs
.iter() .iter()
.flat_map(|s_in| s_in.notes().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)) .fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
.ok_or(ProposalError::Overflow)?; .ok_or(ProposalError::Overflow)?;
let input_total = 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 let request_total = transaction_request
.total() .total()
@ -179,7 +182,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
if is_shielding if is_shielding
&& (transparent_input_total == NonNegativeAmount::ZERO && (transparent_input_total == NonNegativeAmount::ZERO
|| sapling_input_total > NonNegativeAmount::ZERO || shielded_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO) || request_total > NonNegativeAmount::ZERO)
{ {
return Err(ProposalError::ShieldingInvalid); return Err(ProposalError::ShieldingInvalid);
@ -189,7 +192,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
Ok(Self { Ok(Self {
transaction_request, transaction_request,
transparent_inputs, transparent_inputs,
sapling_inputs, shielded_inputs,
balance, balance,
fee_rule, fee_rule,
min_target_height, min_target_height,
@ -212,8 +215,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
&self.transparent_inputs &self.transparent_inputs
} }
/// Returns the Sapling inputs that have been selected to fund the transaction. /// Returns the Sapling inputs that have been selected to fund the transaction.
pub fn sapling_inputs(&self) -> Option<&SaplingInputs<NoteRef>> { pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
self.sapling_inputs.as_ref() self.shielded_inputs.as_ref()
} }
/// Returns the change outputs to be added to the transaction and the fee to be paid. /// Returns the change outputs to be added to the transaction and the fee to be paid.
pub fn balance(&self) -> &TransactionBalance { pub fn balance(&self) -> &TransactionBalance {
@ -244,12 +247,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
.field("transaction_request", &self.transaction_request) .field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs) .field("transparent_inputs", &self.transparent_inputs)
.field( .field(
"sapling_inputs", "shielded_inputs",
&self.sapling_inputs().map(|i| i.notes.len()), &self.shielded_inputs().map(|i| i.notes.len()),
) )
.field( .field(
"sapling_anchor_height", "anchor_height",
&self.sapling_inputs().map(|i| i.anchor_height), &self.shielded_inputs().map(|i| i.anchor_height),
) )
.field("balance", &self.balance) .field("balance", &self.balance)
//.field("fee_rule", &self.fee_rule) //.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. /// The Sapling inputs to a proposed transaction.
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub struct SaplingInputs<NoteRef> { pub struct ShieldedInputs<NoteRef> {
anchor_height: BlockHeight, anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef>>, notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
} }
impl<NoteRef> SaplingInputs<NoteRef> { impl<NoteRef> ShieldedInputs<NoteRef> {
/// Constructs a [`SaplingInputs`] from its constituent parts. /// Constructs a [`ShieldedInputs`] from its constituent parts.
pub fn from_parts(anchor_height: BlockHeight, notes: NonEmpty<ReceivedNote<NoteRef>>) -> Self { pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
) -> Self {
Self { Self {
anchor_height, anchor_height,
notes, notes,
@ -282,7 +288,7 @@ impl<NoteRef> SaplingInputs<NoteRef> {
} }
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction. /// 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 &self.notes
} }
} }
@ -559,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 prior_available = NonNegativeAmount::ZERO;
let mut amount_required = NonNegativeAmount::ZERO; let mut amount_required = NonNegativeAmount::ZERO;
let mut exclude: Vec<DbT::NoteRef> = vec![]; let mut exclude: Vec<DbT::NoteRef> = vec![];
@ -574,13 +580,30 @@ where
&transparent_outputs, &transparent_outputs,
&( &(
::sapling::builder::BundleType::DEFAULT, ::sapling::builder::BundleType::DEFAULT,
&sapling_inputs[..], &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[..], &sapling_outputs[..],
), ),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
&( &(
::orchard::builder::BundleType::DEFAULT, ::orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..], &shielded_inputs
.iter()
.filter_map(|i| {
i.clone().traverse_opt(|wn| match wn {
Note::Orchard(n) => Some(n),
_ => None,
})
})
.collect::<Vec<_>>()[..],
&orchard_outputs[..], &orchard_outputs[..],
), ),
&self.dust_output_policy, &self.dust_output_policy,
@ -591,8 +614,8 @@ where
return Ok(Proposal { return Ok(Proposal {
transaction_request, transaction_request,
transparent_inputs: vec![], transparent_inputs: vec![],
sapling_inputs: NonEmpty::from_vec(sapling_inputs).map(|notes| { shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
SaplingInputs { ShieldedInputs {
anchor_height, anchor_height,
notes, notes,
} }
@ -612,19 +635,19 @@ where
Err(other) => return Err(other.into()), Err(other) => return Err(other.into()),
} }
sapling_inputs = wallet_db shielded_inputs = wallet_db
.select_spendable_notes( .select_spendable_notes(
account, account,
amount_required.into(), amount_required.into(),
&[ShieldedProtocol::Sapling], &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard],
anchor_height, anchor_height,
&exclude, &exclude,
) )
.map_err(InputSelectorError::DataSource)?; .map_err(InputSelectorError::DataSource)?;
let new_available = sapling_inputs let new_available = shielded_inputs
.iter() .iter()
.map(|n| n.value()) .map(|n| n.note().value())
.sum::<Option<NonNegativeAmount>>() .sum::<Option<NonNegativeAmount>>()
.ok_or(BalanceError::Overflow)?; .ok_or(BalanceError::Overflow)?;
@ -737,7 +760,7 @@ where
Ok(Proposal { Ok(Proposal {
transaction_request: TransactionRequest::empty(), transaction_request: TransactionRequest::empty(),
transparent_inputs, transparent_inputs,
sapling_inputs: None, shielded_inputs: None,
balance, balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(), fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_target_height: target_height, min_target_height: target_height,

View File

@ -22,7 +22,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{ use crate::{
data_api::{ data_api::{
wallet::input_selection::{Proposal, ProposalError, SaplingInputs}, wallet::input_selection::{Proposal, ProposalError, ShieldedInputs},
InputSource, InputSource,
}, },
fees::{ChangeValue, TransactionBalance}, fees::{ChangeValue, TransactionBalance},
@ -221,6 +221,8 @@ pub enum ProposalDecodingError<DbError> {
Zip321(Zip321Error), Zip321(Zip321Error),
/// A transaction identifier string did not decode to a valid transaction ID. /// A transaction identifier string did not decode to a valid transaction ID.
TxIdInvalid(TryFromSliceError), 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. /// A failure occurred trying to retrieve an unspent note or UTXO from the wallet database.
InputRetrieval(DbError), InputRetrieval(DbError),
/// The unspent note or UTXO corresponding to a proposal input was not found in the wallet /// 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), ProposalInvalid(ProposalError),
/// An inputs field for the given protocol was present, but contained no input note references. /// An inputs field for the given protocol was present, but contained no input note references.
EmptyShieldedInputs(ShieldedProtocol), EmptyShieldedInputs(ShieldedProtocol),
/// Change outputs to the specified pool are not supported.
InvalidChangeRecipient(PoolType),
} }
impl<E> From<Zip321Error> for ProposalDecodingError<E> { impl<E> From<Zip321Error> for ProposalDecodingError<E> {
@ -253,6 +257,9 @@ impl<E: Display> Display for ProposalDecodingError<E> {
ProposalDecodingError::TxIdInvalid(err) => { ProposalDecodingError::TxIdInvalid(err) => {
write!(f, "Invalid transaction id: {:?}", err) write!(f, "Invalid transaction id: {:?}", err)
} }
ProposalDecodingError::ValuePoolInvalid(id) => {
write!(f, "Invalid value pool identifier: {:?}", id)
}
ProposalDecodingError::InputRetrieval(err) => write!( ProposalDecodingError::InputRetrieval(err) => write!(
f, f,
"An error occurred retrieving a transaction input: {}", "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.", "An inputs field was present for {:?}, but contained no note references.",
protocol 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 { impl proposal::ProposedInput {
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> { pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
Ok(TxId::from_bytes(self.txid[..].try_into()?)) 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 { impl proposal::Proposal {
@ -311,50 +351,40 @@ impl proposal::Proposal {
) -> Option<Self> { ) -> Option<Self> {
let transaction_request = value.transaction_request().to_uri(params)?; 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() .transparent_inputs()
.iter() .iter()
.map(|utxo| proposal::ProposedInput { .map(|utxo| proposal::ProposedInput {
txid: utxo.outpoint().hash().to_vec(), txid: utxo.outpoint().hash().to_vec(),
value_pool: proposal::ValuePool::Transparent.into(),
index: utxo.outpoint().n(), index: utxo.outpoint().n(),
value: utxo.txout().value.into(), value: utxo.txout().value.into(),
}) })
.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.note().value().into(),
})
}))
.collect(); .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 { let balance = Some(proposal::TransactionBalance {
proposed_change: value proposed_change: value
.balance() .balance()
.proposed_change() .proposed_change()
.iter() .iter()
.map(|change| match change.output_pool() { .map(|change| proposal::ChangeValue {
ShieldedProtocol::Sapling => proposal::ChangeValue { value: change.value().into(),
value: Some(proposal::change_value::Value::SaplingValue( value_pool: proposal::ValuePool::from(change.output_pool()).into(),
proposal::SaplingChange { memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
amount: change.value().into(), value: memo_bytes.as_slice().to_vec(),
memo: change.memo().map(|memo_bytes| proposal::MemoBytes { }),
value: memo_bytes.as_slice().to_vec(),
}),
},
)),
},
ShieldedProtocol::Orchard => {
unimplemented!("FIXME: implement Orchard change outputs!")
}
}) })
.collect(), .collect(),
fee_required: value.balance().fee_required().into(), fee_required: value.balance().fee_required().into(),
@ -364,8 +394,8 @@ impl proposal::Proposal {
Some(proposal::Proposal { Some(proposal::Proposal {
proto_version: PROPOSAL_SER_V1, proto_version: PROPOSAL_SER_V1,
transaction_request, transaction_request,
transparent_inputs, anchor_height,
sapling_inputs, inputs,
balance, balance,
fee_rule: match value.fee_rule() { fee_rule: match value.fee_rule() {
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313, StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
@ -402,72 +432,59 @@ impl proposal::Proposal {
let transaction_request = let transaction_request =
TransactionRequest::from_uri(params, &self.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)?;
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::InputNotFound( let transparent_inputs = vec![];
txid, #[cfg(feature = "transparent-inputs")]
PoolType::Transparent, let mut transparent_inputs = vec![];
t_in.index,
));
#[cfg(feature = "transparent-inputs")] let mut received_notes = vec![];
{ for input in self.inputs.iter() {
let outpoint = OutPoint::new(txid.into(), t_in.index); let txid = input
wallet_db .parse_txid()
.get_unspent_transparent_output(&outpoint) .map_err(ProposalDecodingError::TxIdInvalid)?;
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({ match input.pool_type()? {
ProposalDecodingError::InputNotFound( PoolType::Transparent => {
txid, #[cfg(not(feature = "transparent-inputs"))]
PoolType::Transparent, return Err(ProposalDecodingError::ValuePoolInvalid(1));
t_in.index,
) #[cfg(feature = "transparent-inputs")]
}) {
let outpoint = OutPoint::new(txid.into(), input.index);
transparent_inputs.push(
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
input.index,
)
})?,
);
}
} }
}) PoolType::Shielded(protocol) => received_notes.push(
.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 wallet_db
.get_spendable_note(&txid, ShieldedProtocol::Sapling, s_in.index) .get_spendable_note(&txid, protocol, input.index)
.map_err(ProposalDecodingError::InputRetrieval) .map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| { .and_then(|opt| {
opt.ok_or({ opt.ok_or({
ProposalDecodingError::InputNotFound( ProposalDecodingError::InputNotFound(
txid, txid,
PoolType::Shielded(ShieldedProtocol::Sapling), PoolType::Shielded(protocol),
s_in.index, input.index,
) )
}) })
}) })?,
}) ),
.collect::<Result<Vec<_>, _>>() }
.and_then(|notes| { }
NonEmpty::from_vec(notes)
.map(|notes| { let shielded_inputs = NonEmpty::from_vec(received_notes)
SaplingInputs::from_parts(s_in.anchor_height.into(), notes) .map(|notes| ShieldedInputs::from_parts(self.anchor_height.into(), notes));
})
.ok_or({
ProposalDecodingError::EmptyShieldedInputs(
ShieldedProtocol::Sapling,
)
})
})
});
let proto_balance = self let proto_balance = self
.balance .balance
@ -477,31 +494,23 @@ impl proposal::Proposal {
proto_balance proto_balance
.proposed_change .proposed_change
.iter() .iter()
.filter_map(|change| { .map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
// An empty `value` field can be treated as though the whole match cv.pool_type()? {
// `ChangeValue` was absent; this optionality is an artifact of PoolType::Shielded(ShieldedProtocol::Sapling) => {
// protobuf encoding. Ok(ChangeValue::sapling(
change.value.as_ref().map( NonNegativeAmount::from_u64(cv.value)
|cv| -> Result<ChangeValue, ProposalDecodingError<_>> { .map_err(|_| ProposalDecodingError::BalanceInvalid)?,
match cv { cv.memo
proposal::change_value::Value::SaplingValue(sc) => { .as_ref()
Ok(ChangeValue::sapling( .map(|bytes| {
NonNegativeAmount::from_u64(sc.amount).map_err( MemoBytes::from_bytes(&bytes.value)
|_| ProposalDecodingError::BalanceInvalid, .map_err(ProposalDecodingError::MemoInvalid)
)?, })
sc.memo .transpose()?,
.as_ref() ))
.map(|bytes| { }
MemoBytes::from_bytes(&bytes.value).map_err( t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
ProposalDecodingError::MemoInvalid, }
)
})
.transpose()?,
))
}
}
},
)
}) })
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(proto_balance.fee_required) NonNegativeAmount::from_u64(proto_balance.fee_required)
@ -512,7 +521,7 @@ impl proposal::Proposal {
Proposal::from_parts( Proposal::from_parts(
transaction_request, transaction_request,
transparent_inputs, transparent_inputs,
sapling_inputs.transpose()?, shielded_inputs,
balance, balance,
fee_rule, fee_rule,
self.min_target_height.into(), self.min_target_height.into(),

View File

@ -8,13 +8,15 @@ pub struct Proposal {
/// ZIP 321 serialized transaction request /// ZIP 321 serialized transaction request
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub transaction_request: ::prost::alloc::string::String, pub transaction_request: ::prost::alloc::string::String,
/// The transparent UTXOs to use as inputs to the transaction. /// The anchor height to be used in creating the transaction, if any.
#[prost(message, repeated, tag = "3")] /// Setting the anchor height to zero will disallow the use of any shielded
pub transparent_inputs: ::prost::alloc::vec::Vec<ProposedInput>, /// inputs.
/// The Sapling input notes and anchor height to be used in creating the transaction. #[prost(uint32, tag = "3")]
#[prost(message, optional, tag = "4")] pub anchor_height: u32,
pub sapling_inputs: ::core::option::Option<SaplingInputs>, /// The inputs to be used in creating the transaction.
/// The total value, fee amount, and change outputs of the proposed #[prost(message, repeated, tag = "4")]
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
/// The total value, fee value, and change outputs of the proposed
/// transaction /// transaction
#[prost(message, optional, tag = "5")] #[prost(message, optional, tag = "5")]
pub balance: ::core::option::Option<TransactionBalance>, pub balance: ::core::option::Option<TransactionBalance>,
@ -32,28 +34,20 @@ pub struct Proposal {
#[prost(bool, tag = "8")] #[prost(bool, tag = "8")]
pub is_shielding: bool, pub is_shielding: bool,
} }
#[allow(clippy::derive_partial_eq_without_eq)] /// The unique identifier and value for each proposed input.
#[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.
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposedInput { pub struct ProposedInput {
#[prost(bytes = "vec", tag = "1")] #[prost(bytes = "vec", tag = "1")]
pub txid: ::prost::alloc::vec::Vec<u8>, 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, pub index: u32,
#[prost(uint64, tag = "3")] #[prost(uint64, tag = "4")]
pub value: u64, pub value: u64,
} }
/// The proposed change outputs and fee amount. /// The proposed change outputs and fee value.
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TransactionBalance { pub struct TransactionBalance {
@ -62,21 +56,17 @@ pub struct TransactionBalance {
#[prost(uint64, tag = "2")] #[prost(uint64, tag = "2")]
pub fee_required: u64, 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChangeValue { pub struct ChangeValue {
#[prost(oneof = "change_value::Value", tags = "1")] #[prost(uint64, tag = "1")]
pub value: ::core::option::Option<change_value::Value>, pub value: u64,
} #[prost(enumeration = "ValuePool", tag = "2")]
/// Nested message and enum types in `ChangeValue`. pub value_pool: i32,
pub mod change_value { #[prost(message, optional, tag = "3")]
#[allow(clippy::derive_partial_eq_without_eq)] pub memo: ::core::option::Option<MemoBytes>,
#[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 /// An object wrapper for memo bytes, to facilitate representing the
/// `change_memo == None` case. /// `change_memo == None` case.
@ -86,14 +76,44 @@ pub struct MemoBytes {
#[prost(bytes = "vec", tag = "1")] #[prost(bytes = "vec", tag = "1")]
pub value: ::prost::alloc::vec::Vec<u8>, pub value: ::prost::alloc::vec::Vec<u8>,
} }
/// The amount and memo for a proposed Sapling change output. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[allow(clippy::derive_partial_eq_without_eq)] #[repr(i32)]
#[derive(Clone, PartialEq, ::prost::Message)] pub enum ValuePool {
pub struct SaplingChange { /// Protobuf requires that enums have a zero discriminant as the default
#[prost(uint64, tag = "1")] /// value. However, we need to require that a known value pool is selected,
pub amount: u64, /// and we do not want to fall back to any default, so sending the
#[prost(message, optional, tag = "2")] /// PoolNotSpecified value will be treated as an error.
pub memo: ::core::option::Option<MemoBytes>, 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 /// The fee rule used in constructing a Proposal
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]

View File

@ -19,6 +19,9 @@ use zcash_primitives::{
use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, ShieldedProtocol}; 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 /// A unique identifier for a shielded transaction output
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NoteId { pub struct NoteId {
@ -255,26 +258,34 @@ impl 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, /// Information about a note that is tracked by the wallet that is available for spending,
/// with sufficient information for use in note selection. /// with sufficient information for use in note selection.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedNote<NoteRef> { pub struct ReceivedNote<NoteRef, NoteT> {
note_id: NoteRef, note_id: NoteRef,
txid: TxId, txid: TxId,
output_index: u16, output_index: u16,
note: Note, note: NoteT,
spending_key_scope: Scope, spending_key_scope: Scope,
note_commitment_tree_position: Position, note_commitment_tree_position: Position,
} }
impl<NoteRef> ReceivedNote<NoteRef> { impl<NoteRef, NoteT> ReceivedNote<NoteRef, NoteT> {
pub fn from_parts( pub fn from_parts(
note_id: NoteRef, note_id: NoteRef,
txid: TxId, txid: TxId,
output_index: u16, output_index: u16,
note: Note, note: NoteT,
spending_key_scope: Scope, spending_key_scope: Scope,
note_commitment_tree_position: Position, note_commitment_tree_position: Position,
) -> Self { ) -> Self {
@ -297,27 +308,72 @@ impl<NoteRef> ReceivedNote<NoteRef> {
pub fn output_index(&self) -> u16 { pub fn output_index(&self) -> u16 {
self.output_index self.output_index
} }
pub fn note(&self) -> &Note { pub fn note(&self) -> &NoteT {
&self.note &self.note
} }
pub fn value(&self) -> NonNegativeAmount {
self.note.value()
}
pub fn spending_key_scope(&self) -> Scope { pub fn spending_key_scope(&self) -> Scope {
self.spending_key_scope self.spending_key_scope
} }
pub fn note_commitment_tree_position(&self) -> Position { pub fn note_commitment_tree_position(&self) -> Position {
self.note_commitment_tree_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> ReceivedNote<NoteRef, Note> {
pub fn protocol(&self) -> ShieldedProtocol {
match self.note() {
Note::Sapling(_) => ShieldedProtocol::Sapling,
#[cfg(feature = "orchard")]
Note::Orchard(_) => ShieldedProtocol::Orchard,
}
}
}
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef, sapling::Note> {
fn note_id(&self) -> &NoteRef { fn note_id(&self) -> &NoteRef {
&self.note_id &self.note_id
} }
fn value(&self) -> NonNegativeAmount { 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.")
} }
} }

View File

@ -22,6 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"]
zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] } zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] }
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_primitives.workspace = true zcash_primitives.workspace = true
orchard.workspace = true
# Dependencies exposed in a public API: # Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.) # (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -69,7 +69,7 @@ use zcash_client_backend::{
}, },
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType, DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
}; };
@ -172,7 +172,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
txid: &TxId, txid: &TxId,
_protocol: ShieldedProtocol, _protocol: ShieldedProtocol,
index: u32, 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) 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], _sources: &[ShieldedProtocol],
anchor_height: BlockHeight, anchor_height: BlockHeight,
exclude: &[Self::NoteRef], exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> { ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes( wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(), self.conn.borrow(),
&self.params, &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." "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> { impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {

View File

@ -100,7 +100,7 @@ impl ReceivedSaplingOutput for DecryptedOutput<sapling::Note> {
fn to_spendable_note<P: consensus::Parameters>( fn to_spendable_note<P: consensus::Parameters>(
params: &P, params: &P,
row: &Row, row: &Row,
) -> Result<ReceivedNote<ReceivedNoteId>, SqliteClientError> { ) -> Result<ReceivedNote<ReceivedNoteId, Note>, SqliteClientError> {
let note_id = ReceivedNoteId(row.get(0)?); let note_id = ReceivedNoteId(row.get(0)?);
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?; let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
let output_index = row.get(2)?; let output_index = row.get(2)?;
@ -182,7 +182,7 @@ pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
params: &P, params: &P,
txid: &TxId, txid: &TxId,
index: u32, index: u32,
) -> Result<Option<ReceivedNote<ReceivedNoteId>>, SqliteClientError> { ) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
let mut stmt_select_note = conn.prepare_cached( let mut stmt_select_note = conn.prepare_cached(
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position, "SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
accounts.ufvk, recipient_key_scope accounts.ufvk, recipient_key_scope
@ -239,7 +239,7 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
target_value: Amount, target_value: Amount,
anchor_height: BlockHeight, anchor_height: BlockHeight,
exclude: &[ReceivedNoteId], exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedNote<ReceivedNoteId>>, SqliteClientError> { ) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
let birthday_height = match wallet_birthday(conn)? { let birthday_height = match wallet_birthday(conn)? {
Some(birthday) => birthday, Some(birthday) => birthday,
None => { None => {