Merge pull request #659 from nuttycom/wallet/decrypt_with_internal_key

Track inputs sent to wallet-internal recipients
This commit is contained in:
Kris Nuttycombe 2022-10-11 16:57:54 -06:00 committed by GitHub
commit f689018fcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 510 additions and 373 deletions

View File

@ -31,12 +31,17 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::address`: - `zcash_client_backend::address`:
- `RecipientAddress::Unified` - `RecipientAddress::Unified`
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:
- `PoolType`
- `Recipient`
- `SentTransactionOutput`
- `WalletRead::get_unified_full_viewing_keys` - `WalletRead::get_unified_full_viewing_keys`
- `WalletRead::get_current_address` - `WalletRead::get_current_address`
- `WalletRead::get_all_nullifiers` - `WalletRead::get_all_nullifiers`
- `WalletWrite::create_account` - `WalletWrite::create_account`
- `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag). - `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag).
- `WalletWrite::get_next_available_address` - `WalletWrite::get_next_available_address`
- `zcash_client_backend::decrypt`:
- `TransferType`
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx` - `actions` field on `compact_formats::CompactTx`
- `compact_formats::CompactOrchardAction` - `compact_formats::CompactOrchardAction`
@ -44,7 +49,10 @@ and this library adheres to Rust's notion of
- `TransactionRequest::new` for constructing a request from `Vec<Payment>`. - `TransactionRequest::new` for constructing a request from `Vec<Payment>`.
- `TransactionRequest::payments` for accessing the `Payments` that make up a - `TransactionRequest::payments` for accessing the `Payments` that make up a
request. request.
- `zcash_client_backend::encoding::KeyError` - `zcash_client_backend::encoding`
- `KeyError`
- `AddressCodec` implementations for `sapling::PaymentAddress` and
`UnifiedAddress`
- New experimental APIs that should be considered unstable, and are - New experimental APIs that should be considered unstable, and are
likely to be modified and/or moved to a different module in a future likely to be modified and/or moved to a different module in a future
release: release:

View File

@ -11,15 +11,16 @@ use secrecy::SecretVec;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::BlockHeight, consensus::BlockHeight,
legacy::TransparentAddress,
memo::{Memo, MemoBytes}, memo::{Memo, MemoBytes},
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Nullifier}, sapling::{Node, Nullifier, PaymentAddress},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, ExtendedFullViewingKey},
}; };
use crate::{ use crate::{
address::{RecipientAddress, UnifiedAddress}, address::UnifiedAddress,
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
@ -27,10 +28,7 @@ use crate::{
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {crate::wallet::WalletTransparentOutput, zcash_primitives::transaction::components::OutPoint};
crate::wallet::WalletTransparentOutput,
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
};
pub mod chain; pub mod chain;
pub mod error; pub mod error;
@ -248,13 +246,34 @@ pub struct SentTransaction<'a> {
pub tx: &'a Transaction, pub tx: &'a Transaction,
pub created: time::OffsetDateTime, pub created: time::OffsetDateTime,
pub account: AccountId, pub account: AccountId,
pub outputs: Vec<SentTransactionOutput<'a>>, pub outputs: Vec<SentTransactionOutput>,
pub fee_amount: Amount, pub fee_amount: Amount,
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
pub utxos_spent: Vec<OutPoint>, pub utxos_spent: Vec<OutPoint>,
} }
pub struct SentTransactionOutput<'a> { /// A value pool to which the wallet supports sending transaction outputs.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PoolType {
/// The transparent value pool
Transparent,
/// The Sapling value pool
Sapling,
}
/// A type that represents the recipient of a transaction output; a recipient address (and, for
/// unified addresses, the pool to which the payment is sent) in the case of outgoing output, or an
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
/// output.
#[derive(Debug, Clone)]
pub enum Recipient {
Transparent(TransparentAddress),
Sapling(PaymentAddress),
Unified(UnifiedAddress, PoolType),
InternalAccount(AccountId, PoolType),
}
pub struct SentTransactionOutput {
/// The index within the transaction that contains the recipient output. /// The index within the transaction that contains the recipient output.
/// ///
/// - If `recipient_address` is a Sapling address, this is an index into the Sapling /// - If `recipient_address` is a Sapling address, this is an index into the Sapling
@ -262,8 +281,12 @@ pub struct SentTransactionOutput<'a> {
/// - If `recipient_address` is a transparent address, this is an index into the /// - If `recipient_address` is a transparent address, this is an index into the
/// transparent outputs of the transaction. /// transparent outputs of the transaction.
pub output_index: usize, pub output_index: usize,
pub recipient_address: &'a RecipientAddress, /// The recipient address of the transaction, or the account
/// id for wallet-internal transactions.
pub recipient: Recipient,
/// The value of the newly created output
pub value: Amount, pub value: Amount,
/// The memo that was attached to the output, if any
pub memo: Option<MemoBytes>, pub memo: Option<MemoBytes>,
} }

View File

