Merge pull request #1234 from nuttycom/wallet/spendable_value

zcash_client_backend: Update `DecryptedTransaction` to support Orchard
This commit is contained in:
Kris Nuttycombe 2024-03-08 14:12:52 -07:00 committed by GitHub
commit 54addb6ca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 454 additions and 181 deletions

1
Cargo.lock generated
View File

@ -3089,6 +3089,7 @@ dependencies = [
"zcash_note_encryption",
"zcash_primitives",
"zcash_proofs",
"zcash_protocol",
"zip32",
]

View File

@ -7,6 +7,10 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_protocol::memo`:
- `impl TryFrom<&MemoBytes> for Memo`
## [0.1.0] - 2024-03-06
The entries below are relative to the `zcash_primitives` crate as of the tag
`zcash_primitives-0.14.0`.

View File

@ -197,13 +197,25 @@ impl TryFrom<MemoBytes> for Memo {
/// Returns an error if the provided slice does not represent a valid `Memo` (for
/// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical).
fn try_from(bytes: MemoBytes) -> Result<Self, Self::Error> {
Self::try_from(&bytes)
}
}
impl TryFrom<&MemoBytes> for Memo {
type Error = Error;
/// Parses a `Memo` from its ZIP 302 serialization.
///
/// Returns an error if the provided slice does not represent a valid `Memo` (for
/// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical).
fn try_from(bytes: &MemoBytes) -> Result<Self, Self::Error> {
match bytes.0[0] {
0xF6 if bytes.0.iter().skip(1).all(|&b| b == 0) => Ok(Memo::Empty),
0xFF => Ok(Memo::Arbitrary(Box::new(bytes.0[1..].try_into().unwrap()))),
b if b <= 0xF4 => str::from_utf8(bytes.as_slice())
.map(|r| Memo::Text(TextMemo(r.to_owned())))
.map_err(Error::InvalidUtf8),
_ => Ok(Memo::Future(bytes)),
_ => Ok(Memo::Future(bytes.clone())),
}
}
}

View File

@ -16,8 +16,10 @@ and this library adheres to Rust's notion of
- `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier`
- `BlockMetadata::orchard_tree_size`
- `DecryptedTransaction::{new, tx(), orchard_outputs()}`
- `ScannedBlock::orchard`
- `ScannedBlockCommitments::orchard`
- `SentTransaction::new`
- `ORCHARD_SHARD_HEIGHT`
- `BlockMetadata::orchard_tree_size`
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
@ -47,6 +49,14 @@ and this library adheres to Rust's notion of
- Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait:
- Added `get_orchard_nullifiers`
- Changes to the `InputSource` trait:
- `select_spendable_notes` now takes its `target_value` argument as a
`NonNegativeAmount`. Also, the values of the returned map are also
`NonNegativeAmount`s instead of `Amount`s.
- Fields of `DecryptedTransaction` are now private. Use `DecryptedTransaction::new`
and the newly provided accessors instead.
- Fields of `SentTransaction` are now private. Use `SentTransaction::new`
and the newly provided accessors instead.
- `ShieldedProtocol` has a new `Orchard` variant.
- `WalletCommitmentTrees`
- `type OrchardShardStore`
@ -54,6 +64,13 @@ and this library adheres to Rust's notion of
- `fn put_orchard_subtree_roots`
- Added method `WalletRead::validate_seed`
- Removed `Error::AccountNotFound` variant.
- `zcash_client_backend::decrypt`:
- Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new`
and the newly provided accessors instead.
- `decrypt_transaction` now returns a `DecryptedTransaction<AccountId>`
instead of a `DecryptedOutput<sapling::Note>` and will decrypt Orchard
outputs when the `orchard` feature is enabled. In addition, the type
constraint on its `<AccountId>` parameter has been strengthened to `Copy`.
- `zcash_client_backend::fees`:
- Arguments to `ChangeStrategy::compute_balance` have changed.
- `zcash_client_backend::zip321::render::amount_str` now takes a

View File

