Move decrypt_and_store_transaction to zcash_client_backend

This commit is contained in:
Kris Nuttycombe 2020-08-25 15:02:44 -06:00
parent 0165ae7003
commit eab2951c99
6 changed files with 342 additions and 252 deletions

View File

@ -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));

View File

@ -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
}
}

View File

@ -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(())
})
}
}

View File

@ -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,

View File

@ -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(
&mut self,
output: &WalletShieldedOutput,
nf: &Vec<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.
fn put_received_note<T: ShieldedOutput>(
&mut self,
output: &T,
nf: Option<&[u8]>,
tx_ref: Self::TxRef,
) -> Result<Self::NoteRef, Self::Error> {
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);

View File

@ -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;