@ -23,7 +23,8 @@ use {
use crate::{ use crate::{
address::RecipientAddress, address::RecipientAddress,
data_api::{ data_api::{
error::Error, DecryptedTransaction, SentTransaction, SentTransactionOutput, WalletWrite, error::Error, DecryptedTransaction, PoolType, Recipient, SentTransaction,
SentTransactionOutput, WalletWrite,
}, },
decrypt_transaction, decrypt_transaction,
wallet::OvkPolicy, wallet::OvkPolicy,
@ -373,14 +374,21 @@ where
let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?; let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?;
let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| { let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| {
let idx = match &payment.recipient_address { let (output_index, recipient) = match &payment.recipient_address {
// Sapling outputs are shuffled, so we need to look up where the output ended up. // Sapling outputs are shuffled, so we need to look up where the output ended up.
// TODO: When we add Orchard support, we will need to trial-decrypt to find them. RecipientAddress::Shielded(addr) => {
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => let idx = tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment.");
tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."), (idx, Recipient::Sapling(addr.clone()))
}
RecipientAddress::Unified(addr) => {
// TODO: When we add Orchard support, we will need to trial-decrypt to find them,
// and return the appropriate pool type.
let idx = tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment.");
(idx, Recipient::Unified(addr.clone(), PoolType::Sapling))
}
RecipientAddress::Transparent(addr) => { RecipientAddress::Transparent(addr) => {
let script = addr.script(); let script = addr.script();
tx.transparent_bundle() let idx = tx.transparent_bundle()
.and_then(|b| { .and_then(|b| {
b.vout b.vout
.iter() .iter()
@ -388,13 +396,15 @@ where
.find(|(_, tx_out)| tx_out.script_pubkey == script) .find(|(_, tx_out)| tx_out.script_pubkey == script)
}) })
.map(|(index, _)| index) .map(|(index, _)| index)
.expect("An output should exist in the transaction for each transparent payment.") .expect("An output should exist in the transaction for each transparent payment.");
(idx, Recipient::Transparent(*addr))
} }
}; };
SentTransactionOutput { SentTransactionOutput {
output_index: idx, output_index,
recipient_address: &payment.recipient_address, recipient,
value: payment.amount, value: payment.amount,
memo: payment.memo.clone() memo: payment.memo.clone()
} }
@ -512,12 +522,7 @@ where
// add the sapling output to shield the funds // add the sapling output to shield the funds
builder builder
.add_sapling_output( .add_sapling_output(Some(ovk), shielding_address, amount_to_shield, memo.clone())
Some(ovk),
shielding_address.clone(),
amount_to_shield,
memo.clone(),
)
.map_err(Error::Builder)?; .map_err(Error::Builder)?;
let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?; let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?;
@ -531,8 +536,8 @@ where
account, account,
outputs: vec![SentTransactionOutput { outputs: vec![SentTransactionOutput {
output_index, output_index,
recipient_address: &RecipientAddress::Shielded(shielding_address),
value: amount_to_shield, value: amount_to_shield,
recipient: Recipient::InternalAccount(account, PoolType::Sapling),
memo: Some(memo.clone()), memo: Some(memo.clone()),
}], }],
fee_amount: fee, fee_amount: fee,

View File

@ -10,11 +10,25 @@ use zcash_primitives::{
Note, PaymentAddress, Note, PaymentAddress,
}, },
transaction::Transaction, transaction::Transaction,
zip32::AccountId, zip32::{AccountId, Scope},
}; };
use crate::keys::UnifiedFullViewingKey; use crate::keys::UnifiedFullViewingKey;
/// An enumeration of the possible relationships a TXO can have to the wallet.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TransferType {
/// The output was received on one of the wallet's external addresses via decryption using the
/// associated incoming viewing key, or at one of the wallet's transparent addresses.
Incoming,
/// The output was received on one of the wallet's internal-only shielded addresses via trial
/// decryption using one of the wallet's internal incoming viewing keys.
WalletInternal,
/// The output was decrypted using one of the wallet's outgoing viewing keys, or was created
/// in a transaction constructed by this wallet.
Outgoing,
}
/// A decrypted shielded output. /// A decrypted shielded output.
pub struct DecryptedOutput { pub struct DecryptedOutput {
/// The index of the output within [`shielded_outputs`]. /// The index of the output within [`shielded_outputs`].
@ -33,7 +47,7 @@ pub struct DecryptedOutput {
/// this is a logical output of the transaction. /// this is a logical output of the transaction.
/// ///
/// [`OutgoingViewingKey`]: zcash_primitives::keys::OutgoingViewingKey /// [`OutgoingViewingKey`]: zcash_primitives::keys::OutgoingViewingKey
pub outgoing: bool, pub transfer_type: TransferType,
} }
/// Scans a [`Transaction`] for any information that can be decrypted by the set of /// Scans a [`Transaction`] for any information that can be decrypted by the set of
@ -44,37 +58,52 @@ pub fn decrypt_transaction<P: consensus::Parameters>(
tx: &Transaction, tx: &Transaction,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>, ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
) -> Vec<DecryptedOutput> { ) -> Vec<DecryptedOutput> {
let mut decrypted = vec![]; tx.sapling_bundle()
.iter()
.flat_map(|bundle| {
ufvks
.iter()
.flat_map(move |(account, ufvk)| {
ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk))
})
.flat_map(move |(account, dfvk)| {
let ivk_external =
PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External));
let ivk_internal =
PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
let ovk = dfvk.fvk().ovk;
if let Some(bundle) = tx.sapling_bundle() { bundle
for (account, ufvk) in ufvks.iter() { .shielded_outputs
if let Some(dfvk) = ufvk.sapling() { .iter()
let ivk = PreparedIncomingViewingKey::new(&dfvk.fvk().vk.ivk()); .enumerate()
let ovk = dfvk.fvk().ovk; .flat_map(move |(index, output)| {
try_sapling_note_decryption(params, height, &ivk_external, output)
for (index, output) in bundle.shielded_outputs.iter().enumerate() { .map(|ret| (ret, TransferType::Incoming))
let ((note, to, memo), outgoing) = .or_else(|| {
match try_sapling_note_decryption(params, height, &ivk, output) { try_sapling_note_decryption(
Some(ret) => (ret, false), params,
None => match try_sapling_output_recovery(params, height, &ovk, output) height,
{ &ivk_internal,
Some(ret) => (ret, true), output,
None => continue, )
}, .map(|ret| (ret, TransferType::WalletInternal))
}; })
.or_else(|| {
decrypted.push(DecryptedOutput { try_sapling_output_recovery(params, height, &ovk, output)
index, .map(|ret| (ret, TransferType::Outgoing))
note, })
account: *account, .into_iter()
to, .map(move |((note, to, memo), transfer_type)| DecryptedOutput {
memo, index,
outgoing, note,
}) account,
} to,
} memo,
} transfer_type,
} })
})
decrypted })
})
.collect()
} }

