From eab2951c99701cf915d54f7482ef04020c7f43a8 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 25 Aug 2020 15:02:44 -0600 Subject: [PATCH] Move decrypt_and_store_transaction to zcash_client_backend --- zcash_client_backend/src/data_api/chain.rs | 4 +- zcash_client_backend/src/data_api/mod.rs | 90 +++++++- zcash_client_backend/src/data_api/wallet.rs | 59 +++++ zcash_client_sqlite/src/chain.rs | 14 ++ zcash_client_sqlite/src/lib.rs | 241 +++++++++++++++----- zcash_client_sqlite/src/scan.rs | 186 +-------------- 6 files changed, 342 insertions(+), 252 deletions(-) create mode 100644 zcash_client_backend/src/data_api/wallet.rs diff --git a/zcash_client_backend/src/data_api/chain.rs b/zcash_client_backend/src/data_api/chain.rs index 4053bcd66..89bcf8fc2 100644 --- a/zcash_client_backend/src/data_api/chain.rs +++ b/zcash_client_backend/src/data_api/chain.rs @@ -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)); diff --git a/zcash_client_backend/src/data_api/mod.rs b/zcash_client_backend/src/data_api/mod.rs index 527b7ff89..c9772516d 100644 --- a/zcash_client_backend/src/data_api/mod.rs +++ b/zcash_client_backend/src/data_api/mod.rs @@ -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, Self::Error>; + fn get_tx_height(&self, txid: TxId) -> Result, Self::Error>; + fn rewind_to_height( &self, parameters: &P, @@ -53,6 +58,11 @@ pub trait DBOps { account: AccountId, ) -> Result, Self::Error>; + fn get_account_extfvks( + &self, + params: &P, + ) -> Result, Self::Error>; + fn get_balance(&self, account: AccountId) -> Result; fn get_verified_balance(&self, account: AccountId) -> Result; @@ -86,10 +96,6 @@ pub trait DBOps { fn transactionally(&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, ) -> Result<(), Self::Error>; - fn put_tx(&mut self, tx: &WalletTx, height: BlockHeight) -> Result; + fn put_tx_meta( + &mut self, + tx: &WalletTx, + height: BlockHeight, + ) -> Result; + + fn put_tx_data(&mut self, tx: &Transaction) -> Result; fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &Vec) -> Result<(), Self::Error>; - fn put_note( + fn put_received_note( &mut self, - output: &WalletShieldedOutput, - nf: &Vec, + output: &T, + nf: Option<&[u8]>, tx_ref: Self::TxRef, ) -> Result; @@ -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( + &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; +} + +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 { + 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 { + None + } +} diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs new file mode 100644 index 000000000..ccb9c6827 --- /dev/null +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -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>, + P: consensus::Parameters, + &'db D: DBOps, +{ + // 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(()) + }) + } +} diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index 40b55920e..aa2e6cd57 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -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, 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, diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index d16416d42..1f42ccab0 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -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, Self::Error> { + chain::get_tx_height(self, txid).map_err(SqliteClientError::from) + } + fn rewind_to_height( &self, parameters: &P, @@ -125,6 +130,25 @@ impl<'a> DBOps for &'a DataConnection { query::get_address(self, params, account) } + fn get_account_extfvks( + &self, + params: &P, + ) -> Result, 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::, _>, _>>()?? + .ok_or(SqliteClientError(Error::IncorrectHRPExtFVK)) + } + fn get_balance(&self, account: AccountId) -> Result { 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 { + fn put_tx_meta( + &mut self, + tx: &WalletTx, + height: BlockHeight, + ) -> Result { 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 { + 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) -> 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( &mut self, - output: &WalletShieldedOutput, - nf: &Vec, + output: &T, + nf: Option<&[u8]>, tx_ref: Self::TxRef, ) -> Result { - 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( + &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); diff --git a/zcash_client_sqlite/src/scan.rs b/zcash_client_sqlite/src/scan.rs index b6034f12f..dbc4aa773 100644 --- a/zcash_client_sqlite/src/scan.rs +++ b/zcash_client_sqlite/src/scan.rs @@ -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( - 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::, _>, _>>()?? - .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;