Move 'create_spend_to_address' to wallet backend.
This required changing a bit about the relationship between database errors and wallet errors, and opens up the possibility of now simplifying the error situation a bit.
This commit is contained in:
parent
e144015558
commit
cd2729bbd0
|
@ -22,7 +22,9 @@ hex = "0.4"
|
|||
jubjub = "0.5.1"
|
||||
nom = "5.1.2"
|
||||
protobuf = "2.15"
|
||||
rand_core = "0.5.1"
|
||||
subtle = "2.2.3"
|
||||
time = "0.2"
|
||||
zcash_primitives = { version = "0.4", path = "../zcash_primitives" }
|
||||
proptest = { version = "0.10.1", optional = true }
|
||||
percent-encoding = "2.1.0"
|
||||
|
@ -35,6 +37,7 @@ rand_core = "0.5.1"
|
|||
rand_xorshift = "0.2"
|
||||
tempfile = "3.1.0"
|
||||
zcash_client_sqlite = { version = "0.2", path = "../zcash_client_sqlite" }
|
||||
zcash_proofs = { version = "0.4", path = "../zcash_proofs" }
|
||||
|
||||
[features]
|
||||
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::cmp;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
|
@ -16,8 +14,6 @@ use crate::{
|
|||
welding_rig::scan_block,
|
||||
};
|
||||
|
||||
pub const ANCHOR_OFFSET: u32 = 10;
|
||||
|
||||
/// Checks that the scanned blocks in the data database, when combined with the recent
|
||||
/// `CompactBlock`s in the cache database, form a valid chain.
|
||||
///
|
||||
|
@ -91,34 +87,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Determines the target height for a transaction, and the height from which to
|
||||
/// select anchors, based on the current synchronised block chain.
|
||||
pub fn get_target_and_anchor_heights<'db, E0, N, E, D>(
|
||||
data: &'db D,
|
||||
) -> Result<(BlockHeight, BlockHeight), E>
|
||||
where
|
||||
E: From<Error<E0, N>>,
|
||||
&'db D: DBOps<Error = E>,
|
||||
{
|
||||
data.block_height_extrema().and_then(|heights| {
|
||||
match heights {
|
||||
Some((min_height, max_height)) => {
|
||||
let target_height = max_height + 1;
|
||||
|
||||
// Select an anchor ANCHOR_OFFSET back from the target block,
|
||||
// unless that would be before the earliest block we have.
|
||||
let anchor_height = BlockHeight::from(cmp::max(
|
||||
u32::from(target_height).saturating_sub(ANCHOR_OFFSET),
|
||||
u32::from(min_height),
|
||||
));
|
||||
|
||||
Ok((target_height, anchor_height))
|
||||
}
|
||||
None => Err(Error::ScanRequired.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Scans at most `limit` new blocks added to the cache for any transactions received by
|
||||
/// the tracked accounts.
|
||||
///
|
||||
|
|
|
@ -3,9 +3,11 @@ use std::fmt;
|
|||
use zcash_primitives::{
|
||||
consensus::BlockHeight,
|
||||
sapling::Node,
|
||||
transaction::{builder, TxId},
|
||||
transaction::{builder, components::amount::Amount, TxId},
|
||||
};
|
||||
|
||||
use crate::wallet::AccountId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChainInvalid {
|
||||
PrevHashMismatch,
|
||||
|
@ -17,9 +19,9 @@ pub enum ChainInvalid {
|
|||
pub enum Error<DbError, NoteId> {
|
||||
CorruptedData(&'static str),
|
||||
IncorrectHRPExtFVK,
|
||||
InsufficientBalance(u64, u64),
|
||||
InsufficientBalance(Amount, Amount),
|
||||
InvalidChain(BlockHeight, ChainInvalid),
|
||||
InvalidExtSK(u32),
|
||||
InvalidExtSK(AccountId),
|
||||
InvalidMemo(std::str::Utf8Error),
|
||||
InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node),
|
||||
InvalidNote,
|
||||
|
@ -53,13 +55,13 @@ impl<E: fmt::Display, N: fmt::Display> fmt::Display for Error<E, N> {
|
|||
Error::InsufficientBalance(have, need) => write!(
|
||||
f,
|
||||
"Insufficient balance (have {}, need {} including fee)",
|
||||
have, need
|
||||
i64::from(*have), i64::from(*need)
|
||||
),
|
||||
Error::InvalidChain(upper_bound, cause) => {
|
||||
write!(f, "Invalid chain (upper bound: {}): {:?}", u32::from(*upper_bound), cause)
|
||||
}
|
||||
Error::InvalidExtSK(account) => {
|
||||
write!(f, "Incorrect ExtendedSpendingKey for account {}", account)
|
||||
write!(f, "Incorrect ExtendedSpendingKey for account {}", account.0)
|
||||
}
|
||||
Error::InvalidMemo(e) => write!(f, "{}", e),
|
||||
Error::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::cmp;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
|
@ -11,9 +13,10 @@ use zcash_primitives::{
|
|||
|
||||
use crate::{
|
||||
address::RecipientAddress,
|
||||
data_api::wallet::ANCHOR_OFFSET,
|
||||
decrypt::DecryptedOutput,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||
wallet::{AccountId, SpendableNote, WalletShieldedOutput, WalletTx},
|
||||
};
|
||||
|
||||
pub mod chain;
|
||||
|
@ -23,7 +26,8 @@ pub mod wallet;
|
|||
pub trait DBOps {
|
||||
type Error;
|
||||
type NoteRef: Copy; // Backend-specific note identifier
|
||||
type UpdateOps: DBUpdate<Error = Self::Error, NoteRef = Self::NoteRef>;
|
||||
type TxRef: Copy;
|
||||
type UpdateOps: DBUpdate<Error = Self::Error, NoteRef = Self::NoteRef, TxRef = Self::TxRef>;
|
||||
|
||||
fn init_db(&self) -> Result<(), Self::Error>;
|
||||
|
||||
|
@ -43,6 +47,25 @@ pub trait DBOps {
|
|||
|
||||
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error>;
|
||||
|
||||
fn get_target_and_anchor_heights(
|
||||
&self,
|
||||
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
||||
self.block_height_extrema().map(|heights| {
|
||||
heights.map(|(min_height, max_height)| {
|
||||
let target_height = max_height + 1;
|
||||
|
||||
// Select an anchor ANCHOR_OFFSET back from the target block,
|
||||
// unless that would be before the earliest block we have.
|
||||
let anchor_height = BlockHeight::from(cmp::max(
|
||||
u32::from(target_height).saturating_sub(ANCHOR_OFFSET),
|
||||
u32::from(min_height),
|
||||
));
|
||||
|
||||
(target_height, anchor_height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
|
||||
|
||||
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
|
||||
|
@ -64,9 +87,20 @@ pub trait DBOps {
|
|||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error>;
|
||||
|
||||
fn is_valid_account_extfvk<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
|
||||
|
||||
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
|
||||
fn get_verified_balance(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error>;
|
||||
|
||||
fn get_received_memo_as_utf8(
|
||||
&self,
|
||||
|
@ -89,15 +123,22 @@ pub trait DBOps {
|
|||
|
||||
fn get_update_ops(&self) -> Result<Self::UpdateOps, Self::Error>;
|
||||
|
||||
fn transactionally<F>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<(), Self::Error>
|
||||
fn select_spendable_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error>;
|
||||
|
||||
fn transactionally<F, A>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<A, Self::Error>
|
||||
where
|
||||
F: FnOnce(&mut Self::UpdateOps) -> Result<(), Self::Error>;
|
||||
F: FnOnce(&mut Self::UpdateOps) -> Result<A, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait DBUpdate {
|
||||
type Error;
|
||||
type TxRef: Copy;
|
||||
type NoteRef: Copy;
|
||||
type TxRef: Copy;
|
||||
|
||||
fn insert_block(
|
||||
&mut self,
|
||||
|
@ -113,7 +154,11 @@ pub trait DBUpdate {
|
|||
height: BlockHeight,
|
||||
) -> Result<Self::TxRef, Self::Error>;
|
||||
|
||||
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error>;
|
||||
fn put_tx_data(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
created_at: Option<time::OffsetDateTime>,
|
||||
) -> Result<Self::TxRef, Self::Error>;
|
||||
|
||||
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &[u8]) -> Result<(), Self::Error>;
|
||||
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
//! Functions for scanning the chain and extracting relevant information.
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, NetworkUpgrade},
|
||||
transaction::Transaction,
|
||||
consensus::{self, BranchId, NetworkUpgrade},
|
||||
note_encryption::Memo,
|
||||
prover::TxProver,
|
||||
transaction::{
|
||||
builder::Builder,
|
||||
components::{amount::DEFAULT_FEE, Amount},
|
||||
Transaction,
|
||||
},
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::RecipientAddress,
|
||||
data_api::{error::Error, DBOps, DBUpdate},
|
||||
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<'db, E0, N, E, P, D>(
|
||||
|
@ -43,7 +54,7 @@ where
|
|||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.transactionally(&mut db_update, |up| {
|
||||
let tx_ref = up.put_tx_data(tx)?;
|
||||
let tx_ref = up.put_tx_data(tx, None)?;
|
||||
|
||||
for output in outputs {
|
||||
if output.outgoing {
|
||||
|
@ -57,3 +68,201 @@ where
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::{
|
||||
/// api::AccountId,
|
||||
/// keys::spending_key,
|
||||
/// data_api::wallet::create_spend_to_address,
|
||||
/// wallet::OvkPolicy,
|
||||
/// };
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// };
|
||||
///
|
||||
/// 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().unwrap().1.into();
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// match create_spend_to_address(
|
||||
/// &db,
|
||||
/// &Network::TestNetwork,
|
||||
/// tx_prover,
|
||||
/// account,
|
||||
/// &extsk,
|
||||
/// &to,
|
||||
/// Amount::from_u64(1).unwrap(),
|
||||
/// None,
|
||||
/// OvkPolicy::Sender,
|
||||
/// ) {
|
||||
/// Ok(tx_row) => (),
|
||||
/// Err(e) => (),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_spend_to_address<'db, E0, N, E, P, D, R>(
|
||||
data: &'db D,
|
||||
params: &P,
|
||||
prover: impl TxProver,
|
||||
account: AccountId,
|
||||
extsk: &ExtendedSpendingKey,
|
||||
to: &RecipientAddress,
|
||||
value: Amount,
|
||||
memo: Option<Memo>,
|
||||
ovk_policy: OvkPolicy,
|
||||
) -> Result<R, Error<E, N>>
|
||||
where
|
||||
E0: Into<Error<E, N>>,
|
||||
P: consensus::Parameters + Clone,
|
||||
R: Copy,
|
||||
&'db D: DBOps<Error = E0, 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 !data
|
||||
.is_valid_account_extfvk(params, account, &extfvk)
|
||||
.map_err(|e| e.into())?
|
||||
{
|
||||
return Err(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) = data
|
||||
.get_target_and_anchor_heights()
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|x| x.ok_or(Error::ScanRequired))?;
|
||||
|
||||
let target_value = value + DEFAULT_FEE;
|
||||
let spendable_notes = data
|
||||
.select_spendable_notes(account, target_value, anchor_height)
|
||||
.map_err(|e| e.into())?;
|
||||
|
||||
// Confirm we were able to select sufficient value
|
||||
let selected_value = spendable_notes.iter().map(|n| n.note_value).sum();
|
||||
if selected_value < target_value {
|
||||
return Err(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(); //JUBJUB would have to unexpectedly be the zero point for this to be None
|
||||
|
||||
let note = from
|
||||
.create_note(u64::from(selected.note_value), 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),
|
||||
}?;
|
||||
|
||||
let consensus_branch_id = BranchId::for_height(params, height);
|
||||
let (tx, tx_metadata) = builder
|
||||
.build(consensus_branch_id, &prover)
|
||||
.map_err(Error::Builder)?;
|
||||
|
||||
// We only called add_sapling_output() once.
|
||||
let output_index = match tx_metadata.output_index(0) {
|
||||
Some(idx) => idx as i64,
|
||||
None => panic!("Output 0 should exist in the transaction"),
|
||||
};
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
let mut db_update = data.get_update_ops().map_err(|e| e.into())?;
|
||||
data.transactionally(&mut db_update, |up| {
|
||||
let created = time::OffsetDateTime::now_utc();
|
||||
let tx_ref = up.put_tx_data(&tx, Some(created))?;
|
||||
|
||||
// Mark notes as spent.
|
||||
//
|
||||
// This locks the notes so they aren't selected again by a subsequent call to
|
||||
// create_spend_to_address() before this transaction has been mined (at which point the notes
|
||||
// get re-marked as spent).
|
||||
//
|
||||
// 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.
|
||||
for spend in &tx.shielded_spends {
|
||||
up.mark_spent(tx_ref, &spend.nullifier)?;
|
||||
}
|
||||
|
||||
up.insert_sent_note(
|
||||
params,
|
||||
tx_ref,
|
||||
output_index as usize,
|
||||
account,
|
||||
to,
|
||||
value,
|
||||
memo,
|
||||
)?;
|
||||
|
||||
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||
Ok(tx_ref)
|
||||
})
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
//! light client.
|
||||
|
||||
use zcash_primitives::{
|
||||
keys::OutgoingViewingKey,
|
||||
merkle_tree::IncrementalWitness,
|
||||
primitives::{Note, PaymentAddress},
|
||||
primitives::{Diversifier, Note, PaymentAddress, Rseed},
|
||||
sapling::Node,
|
||||
transaction::TxId,
|
||||
transaction::{components::Amount, TxId},
|
||||
};
|
||||
|
||||
/// A type-safe wrapper for account identifiers.
|
||||
|
@ -46,3 +47,36 @@ pub struct WalletShieldedOutput {
|
|||
pub is_change: bool,
|
||||
pub witness: IncrementalWitness<Node>,
|
||||
}
|
||||
|
||||
pub struct SpendableNote {
|
||||
pub diversifier: Diversifier,
|
||||
pub note_value: Amount,
|
||||
pub rseed: Rseed,
|
||||
pub witness: IncrementalWitness<Node>,
|
||||
}
|
||||
|
||||
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
||||
/// transaction outputs.
|
||||
///
|
||||
/// For details on what transaction information is visible to the holder of an outgoing
|
||||
/// viewing key, refer to [ZIP 310].
|
||||
///
|
||||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
pub enum OvkPolicy {
|
||||
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the sender, in addition to the
|
||||
/// recipients.
|
||||
Sender,
|
||||
|
||||
/// Use a custom outgoing viewing key. This might for instance be derived from a
|
||||
/// separate seed than the wallet's spending keys.
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the recipients, and whoever controls
|
||||
/// the provided outgoing viewing key.
|
||||
Custom(OutgoingViewingKey),
|
||||
|
||||
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
|
||||
/// recipients, but not by the sender.
|
||||
Discard,
|
||||
}
|
||||
|
|
|
@ -56,3 +56,9 @@ impl From<protobuf::ProtobufError> for SqliteClientError {
|
|||
SqliteClientError(Error::Protobuf(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SqliteClientError> for Error<rusqlite::Error, NoteId> {
|
||||
fn from(e: SqliteClientError) -> Self {
|
||||
e.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ use zcash_client_backend::{
|
|||
data_api::{error::Error, CacheOps, DBOps, DBUpdate, ShieldedOutput},
|
||||
encoding::encode_payment_address,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, WalletTx},
|
||||
wallet::{AccountId, SpendableNote, WalletTx},
|
||||
DecryptedOutput,
|
||||
};
|
||||
|
||||
|
@ -77,6 +77,7 @@ impl DataConnection {
|
|||
impl<'a> DBOps for &'a DataConnection {
|
||||
type Error = SqliteClientError;
|
||||
type NoteRef = NoteId;
|
||||
type TxRef = i64;
|
||||
type UpdateOps = DataConnStmtCache<'a>;
|
||||
|
||||
fn init_db(&self) -> Result<(), Self::Error> {
|
||||
|
@ -121,6 +122,13 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
wallet::rewind_to_height(self, parameters, block_height)
|
||||
}
|
||||
|
||||
fn get_extended_full_viewing_keys<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error> {
|
||||
wallet::get_extended_full_viewing_keys(self, params)
|
||||
}
|
||||
|
||||
fn get_address<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
|
@ -129,12 +137,25 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
wallet::get_address(self, params, account)
|
||||
}
|
||||
|
||||
fn is_valid_account_extfvk<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, Self::Error> {
|
||||
wallet::is_valid_account_extfvk(self, params, account, extfvk)
|
||||
}
|
||||
|
||||
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
|
||||
wallet::get_balance(self, account)
|
||||
}
|
||||
|
||||
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
|
||||
wallet::get_verified_balance(self, account)
|
||||
fn get_verified_balance(
|
||||
&self,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, Self::Error> {
|
||||
wallet::get_verified_balance(self, account, anchor_height)
|
||||
}
|
||||
|
||||
fn get_received_memo_as_utf8(
|
||||
|
@ -148,13 +169,6 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
wallet::get_sent_memo_as_utf8(self, id_note)
|
||||
}
|
||||
|
||||
fn get_extended_full_viewing_keys<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error> {
|
||||
wallet::get_extended_full_viewing_keys(self, params)
|
||||
}
|
||||
|
||||
fn get_commitment_tree(
|
||||
&self,
|
||||
block_height: BlockHeight,
|
||||
|
@ -173,6 +187,20 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
wallet::get_nullifiers(self)
|
||||
}
|
||||
|
||||
fn select_spendable_notes(
|
||||
&self,
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, Self::Error> {
|
||||
wallet::transact::select_spendable_notes(
|
||||
self,
|
||||
account,
|
||||
target_value,
|
||||
anchor_height,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_update_ops(&self) -> Result<Self::UpdateOps, Self::Error> {
|
||||
Ok(
|
||||
DataConnStmtCache {
|
||||
|
@ -190,8 +218,8 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
SET block = ?, tx_index = ? WHERE txid = ?",
|
||||
)?,
|
||||
stmt_insert_tx_data: self.0.prepare(
|
||||
"INSERT INTO transactions (txid, expiry_height, raw)
|
||||
VALUES (?, ?, ?)",
|
||||
"INSERT INTO transactions (txid, created, expiry_height, raw)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_update_tx_data: self.0.prepare(
|
||||
"UPDATE transactions
|
||||
|
@ -210,12 +238,12 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
stmt_update_received_note: self.0.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = :account,
|
||||
diversifier = :diversifier,
|
||||
value = :value,
|
||||
rcm = :rcm,
|
||||
nf = IFNULL(:memo, nf),
|
||||
memo = IFNULL(:nf, memo),
|
||||
is_change = :is_change
|
||||
diversifier = :diversifier,
|
||||
value = :value,
|
||||
rcm = :rcm,
|
||||
nf = IFNULL(:memo, nf),
|
||||
memo = IFNULL(:nf, memo),
|
||||
is_change = :is_change
|
||||
WHERE tx = :tx AND output_index = :output_index",
|
||||
)?,
|
||||
stmt_select_received_note: self.0.prepare(
|
||||
|
@ -247,27 +275,28 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
)
|
||||
}
|
||||
|
||||
fn transactionally<F>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<(), Self::Error>
|
||||
fn transactionally<F, A>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<A, Self::Error>
|
||||
where
|
||||
F: FnOnce(&mut Self::UpdateOps) -> Result<(), Self::Error>,
|
||||
F: FnOnce(&mut Self::UpdateOps) -> Result<A, Self::Error>,
|
||||
{
|
||||
self.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
match f(mutator) {
|
||||
Ok(_) => {
|
||||
Ok(result) => {
|
||||
self.0.execute("COMMIT", NO_PARAMS)?;
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
Err(error) => {
|
||||
match self.0.execute("ROLLBACK", NO_PARAMS) {
|
||||
Ok(_) => Err(error),
|
||||
// REVIEW: If rollback fails, what do we want to do? I think that
|
||||
// panicking here is probably the right thing to do, because it
|
||||
// means the database is corrupt?
|
||||
Err(e) => panic!(
|
||||
"Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
|
||||
e,
|
||||
error.0
|
||||
)
|
||||
Err(e) =>
|
||||
// REVIEW: If rollback fails, what do we want to do? I think that
|
||||
// panicking here is probably the right thing to do, because it
|
||||
// means the database is corrupt?
|
||||
panic!(
|
||||
"Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
|
||||
e,
|
||||
error.0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -355,7 +384,11 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error> {
|
||||
fn put_tx_data(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
created_at: Option<time::OffsetDateTime>,
|
||||
) -> Result<Self::TxRef, Self::Error> {
|
||||
let txid = tx.txid().0.to_vec();
|
||||
|
||||
let mut raw_tx = vec![];
|
||||
|
@ -371,6 +404,7 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
|
|||
self.stmt_insert_tx_data.execute(&[
|
||||
txid.to_sql()?,
|
||||
u32::from(tx.expiry_height).to_sql()?,
|
||||
created_at.to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Functions for querying information in the data database.
|
||||
|
||||
use rusqlite::{OptionalExtension, NO_PARAMS};
|
||||
use rusqlite::{OptionalExtension, ToSql, NO_PARAMS};
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
|
@ -14,8 +14,10 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
data_api::{chain::get_target_and_anchor_heights, error::Error},
|
||||
encoding::{decode_extended_full_viewing_key, decode_payment_address},
|
||||
data_api::error::Error,
|
||||
encoding::{
|
||||
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, AccountId, DataConnection, NoteId};
|
||||
|
@ -58,6 +60,51 @@ pub fn get_address<P: consensus::Parameters>(
|
|||
.map_err(|e| SqliteClientError(e.into()))
|
||||
}
|
||||
|
||||
pub fn get_extended_full_viewing_keys<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, SqliteClientError> {
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts = data
|
||||
.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
|
||||
let rows = stmt_fetch_accounts
|
||||
.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
&extfvk,
|
||||
)
|
||||
.map_err(|e| Error::Bech32(e))
|
||||
.and_then(|k| k.ok_or(Error::IncorrectHRPExtFVK))
|
||||
.map_err(SqliteClientError)
|
||||
})
|
||||
})
|
||||
.map_err(SqliteClientError::from)?;
|
||||
|
||||
rows.collect::<Result<Result<_, _>, _>>()?
|
||||
}
|
||||
|
||||
pub fn is_valid_account_extfvk<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
extfvk: &ExtendedFullViewingKey,
|
||||
) -> Result<bool, SqliteClientError> {
|
||||
data.0
|
||||
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
||||
.exists(&[
|
||||
account.0.to_sql()?,
|
||||
encode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
extfvk,
|
||||
)
|
||||
.to_sql()?,
|
||||
])
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
/// Returns the balance for the account, including all mined unspent notes that we know
|
||||
/// about.
|
||||
///
|
||||
|
@ -117,9 +164,8 @@ pub fn get_balance(data: &DataConnection, account: AccountId) -> Result<Amount,
|
|||
pub fn get_verified_balance(
|
||||
data: &DataConnection,
|
||||
account: AccountId,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Amount, SqliteClientError> {
|
||||
let (_, anchor_height) = get_target_and_anchor_heights(data)?;
|
||||
|
||||
let balance = data.0.query_row(
|
||||
"SELECT SUM(value) FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
|
@ -320,31 +366,6 @@ pub fn rewind_to_height<P: consensus::Parameters>(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
pub fn get_extended_full_viewing_keys<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, SqliteClientError> {
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts = data
|
||||
.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
|
||||
let rows = stmt_fetch_accounts
|
||||
.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
&extfvk,
|
||||
)
|
||||
.map_err(|e| Error::Bech32(e))
|
||||
.and_then(|k| k.ok_or(Error::IncorrectHRPExtFVK))
|
||||
.map_err(SqliteClientError)
|
||||
})
|
||||
})
|
||||
.map_err(SqliteClientError::from)?;
|
||||
|
||||
rows.collect::<Result<Result<_, _>, _>>()?
|
||||
}
|
||||
|
||||
pub fn get_commitment_tree(
|
||||
data: &DataConnection,
|
||||
|
@ -424,7 +445,7 @@ mod tests {
|
|||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use zcash_client_backend::data_api::error::Error;
|
||||
use zcash_client_backend::data_api::{error::Error, DBOps};
|
||||
|
||||
use crate::{
|
||||
tests,
|
||||
|
@ -449,7 +470,8 @@ mod tests {
|
|||
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
|
||||
|
||||
// The account should have no verified balance, as we haven't scanned any blocks
|
||||
let e = get_verified_balance(&db_data, AccountId(0)).unwrap_err();
|
||||
let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
|
||||
let e = get_verified_balance(&db_data, AccountId(0), anchor_height).unwrap_err();
|
||||
match e.0 {
|
||||
Error::ScanRequired => (),
|
||||
_ => panic!("Unexpected error: {:?}", e),
|
||||
|
|
|
@ -1,176 +1,29 @@
|
|||
//! Functions for creating transactions.
|
||||
|
||||
use ff::PrimeField;
|
||||
use rusqlite::{types::ToSql, NO_PARAMS};
|
||||
//!
|
||||
use std::convert::TryInto;
|
||||
|
||||
use ff::PrimeField;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus,
|
||||
keys::OutgoingViewingKey,
|
||||
merkle_tree::{IncrementalWitness, MerklePath},
|
||||
note_encryption::Memo,
|
||||
primitives::{Diversifier, Note, Rseed},
|
||||
prover::TxProver,
|
||||
sapling::Node,
|
||||
transaction::{
|
||||
builder::Builder,
|
||||
components::{amount::DEFAULT_FEE, Amount},
|
||||
},
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
consensus::{BlockHeight},
|
||||
merkle_tree::IncrementalWitness,
|
||||
primitives::{Diversifier, Rseed},
|
||||
transaction::components::Amount,
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress,
|
||||
data_api::{chain::get_target_and_anchor_heights, error::Error, DBOps, DBUpdate},
|
||||
encoding::encode_extended_full_viewing_key,
|
||||
wallet::AccountId,
|
||||
data_api::{error::Error},
|
||||
wallet::{AccountId, SpendableNote},
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, DataConnection};
|
||||
|
||||
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
||||
/// transaction outputs.
|
||||
///
|
||||
/// For details on what transaction information is visible to the holder of an outgoing
|
||||
/// viewing key, refer to [ZIP 310].
|
||||
///
|
||||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
pub enum OvkPolicy {
|
||||
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the sender, in addition to the
|
||||
/// recipients.
|
||||
Sender,
|
||||
|
||||
/// Use a custom outgoing viewing key. This might for instance be derived from a
|
||||
/// separate seed than the wallet's spending keys.
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the recipients, and whoever controls
|
||||
/// the provided outgoing viewing key.
|
||||
Custom(OutgoingViewingKey),
|
||||
|
||||
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
|
||||
/// recipients, but not by the sender.
|
||||
Discard,
|
||||
}
|
||||
|
||||
struct SelectedNoteRow {
|
||||
diversifier: Diversifier,
|
||||
note: Note,
|
||||
merkle_path: MerklePath<Node>,
|
||||
}
|
||||
|
||||
/// 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_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,
|
||||
/// };
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// transact::{create_to_address, OvkPolicy},
|
||||
/// };
|
||||
///
|
||||
/// 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 = 0;
|
||||
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account);
|
||||
/// let to = extsk.default_address().unwrap().1.into();
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// match create_to_address(
|
||||
/// &db,
|
||||
/// &Network::TestNetwork,
|
||||
/// consensus::BranchId::Sapling,
|
||||
/// tx_prover,
|
||||
/// (account, &extsk),
|
||||
/// &to,
|
||||
/// Amount::from_u64(1).unwrap(),
|
||||
/// None,
|
||||
/// OvkPolicy::Sender,
|
||||
/// ) {
|
||||
/// Ok(tx_row) => (),
|
||||
/// Err(e) => (),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_to_address<P: consensus::Parameters>(
|
||||
pub fn select_spendable_notes(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
consensus_branch_id: consensus::BranchId,
|
||||
prover: impl TxProver,
|
||||
(account, extsk): (u32, &ExtendedSpendingKey),
|
||||
to: &RecipientAddress,
|
||||
value: Amount,
|
||||
memo: Option<Memo>,
|
||||
ovk_policy: OvkPolicy,
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
// 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 !data
|
||||
.0
|
||||
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
||||
.exists(&[
|
||||
account.to_sql()?,
|
||||
encode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
&extfvk,
|
||||
)
|
||||
.to_sql()?,
|
||||
])?
|
||||
{
|
||||
return Err(Error::InvalidExtSK(account).into());
|
||||
}
|
||||
|
||||
// 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) = get_target_and_anchor_heights(data)?;
|
||||
|
||||
account: AccountId,
|
||||
target_value: Amount,
|
||||
anchor_height: BlockHeight,
|
||||
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
||||
// The goal of this SQL statement is to select the oldest notes until the required
|
||||
// value has been reached, and then fetch the witnesses at the desired height for the
|
||||
// selected notes. This is achieved in several steps:
|
||||
|
@ -189,7 +42,6 @@ pub fn create_to_address<P: consensus::Parameters>(
|
|||
// required value, bringing the sum of all selected notes across the threshold.
|
||||
//
|
||||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let target_value = i64::from(value + DEFAULT_FEE);
|
||||
let mut stmt_select_notes = data.0.prepare(
|
||||
"WITH selected AS (
|
||||
WITH eligible AS (
|
||||
|
@ -198,14 +50,14 @@ pub fn create_to_address<P: consensus::Parameters>(
|
|||
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
||||
FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block <= ?
|
||||
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
|
||||
)
|
||||
SELECT * FROM eligible WHERE so_far < ?
|
||||
SELECT * FROM eligible WHERE so_far < :target_value
|
||||
UNION
|
||||
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1)
|
||||
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1)
|
||||
), witnesses AS (
|
||||
SELECT note, witness FROM sapling_witnesses
|
||||
WHERE block = ?
|
||||
WHERE block = :anchor_height
|
||||
)
|
||||
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
||||
FROM selected
|
||||
|
@ -213,13 +65,11 @@ pub fn create_to_address<P: consensus::Parameters>(
|
|||
)?;
|
||||
|
||||
// Select notes
|
||||
let notes = stmt_select_notes.query_and_then::<_, SqliteClientError, _, _>(
|
||||
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
|
||||
&[
|
||||
i64::from(account),
|
||||
i64::from(anchor_height),
|
||||
target_value,
|
||||
target_value,
|
||||
i64::from(anchor_height),
|
||||
(&"account", &i64::from(account.0)),
|
||||
(&"anchor_height", &u32::from(anchor_height)),
|
||||
(&"target_value", &i64::from(target_value)),
|
||||
],
|
||||
|row| {
|
||||
let diversifier = {
|
||||
|
@ -234,7 +84,7 @@ pub fn create_to_address<P: consensus::Parameters>(
|
|||
Diversifier(tmp)
|
||||
};
|
||||
|
||||
let note_value: i64 = row.get(1)?;
|
||||
let note_value = Amount::from_i64(row.get(1)?).unwrap();
|
||||
|
||||
let rseed = {
|
||||
let rcm_bytes: Vec<_> = row.get(2)?;
|
||||
|
@ -251,104 +101,22 @@ pub fn create_to_address<P: consensus::Parameters>(
|
|||
Rseed::BeforeZip212(rcm)
|
||||
};
|
||||
|
||||
let from = extfvk.fvk.vk.to_payment_address(diversifier).unwrap();
|
||||
let note = from.create_note(note_value as u64, rseed).unwrap();
|
||||
|
||||
let merkle_path = {
|
||||
let witness = {
|
||||
let d: Vec<_> = row.get(3)?;
|
||||
IncrementalWitness::read(&d[..])?
|
||||
.path()
|
||||
.expect("the tree is not empty")
|
||||
};
|
||||
|
||||
Ok(SelectedNoteRow {
|
||||
Ok(SpendableNote {
|
||||
diversifier,
|
||||
note,
|
||||
merkle_path,
|
||||
note_value,
|
||||
rseed,
|
||||
witness,
|
||||
})
|
||||
},
|
||||
)?;
|
||||
let notes: Vec<SelectedNoteRow> = notes.collect::<Result<_, _>>()?;
|
||||
|
||||
// Confirm we were able to select sufficient value
|
||||
let selected_value = notes
|
||||
.iter()
|
||||
.fold(0, |acc, selected| acc + selected.note.value);
|
||||
if selected_value < target_value as u64 {
|
||||
return Err(Error::InsufficientBalance(selected_value, target_value as u64).into());
|
||||
}
|
||||
|
||||
// Create the transaction
|
||||
let mut builder = Builder::new(params.clone(), height);
|
||||
for selected in notes {
|
||||
builder.add_sapling_spend(
|
||||
extsk.clone(),
|
||||
selected.diversifier,
|
||||
selected.note,
|
||||
selected.merkle_path,
|
||||
)?;
|
||||
}
|
||||
match to {
|
||||
RecipientAddress::Shielded(to) => {
|
||||
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
|
||||
}
|
||||
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
|
||||
}?;
|
||||
let (tx, tx_metadata) = builder.build(consensus_branch_id, &prover)?;
|
||||
// We only called add_sapling_output() once.
|
||||
let output_index = match tx_metadata.output_index(0) {
|
||||
Some(idx) => idx as i64,
|
||||
None => panic!("Output 0 should exist in the transaction"),
|
||||
};
|
||||
let created = time::OffsetDateTime::now_utc();
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
let mut db_update = data.get_update_ops()?;
|
||||
|
||||
// Save the transaction in the database.
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
let mut stmt_insert_tx = data.0.prepare(
|
||||
"INSERT INTO transactions (txid, created, expiry_height, raw)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?;
|
||||
stmt_insert_tx.execute(&[
|
||||
tx.txid().0.to_sql()?,
|
||||
created.to_sql()?,
|
||||
i64::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
let tx_ref = data.0.last_insert_rowid();
|
||||
|
||||
// Mark notes as spent.
|
||||
//
|
||||
// This locks the notes so they aren't selected again by a subsequent call to
|
||||
// create_to_address() before this transaction has been mined (at which point the notes
|
||||
// get re-marked as spent).
|
||||
//
|
||||
// Assumes that create_to_address() will never be called in parallel, which is a
|
||||
// reasonable assumption for a light client such as a mobile phone.
|
||||
for spend in &tx.shielded_spends {
|
||||
db_update.mark_spent(tx_ref, &spend.nullifier)?;
|
||||
}
|
||||
|
||||
// Save the sent note in the database.
|
||||
// TODO: Decide how to save transparent output information.
|
||||
db_update.insert_sent_note(
|
||||
params,
|
||||
tx_ref,
|
||||
output_index as usize,
|
||||
AccountId(account),
|
||||
to,
|
||||
value,
|
||||
memo,
|
||||
)?;
|
||||
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||
Ok(tx_ref)
|
||||
let notes: Vec<SpendableNote> = notes.collect::<Result<_, _>>()?;
|
||||
Ok(notes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -358,7 +126,7 @@ mod tests {
|
|||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
consensus::BlockHeight,
|
||||
note_encryption::try_sapling_output_recovery,
|
||||
prover::TxProver,
|
||||
transaction::{components::Amount, Transaction},
|
||||
|
@ -367,7 +135,10 @@ mod tests {
|
|||
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
|
||||
use zcash_client_backend::data_api::chain::scan_cached_blocks;
|
||||
use zcash_client_backend::{
|
||||
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, DBOps},
|
||||
wallet::OvkPolicy,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
chain::init::init_cache_database,
|
||||
|
@ -379,8 +150,6 @@ mod tests {
|
|||
AccountId, CacheConnection, DataConnection,
|
||||
};
|
||||
|
||||
use super::{create_to_address, OvkPolicy};
|
||||
|
||||
fn test_prover() -> impl TxProver {
|
||||
match LocalTxProver::with_default_location() {
|
||||
Some(tx_prover) => tx_prover,
|
||||
|
@ -407,12 +176,12 @@ mod tests {
|
|||
let to = extsk0.default_address().unwrap().1.into();
|
||||
|
||||
// Invalid extsk for the given account should cause an error
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk1),
|
||||
AccountId(0),
|
||||
&extsk1,
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
|
@ -422,12 +191,12 @@ mod tests {
|
|||
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
||||
}
|
||||
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(1, &extsk0),
|
||||
AccountId(1),
|
||||
&extsk0,
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
|
@ -451,12 +220,12 @@ mod tests {
|
|||
let to = extsk.default_address().unwrap().1.into();
|
||||
|
||||
// We cannot do anything if we aren't synchronised
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
|
@ -491,12 +260,12 @@ mod tests {
|
|||
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
|
||||
|
||||
// We cannot spend anything
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(1).unwrap(),
|
||||
None,
|
||||
|
@ -537,8 +306,12 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value);
|
||||
assert_eq!(get_verified_balance(&db_data, AccountId(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_verified_balance(&db_data, AccountId(0), anchor_height).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Add more funds to the wallet in a second note
|
||||
let (cb, _) = fake_compact_block(
|
||||
|
@ -551,18 +324,22 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Verified balance does not include the second note
|
||||
let (_, anchor_height2) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
|
||||
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value);
|
||||
assert_eq!(get_verified_balance(&db_data, AccountId(0)).unwrap(), value);
|
||||
assert_eq!(
|
||||
get_verified_balance(&db_data, AccountId(0), anchor_height2).unwrap(),
|
||||
value
|
||||
);
|
||||
|
||||
// Spend fails because there are insufficient verified notes
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
|
@ -589,12 +366,12 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
|
@ -618,12 +395,12 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(70000).unwrap(),
|
||||
None,
|
||||
|
@ -662,12 +439,12 @@ mod tests {
|
|||
// Send some of the funds to another address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
create_to_address(
|
||||
create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(15000).unwrap(),
|
||||
None,
|
||||
|
@ -676,12 +453,12 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// A second spend fails because there are no usable notes
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
|
@ -708,12 +485,12 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
match create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
|
@ -737,12 +514,12 @@ mod tests {
|
|||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
create_spend_to_address(
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(2000).unwrap(),
|
||||
None,
|
||||
|
@ -784,12 +561,12 @@ mod tests {
|
|||
let to = addr2.clone().into();
|
||||
|
||||
let send_and_recover_with_policy = |ovk_policy| {
|
||||
let tx_row = create_to_address(
|
||||
let tx_row = create_spend_to_address(
|
||||
&db_data,
|
||||
&network,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
(0, &extsk),
|
||||
AccountId(0),
|
||||
&extsk,
|
||||
&to,
|
||||
Amount::from_u64(15000).unwrap(),
|
||||
None,
|
||||
|
|
Loading…
Reference in New Issue