View File

@ -5,14 +5,16 @@
//! //!
//! [constants]: zcash_primitives::constants //! [constants]: zcash_primitives::constants
use crate::address::UnifiedAddress;
use bech32::{self, Error, FromBase32, ToBase32, Variant}; use bech32::{self, Error, FromBase32, ToBase32, Variant};
use bs58::{self, decode::Error as Bs58Error}; use bs58::{self, decode::Error as Bs58Error};
use std::fmt; use std::fmt;
use std::io::{self, Write}; use std::io::{self, Write};
use zcash_address::unified::{self, Encoding};
use zcash_primitives::{ use zcash_primitives::{
consensus, consensus,
legacy::TransparentAddress, legacy::TransparentAddress,
sapling::PaymentAddress, sapling,
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
}; };
@ -132,6 +134,41 @@ impl<P: consensus::Parameters> AddressCodec<P> for TransparentAddress {
} }
} }
impl<P: consensus::Parameters> AddressCodec<P> for sapling::PaymentAddress {
type Error = Bech32DecodeError;
fn encode(&self, params: &P) -> String {
encode_payment_address(params.hrp_sapling_payment_address(), self)
}
fn decode(params: &P, address: &str) -> Result<Self, Bech32DecodeError> {
decode_payment_address(params.hrp_sapling_payment_address(), address)
}
}
impl<P: consensus::Parameters> AddressCodec<P> for UnifiedAddress {
type Error = String;
fn encode(&self, params: &P) -> String {
self.encode(params)
}
fn decode(params: &P, address: &str) -> Result<Self, String> {
unified::Address::decode(address)
.map_err(|e| format!("{}", e))
.and_then(|(network, addr)| {
if params.address_network() == Some(network) {
UnifiedAddress::try_from(addr).map_err(|e| e.to_owned())
} else {
Err(format!(
"Address {} is for a different network: {:?}",
address, network
))
}
})
}
}
/// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string. /// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string.
/// ///
/// # Examples /// # Examples
@ -232,16 +269,18 @@ pub fn decode_extended_full_viewing_key(
/// ); /// );
/// ``` /// ```
/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress /// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress
pub fn encode_payment_address(hrp: &str, addr: &PaymentAddress) -> String { pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> String {
bech32_encode(hrp, |w| w.write_all(&addr.to_bytes())) bech32_encode(hrp, |w| w.write_all(&addr.to_bytes()))
} }
/// Writes a [`PaymentAddress`] as a Bech32-encoded string /// Writes a [`PaymentAddress`] as a Bech32-encoded string
/// using the human-readable prefix values defined in the specified /// using the human-readable prefix values defined in the specified
/// network parameters. /// network parameters.
///
/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress
pub fn encode_payment_address_p<P: consensus::Parameters>( pub fn encode_payment_address_p<P: consensus::Parameters>(
params: &P, params: &P,
addr: &PaymentAddress, addr: &sapling::PaymentAddress,
) -> String { ) -> String {
encode_payment_address(params.hrp_sapling_payment_address(), addr) encode_payment_address(params.hrp_sapling_payment_address(), addr)
} }
@ -259,7 +298,7 @@ pub fn encode_payment_address_p<P: consensus::Parameters>(
/// encoding::decode_payment_address, /// encoding::decode_payment_address,
/// }; /// };
/// use zcash_primitives::{ /// use zcash_primitives::{
/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, /// consensus::{TEST_NETWORK, Parameters},
/// sapling::{Diversifier, PaymentAddress}, /// sapling::{Diversifier, PaymentAddress},
/// }; /// };
/// ///
@ -276,14 +315,17 @@ pub fn encode_payment_address_p<P: consensus::Parameters>(
/// ///
/// assert_eq!( /// assert_eq!(
/// decode_payment_address( /// decode_payment_address(
/// HRP_SAPLING_PAYMENT_ADDRESS, /// TEST_NETWORK.hrp_sapling_payment_address(),
/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", /// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk",
/// ), /// ),
/// Ok(pa), /// Ok(pa),
/// ); /// );
/// ``` /// ```
/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress /// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress
pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech32DecodeError> { pub fn decode_payment_address(
hrp: &str,
s: &str,
) -> Result<sapling::PaymentAddress, Bech32DecodeError> {
bech32_decode(hrp, s, |data| { bech32_decode(hrp, s, |data| {
if data.len() != 43 { if data.len() != 43 {
return None; return None;
@ -291,7 +333,7 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech
let mut bytes = [0; 43]; let mut bytes = [0; 43];
bytes.copy_from_slice(&data); bytes.copy_from_slice(&data);
PaymentAddress::from_bytes(&bytes) sapling::PaymentAddress::from_bytes(&bytes)
}) })
} }
@ -304,14 +346,14 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech
/// encoding::encode_transparent_address, /// encoding::encode_transparent_address,
/// }; /// };
/// use zcash_primitives::{ /// use zcash_primitives::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX}, /// consensus::{TEST_NETWORK, Parameters},
/// legacy::TransparentAddress, /// legacy::TransparentAddress,
/// }; /// };
/// ///
/// assert_eq!( /// assert_eq!(
/// encode_transparent_address( /// encode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &B58_SCRIPT_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_script_address_prefix(),
/// &TransparentAddress::PublicKey([0; 20]), /// &TransparentAddress::PublicKey([0; 20]),
/// ), /// ),
/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", /// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma",
@ -319,8 +361,8 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech
/// ///
/// assert_eq!( /// assert_eq!(
/// encode_transparent_address( /// encode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &B58_SCRIPT_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_script_address_prefix(),
/// &TransparentAddress::Script([0; 20]), /// &TransparentAddress::Script([0; 20]),
/// ), /// ),
/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", /// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2",
@ -369,7 +411,7 @@ pub fn encode_transparent_address_p<P: consensus::Parameters>(
/// ///
/// ``` /// ```
/// use zcash_primitives::{ /// use zcash_primitives::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX}, /// consensus::{TEST_NETWORK, Parameters},
/// }; /// };
/// use zcash_client_backend::{ /// use zcash_client_backend::{
/// encoding::decode_transparent_address, /// encoding::decode_transparent_address,
@ -378,8 +420,8 @@ pub fn encode_transparent_address_p<P: consensus::Parameters>(
/// ///
/// assert_eq!( /// assert_eq!(
/// decode_transparent_address( /// decode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &B58_SCRIPT_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_script_address_prefix(),
/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", /// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma",
/// ), /// ),
/// Ok(Some(TransparentAddress::PublicKey([0; 20]))), /// Ok(Some(TransparentAddress::PublicKey([0; 20]))),
@ -387,8 +429,8 @@ pub fn encode_transparent_address_p<P: consensus::Parameters>(
/// ///
/// assert_eq!( /// assert_eq!(
/// decode_transparent_address( /// decode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &B58_SCRIPT_ADDRESS_PREFIX, /// &TEST_NETWORK.b58_script_address_prefix(),
/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", /// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2",
/// ), /// ),
/// Ok(Some(TransparentAddress::Script([0; 20]))), /// Ok(Some(TransparentAddress::Script([0; 20]))),

View File

@ -19,4 +19,4 @@ pub mod wallet;
pub mod welding_rig; pub mod welding_rig;
pub mod zip321; pub mod zip321;
pub use decrypt::{decrypt_transaction, DecryptedOutput}; pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType};

View File

@ -76,11 +76,6 @@ and this library adheres to Rust's notion of
- `zcash_client_sqlite::wallet`: - `zcash_client_sqlite::wallet`:
- `get_spendable_notes` to `get_spendable_sapling_notes`. - `get_spendable_notes` to `get_spendable_sapling_notes`.
- `select_spendable_notes` to `select_spendable_sapling_notes`. - `select_spendable_notes` to `select_spendable_sapling_notes`.
- Altered the arguments to `zcash_client_sqlite::wallet::put_sent_note`
to take the components of a `DecryptedOutput` value to allow this
method to be used in contexts where a transaction has just been
constructed, rather than only in the case that a transaction has
been decrypted after being retrieved from the network.
- `zcash_client_sqlite::wallet::init_wallet_db` has been modified to - `zcash_client_sqlite::wallet::init_wallet_db` has been modified to
take the wallet seed as an argument so that it can correctly perform take the wallet seed as an argument so that it can correctly perform
migrations that require re-deriving key material. In particular for migrations that require re-deriving key material. In particular for
@ -89,12 +84,15 @@ and this library adheres to Rust's notion of
migration process. migration process.
### Removed ### Removed
- `zcash_client_sqlite::wallet`: - The following functions have been removed from the public interface of
- `get_extended_full_viewing_keys` (use `zcash_client_sqlite::wallet`. Prefer methods defined on
`zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys` `zcash_client_backend::data_api::{WalletRead, WalletWrite}` instead.
instead). - `get_extended_full_viewing_keys` (use `WalletRead::get_unified_full_viewing_keys` instead).
- `delete_utxos_above` (use - `insert_sent_note` (use `WalletWrite::store_sent_tx` instead)
`zcash_client_backend::data_api::WalletWrite::rewind_to_height` instead) - `insert_sent_utxo` (use `WalletWrite::store_sent_tx` instead)
- `put_sent_note` (use `WalletWrite::store_decrypted_tx` instead)
- `put_sent_utxo` (use `WalletWrite::store_decrypted_tx` instead)
- `delete_utxos_above` (use `WalletWrite::rewind_to_height` instead)
- `zcash_client_sqlite::with_blocks` (use - `zcash_client_sqlite::with_blocks` (use
`zcash_client_backend::data_api::BlockSource::with_blocks` instead) `zcash_client_backend::data_api::BlockSource::with_blocks` instead)
@ -111,7 +109,6 @@ and this library adheres to Rust's notion of
`zcash_client_backend::data_api` traits mentioned above instead. `zcash_client_backend::data_api` traits mentioned above instead.
- Deprecated in `zcash_client_sqlite::wallet`: - Deprecated in `zcash_client_sqlite::wallet`:
- `get_address` - `get_address`
- `get_extended_full_viewing_keys`
- `is_valid_account_extfvk` - `is_valid_account_extfvk`
- `get_balance` - `get_balance`
- `get_balance_at` - `get_balance_at`
@ -131,10 +128,6 @@ and this library adheres to Rust's notion of
- `insert_witness` - `insert_witness`
- `prune_witnesses` - `prune_witnesses`
- `update_expired_notes` - `update_expired_notes`
- `put_sent_note`
- `put_sent_utxo`
- `insert_sent_note`
- `insert_sent_utxo`
- `get_address` - `get_address`
- Deprecated in `zcash_client_sqlite::wallet::transact`: - Deprecated in `zcash_client_sqlite::wallet::transact`:
- `get_spendable_sapling_notes` - `get_spendable_sapling_notes`

View File

@ -53,13 +53,15 @@ use zcash_primitives::{
}; };
use zcash_client_backend::{ use zcash_client_backend::{
address::{RecipientAddress, UnifiedAddress}, address::UnifiedAddress,
data_api::{ data_api::{
BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite, BlockSource, DecryptedTransaction, PoolType, PrunedBlock, Recipient, SentTransaction,
WalletRead, WalletWrite,
}, },
keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::SpendableNote, wallet::SpendableNote,
TransferType,
}; };
use crate::error::SqliteClientError; use crate::error::SqliteClientError;
@ -529,29 +531,38 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
let mut spending_account_id: Option<AccountId> = None; let mut spending_account_id: Option<AccountId> = None;
for output in d_tx.sapling_outputs { for output in d_tx.sapling_outputs {
if output.outgoing { match output.transfer_type {
wallet::put_sent_note( TransferType::Outgoing | TransferType::WalletInternal => {
up, let recipient = if output.transfer_type == TransferType::Outgoing {
tx_ref, Recipient::Sapling(output.to.clone())
output.index, } else {
output.account, Recipient::InternalAccount(output.account, PoolType::Sapling)
&output.to, };
Amount::from_u64(output.note.value)
.map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?,
Some(&output.memo),
)?;
} else {
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);
}
}
wallet::put_received_note(up, output, tx_ref)?; wallet::put_sent_output(
up,
output.account,
tx_ref,
output.index,
&recipient,
Amount::from_u64(output.note.value).map_err(|_|
SqliteClientError::CorruptedData("Note value is not a valid Zcash amount.".to_string()))?,
Some(&output.memo),
)?;
}
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);
}
}
wallet::put_received_note(up, output, tx_ref)?;
}
} }
} }
@ -566,13 +577,15 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
.any(|input| *nf == input.nullifier) .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() {
wallet::put_sent_utxo( let recipient = Recipient::Transparent(txout.script_pubkey.address().unwrap());
wallet::put_sent_output(
up, up,
*account_id,
tx_ref, tx_ref,
output_index, output_index,
*account_id, &recipient,
&txout.script_pubkey.address().unwrap(),
txout.value, txout.value,
None
)?; )?;
} }
} }
@ -611,36 +624,7 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
} }
for output in &sent_tx.outputs { for output in &sent_tx.outputs {
match output.recipient_address { wallet::insert_sent_output(up, tx_ref, sent_tx.account, output)?;
// TODO: Store the entire UA, not just the Sapling component.
// This will require more info about the output index.
RecipientAddress::Unified(ua) => wallet::insert_sent_note(
up,
tx_ref,
output.output_index,
sent_tx.account,
ua.sapling().expect("TODO: Add Orchard support"),
output.value,
output.memo.as_ref(),
)?,
RecipientAddress::Shielded(addr) => wallet::insert_sent_note(
up,
tx_ref,
output.output_index,
sent_tx.account,
addr,
output.value,
output.memo.as_ref(),
)?,
RecipientAddress::Transparent(addr) => wallet::insert_sent_utxo(
up,
tx_ref,
output.output_index,
sent_tx.account,
addr,
output.value,
)?,
}
} }
// Return the row number of the transaction, so the caller can fetch it for sending. // Return the row number of the transaction, so the caller can fetch it for sending.

