263 lines
8.5 KiB
Rust
263 lines
8.5 KiB
Rust
//! Functions for scanning the chain and extracting relevant information.
|
|
use std::fmt::Debug;
|
|
|
|
use zcash_primitives::{
|
|
consensus::{self, NetworkUpgrade},
|
|
memo::MemoBytes,
|
|
sapling::prover::TxProver,
|
|
transaction::{
|
|
builder::Builder,
|
|
components::{amount::DEFAULT_FEE, Amount},
|
|
Transaction,
|
|
},
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
|
};
|
|
|
|
use crate::{
|
|
address::RecipientAddress,
|
|
data_api::{error::Error, ReceivedTransaction, SentTransaction, WalletWrite},
|
|
decrypt_transaction,
|
|
wallet::{AccountId, OvkPolicy},
|
|
};
|
|
|
|
pub const ANCHOR_OFFSET: u32 = 10;
|
|
|
|
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
|
/// the wallet, and saves it to the wallet.
|
|
pub fn decrypt_and_store_transaction<N, E, P, D>(
|
|
params: &P,
|
|
data: &mut D,
|
|
tx: &Transaction,
|
|
) -> Result<(), E>
|
|
where
|
|
E: From<Error<N>>,
|
|
P: consensus::Parameters,
|
|
D: WalletWrite<Error = E>,
|
|
{
|
|
// Fetch the ExtendedFullViewingKeys we are tracking
|
|
let extfvks = data.get_extended_full_viewing_keys()?;
|
|
|
|
// Height is block height for mined transactions, and the "mempool height" (chain height + 1)
|
|
// for mempool transactions.
|
|
let height = data
|
|
.get_tx_height(tx.txid())?
|
|
.or(data
|
|
.block_height_extrema()?
|
|
.map(|(_, max_height)| max_height + 1))
|
|
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
|
|
.ok_or(Error::SaplingNotActive)?;
|
|
|
|
let outputs = decrypt_transaction(params, height, tx, &extfvks);
|
|
if outputs.is_empty() {
|
|
Ok(())
|
|
} else {
|
|
data.store_received_tx(&ReceivedTransaction {
|
|
tx,
|
|
outputs: &outputs,
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::needless_doctest_main)]
|
|
/// Creates a transaction paying the specified address from the given account.
|
|
///
|
|
/// Returns the row index of the newly-created transaction in the `transactions` table
|
|
/// within the data database. The caller can read the raw transaction bytes from the `raw`
|
|
/// column in order to broadcast the transaction to the network.
|
|
///
|
|
/// Do not call this multiple times in parallel, or you will generate transactions that
|
|
/// double-spend the same notes.
|
|
///
|
|
/// # Transaction privacy
|
|
///
|
|
/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be
|
|
/// able to decrypt the outputs of this transaction. This is primarily relevant to
|
|
/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the
|
|
/// recipient's address, and the contents of `memo`, from ever being recovered from the
|
|
/// block chain. (The total value sent can always be inferred by the sender from the spent
|
|
/// notes and received change.)
|
|
///
|
|
/// Regardless of the specified policy, `create_spend_to_address` saves `to`, `value`, and
|
|
/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`.
|
|
///
|
|
/// For details on what transaction information is visible to the holder of a full or
|
|
/// outgoing viewing key, refer to [ZIP 310].
|
|
///
|
|
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tempfile::NamedTempFile;
|
|
/// use zcash_primitives::{
|
|
/// consensus::{self, Network},
|
|
/// constants::testnet::COIN_TYPE,
|
|
/// transaction::components::Amount
|
|
/// };
|
|
/// use zcash_proofs::prover::LocalTxProver;
|
|
/// use zcash_client_backend::{
|
|
/// keys::spending_key,
|
|
/// data_api::wallet::create_spend_to_address,
|
|
/// wallet::{AccountId, OvkPolicy},
|
|
/// };
|
|
/// use zcash_client_sqlite::{
|
|
/// WalletDb,
|
|
/// error::SqliteClientError,
|
|
/// wallet::init::init_wallet_db,
|
|
/// };
|
|
///
|
|
/// # // doctests have a problem with sqlite IO, so we ignore errors
|
|
/// # // generated in this example code as it's not really testing anything
|
|
/// # fn main() {
|
|
/// # test();
|
|
/// # }
|
|
/// #
|
|
/// # fn test() -> Result<(), SqliteClientError> {
|
|
/// let tx_prover = match LocalTxProver::with_default_location() {
|
|
/// Some(tx_prover) => tx_prover,
|
|
/// None => {
|
|
/// panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
|
/// }
|
|
/// };
|
|
///
|
|
/// let account = AccountId(0);
|
|
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account.0);
|
|
/// let to = extsk.default_address().1.into();
|
|
///
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
/// let db_read = WalletDb::for_path(data_file, Network::TestNetwork).unwrap();
|
|
/// init_wallet_db(&db_read)?;
|
|
/// let mut db = db_read.get_update_ops()?;
|
|
///
|
|
/// create_spend_to_address(
|
|
/// &mut db,
|
|
/// &Network::TestNetwork,
|
|
/// tx_prover,
|
|
/// account,
|
|
/// &extsk,
|
|
/// &to,
|
|
/// Amount::from_u64(1).unwrap(),
|
|
/// None,
|
|
/// OvkPolicy::Sender,
|
|
/// )?;
|
|
///
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn create_spend_to_address<E, N, P, D, R>(
|
|
wallet_db: &mut D,
|
|
params: &P,
|
|
prover: impl TxProver,
|
|
account: AccountId,
|
|
extsk: &ExtendedSpendingKey,
|
|
to: &RecipientAddress,
|
|
value: Amount,
|
|
memo: Option<MemoBytes>,
|
|
ovk_policy: OvkPolicy,
|
|
) -> Result<R, E>
|
|
where
|
|
E: From<Error<N>>,
|
|
P: consensus::Parameters + Clone,
|
|
R: Copy + Debug,
|
|
D: WalletWrite<Error = E, TxRef = R>,
|
|
{
|
|
// Check that the ExtendedSpendingKey we have been given corresponds to the
|
|
// ExtendedFullViewingKey for the account we are spending from.
|
|
let extfvk = ExtendedFullViewingKey::from(extsk);
|
|
if !wallet_db.is_valid_account_extfvk(account, &extfvk)? {
|
|
return Err(E::from(Error::InvalidExtSk(account)));
|
|
}
|
|
|
|
// Apply the outgoing viewing key policy.
|
|
let ovk = match ovk_policy {
|
|
OvkPolicy::Sender => Some(extfvk.fvk.ovk),
|
|
OvkPolicy::Custom(ovk) => Some(ovk),
|
|
OvkPolicy::Discard => None,
|
|
};
|
|
|
|
// Target the next block, assuming we are up-to-date.
|
|
let (height, anchor_height) = wallet_db
|
|
.get_target_and_anchor_heights()
|
|
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
|
|
|
|
let target_value = (value + DEFAULT_FEE).ok_or_else(|| E::from(Error::InvalidAmount))?;
|
|
let spendable_notes = wallet_db.select_spendable_notes(account, target_value, anchor_height)?;
|
|
|
|
// Confirm we were able to select sufficient value
|
|
let selected_value = spendable_notes
|
|
.iter()
|
|
.map(|n| n.note_value)
|
|
.sum::<Option<_>>()
|
|
.ok_or_else(|| E::from(Error::InvalidAmount))?;
|
|
if selected_value < target_value {
|
|
return Err(E::from(Error::InsufficientBalance(
|
|
selected_value,
|
|
target_value,
|
|
)));
|
|
}
|
|
|
|
// Create the transaction
|
|
let mut builder = Builder::new(params.clone(), height);
|
|
for selected in spendable_notes {
|
|
let from = extfvk
|
|
.fvk
|
|
.vk
|
|
.to_payment_address(selected.diversifier)
|
|
.unwrap(); //DiversifyHash would have to unexpectedly return the zero point for this to be None
|
|
|
|
let note = from
|
|
.create_note(selected.note_value.into(), selected.rseed)
|
|
.unwrap();
|
|
|
|
let merkle_path = selected.witness.path().expect("the tree is not empty");
|
|
|
|
builder
|
|
.add_sapling_spend(extsk.clone(), selected.diversifier, note, merkle_path)
|
|
.map_err(Error::Builder)?;
|
|
}
|
|
|
|
match to {
|
|
RecipientAddress::Shielded(to) => {
|
|
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
|
|
}
|
|
|
|
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
|
|
}
|
|
.map_err(Error::Builder)?;
|
|
|
|
let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?;
|
|
|
|
let output_index = match to {
|
|
// Sapling outputs are shuffled, so we need to look up where the output ended up.
|
|
RecipientAddress::Shielded(_) => match tx_metadata.output_index(0) {
|
|
Some(idx) => idx,
|
|
None => panic!("Output 0 should exist in the transaction"),
|
|
},
|
|
RecipientAddress::Transparent(addr) => {
|
|
let script = addr.script();
|
|
tx.transparent_bundle()
|
|
.and_then(|b| {
|
|
b.vout
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, tx_out)| tx_out.script_pubkey == script)
|
|
})
|
|
.map(|(index, _)| index)
|
|
.expect("we sent to this address")
|
|
}
|
|
};
|
|
|
|
wallet_db.store_sent_tx(&SentTransaction {
|
|
tx: &tx,
|
|
created: time::OffsetDateTime::now_utc(),
|
|
output_index,
|
|
account,
|
|
recipient_address: to,
|
|
value,
|
|
memo,
|
|
})
|
|
}
|