@ -81,7 +81,7 @@ use zcash_primitives::{
consensus::BlockHeight,
memo::{Memo, MemoBytes},
transaction::{
components::amount::{Amount, BalanceError, NonNegativeAmount},
components::amount::{BalanceError, NonNegativeAmount},
Transaction, TxId,
},
};
@ -445,7 +445,7 @@ pub trait InputSource {
fn select_spendable_notes(
&self,
account: Self::AccountId,
target_value: Amount,
target_value: NonNegativeAmount,
sources: &[ShieldedProtocol],
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
@ -662,7 +662,7 @@ pub trait WalletRead {
&self,
_account: Self::AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new())
}
@ -872,13 +872,47 @@ impl<A> ScannedBlock<A> {
}
/// A transaction that was detected during scanning of the blockchain,
/// including its decrypted Sapling outputs.
/// including its decrypted Sapling and/or Orchard outputs.
///
/// The purpose of this struct is to permit atomic updates of the
/// wallet database when transactions are successfully decrypted.
pub struct DecryptedTransaction<'a, AccountId> {
pub tx: &'a Transaction,
pub sapling_outputs: &'a Vec<DecryptedOutput<sapling::Note, AccountId>>,
tx: &'a Transaction,
sapling_outputs: Vec<DecryptedOutput<sapling::Note, AccountId>>,
#[cfg(feature = "orchard")]
orchard_outputs: Vec<DecryptedOutput<orchard::note::Note, AccountId>>,
}
impl<'a, AccountId> DecryptedTransaction<'a, AccountId> {
/// Constructs a new [`DecryptedTransaction`] from its constituent parts.
pub fn new(
tx: &'a Transaction,
sapling_outputs: Vec<DecryptedOutput<sapling::Note, AccountId>>,
#[cfg(feature = "orchard")] orchard_outputs: Vec<
DecryptedOutput<orchard::note::Note, AccountId>,
>,
) -> Self {
Self {
tx,
sapling_outputs,
#[cfg(feature = "orchard")]
orchard_outputs,
}
}
/// Returns the raw transaction data.
pub fn tx(&self) -> &Transaction {
self.tx
}
/// Returns the Sapling outputs that were decrypted from the transaction.
pub fn sapling_outputs(&self) -> &[DecryptedOutput<sapling::Note, AccountId>] {
&self.sapling_outputs
}
/// Returns the Orchard outputs that were decrypted from the transaction.
#[cfg(feature = "orchard")]
pub fn orchard_outputs(&self) -> &[DecryptedOutput<orchard::note::Note, AccountId>] {
&self.orchard_outputs
}
}
/// A transaction that was constructed and sent by the wallet.
@ -887,13 +921,61 @@ pub struct DecryptedTransaction<'a, AccountId> {
/// wallet database when transactions are created and submitted
/// to the network.
pub struct SentTransaction<'a, AccountId> {
pub tx: &'a Transaction,
pub created: time::OffsetDateTime,
pub account: AccountId,
pub outputs: Vec<SentTransactionOutput<AccountId>>,
pub fee_amount: Amount,
tx: &'a Transaction,
created: time::OffsetDateTime,
account: AccountId,
outputs: Vec<SentTransactionOutput<AccountId>>,
fee_amount: NonNegativeAmount,
#[cfg(feature = "transparent-inputs")]
pub utxos_spent: Vec<OutPoint>,
utxos_spent: Vec<OutPoint>,
}
impl<'a, AccountId> SentTransaction<'a, AccountId> {
/// Constructs a new [`SentTransaction`] from its constituent parts.
pub fn new(
tx: &'a Transaction,
created: time::OffsetDateTime,
account: AccountId,
outputs: Vec<SentTransactionOutput<AccountId>>,
fee_amount: NonNegativeAmount,
#[cfg(feature = "transparent-inputs")] utxos_spent: Vec<OutPoint>,
) -> Self {
Self {
tx,
created,
account,
outputs,
fee_amount,
#[cfg(feature = "transparent-inputs")]
utxos_spent,
}
}
/// Returns the transaction that was sent.
pub fn tx(&self) -> &Transaction {
self.tx
}
/// Returns the timestamp of the transaction's creation.
pub fn created(&self) -> time::OffsetDateTime {
self.created
}
/// Returns the id for the account that created the outputs.
pub fn account_id(&self) -> &AccountId {
&self.account
}
/// Returns the outputs of the transaction.
pub fn outputs(&self) -> &[SentTransactionOutput<AccountId>] {
self.outputs.as_ref()
}
/// Returns the fee paid by the transaction.
pub fn fee_amount(&self) -> NonNegativeAmount {
self.fee_amount
}
/// Returns the list of UTXOs spent in the created transaction.
#[cfg(feature = "transparent-inputs")]
pub fn utxos_spent(&self) -> &[OutPoint] {
self.utxos_spent.as_ref()
}
}
/// An output of a transaction generated by the wallet.
@ -1279,7 +1361,7 @@ pub mod testing {
block::BlockHash,
consensus::{BlockHeight, Network},
memo::Memo,
transaction::{components::Amount, Transaction, TxId},
transaction::{components::amount::NonNegativeAmount, Transaction, TxId},
};
use crate::{
@ -1344,7 +1426,7 @@ pub mod testing {
fn select_spendable_notes(
&self,
_account: Self::AccountId,
_target_value: Amount,
_target_value: NonNegativeAmount,
_sources: &[ShieldedProtocol],
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
@ -1489,7 +1571,7 @@ pub mod testing {
&self,
_account: Self::AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new())
}

View File

@ -40,8 +40,8 @@ use super::InputSource;
use crate::{
address::Address,
data_api::{
error::Error, DecryptedTransaction, SentTransaction, SentTransactionOutput,
WalletCommitmentTrees, WalletRead, WalletWrite,
error::Error, SentTransaction, SentTransactionOutput, WalletCommitmentTrees, WalletRead,
WalletWrite,
},
decrypt_transaction,
fees::{self, DustOutputPolicy},
@ -51,20 +51,17 @@ use crate::{
zip321::{self, Payment},
PoolType, ShieldedProtocol,
};
use zcash_primitives::{
use zcash_primitives::transaction::{
builder::{BuildConfig, BuildResult, Builder},
components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
Transaction, TxId,
};
use zcash_protocol::{
consensus::{self, BlockHeight, NetworkUpgrade},
memo::MemoBytes,
transaction::{
builder::{BuildConfig, BuildResult, Builder},
components::{
amount::{Amount, NonNegativeAmount},
sapling::zip212_enforcement,
},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
Transaction, TxId,
},
zip32::Scope,
};
use zip32::Scope;
#[cfg(feature = "transparent-inputs")]
use {
@ -102,10 +99,7 @@ where
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
.expect("Sapling activation height must be known.");
data.store_decrypted_tx(DecryptedTransaction {
tx,
sapling_outputs: &decrypt_transaction(params, height, tx, &ufvks),
})?;
data.store_decrypted_tx(decrypt_transaction(params, height, tx, &ufvks))?;
Ok(())
}
@ -1180,7 +1174,7 @@ where
created: time::OffsetDateTime::now_utc(),
account,
outputs,
fee_amount: Amount::from(proposal_step.balance().fee_required()),
fee_amount: proposal_step.balance().fee_required(),
#[cfg(feature = "transparent-inputs")]
utxos_spent,
})

View File

@ -462,7 +462,7 @@ where
shielded_inputs = wallet_db
.select_spendable_notes(
account,
amount_required.into(),
amount_required,
selectable_pools,
anchor_height,
&exclude,

View File

@ -1,17 +1,19 @@
use std::collections::HashMap;
use sapling::note_encryption::{
try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey,
};
use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain};
use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
transaction::components::sapling::zip212_enforcement,
transaction::components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
transaction::Transaction,
zip32::Scope,
};
use crate::keys::UnifiedFullViewingKey;
use crate::{data_api::DecryptedTransaction, keys::UnifiedFullViewingKey};
#[cfg(feature = "orchard")]
use orchard::note_encryption::OrchardDomain;
/// An enumeration of the possible relationships a TXO can have to the wallet.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -29,43 +31,91 @@ pub enum TransferType {
/// A decrypted shielded output.
pub struct DecryptedOutput<Note, AccountId> {
/// The index of the output within [`shielded_outputs`].
///
/// [`shielded_outputs`]: zcash_primitives::transaction::TransactionData
pub index: usize,
index: usize,
note: Note,
account: AccountId,
memo: MemoBytes,
transfer_type: TransferType,
}
impl<Note, AccountId: Copy> DecryptedOutput<Note, AccountId> {
pub fn new(
index: usize,
note: Note,
account: AccountId,
memo: MemoBytes,
transfer_type: TransferType,
) -> Self {
Self {
index,
note,
account,
memo,
transfer_type,
}
}
/// The index of the output within the shielded outputs of the Sapling bundle or the actions of
/// the Orchard bundle, depending upon the type of [`Self::note`].
pub fn index(&self) -> usize {
self.index
}
/// The note within the output.
pub note: Note,
pub fn note(&self) -> &Note {
&self.note
}
/// The account that decrypted the note.
pub account: AccountId,
pub fn account(&self) -> &AccountId {
&self.account
}
/// The memo bytes included with the note.
pub memo: MemoBytes,
/// True if this output was recovered using an [`OutgoingViewingKey`], meaning that
/// this is a logical output of the transaction.
///
/// [`OutgoingViewingKey`]: sapling::keys::OutgoingViewingKey
pub transfer_type: TransferType,
pub fn memo(&self) -> &MemoBytes {
&self.memo
}
/// Returns a [`TransferType`] value that is determined based upon what type of key was used to
/// decrypt the transaction.
pub fn transfer_type(&self) -> TransferType {
self.transfer_type
}
}
impl<A> DecryptedOutput<sapling::Note, A> {
pub fn note_value(&self) -> NonNegativeAmount {
NonNegativeAmount::from_u64(self.note.value().inner())
.expect("Sapling note value is expected to have been validated by consensus.")
}
}
#[cfg(feature = "orchard")]
impl<A> DecryptedOutput<orchard::note::Note, A> {
pub fn note_value(&self) -> NonNegativeAmount {
NonNegativeAmount::from_u64(self.note.value().inner())
.expect("Orchard note value is expected to have been validated by consensus.")
}
}
/// Scans a [`Transaction`] for any information that can be decrypted by the set of
/// [`UnifiedFullViewingKey`]s.
pub fn decrypt_transaction<P: consensus::Parameters, A: Clone>(
pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>(
params: &P,
height: BlockHeight,
tx: &Transaction,
ufvks: &HashMap<A, UnifiedFullViewingKey>,
) -> Vec<DecryptedOutput<sapling::Note, A>> {
tx: &'a Transaction,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
) -> DecryptedTransaction<'a, AccountId> {
let zip212_enforcement = zip212_enforcement(params, height);
tx.sapling_bundle()
let sapling_bundle = tx.sapling_bundle();
let sapling_outputs = sapling_bundle
.iter()
.flat_map(|bundle| {
ufvks
.iter()
.flat_map(move |(account, ufvk)| {
ufvk.sapling()
.into_iter()
.map(|dfvk| (account.to_owned(), dfvk))
})
.flat_map(move |(account, dfvk)| {
.flat_map(|(account, ufvk)| ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk)))
.flat_map(|(account, dfvk)| {
let sapling_domain = SaplingDomain::new(zip212_enforcement);
let ivk_external =
PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External));
let ivk_internal =
@ -77,31 +127,101 @@ pub fn decrypt_transaction<P: consensus::Parameters, A: Clone>(
.iter()
.enumerate()
.flat_map(move |(index, output)| {
let account = account.clone();
try_sapling_note_decryption(&ivk_external, output, zip212_enforcement)
try_note_decryption(&sapling_domain, &ivk_external, output)
.map(|ret| (ret, TransferType::Incoming))
.or_else(|| {
try_sapling_note_decryption(
&ivk_internal,
output,
zip212_enforcement,
)
.map(|ret| (ret, TransferType::WalletInternal))
try_note_decryption(&sapling_domain, &ivk_internal, output)
.map(|ret| (ret, TransferType::WalletInternal))
})
.or_else(|| {
try_sapling_output_recovery(&ovk, output, zip212_enforcement)
.map(|ret| (ret, TransferType::Outgoing))
try_output_recovery_with_ovk(
&sapling_domain,
&ovk,
output,
output.cv(),
output.out_ciphertext(),
)
.map(|ret| (ret, TransferType::Outgoing))
})
.into_iter()
.map(move |((note, _, memo), transfer_type)| DecryptedOutput {
index,
note,
account: account.clone(),
memo: MemoBytes::from_bytes(&memo).expect("correct length"),
transfer_type,
.map(move |((note, _, memo), transfer_type)| {
DecryptedOutput::new(
index,
note,
account,
MemoBytes::from_bytes(&memo).expect("correct length"),
transfer_type,
)
})
})
})
})
.collect()
.collect();
#[cfg(feature = "orchard")]
let orchard_bundle = tx.orchard_bundle();
#[cfg(feature = "orchard")]
let orchard_outputs = orchard_bundle
.iter()
.flat_map(|bundle| {
ufvks
.iter()
.flat_map(move |(account, ufvk)| {
ufvk.orchard()
.into_iter()
.map(|fvk| (account.to_owned(), fvk))
})
.flat_map(move |(account, fvk)| {
let ivk_external = orchard::keys::PreparedIncomingViewingKey::new(
&fvk.to_ivk(Scope::External),
);
let ivk_internal = orchard::keys::PreparedIncomingViewingKey::new(
&fvk.to_ivk(Scope::Internal),
);
let ovk = fvk.to_ovk(Scope::External);
bundle
.actions()
.iter()
.enumerate()
.flat_map(move |(index, action)| {
let domain = OrchardDomain::for_nullifier(*action.nullifier());
let account = account;
try_note_decryption(&domain, &ivk_external, action)
.map(|ret| (ret, TransferType::Incoming))
.or_else(|| {
try_note_decryption(&domain, &ivk_internal, action)
.map(|ret| (ret, TransferType::WalletInternal))
})
.or_else(|| {
try_output_recovery_with_ovk(
&domain,
&ovk,
action,
action.cv_net(),
&action.encrypted_note().out_ciphertext,
)
.map(|ret| (ret, TransferType::Outgoing))
})
.into_iter()
.map(move |((note, _, memo), transfer_type)| {
DecryptedOutput::new(
index,
note,
account,
MemoBytes::from_bytes(&memo).expect("correct length"),
transfer_type,
)
})
})
})
})
.collect();
DecryptedTransaction::new(
tx,
sapling_outputs,
#[cfg(feature = "orchard")]
orchard_outputs,
)
}

View File

@ -24,6 +24,7 @@ zcash_client_backend = { workspace = true, features = ["unstable-serialization",
zcash_encoding.workspace = true
zcash_keys = { workspace = true, features = ["orchard", "sapling"] }
zcash_primitives.workspace = true
zcash_protocol.workspace = true
zip32.workspace = true
# Dependencies exposed in a public API:

View File

@ -50,10 +50,7 @@ use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
memo::{Memo, MemoBytes},
transaction::{
components::amount::{Amount, NonNegativeAmount},
Transaction, TxId,
},
transaction::{components::amount::NonNegativeAmount, Transaction, TxId},
zip32::{self, DiversifierIndex, Scope},
};
@ -78,7 +75,7 @@ use zcash_client_backend::{
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, PoolType};
#[cfg(feature = "transparent-inputs")]
use {
@ -216,7 +213,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
fn select_spendable_notes(
&self,
account: AccountId,
target_value: Amount,
target_value: NonNegativeAmount,
_sources: &[ShieldedProtocol],
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
@ -439,7 +436,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
&self,
account: AccountId,
max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
wallet::get_transparent_balances(self.conn.borrow(), &self.params, account, max_height)
}
@ -665,35 +662,31 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
d_tx: DecryptedTransaction<AccountId>,
) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None)?;
let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx(), None, None)?;
let mut spending_account_id: Option<AccountId> = None;
for output in d_tx.sapling_outputs {
match output.transfer_type {
for output in d_tx.sapling_outputs() {
match output.transfer_type() {
TransferType::Outgoing | TransferType::WalletInternal => {
let value = output.note.value();
let recipient = if output.transfer_type == TransferType::Outgoing {
Recipient::Sapling(output.note.recipient())
let recipient = if output.transfer_type() == TransferType::Outgoing {
//TODO: Recover the UA, if possible.
Recipient::Sapling(output.note().recipient())
} else {
Recipient::InternalAccount(
output.account,
Note::Sapling(output.note.clone()),
*output.account(),
Note::Sapling(output.note().clone()),
)
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
output.account,
*output.account(),
tx_ref,
output.index,
output.index(),
&recipient,
NonNegativeAmount::from_u64(value.inner()).map_err(|_| {
SqliteClientError::CorruptedData(
"Note value is not a valid Zcash amount.".to_string(),
)
})?,
Some(&output.memo),
output.note_value(),
Some(output.memo()),
)?;
if matches!(recipient, Recipient::InternalAccount(_, _)) {
@ -703,12 +696,12 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
TransferType::Incoming => {
match spending_account_id {
Some(id) => {
if id != output.account {
if id != *output.account() {
panic!("Unable to determine a unique account identifier for z->t spend.");
}
}
None => {
spending_account_id = Some(output.account);
spending_account_id = Some(*output.account());
}
}
@ -717,24 +710,80 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
}
#[cfg(feature = "orchard")]
#[allow(unused_assignments)] // Remove this when the todo!()s below are implemented.
for output in d_tx.orchard_outputs() {
match output.transfer_type() {
TransferType::Outgoing | TransferType::WalletInternal => {
let recipient = if output.transfer_type() == TransferType::Outgoing {
// TODO: Recover the actual UA, if possible.
Recipient::Unified(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None
).expect("UA has an Orchard receiver by construction."),
PoolType::Shielded(ShieldedProtocol::Orchard)
)
} else {
Recipient::InternalAccount(
*output.account(),
Note::Orchard(*output.note()),
)
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*output.account(),
tx_ref,
output.index(),
&recipient,
output.note_value(),
Some(output.memo()),
)?;
if matches!(recipient, Recipient::InternalAccount(_, _)) {
todo!();
//wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
TransferType::Incoming => {
match spending_account_id {
Some(id) => {
if id != *output.account() {
panic!("Unable to determine a unique account identifier for z->t spend.");
}
}
None => {
spending_account_id = Some(*output.account());
}
}
todo!()
//wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
}
// If any of the utxos spent in the transaction are ours, mark them as spent.
#[cfg(feature = "transparent-inputs")]
for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
for txin in d_tx.tx().transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, &txin.prevout)?;
}
// If we have some transparent outputs:
if d_tx.tx.transparent_bundle().iter().any(|b| !b.vout.is_empty()) {
if d_tx.tx().transparent_bundle().iter().any(|b| !b.vout.is_empty()) {
let nullifiers = wdb.get_sapling_nullifiers(NullifierQuery::All)?;
// If the transaction contains shielded spends from our wallet, we will store z->t
// transactions we observe in the same way they would be stored by
// create_spend_to_address.
if let Some((account_id, _)) = nullifiers.iter().find(
|(_, nf)|
d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter())
d_tx.tx().sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter())
.any(|input| nf == input.nullifier())
) {
for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
for (output_index, txout) in d_tx.tx().transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
if let Some(address) = txout.recipient_address() {
wallet::put_sent_output(
wdb.conn.0,
@ -759,9 +808,9 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
self.transactionally(|wdb| {
let tx_ref = wallet::put_tx_data(
wdb.conn.0,
sent_tx.tx,
Some(sent_tx.fee_amount),
Some(sent_tx.created),
sent_tx.tx(),
Some(sent_tx.fee_amount()),
Some(sent_tx.created()),
)?;
// Mark notes as spent.
@ -772,7 +821,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
//
// Assumes that create_spend_to_address() will never be called in parallel, which is a
// reasonable assumption for a light client such as a mobile phone.
if let Some(bundle) = sent_tx.tx.sapling_bundle() {
if let Some(bundle) = sent_tx.tx().sapling_bundle() {
for spend in bundle.shielded_spends() {
wallet::sapling::mark_sapling_note_spent(
wdb.conn.0,
@ -783,16 +832,16 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}
#[cfg(feature = "transparent-inputs")]
for utxo_outpoint in &sent_tx.utxos_spent {
for utxo_outpoint in sent_tx.utxos_spent() {
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?;
}
for output in &sent_tx.outputs {
for output in sent_tx.outputs() {
wallet::insert_sent_output(
wdb.conn.0,
&wdb.params,
tx_ref,
sent_tx.account,
*sent_tx.account_id(),
output,
)?;
@ -800,15 +849,15 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
Recipient::InternalAccount(account, Note::Sapling(note)) => {
wallet::sapling::put_received_note(
wdb.conn.0,
&DecryptedOutput {
index: output.output_index(),
note: note.clone(),
account: *account,
memo: output
&DecryptedOutput::new(
output.output_index(),
note.clone(),
*account,
output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
transfer_type: TransferType::WalletInternal,
},
TransferType::WalletInternal,
),
tx_ref,
None,
)?;

View File

@ -1727,7 +1727,7 @@ pub(crate) fn get_transparent_balances<P: consensus::Parameters>(
params: &P,
account: AccountId,
max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, SqliteClientError> {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, SqliteClientError> {
let chain_tip_height = scan_queue_extrema(conn)?.map(|range| *range.end());
let stable_height = chain_tip_height
.unwrap_or(max_height)
@ -1753,7 +1753,7 @@ pub(crate) fn get_transparent_balances<P: consensus::Parameters>(
while let Some(row) = rows.next()? {
let taddr_str: String = row.get(0)?;
let taddr = TransparentAddress::decode(params, &taddr_str)?;
let value = Amount::from_i64(row.get(1)?).unwrap();
let value = NonNegativeAmount::from_nonnegative_i64(row.get(1)?)?;
res.insert(taddr, value);
}
@ -1874,7 +1874,7 @@ pub(crate) fn put_tx_meta(
pub(crate) fn put_tx_data(
conn: &rusqlite::Connection,
tx: &Transaction,
fee: Option<Amount>,
fee: Option<NonNegativeAmount>,
created_at: Option<time::OffsetDateTime>,
) -> Result<i64, SqliteClientError> {
let mut stmt_upsert_tx_data = conn.prepare_cached(
@ -1896,7 +1896,7 @@ pub(crate) fn put_tx_data(
":created_at": created_at,
":expiry_height": u32::from(tx.expiry_height()),
":raw": raw_tx,
":fee": fee.map(i64::from),
":fee": fee.map(u64::from),
];
stmt_upsert_tx_data
@ -2330,7 +2330,7 @@ mod tests {
zcash_primitives::{
consensus::BlockHeight,
transaction::{
components::{Amount, OutPoint, TxOut},
components::{OutPoint, TxOut},
fees::fixed::FeeRule as FixedFeeRule,
},
},
@ -2437,7 +2437,7 @@ mod tests {
assert_matches!(
st.wallet().get_transparent_balances(account_id, height_2),
Ok(h) if h.get(taddr) == Some(&value.into())
Ok(h) if h.get(taddr) == Some(&value)
);
// Artificially delete the address from the addresses table so that
@ -2517,8 +2517,8 @@ mod tests {
.unwrap()
.get(taddr)
.cloned()
.unwrap_or(Amount::zero()),
Amount::from(expected),
.unwrap_or(NonNegativeAmount::ZERO),
expected,
);
assert_eq!(
st.wallet()

View File

@ -290,14 +290,11 @@ mod tests {
use rusqlite::{named_params, params, Connection};
use tempfile::NamedTempFile;
use zcash_client_backend::{
data_api::{
BlockMetadata, DecryptedTransaction, WalletCommitmentTrees, SAPLING_SHARD_HEIGHT,
},
data_api::{BlockMetadata, WalletCommitmentTrees, SAPLING_SHARD_HEIGHT},
decrypt_transaction,
proto::compact_formats::{CompactBlock, CompactTx},
scanning::{scan_block, Nullifiers, ScanningKeys},
wallet::Recipient,
PoolType, ShieldedProtocol, TransferType,
TransferType,
};
use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey};
use zcash_primitives::{
@ -496,34 +493,26 @@ mod tests {
// We can't use `decrypt_and_store_transaction` because we haven't migrated yet.
// Replicate its relevant innards here.
let d_tx = DecryptedTransaction {
let d_tx = decrypt_transaction(
&params,
height,
tx,
sapling_outputs: &decrypt_transaction(
&params,
height,
tx,
&[(account_id, ufvk0)].into_iter().collect(),
),
};
&[(account_id, ufvk0)].into_iter().collect(),
);
db_data
.transactionally::<_, _, rusqlite::Error>(|wdb| {
let tx_ref = crate::wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None).unwrap();
let tx_ref = crate::wallet::put_tx_data(wdb.conn.0, d_tx.tx(), None, None).unwrap();
let mut spending_account_id: Option<AccountId> = None;
for output in d_tx.sapling_outputs {
match output.transfer_type {
TransferType::Outgoing | TransferType::WalletInternal => {
let recipient = if output.transfer_type == TransferType::Outgoing {
Recipient::Sapling(output.note.recipient())
} else {
Recipient::InternalAccount(
output.account,
PoolType::Shielded(ShieldedProtocol::Sapling),
)
};
// Orchard outputs were not supported as of the wallet states that could require this
// migration.
for output in d_tx.sapling_outputs() {
match output.transfer_type() {
TransferType::Outgoing | TransferType::WalletInternal => {
// Don't need to bother with sent outputs for this test.
if matches!(recipient, Recipient::InternalAccount(_, _)) {
if output.transfer_type() != TransferType::Outgoing {
put_received_note_before_migration(
wdb.conn.0, output, tx_ref, None,
)
@ -532,11 +521,12 @@ mod tests {
}
TransferType::Incoming => {
match spending_account_id {
Some(id) => assert_eq!(id, output.account),
Some(id) => assert_eq!(id, *output.account()),
None => {
spending_account_id = Some(output.account);
spending_account_id = Some(*output.account());
}
}
put_received_note_before_migration(wdb.conn.0, output, tx_ref, None)
.unwrap();
}

View File

@ -100,11 +100,14 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
})?;
let decrypted_outputs = decrypt_transaction(&self.params, block_height, &tx, &ufvks);
for d_out in decrypted_outputs {
// Orchard outputs were not supported as of the wallet states that could require this
// migration.
for d_out in decrypted_outputs.sapling_outputs() {
stmt_update_sent_memo.execute(named_params![
":id_tx": id_tx,
":output_index": d_out.index,
":memo": memo_repr(Some(&d_out.memo))
":output_index": d_out.index(),
":memo": memo_repr(Some(d_out.memo()))
])?;
}
}

View File

@ -11,10 +11,10 @@ use zcash_client_backend::{
wallet::{Note, ReceivedNote, WalletSaplingOutput},
DecryptedOutput, TransferType,
};
use zcash_primitives::{
use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId};
use zcash_protocol::{
consensus::{self, BlockHeight},
memo::MemoBytes,
transaction::{components::Amount, TxId},
};
use zip32::Scope;
@ -63,19 +63,19 @@ impl ReceivedSaplingOutput for WalletSaplingOutput<AccountId> {
impl ReceivedSaplingOutput for DecryptedOutput<sapling::Note, AccountId> {
fn index(&self) -> usize {
self.index
self.index()
}
fn account_id(&self) -> AccountId {
self.account
*self.account()
}
fn note(&self) -> &sapling::Note {
&self.note
self.note()
}
fn memo(&self) -> Option<&MemoBytes> {
Some(&self.memo)
Some(self.memo())
}
fn is_change(&self) -> bool {
self.transfer_type == TransferType::WalletInternal
self.transfer_type() == TransferType::WalletInternal
}
fn nullifier(&self) -> Option<&sapling::Nullifier> {
None
@ -84,7 +84,7 @@ impl ReceivedSaplingOutput for DecryptedOutput<sapling::Note, AccountId> {
None
}
fn recipient_key_scope(&self) -> Option<Scope> {
if self.transfer_type == TransferType::WalletInternal {
if self.transfer_type() == TransferType::WalletInternal {
Some(Scope::Internal)
} else {
Some(Scope::External)
@ -231,7 +231,7 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
conn: &Connection,
params: &P,
account: AccountId,
target_value: Amount,
target_value: NonNegativeAmount,
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
@ -305,7 +305,7 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
named_params![
":account": account.0,
":anchor_height": &u32::from(anchor_height),
":target_value": &i64::from(target_value),
":target_value": &u64::from(target_value),
":exclude": &excluded_ptr,
":wallet_birthday": u32::from(birthday_height)
],
@ -480,7 +480,7 @@ pub(crate) mod tests {
legacy::TransparentAddress,
memo::{Memo, MemoBytes},
transaction::{
components::{amount::NonNegativeAmount, sapling::zip212_enforcement, Amount},
components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
fees::{
fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule,
},
@ -607,16 +607,16 @@ pub(crate) mod tests {
let ufvks = [(account, usk.to_unified_full_viewing_key())]
.into_iter()
.collect();
let decrypted_outputs = decrypt_transaction(&st.network(), h + 1, &tx, &ufvks);
assert_eq!(decrypted_outputs.len(), 2);
let d_tx = decrypt_transaction(&st.network(), h + 1, &tx, &ufvks);
assert_eq!(d_tx.sapling_outputs().len(), 2);
let mut found_tx_change_memo = false;
let mut found_tx_empty_memo = false;
for output in decrypted_outputs {
if output.memo == change_memo.clone().into() {
for output in d_tx.sapling_outputs() {
if Memo::try_from(output.memo()).unwrap() == change_memo {
found_tx_change_memo = true
}
if output.memo == Memo::Empty.into() {
if Memo::try_from(output.memo()).unwrap() == Memo::Empty {
found_tx_empty_memo = true
}
}
@ -1743,7 +1743,7 @@ pub(crate) mod tests {
&st.wallet().conn,
&st.wallet().params,
account.0,
Amount::const_from_i64(300000),
NonNegativeAmount::const_from_u64(300000),
received_tx_height + 10,
&[],
)
@ -1759,7 +1759,7 @@ pub(crate) mod tests {
&st.wallet().conn,
&st.wallet().params,
account.0,
Amount::const_from_i64(300000),
NonNegativeAmount::const_from_u64(300000),
received_tx_height + 10,
&[],
)
@ -1813,7 +1813,7 @@ pub(crate) mod tests {
&st.wallet().conn,
&st.wallet().params,
account,
Amount::const_from_i64(300000),
NonNegativeAmount::const_from_u64(300000),
birthday.height() + 5,
&[],
)