View File

@ -7,7 +7,7 @@
//! - Build the statement in [`DataConnStmtCache::new`]. //! - Build the statement in [`DataConnStmtCache::new`].
//! - Add a crate-private helper method to `DataConnStmtCache` for running the statement. //! - Add a crate-private helper method to `DataConnStmtCache` for running the statement.
use rusqlite::{params, Statement, ToSql}; use rusqlite::{named_params, params, Statement, ToSql};
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
@ -18,15 +18,18 @@ use zcash_primitives::{
zip32::{AccountId, DiversifierIndex}, zip32::{AccountId, DiversifierIndex},
}; };
use zcash_client_backend::address::UnifiedAddress; use zcash_client_backend::{
address::UnifiedAddress,
data_api::{PoolType, Recipient},
encoding::AddressCodec,
};
use crate::{error::SqliteClientError, wallet::PoolType, NoteId, WalletDb}; use crate::{error::SqliteClientError, wallet::pool_code, NoteId, WalletDb};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
crate::UtxoId, crate::UtxoId, rusqlite::OptionalExtension,
rusqlite::{named_params, OptionalExtension}, zcash_client_backend::wallet::WalletTransparentOutput,
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::transaction::components::transparent::OutPoint, zcash_primitives::transaction::components::transparent::OutPoint,
}; };
@ -60,8 +63,8 @@ pub struct DataConnStmtCache<'a, P> {
stmt_update_received_note: Statement<'a>, stmt_update_received_note: Statement<'a>,
stmt_select_received_note: Statement<'a>, stmt_select_received_note: Statement<'a>,
stmt_insert_sent_note: Statement<'a>, stmt_insert_sent_output: Statement<'a>,
stmt_update_sent_note: Statement<'a>, stmt_update_sent_output: Statement<'a>,
stmt_insert_witness: Statement<'a>, stmt_insert_witness: Statement<'a>,
stmt_prune_witnesses: Statement<'a>, stmt_prune_witnesses: Statement<'a>,
@ -152,19 +155,24 @@ impl<'a, P> DataConnStmtCache<'a, P> {
stmt_select_received_note: wallet_db.conn.prepare( stmt_select_received_note: wallet_db.conn.prepare(
"SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?" "SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?"
)?, )?,
stmt_update_sent_note: wallet_db.conn.prepare( stmt_update_sent_output: wallet_db.conn.prepare(
"UPDATE sent_notes "UPDATE sent_notes
SET from_account = :account, SET from_account = :from_account,
address = :address, to_address = :to_address,
to_account = :to_account,
value = :value, value = :value,
memo = IFNULL(:memo, memo) memo = IFNULL(:memo, memo)
WHERE tx = :tx WHERE tx = :tx
AND output_pool = :output_pool AND output_pool = :output_pool
AND output_index = :output_index", AND output_index = :output_index",
)?, )?,
stmt_insert_sent_note: wallet_db.conn.prepare( stmt_insert_sent_output: wallet_db.conn.prepare(
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo) "INSERT INTO sent_notes (
VALUES (:tx, :output_pool, :output_index, :from_account, :address, :value, :memo)" tx, output_pool, output_index, from_account,
to_address, to_account, value, memo)
VALUES (
:tx, :output_pool, :output_index, :from_account,
:to_address, :to_account, :value, :memo)"
)?, )?,
stmt_insert_witness: wallet_db.conn.prepare( stmt_insert_witness: wallet_db.conn.prepare(
"INSERT INTO sapling_witnesses (note, block, witness) "INSERT INTO sapling_witnesses (note, block, witness)
@ -346,6 +354,105 @@ impl<'a, P> DataConnStmtCache<'a, P> {
} }
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> { impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
/// Inserts a sent note into the wallet database.
///
/// `output_index` is the index within the transaction that contains the recipient output:
///
/// - If `to` is a Unified address, this is an index into the outputs of the transaction
/// within the bundle associated with the recipient's output pool.
/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the
/// transaction.
/// - If `to` is a transparent address, this is an index into the transparent outputs of
/// the transaction.
/// - If `to` is an internal account, this is an index into the Sapling outputs of the
/// transaction.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_insert_sent_output(
&mut self,
tx_ref: i64,
output_index: usize,
from_account: AccountId,
to: &Recipient,
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let (to_address, to_account, pool_type) = match to {
Recipient::Transparent(addr) => (
Some(addr.encode(&self.wallet_db.params)),
None,
PoolType::Transparent,
),
Recipient::Sapling(addr) => (
Some(addr.encode(&self.wallet_db.params)),
None,
PoolType::Sapling,
),
Recipient::Unified(addr, pool) => {
(Some(addr.encode(&self.wallet_db.params)), None, *pool)
}
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool),
};
self.stmt_insert_sent_output.execute(named_params![
":tx": &tx_ref,
":output_pool": &pool_code(pool_type),
":output_index": &i64::try_from(output_index).unwrap(),
":from_account": &u32::from(from_account),
":to_address": &to_address,
":to_account": &to_account,
":value": &i64::from(value),
":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()),
])?;
Ok(())
}
/// Updates the data for the given sent note.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_update_sent_output(
&mut self,
from_account: AccountId,
to: &Recipient,
value: Amount,
memo: Option<&MemoBytes>,
tx_ref: i64,
output_index: usize,
) -> Result<bool, SqliteClientError> {
let (to_address, to_account, pool_type) = match to {
Recipient::Transparent(addr) => (
Some(addr.encode(&self.wallet_db.params)),
None,
PoolType::Transparent,
),
Recipient::Sapling(addr) => (
Some(addr.encode(&self.wallet_db.params)),
None,
PoolType::Sapling,
),
Recipient::Unified(addr, pool) => {
(Some(addr.encode(&self.wallet_db.params)), None, *pool)
}
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool),
};
match self.stmt_update_sent_output.execute(named_params![
":from_account": &u32::from(from_account),
":to_address": &to_address,
":to_account": &to_account,
":value": &i64::from(value),
":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()),
":tx": &tx_ref,
":output_pool": &pool_code(pool_type),
":output_index": &i64::try_from(output_index).unwrap(),
])? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
}
}
/// Adds the given received UTXO to the datastore. /// Adds the given received UTXO to the datastore.
/// ///
/// Returns the database row for the newly-inserted UTXO, or an error if the UTXO /// Returns the database row for the newly-inserted UTXO, or an error if the UTXO
@ -528,78 +635,6 @@ impl<'a, P> DataConnStmtCache<'a, P> {
.map_err(SqliteClientError::from) .map_err(SqliteClientError::from)
} }
/// Inserts a sent note into the wallet database.
///
/// `output_index` is the index within the transaction that contains the recipient output:
///
/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the
/// transaction.
/// - If `to` is a transparent address, this is an index into the transparent outputs of
/// the transaction.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_insert_sent_note(
&mut self,
tx_ref: i64,
pool_type: PoolType,
output_index: usize,
account: AccountId,
to_str: &str,
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":tx", &tx_ref),
(":output_pool", &pool_type.typecode()),
(":output_index", &i64::try_from(output_index).unwrap()),
(":from_account", &u32::from(account)),
(":address", &to_str),
(":value", &i64::from(value)),
(
":memo",
&memo
.filter(|m| *m != &MemoBytes::empty())
.map(|m| m.as_slice()),
),
];
self.stmt_insert_sent_note.execute(sql_args)?;
Ok(())
}
/// Updates the data for the given sent note.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
#[allow(clippy::too_many_arguments)]
pub(crate) fn stmt_update_sent_note(
&mut self,
account: AccountId,
to_str: &str,
value: Amount,
memo: Option<&MemoBytes>,
tx_ref: i64,
pool_type: PoolType,
output_index: usize,
) -> Result<bool, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":account", &u32::from(account)),
(":address", &to_str),
(":value", &i64::from(value)),
(
":memo",
&memo
.filter(|m| *m != &MemoBytes::empty())
.map(|m| m.as_slice()),
),
(":tx", &tx_ref),
(":output_pool", &pool_type.typecode()),
(":output_index", &i64::try_from(output_index).unwrap()),
];
match self.stmt_update_sent_note.execute(sql_args)? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("tx_output constraint is marked as UNIQUE"),
}
}
/// Records the incremental witness for the specified note, as of the given block /// Records the incremental witness for the specified note, as of the given block
/// height. /// height.
/// ///

