Move decrypt_and_store_transaction to zcash_client_backend
This commit is contained in:
parent
0165ae7003
commit
eab2951c99
|
@ -260,7 +260,7 @@ where
|
|||
db_update.insert_block(height, block_hash, block_time, &tree)?;
|
||||
|
||||
for tx in txs {
|
||||
let tx_row = db_update.put_tx(&tx, height)?;
|
||||
let tx_row = db_update.put_tx_meta(&tx, height)?;
|
||||
|
||||
// Mark notes as spent and remove them from the scanning cache
|
||||
for spend in &tx.shielded_spends {
|
||||
|
@ -280,7 +280,7 @@ where
|
|||
output.witness.position() as u64,
|
||||
);
|
||||
|
||||
let note_id = db_update.put_note(&output, &nf, tx_row)?;
|
||||
let note_id = db_update.put_received_note(&output, Some(&nf), tx_row)?;
|
||||
|
||||
// Save witness for note.
|
||||
witnesses.push((note_id, output.witness));
|
||||
|
|
|
@ -2,19 +2,22 @@ use zcash_primitives::{
|
|||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
primitives::PaymentAddress,
|
||||
note_encryption::Memo,
|
||||
primitives::{Note, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::components::Amount,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decrypt::DecryptedOutput,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||
};
|
||||
|
||||
pub mod chain;
|
||||
pub mod error;
|
||||
pub mod wallet;
|
||||
|
||||
pub trait DBOps {
|
||||
type Error;
|
||||
|
@ -41,6 +44,8 @@ pub trait DBOps {
|
|||
|
||||
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>;
|
||||
|
||||
fn rewind_to_height<P: consensus::Parameters>(
|
||||
&self,
|
||||
parameters: &P,
|
||||
|
@ -53,6 +58,11 @@ pub trait DBOps {
|
|||
account: AccountId,
|
||||
) -> Result<Option<PaymentAddress>, Self::Error>;
|
||||
|
||||
fn get_account_extfvks<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error>;
|
||||
|
||||
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
|
||||
|
||||
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
|
||||
|
@ -86,10 +96,6 @@ pub trait DBOps {
|
|||
fn transactionally<F>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<(), Self::Error>
|
||||
where
|
||||
F: FnOnce(&mut Self::UpdateOps) -> Result<(), Self::Error>;
|
||||
|
||||
// fn put_sent_note(tx_ref: Self::TxRef, output: DecryptedOutput) -> Result<(), Self::Error>;
|
||||
//
|
||||
// fn put_received_note(tx_ref: Self::TxRef, output: DecryptedOutput) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait DBUpdate {
|
||||
|
@ -105,14 +111,20 @@ pub trait DBUpdate {
|
|||
commitment_tree: &CommitmentTree<Node>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
fn put_tx(&mut self, tx: &WalletTx, height: BlockHeight) -> Result<Self::TxRef, Self::Error>;
|
||||
fn put_tx_meta(
|
||||
&mut self,
|
||||
tx: &WalletTx,
|
||||
height: BlockHeight,
|
||||
) -> Result<Self::TxRef, Self::Error>;
|
||||
|
||||
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error>;
|
||||
|
||||
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &Vec<u8>) -> Result<(), Self::Error>;
|
||||
|
||||
fn put_note(
|
||||
fn put_received_note<T: ShieldedOutput>(
|
||||
&mut self,
|
||||
output: &WalletShieldedOutput,
|
||||
nf: &Vec<u8>,
|
||||
output: &T,
|
||||
nf: Option<&[u8]>,
|
||||
tx_ref: Self::TxRef,
|
||||
) -> Result<Self::NoteRef, Self::Error>;
|
||||
|
||||
|
@ -126,6 +138,13 @@ pub trait DBUpdate {
|
|||
fn prune_witnesses(&mut self, from_height: BlockHeight) -> Result<(), Self::Error>;
|
||||
|
||||
fn update_expired_notes(&mut self, from_height: BlockHeight) -> Result<(), Self::Error>;
|
||||
|
||||
fn put_sent_note<P: consensus::Parameters>(
|
||||
&mut self,
|
||||
params: &P,
|
||||
output: &DecryptedOutput,
|
||||
tx_ref: Self::TxRef,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait CacheOps {
|
||||
|
@ -154,3 +173,54 @@ pub trait CacheOps {
|
|||
where
|
||||
F: FnMut(BlockHeight, CompactBlock) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait ShieldedOutput {
|
||||
fn index(&self) -> usize;
|
||||
fn account(&self) -> AccountId;
|
||||
fn to(&self) -> &PaymentAddress;
|
||||
fn note(&self) -> &Note;
|
||||
fn memo(&self) -> Option<&Memo>;
|
||||
fn is_change(&self) -> Option<bool>;
|
||||
}
|
||||
|
||||
impl ShieldedOutput for WalletShieldedOutput {
|
||||
fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
fn account(&self) -> AccountId {
|
||||
AccountId(self.account as u32)
|
||||
}
|
||||
fn to(&self) -> &PaymentAddress {
|
||||
&self.to
|
||||
}
|
||||
fn note(&self) -> &Note {
|
||||
&self.note
|
||||
}
|
||||
fn memo(&self) -> Option<&Memo> {
|
||||
None
|
||||
}
|
||||
fn is_change(&self) -> Option<bool> {
|
||||
Some(self.is_change)
|
||||
}
|
||||
}
|
||||
|
||||
impl ShieldedOutput for DecryptedOutput {
|
||||
fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
fn account(&self) -> AccountId {
|
||||
AccountId(self.account as u32)
|
||||
}
|
||||
fn to(&self) -> &PaymentAddress {
|
||||
&self.to
|
||||
}
|
||||
fn note(&self) -> &Note {
|
||||
&self.note
|
||||
}
|
||||
fn memo(&self) -> Option<&Memo> {
|
||||
Some(&self.memo)
|
||||
}
|
||||
fn is_change(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//! Functions for scanning the chain and extracting relevant information.
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, NetworkUpgrade},
|
||||
transaction::Transaction,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data_api::{error::Error, DBOps, DBUpdate},
|
||||
decrypt_transaction,
|
||||
};
|
||||
|
||||
/// 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>(
|
||||
params: &P,
|
||||
data: &'db D,
|
||||
tx: &Transaction,
|
||||
) -> Result<(), E>
|
||||
where
|
||||
E: From<Error<E0, N>>,
|
||||
P: consensus::Parameters,
|
||||
&'db D: DBOps<Error = E>,
|
||||
{
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let extfvks = data.get_account_extfvks(params)?;
|
||||
|
||||
// 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(params.activation_height(NetworkUpgrade::Sapling))
|
||||
.ok_or(Error::SaplingNotActive.into())?;
|
||||
|
||||
let outputs = decrypt_transaction(params, height, tx, &extfvks);
|
||||
if outputs.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
let mut db_update = data.get_update_ops()?;
|
||||
|
||||
// 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)?;
|
||||
|
||||
for output in outputs {
|
||||
if output.outgoing {
|
||||
up.put_sent_note(params, &output, tx_ref)?;
|
||||
} else {
|
||||
up.put_received_note(&output, None, tx_ref)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -78,6 +78,7 @@ use rusqlite::{OptionalExtension, NO_PARAMS};
|
|||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
transaction::TxId,
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
|
@ -165,6 +166,19 @@ pub fn block_height_extrema(
|
|||
.or(Ok(None))
|
||||
}
|
||||
|
||||
pub fn get_tx_height(
|
||||
conn: &DataConnection,
|
||||
txid: TxId,
|
||||
) -> Result<Option<BlockHeight>, rusqlite::Error> {
|
||||
conn.0
|
||||
.query_row(
|
||||
"SELECT block FROM transactions WHERE txid = ?",
|
||||
&[txid.0.to_vec()],
|
||||
|row| row.get(0).map(u32::into),
|
||||
)
|
||||
.optional()
|
||||
}
|
||||
|
||||
pub fn get_block_hash(
|
||||
conn: &DataConnection,
|
||||
block_height: BlockHeight,
|
||||
|
|
|
@ -37,15 +37,16 @@ use zcash_primitives::{
|
|||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
primitives::PaymentAddress,
|
||||
sapling::Node,
|
||||
transaction::components::Amount,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
data_api::{CacheOps, DBOps, DBUpdate},
|
||||
encoding::encode_payment_address,
|
||||
data_api::{error::Error, CacheOps, DBOps, DBUpdate, ShieldedOutput},
|
||||
encoding::{decode_extended_full_viewing_key, encode_payment_address},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||
wallet::{AccountId, WalletTx},
|
||||
DecryptedOutput,
|
||||
};
|
||||
|
||||
use crate::error::SqliteClientError;
|
||||
|
@ -109,6 +110,10 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
chain::get_block_hash(self, block_height).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
|
||||
chain::get_tx_height(self, txid).map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
fn rewind_to_height<P: consensus::Parameters>(
|
||||
&self,
|
||||
parameters: &P,
|
||||
|
@ -125,6 +130,25 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
query::get_address(self, params, account)
|
||||
}
|
||||
|
||||
fn get_account_extfvks<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error> {
|
||||
self.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?
|
||||
.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
&extfvk,
|
||||
)
|
||||
})
|
||||
})?
|
||||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(SqliteClientError(Error::IncorrectHRPExtFVK))
|
||||
}
|
||||
|
||||
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
|
||||
query::get_balance(self, account)
|
||||
}
|
||||
|
@ -177,31 +201,54 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_insert_tx: self.0.prepare(
|
||||
stmt_insert_tx_meta: self.0.prepare(
|
||||
"INSERT INTO transactions (txid, block, tx_index)
|
||||
VALUES (?, ?, ?)",
|
||||
)?,
|
||||
stmt_update_tx: self.0.prepare(
|
||||
stmt_update_tx_meta: self.0.prepare(
|
||||
"UPDATE transactions
|
||||
SET block = ?, tx_index = ? WHERE txid = ?",
|
||||
)?,
|
||||
stmt_select_tx: self.0.prepare(
|
||||
stmt_insert_tx_data: self.0.prepare(
|
||||
"INSERT INTO transactions (txid, expiry_height, raw)
|
||||
VALUES (?, ?, ?)",
|
||||
)?,
|
||||
stmt_update_tx_data: self.0.prepare(
|
||||
"UPDATE transactions
|
||||
SET expiry_height = ?, raw = ? WHERE txid = ?",
|
||||
)?,
|
||||
stmt_select_tx_ref: self.0.prepare(
|
||||
"SELECT id_tx FROM transactions WHERE txid = ?",
|
||||
)?,
|
||||
stmt_mark_spent_note: self.0.prepare(
|
||||
stmt_mark_recived_note_spent: self.0.prepare(
|
||||
"UPDATE received_notes SET spent = ? WHERE nf = ?"
|
||||
)?,
|
||||
stmt_update_note: self.0.prepare(
|
||||
stmt_insert_received_note: self.0.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
|
||||
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
|
||||
)?,
|
||||
stmt_update_received_note: self.0.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, nf = ?, is_change = ?
|
||||
SET account = :account,
|
||||
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(
|
||||
"SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?"
|
||||
)?,
|
||||
stmt_update_sent_note: self.0.prepare(
|
||||
"UPDATE sent_notes
|
||||
SET from_account = ?, address = ?, value = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?,
|
||||
stmt_insert_note: self.0.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_select_note: self.0.prepare(
|
||||
"SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?"
|
||||
stmt_insert_sent_note: self.0.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?,
|
||||
stmt_insert_witness: self.0.prepare(
|
||||
"INSERT INTO sapling_witnesses (note, block, witness)
|
||||
|
@ -250,13 +297,23 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
pub struct DataConnStmtCache<'a> {
|
||||
conn: &'a DataConnection,
|
||||
stmt_insert_block: Statement<'a>,
|
||||
stmt_insert_tx: Statement<'a>,
|
||||
stmt_update_tx: Statement<'a>,
|
||||
stmt_select_tx: Statement<'a>,
|
||||
stmt_mark_spent_note: Statement<'a>,
|
||||
stmt_insert_note: Statement<'a>,
|
||||
stmt_update_note: Statement<'a>,
|
||||
stmt_select_note: Statement<'a>,
|
||||
|
||||
stmt_insert_tx_meta: Statement<'a>,
|
||||
stmt_update_tx_meta: Statement<'a>,
|
||||
|
||||
stmt_insert_tx_data: Statement<'a>,
|
||||
stmt_update_tx_data: Statement<'a>,
|
||||
stmt_select_tx_ref: Statement<'a>,
|
||||
|
||||
stmt_mark_recived_note_spent: Statement<'a>,
|
||||
|
||||
stmt_insert_received_note: Statement<'a>,
|
||||
stmt_update_received_note: Statement<'a>,
|
||||
stmt_select_received_note: Statement<'a>,
|
||||
|
||||
stmt_insert_sent_note: Statement<'a>,
|
||||
stmt_update_sent_note: Statement<'a>,
|
||||
|
||||
stmt_insert_witness: Statement<'a>,
|
||||
stmt_prune_witnesses: Statement<'a>,
|
||||
stmt_update_expired: Statement<'a>,
|
||||
|
@ -290,16 +347,20 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn put_tx(&mut self, tx: &WalletTx, height: BlockHeight) -> Result<Self::TxRef, Self::Error> {
|
||||
fn put_tx_meta(
|
||||
&mut self,
|
||||
tx: &WalletTx,
|
||||
height: BlockHeight,
|
||||
) -> Result<Self::TxRef, Self::Error> {
|
||||
let txid = tx.txid.0.to_vec();
|
||||
if self.stmt_update_tx.execute(&[
|
||||
if self.stmt_update_tx_meta.execute(&[
|
||||
u32::from(height).to_sql()?,
|
||||
(tx.index as i64).to_sql()?,
|
||||
txid.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
self.stmt_insert_tx.execute(&[
|
||||
self.stmt_insert_tx_meta.execute(&[
|
||||
txid.to_sql()?,
|
||||
u32::from(height).to_sql()?,
|
||||
(tx.index as i64).to_sql()?,
|
||||
|
@ -308,59 +369,88 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
|
|||
Ok(self.conn.0.last_insert_rowid())
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
self.stmt_select_tx
|
||||
self.stmt_select_tx_ref
|
||||
.query_row(&[txid], |row| row.get(0))
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error> {
|
||||
let txid = tx.txid().0.to_vec();
|
||||
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
|
||||
if self.stmt_update_tx_data.execute(&[
|
||||
u32::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
txid.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
self.stmt_insert_tx_data.execute(&[
|
||||
txid.to_sql()?,
|
||||
u32::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
|
||||
Ok(self.conn.0.last_insert_rowid())
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
self.stmt_select_tx_ref
|
||||
.query_row(&[txid], |row| row.get(0))
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &Vec<u8>) -> Result<(), Self::Error> {
|
||||
self.stmt_mark_spent_note
|
||||
self.stmt_mark_recived_note_spent
|
||||
.execute(&[tx_ref.to_sql()?, nf.to_sql()?])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn put_note(
|
||||
// Assumptions:
|
||||
// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
// - A note value will never exceed 2^63 zatoshis.
|
||||
fn put_received_note<T: ShieldedOutput>(
|
||||
&mut self,
|
||||
output: &WalletShieldedOutput,
|
||||
nf: &Vec<u8>,
|
||||
output: &T,
|
||||
nf: Option<&[u8]>,
|
||||
tx_ref: Self::TxRef,
|
||||
) -> Result<Self::NoteRef, Self::Error> {
|
||||
let rcm = output.note.rcm().to_repr();
|
||||
// Assumptions:
|
||||
// - A transaction will not contain more than 2^63 shielded outputs.
|
||||
// - A note value will never exceed 2^63 zatoshis.
|
||||
let rcm = output.note().rcm().to_repr();
|
||||
let account = output.account().0 as i64;
|
||||
let diversifier = output.to().diversifier().0.to_vec();
|
||||
let value = output.note().value as i64;
|
||||
let rcm = rcm.as_ref();
|
||||
let memo = output.memo().map(|m| m.as_bytes());
|
||||
let is_change = output.is_change();
|
||||
let tx = tx_ref;
|
||||
let output_index = output.index() as i64;
|
||||
|
||||
let sql_args: Vec<(&str, &dyn ToSql)> = vec![
|
||||
(&":account", &account),
|
||||
(&":diversifier", &diversifier),
|
||||
(&":value", &value),
|
||||
(&":rcm", &rcm),
|
||||
(&":nf", &nf),
|
||||
(&":memo", &memo),
|
||||
(&":is_change", &is_change),
|
||||
(&":tx", &tx),
|
||||
(&":output_index", &output_index),
|
||||
];
|
||||
|
||||
// First try updating an existing received note into the database.
|
||||
if self.stmt_update_note.execute(&[
|
||||
(output.account as i64).to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
(output.note.value as i64).to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
nf.to_sql()?,
|
||||
output.is_change.to_sql()?,
|
||||
tx_ref.to_sql()?,
|
||||
(output.index as i64).to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
if self.stmt_update_received_note.execute_named(&sql_args)? == 0 {
|
||||
// It isn't there, so insert our note into the database.
|
||||
self.stmt_insert_note.execute(&[
|
||||
tx_ref.to_sql()?,
|
||||
(output.index as i64).to_sql()?,
|
||||
(output.account as i64).to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
(output.note.value as i64).to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
nf.to_sql()?,
|
||||
output.is_change.to_sql()?,
|
||||
])?;
|
||||
self.stmt_insert_received_note.execute_named(&sql_args)?;
|
||||
|
||||
Ok(NoteId(self.conn.0.last_insert_rowid()))
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
self.stmt_select_note
|
||||
self.stmt_select_received_note
|
||||
.query_row(
|
||||
&[tx_ref.to_sql()?, (output.index as i64).to_sql()?],
|
||||
&[tx_ref.to_sql()?, (output.index() as i64).to_sql()?],
|
||||
|row| row.get(0).map(NoteId),
|
||||
)
|
||||
.map_err(SqliteClientError::from)
|
||||
|
@ -396,6 +486,41 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
|
|||
self.stmt_update_expired.execute(&[u32::from(height)])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn put_sent_note<P: consensus::Parameters>(
|
||||
&mut self,
|
||||
params: &P,
|
||||
output: &DecryptedOutput,
|
||||
tx_ref: Self::TxRef,
|
||||
) -> Result<(), Self::Error> {
|
||||
let output_index = output.index as i64;
|
||||
let account = output.account as i64;
|
||||
let value = output.note.value as i64;
|
||||
let to_str = encode_payment_address(params.hrp_sapling_payment_address(), &output.to);
|
||||
|
||||
// Try updating an existing sent note.
|
||||
if self.stmt_update_sent_note.execute(&[
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
tx_ref.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert.
|
||||
self.stmt_insert_sent_note.execute(&[
|
||||
tx_ref.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CacheConnection(Connection);
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
//! Functions for scanning the chain and extracting relevant information.
|
||||
|
||||
use ff::PrimeField;
|
||||
use protobuf::parse_from_bytes;
|
||||
|
||||
use rusqlite::{types::ToSql, OptionalExtension, NO_PARAMS};
|
||||
use rusqlite::types::ToSql;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
transaction::Transaction,
|
||||
};
|
||||
use zcash_primitives::consensus::BlockHeight;
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress, data_api::error::Error, decrypt_transaction,
|
||||
encoding::decode_extended_full_viewing_key, proto::compact_formats::CompactBlock,
|
||||
};
|
||||
use zcash_client_backend::proto::compact_formats::CompactBlock;
|
||||
|
||||
use crate::{error::SqliteClientError, CacheConnection, DataConnection};
|
||||
use crate::{error::SqliteClientError, CacheConnection};
|
||||
|
||||
struct CompactBlockRow {
|
||||
height: BlockHeight,
|
||||
|
@ -56,177 +49,6 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
tx: &Transaction,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts = data
|
||||
.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
|
||||
let extfvks = 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,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
let extfvks: Vec<_> = extfvks
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(SqliteClientError(Error::IncorrectHRPExtFVK))?;
|
||||
|
||||
// Height is block height for mined transactions, and the "mempool height" (chain height + 1) for mempool transactions.
|
||||
let mut stmt_select_block = data
|
||||
.0
|
||||
.prepare("SELECT block FROM transactions WHERE txid = ?")?;
|
||||
let height = match stmt_select_block
|
||||
.query_row(&[tx.txid().0.to_vec()], |row| {
|
||||
row.get::<_, u32>(0).map(BlockHeight::from)
|
||||
})
|
||||
.optional()?
|
||||
{
|
||||
Some(height) => height,
|
||||
None => data
|
||||
.0
|
||||
.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.optional()?
|
||||
.map(|last_height: u32| BlockHeight::from(last_height + 1))
|
||||
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
|
||||
.ok_or(SqliteClientError(Error::SaplingNotActive))?,
|
||||
};
|
||||
|
||||
let outputs = decrypt_transaction(params, height, tx, &extfvks);
|
||||
|
||||
if outputs.is_empty() {
|
||||
// Nothing to see here
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut stmt_update_tx = data.0.prepare(
|
||||
"UPDATE transactions
|
||||
SET expiry_height = ?, raw = ? WHERE txid = ?",
|
||||
)?;
|
||||
let mut stmt_insert_tx = data.0.prepare(
|
||||
"INSERT INTO transactions (txid, expiry_height, raw)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_tx = data
|
||||
.0
|
||||
.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_update_sent_note = data.0.prepare(
|
||||
"UPDATE sent_notes
|
||||
SET from_account = ?, address = ?, value = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_sent_note = data.0.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_update_received_note = data.0.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_received_note = data.0.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// First try update an existing transaction in the database.
|
||||
let txid = tx.txid().0.to_vec();
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
let tx_row = if stmt_update_tx.execute(&[
|
||||
u32::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
txid.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert our transaction into the database.
|
||||
stmt_insert_tx.execute(&[
|
||||
txid.to_sql()?,
|
||||
u32::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
data.0.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
|
||||
};
|
||||
|
||||
for output in outputs {
|
||||
let output_index = output.index as i64;
|
||||
let account = output.account as i64;
|
||||
let value = output.note.value as i64;
|
||||
|
||||
if output.outgoing {
|
||||
let to_str = RecipientAddress::from(output.to).encode(params);
|
||||
|
||||
// Try updating an existing sent note.
|
||||
if stmt_update_sent_note.execute(&[
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert.
|
||||
stmt_insert_sent_note.execute(&[
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
to_str.to_sql()?,
|
||||
value.to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
}
|
||||
} else {
|
||||
let rcm = output.note.rcm().to_repr();
|
||||
|
||||
// Try updating an existing received note.
|
||||
if stmt_update_received_note.execute(&[
|
||||
account.to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
value.to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
])? == 0
|
||||
{
|
||||
// It isn't there, so insert.
|
||||
stmt_insert_received_note.execute(&[
|
||||
tx_row.to_sql()?,
|
||||
output_index.to_sql()?,
|
||||
account.to_sql()?,
|
||||
output.to.diversifier().0.to_sql()?,
|
||||
value.to_sql()?,
|
||||
rcm.as_ref().to_sql()?,
|
||||
output.memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::Connection;
|
||||
|
|
Loading…
Reference in New Issue