View File

@ -27,8 +27,7 @@ use zcash_primitives::{
use zcash_client_backend::{ use zcash_client_backend::{
address::{RecipientAddress, UnifiedAddress}, address::{RecipientAddress, UnifiedAddress},
data_api::error::Error, data_api::{error::Error, PoolType, Recipient, SentTransactionOutput},
encoding::{encode_payment_address_p, encode_transparent_address_p},
keys::UnifiedFullViewingKey, keys::UnifiedFullViewingKey,
wallet::{WalletShieldedOutput, WalletTx}, wallet::{WalletShieldedOutput, WalletTx},
DecryptedOutput, DecryptedOutput,
@ -36,8 +35,6 @@ use zcash_client_backend::{
use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb, PRUNING_HEIGHT}; use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb, PRUNING_HEIGHT};
use zcash_primitives::legacy::TransparentAddress;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
crate::UtxoId, crate::UtxoId,
@ -45,7 +42,7 @@ use {
std::collections::HashSet, std::collections::HashSet,
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput}, zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::{ zcash_primitives::{
legacy::{keys::IncomingViewingKey, Script}, legacy::{keys::IncomingViewingKey, Script, TransparentAddress},
transaction::components::{OutPoint, TxOut}, transaction::components::{OutPoint, TxOut},
}, },
}; };
@ -53,20 +50,13 @@ use {
pub mod init; pub mod init;
pub mod transact; pub mod transact;
pub(crate) enum PoolType { pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
Transparent, // These constants are *incidentally* shared with the typecodes
Sapling, // for unified addresses, but this is exclusively an internal
} // implementation detail.
match pool_type {
impl PoolType { PoolType::Transparent => 0i64,
pub(crate) fn typecode(&self) -> i64 { PoolType::Sapling => 2i64,
// These constants are *incidentally* shared with the typecodes
// for unified addresses, but this is exclusively an internal
// implementation detail.
match self {
PoolType::Transparent => 0i64,
PoolType::Sapling => 2i64,
}
} }
} }
@ -1153,128 +1143,50 @@ pub fn update_expired_notes<P>(
stmts.stmt_update_expired(height) stmts.stmt_update_expired(height)
} }
/// Records information about a note that your wallet created. /// Records information about a transaction output that your wallet created.
#[deprecated( ///
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_decrypted_tx instead." /// This is a crate-internal convenience method.
)] pub(crate) fn insert_sent_output<'a, P: consensus::Parameters>(
#[allow(deprecated)]
pub fn put_sent_note<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>, stmts: &mut DataConnStmtCache<'a, P>,
tx_ref: i64, tx_ref: i64,
output_index: usize, from_account: AccountId,
account: AccountId, output: &SentTransactionOutput,
to: &PaymentAddress,
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> { ) -> Result<(), SqliteClientError> {
// Try updating an existing sent note. stmts.stmt_insert_sent_output(
if !stmts.stmt_update_sent_note(
account,
&encode_payment_address_p(&stmts.wallet_db.params, to),
value,
memo,
tx_ref, tx_ref,
PoolType::Sapling, output.output_index,
output_index, from_account,
)? { &output.recipient,
// It isn't there, so insert. output.value,
insert_sent_note(stmts, tx_ref, output_index, account, to, value, memo)? output.memo.as_ref(),
}
Ok(())
}
/// Adds information about a sent transparent UTXO to the database if it does not already
/// exist, or updates it if a record for the UTXO already exists.
///
/// `output_index` is the index within transparent UTXOs of the transaction that contains the recipient output.
#[deprecated(
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_decrypted_tx instead."
)]
#[allow(deprecated)]
pub fn put_sent_utxo<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>,
tx_ref: i64,
output_index: usize,
account: AccountId,
to: &TransparentAddress,
value: Amount,
) -> Result<(), SqliteClientError> {
// Try updating an existing sent UTXO.
if !stmts.stmt_update_sent_note(
account,
&encode_transparent_address_p(&stmts.wallet_db.params, to),
value,
None,
tx_ref,
PoolType::Transparent,
output_index,
)? {
// It isn't there, so insert.
insert_sent_utxo(stmts, tx_ref, output_index, account, to, value)?
}
Ok(())
}
/// Inserts a sent note into the wallet database.
///
/// `output_index` is the index within the transaction that contains the recipient output:
///
/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the
/// transaction.
/// - If `to` is a transparent address, this is an index into the transparent outputs of
/// the transaction.
#[deprecated(
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_sent_tx instead."
)]
pub fn insert_sent_note<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>,
tx_ref: i64,
output_index: usize,
account: AccountId,
to: &PaymentAddress,
value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> {
let to_str = encode_payment_address_p(&stmts.wallet_db.params, to);
stmts.stmt_insert_sent_note(
tx_ref,
PoolType::Sapling,
output_index,
account,
&to_str,
value,
memo,
) )
} }
/// Inserts information about a sent transparent UTXO into the wallet database. /// Records information about a transaction output that your wallet created.
/// ///
/// `output_index` is the index within transparent UTXOs of the transaction that contains the recipient output. /// This is a crate-internal convenience method.
#[deprecated( #[allow(clippy::too_many_arguments)]
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_sent_tx instead." pub(crate) fn put_sent_output<'a, P: consensus::Parameters>(
)]
pub fn insert_sent_utxo<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>, stmts: &mut DataConnStmtCache<'a, P>,
from_account: AccountId,
tx_ref: i64, tx_ref: i64,
output_index: usize, output_index: usize,
account: AccountId, recipient: &Recipient,
to: &TransparentAddress,
value: Amount, value: Amount,
memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> { ) -> Result<(), SqliteClientError> {
let to_str = encode_transparent_address_p(&stmts.wallet_db.params, to); if !stmts.stmt_update_sent_output(from_account, recipient, value, memo, tx_ref, output_index)? {
stmts.stmt_insert_sent_output(
tx_ref,
output_index,
from_account,
recipient,
value,
memo,
)?;
}
stmts.stmt_insert_sent_note( Ok(())
tx_ref,
PoolType::Transparent,
output_index,
account,
&to_str,
value,
None,
)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -122,8 +122,13 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
seed: Option<SecretVec<u8>>, seed: Option<SecretVec<u8>>,
target_migration: Option<Uuid>, target_migration: Option<Uuid>,
) -> Result<(), MigratorError<WalletMigrationError>> { ) -> Result<(), MigratorError<WalletMigrationError>> {
// Turn off foreign keys, and ensure that table replacement/modification
// does not break views
wdb.conn wdb.conn
.execute("PRAGMA foreign_keys = OFF", []) .execute_batch(
"PRAGMA foreign_keys = OFF;
PRAGMA legacy_alter_table = TRUE;",
)
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string())); let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string()));
adapter.init().expect("Migrations table setup succeeds."); adapter.init().expect("Migrations table setup succeeds.");
@ -311,7 +316,10 @@ mod tests {
use super::{init_accounts_table, init_blocks_table, init_wallet_db}; use super::{init_accounts_table, init_blocks_table, init_wallet_db};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use {crate::wallet::PoolType, zcash_primitives::legacy::keys as transparent}; use {
crate::wallet::{pool_code, PoolType},
zcash_primitives::legacy::keys as transparent,
};
#[test] #[test]
fn verify_schema() { fn verify_schema() {
@ -375,12 +383,17 @@ mod tests {
output_pool INTEGER NOT NULL , output_pool INTEGER NOT NULL ,
output_index INTEGER NOT NULL, output_index INTEGER NOT NULL,
from_account INTEGER NOT NULL, from_account INTEGER NOT NULL,
address TEXT NOT NULL, to_address TEXT,
to_account INTEGER,
value INTEGER NOT NULL, value INTEGER NOT NULL,
memo BLOB, memo BLOB,
FOREIGN KEY (tx) REFERENCES transactions(id_tx), FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (from_account) REFERENCES accounts(account), FOREIGN KEY (from_account) REFERENCES accounts(account),
CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index) FOREIGN KEY (to_account) REFERENCES accounts(account),
CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index),
CONSTRAINT note_recipient CHECK (
(to_address IS NOT NULL) != (to_account IS NOT NULL)
)
)", )",
"CREATE TABLE transactions ( "CREATE TABLE transactions (
id_tx INTEGER PRIMARY KEY, id_tx INTEGER PRIMARY KEY,
@ -943,7 +956,7 @@ mod tests {
wdb.conn.execute( wdb.conn.execute(
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value) "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
VALUES (0, ?, 0, ?, ?, 0)", VALUES (0, ?, 0, ?, ?, 0)",
[PoolType::Transparent.typecode().to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?; [pool_code(PoolType::Transparent).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
} }
Ok(()) Ok(())

View File

@ -2,6 +2,7 @@ mod add_transaction_views;
mod add_utxo_account; mod add_utxo_account;
mod addresses_table; mod addresses_table;
mod initial_setup; mod initial_setup;
mod sent_notes_to_internal;
mod ufvk_support; mod ufvk_support;
mod utxos_table; mod utxos_table;
@ -31,5 +32,6 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
Box::new(add_utxo_account::Migration { Box::new(add_utxo_account::Migration {
_params: params.clone(), _params: params.clone(),
}), }),
Box::new(sent_notes_to_internal::Migration {}),
] ]
} }

View File

@ -0,0 +1,89 @@
//! A migration that adds an identifier for the account that received a sent note
//! on an internal address to the sent_notes table.
use std::collections::HashSet;
use rusqlite;
use schemer;
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
use super::{addresses_table, utxos_table};
use crate::wallet::init::WalletMigrationError;
/// This migration adds the `to_account` field to the `sent_notes` table.
///
/// 0ddbe561-8259-4212-9ab7-66fdc4a74e1d
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
0x0ddbe561,
0x8259,
0x4212,
b"\x9a\xb7\x66\xfd\xc4\xa7\x4e\x1d",
);
pub(super) struct Migration;
impl schemer::Migration for Migration {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[utxos_table::MIGRATION_ID, addresses_table::MIGRATION_ID]
.into_iter()
.collect()
}
fn description(&self) -> &'static str {
"Adds an identifier for the account that received an internal note to the sent_notes table"
}
}
impl RusqliteMigration for Migration {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// Adds the `to_account` column to the `sent_notes` table and establishes the
// foreign key relationship with the `account` table.
transaction.execute_batch(
"CREATE TABLE sent_notes_new (
id_note INTEGER PRIMARY KEY,
tx INTEGER NOT NULL,
output_pool INTEGER NOT NULL ,
output_index INTEGER NOT NULL,
from_account INTEGER NOT NULL,
to_address TEXT,
to_account INTEGER,
value INTEGER NOT NULL,
memo BLOB,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (from_account) REFERENCES accounts(account),
FOREIGN KEY (to_account) REFERENCES accounts(account),
CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index),
CONSTRAINT note_recipient CHECK (
(to_address IS NOT NULL) != (to_account IS NOT NULL)
)
);
INSERT INTO sent_notes_new (
id_note, tx, output_pool, output_index,
from_account, to_address,
value, memo)
SELECT
id_note, tx, output_pool, output_index,
from_account, address,
value, memo
FROM sent_notes;",
)?;
transaction.execute_batch(
"DROP TABLE sent_notes;
ALTER TABLE sent_notes_new RENAME TO sent_notes;",
)?;
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic?
panic!("Cannot revert this migration.");
}
}

View File

@ -7,7 +7,9 @@ use schemer_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec}; use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid; use uuid::Uuid;
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedSpendingKey}; use zcash_client_backend::{
address::RecipientAddress, data_api::PoolType, keys::UnifiedSpendingKey,
};
use zcash_primitives::{consensus, zip32::AccountId}; use zcash_primitives::{consensus, zip32::AccountId};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -18,7 +20,7 @@ use zcash_client_backend::encoding::AddressCodec;
use crate::wallet::{ use crate::wallet::{
init::{migrations::utxos_table, WalletMigrationError}, init::{migrations::utxos_table, WalletMigrationError},
PoolType, pool_code,
}; };
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
@ -227,8 +229,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
)) ))
})?; })?;
let output_pool = match decoded_address { let output_pool = match decoded_address {
RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()), RecipientAddress::Shielded(_) => Ok(pool_code(PoolType::Sapling)),
RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()), RecipientAddress::Transparent(_) => Ok(pool_code(PoolType::Transparent)),
RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData( RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData(
"Unified addresses should not yet appear in the sent_notes table." "Unified addresses should not yet appear in the sent_notes table."
.to_string(), .to_string(),