#![doc = document_features::document_features!()]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, doc(auto_cfg))]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
use incrementalmerkletree::Position;
use nonempty::NonEmpty;
use rand::RngCore;
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{ShardTree, error::ShardTreeError, store::ShardStore};
use std::{
borrow::{Borrow, BorrowMut},
cmp::{max, min},
collections::{HashMap, HashSet},
convert::AsRef,
fmt,
num::NonZeroU32,
ops::Range,
path::Path,
};
use subtle::ConditionallySelectable;
use tracing::warn;
use util::Clock;
use uuid::Uuid;
use zcash_client_backend::{
TransferType,
data_api::{
self, Account, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, AddressInfo,
BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery,
OutputLockStore, ReceivedNotes, ReceivedTransactionOutput, SAPLING_SHARD_HEIGHT,
ScannedBlock, SeedRelevance, SentTransaction, TargetValue, TransactionDataRequest,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, Zip32Derivation,
anchor_retention::{AnchorRetention, AnchorRetentionInterval},
chain::{BlockSource, ChainState, CommitmentTreeRoot},
error::{FindAccountForAddressError, LockError, RewindError},
ll::{
self, LowLevelWalletRead, LowLevelWalletWrite, ReceivedSaplingOutput,
wallet::store_decrypted_tx,
},
scanning::{ScanPriority, ScanRange},
wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
},
proto::compact_formats::CompactBlock,
wallet::{LockOwner, Note, NoteId, OutputRef, ReceivedNote, WalletTransparentOutput, WalletTx},
};
use zcash_keys::{
address::UnifiedAddress,
keys::{
AddressGenerationError::*, ReceiverRequirement, UnifiedAddressRequest,
UnifiedFullViewingKey, UnifiedSpendingKey,
},
};
use zcash_primitives::{
block::BlockHash,
transaction::{Transaction, TxId},
};
use zcash_protocol::{
ShieldedPool,
consensus::{self, BlockHeight, TxIndex},
memo::Memo,
value::Zatoshis,
};
use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
use crate::{
error::SqliteClientError,
wallet::{chain_tip_height, commitment_tree::SqliteShardStore},
};
use wallet::{
SubtreeProgressEstimator,
commitment_tree::{self, put_shard_roots},
common::{TableConstants, unspent_notes_meta},
scanning::replace_queue_entries,
upsert_address,
};
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::{
IRONWOOD_SHARD_HEIGHT, ORCHARD_SHARD_HEIGHT, ll::ReceivedOrchardOutput,
};
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks,
::transparent::{
address::TransparentAddress,
bundle::OutPoint,
keys::{NonHardenedChildIndex, TransparentKeyScope},
},
ReceiverRequirement::*,
std::time::SystemTime,
zcash_client_backend::{
data_api::{
CoinbaseFilter, TransactionsInvolvingAddress, TransparentBalances,
ll::wallet::generate_transparent_gap_addresses,
},
fees::StandardFeeRule,
wallet::TransparentAddressMetadata,
},
zcash_keys::keys::transparent::gap_limits::{AddressStore, GapLimits},
};
#[cfg(all(
any(test, feature = "test-dependencies"),
feature = "transparent-inputs"
))]
use zcash_keys::encoding::AddressCodec;
#[cfg(any(test, feature = "test-dependencies"))]
use {
crate::wallet::encoding::pool_code,
rusqlite::named_params,
zcash_client_backend::data_api::{OutputOfSentTx, WalletTest, testing::TransactionSummary},
};
#[cfg(any(test, feature = "test-dependencies", feature = "transparent-inputs"))]
use {crate::wallet::encoding::KeyScope, zcash_keys::address::Address};
#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))]
use zcash_protocol::PoolType;
use rusqlite::hooks::{AuthAction, Authorization};
#[cfg(feature = "unstable")]
use {
crate::chain::{BlockMeta, fsblockdb_with_blocks},
std::{fs, io, path::PathBuf},
};
pub mod chain;
pub mod error;
pub mod util;
pub mod wallet;
#[cfg(feature = "zewif")]
pub mod zewif;
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing;
pub(crate) const PRUNING_DEPTH: u32 = 100;
pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
pub(crate) const IRONWOOD_TABLES_PREFIX: &str = "ironwood";
#[cfg(not(feature = "orchard"))]
pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Omit;
#[cfg(feature = "orchard")]
pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Require;
#[cfg(not(feature = "transparent-inputs"))]
pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Omit;
#[cfg(feature = "transparent-inputs")]
pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Require;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AccountUuid(#[cfg_attr(feature = "serde", serde(with = "uuid::serde::compact"))] Uuid);
impl ConditionallySelectable for AccountUuid {
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
AccountUuid(Uuid::from_u128(
ConditionallySelectable::conditional_select(&a.0.as_u128(), &b.0.as_u128(), choice),
))
}
}
impl AccountUuid {
pub fn from_uuid(value: Uuid) -> Self {
AccountUuid(value)
}
pub fn expose_uuid(&self) -> Uuid {
self.0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
pub struct AccountRef(i64);
#[cfg(test)]
impl ConditionallySelectable for AccountRef {
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
AccountRef(ConditionallySelectable::conditional_select(
&a.0, &b.0, choice,
))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ReceivedNoteId(pub(crate) ShieldedPool, pub(crate) i64);
impl fmt::Display for ReceivedNoteId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReceivedNoteId(protocol, id) => write!(f, "Received {protocol:?} Note: {id}"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UtxoId(pub(crate) i64);
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TxRef(pub(crate) i64);
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct AddressRef(pub(crate) i64);
pub struct WalletDb<C, P, CL, R> {
conn: C,
params: P,
clock: CL,
rng: R,
anchor_retention_interval: AnchorRetentionInterval,
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits,
}
pub struct SqlTransaction<'conn>(&'conn rusqlite::Transaction<'conn>);
impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
fn borrow(&self) -> &rusqlite::Connection {
self.0
}
}
impl<'a> Borrow<rusqlite::Transaction<'a>> for SqlTransaction<'a> {
fn borrow(&self) -> &rusqlite::Transaction<'a> {
self.0
}
}
const EXTENSION_SCHEMA_PREFIX: &str = "ext_";
pub struct ExtensionTransaction<'conn> {
conn: &'conn rusqlite::Connection,
}
struct AuthorizerGuard<'conn> {
conn: &'conn rusqlite::Connection,
}
impl Drop for AuthorizerGuard<'_> {
fn drop(&mut self) {
self.conn.authorizer(
None::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>,
);
}
}
fn extension_authorizer(ctx: rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization {
let allow_if_extension = |table: &str| {
if table.starts_with(EXTENSION_SCHEMA_PREFIX) {
Authorization::Allow
} else {
Authorization::Deny
}
};
match ctx.action {
AuthAction::Select
| AuthAction::Read { .. }
| AuthAction::Function { .. }
| AuthAction::Recursive => Authorization::Allow,
AuthAction::Insert { table_name } | AuthAction::Delete { table_name } => {
allow_if_extension(table_name)
}
AuthAction::Update { table_name, .. } => allow_if_extension(table_name),
_ => Authorization::Deny,
}
}
impl<'conn> ExtensionTransaction<'conn> {
fn with_authorizer<T>(
&self,
f: impl FnOnce() -> Result<T, rusqlite::Error>,
) -> Result<T, rusqlite::Error> {
self.conn.authorizer(Some(extension_authorizer));
let _guard = AuthorizerGuard { conn: self.conn };
f()
}
pub fn execute(
&self,
sql: &str,
params: impl rusqlite::Params,
) -> Result<usize, rusqlite::Error> {
self.with_authorizer(|| self.conn.execute(sql, params))
}
pub fn query_row<T, F>(
&self,
sql: &str,
params: impl rusqlite::Params,
f: F,
) -> Result<T, rusqlite::Error>
where
F: FnOnce(&rusqlite::Row<'_>) -> Result<T, rusqlite::Error>,
{
self.with_authorizer(|| self.conn.query_row(sql, params, f))
}
}
impl<C, P, CL, R> WalletDb<C, P, CL, R> {
pub fn params(&self) -> &P {
&self.params
}
}
impl<P, CL, R> WalletDb<rusqlite::Connection, P, CL, R> {
pub fn for_path<F: AsRef<Path>>(
path: F,
params: P,
clock: CL,
rng: R,
) -> Result<Self, rusqlite::Error> {
rusqlite::Connection::open(path).and_then(move |conn| {
rusqlite::vtab::array::load_module(&conn)?;
Ok(WalletDb {
conn,
params,
clock,
rng,
anchor_retention_interval: AnchorRetentionInterval::default(),
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits::default(),
})
})
}
}
impl<C, P, CL, R> WalletDb<C, P, CL, R> {
pub fn with_anchor_retention_interval(mut self, interval: AnchorRetentionInterval) -> Self {
self.set_anchor_retention_interval(interval);
self
}
pub fn set_anchor_retention_interval(&mut self, interval: AnchorRetentionInterval) {
self.anchor_retention_interval = interval;
}
}
#[cfg(feature = "transparent-inputs")]
impl<C, P, CL, R> WalletDb<C, P, CL, R> {
pub fn with_gap_limits(mut self, gap_limits: GapLimits) -> Self {
self.gap_limits = gap_limits;
self
}
}
impl<C: Borrow<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
pub fn from_connection(conn: C, params: P, clock: CL, rng: R) -> Self {
WalletDb {
conn,
params,
clock,
rng,
anchor_retention_interval: AnchorRetentionInterval::default(),
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits::default(),
}
}
}
impl<C: BorrowMut<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
where
F: FnOnce(&mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>) -> Result<A, E>,
{
let tx = self.conn.borrow_mut().transaction()?;
let mut wdb = WalletDb {
conn: SqlTransaction(&tx),
params: &self.params,
clock: &self.clock,
rng: &mut self.rng,
anchor_retention_interval: self.anchor_retention_interval,
#[cfg(feature = "transparent-inputs")]
gap_limits: self.gap_limits,
};
let result = f(&mut wdb)?;
tx.commit()?;
Ok(result)
}
pub fn transactionally_with_extension<F, A, E: From<rusqlite::Error>>(
&mut self,
f: F,
) -> Result<A, E>
where
F: FnOnce(
&mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>,
&ExtensionTransaction<'_>,
) -> Result<A, E>,
{
let tx = self.conn.borrow_mut().transaction()?;
let mut wdb = WalletDb {
conn: SqlTransaction(&tx),
params: &self.params,
clock: &self.clock,
rng: &mut self.rng,
anchor_retention_interval: self.anchor_retention_interval,
#[cfg(feature = "transparent-inputs")]
gap_limits: self.gap_limits,
};
let ext = ExtensionTransaction { conn: &tx };
let result = f(&mut wdb, &ext)?;
tx.commit()?;
Ok(result)
}
pub fn check_witnesses(&mut self) -> Result<Vec<Range<BlockHeight>>, SqliteClientError> {
self.transactionally(|wdb| {
if let Some(anchor_height) = chain_tip_height(wdb.conn.0)? {
wallet::commitment_tree::check_witnesses(wdb.conn.0, anchor_height)
} else {
Ok(vec![])
}
})
}
pub fn queue_rescans(
&mut self,
rescan_ranges: NonEmpty<Range<BlockHeight>>,
priority: ScanPriority,
) -> Result<(), SqliteClientError> {
let query_range = rescan_ranges
.iter()
.fold(None, |acc: Option<Range<BlockHeight>>, scan_range| {
if let Some(range) = acc {
Some(min(range.start, scan_range.start)..max(range.end, scan_range.end))
} else {
Some(scan_range.clone())
}
})
.expect("rescan_ranges is nonempty");
self.transactionally::<_, _, SqliteClientError>(|wdb| {
replace_queue_entries(
wdb.conn.0,
&query_range,
rescan_ranges
.into_iter()
.map(|r| ScanRange::from_parts(r, priority)),
true,
)
})?;
Ok(())
}
}
#[cfg(feature = "transparent-inputs")]
impl<C: BorrowMut<rusqlite::Connection>, P, CL: Clock, R: rand::RngCore> WalletDb<C, P, CL, R> {
pub fn schedule_ephemeral_address_checks(&mut self) -> Result<(), SqliteClientError> {
self.borrow_mut().transactionally(|wdb| {
schedule_ephemeral_address_checks(wdb.conn.0, wdb.clock, &mut wdb.rng)
})
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> InputSource
for WalletDb<C, P, CL, R>
{
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
type AccountId = AccountUuid;
fn get_spendable_note(
&self,
txid: &TxId,
protocol: ShieldedPool,
index: u32,
target_height: TargetHeight,
lock_filter: LockFilter<'_>,
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
match protocol {
ShieldedPool::Sapling => wallet::sapling::get_spendable_sapling_note(
self.conn.borrow(),
&self.params,
txid,
index,
target_height,
lock_filter,
)
.map(|opt| opt.map(|n| n.map_note(Note::Sapling))),
ShieldedPool::Orchard => {
#[cfg(feature = "orchard")]
return wallet::orchard::get_spendable_orchard_note(
self.conn.borrow(),
&self.params,
txid,
index,
target_height,
lock_filter,
)
.map(|opt| {
opt.map(|n| {
n.map_note(|note| Note::Orchard {
note,
pool: ::orchard::ValuePool::Orchard,
})
})
});
#[cfg(not(feature = "orchard"))]
return Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD));
}
ShieldedPool::Ironwood => {
#[cfg(feature = "orchard")]
return wallet::orchard::get_spendable_ironwood_note(
self.conn.borrow(),
&self.params,
txid,
index,
target_height,
lock_filter,
)
.map(|opt| {
opt.map(|n| {
n.map_note(|note| Note::Orchard {
note,
pool: ::orchard::ValuePool::Ironwood,
})
})
});
#[cfg(not(feature = "orchard"))]
return Err(SqliteClientError::UnsupportedPoolType(PoolType::IRONWOOD));
}
}
}
fn anchor_computable(
&self,
protocol: ShieldedPool,
height: BlockHeight,
) -> Result<bool, Self::Error> {
wallet::anchor_computable(self.conn.borrow(), protocol, height)
}
fn select_spendable_notes(
&self,
account: Self::AccountId,
target_value: TargetValue,
sources: &[ShieldedPool],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
Ok(ReceivedNotes::new(
if sources.contains(&ShieldedPool::Sapling) {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)?
} else {
vec![]
},
#[cfg(feature = "orchard")]
if sources.contains(&ShieldedPool::Orchard) {
wallet::orchard::select_spendable_orchard_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)?
} else {
vec![]
},
#[cfg(feature = "orchard")]
if sources.contains(&ShieldedPool::Ironwood) {
wallet::orchard::select_spendable_ironwood_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)?
} else {
vec![]
},
))
}
fn select_single_spendable_note(
&self,
account: Self::AccountId,
value: Zatoshis,
sources: &[ShieldedPool],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
for pool in sources {
match pool {
ShieldedPool::Sapling => {
if let Some(note) = wallet::sapling::select_single_spendable_sapling_note(
self.conn.borrow(),
&self.params,
account,
value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)? {
return Ok(ReceivedNotes::new(
vec![note],
#[cfg(feature = "orchard")]
vec![],
#[cfg(feature = "orchard")]
vec![],
));
}
}
#[cfg(feature = "orchard")]
ShieldedPool::Orchard => {
if let Some(note) = wallet::orchard::select_single_spendable_orchard_note(
self.conn.borrow(),
&self.params,
account,
value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)? {
return Ok(ReceivedNotes::new(vec![], vec![note], vec![]));
}
}
#[cfg(feature = "orchard")]
ShieldedPool::Ironwood => {
if let Some(note) = wallet::orchard::select_single_spendable_ironwood_note(
self.conn.borrow(),
&self.params,
account,
value,
target_height,
confirmations_policy,
exclude,
lock_filter,
)? {
return Ok(ReceivedNotes::new(vec![], vec![], vec![note]));
}
}
#[cfg(not(feature = "orchard"))]
ShieldedPool::Orchard | ShieldedPool::Ironwood => {}
}
}
Ok(ReceivedNotes::empty())
}
fn select_unspent_notes(
&self,
account: Self::AccountId,
sources: &[ShieldedPool],
target_height: TargetHeight,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
Ok(ReceivedNotes::new(
if sources.contains(&ShieldedPool::Sapling) {
wallet::common::select_unspent_notes(
self.conn.borrow(),
&self.params,
account,
target_height,
ConfirmationsPolicy::MIN,
exclude,
ShieldedPool::Sapling,
wallet::sapling::to_received_note,
wallet::common::NoteRequest::Unspent,
lock_filter,
)?
} else {
vec![]
},
#[cfg(feature = "orchard")]
if sources.contains(&ShieldedPool::Orchard) {
wallet::common::select_unspent_notes(
self.conn.borrow(),
&self.params,
account,
target_height,
ConfirmationsPolicy::MIN,
exclude,
ShieldedPool::Orchard,
wallet::orchard::to_received_note,
wallet::common::NoteRequest::Unspent,
lock_filter,
)?
} else {
vec![]
},
#[cfg(feature = "orchard")]
if sources.contains(&ShieldedPool::Ironwood) {
wallet::common::select_unspent_notes(
self.conn.borrow(),
&self.params,
account,
target_height,
ConfirmationsPolicy::MIN,
exclude,
ShieldedPool::Ironwood,
wallet::orchard::to_received_note,
wallet::common::NoteRequest::Unspent,
lock_filter,
)?
} else {
vec![]
},
))
}
#[cfg(feature = "transparent-inputs")]
fn get_unspent_transparent_output(
&self,
outpoint: &OutPoint,
target_height: TargetHeight,
) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
wallet::transparent::get_wallet_transparent_output(
self.conn.borrow(),
outpoint,
Some(target_height),
)
}
#[cfg(feature = "transparent-inputs")]
fn get_spendable_transparent_outputs(
&self,
address: &TransparentAddress,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
wallet::transparent::get_spendable_transparent_outputs(
self.conn.borrow(),
&self.params,
address,
target_height,
confirmations_policy,
output_filter,
lock_filter,
)
}
#[cfg(feature = "transparent-inputs")]
fn get_spendable_transparent_outputs_for_addresses(
&self,
addresses: &[TransparentAddress],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
wallet::transparent::get_spendable_transparent_outputs_for_addresses(
self.conn.borrow(),
&self.params,
addresses,
target_height,
confirmations_policy,
output_filter,
lock_filter,
)
}
#[cfg(feature = "transparent-inputs")]
fn select_spendable_transparent_outputs(
&self,
account: Self::AccountId,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
address_allow_list: Option<&[TransparentAddress]>,
target_value: TargetValue,
max_inputs: usize,
fee_rule: &StandardFeeRule,
lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
wallet::transparent::select_spendable_transparent_outputs(
self.conn.borrow(),
&self.params,
account,
target_height,
confirmations_policy,
output_filter,
address_allow_list,
target_value,
max_inputs,
fee_rule,
lock_filter,
)
}
fn get_account_metadata(
&self,
account_id: Self::AccountId,
selector: &NoteFilter,
target_height: TargetHeight,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<AccountMeta, Self::Error> {
let sapling_pool_meta = unspent_notes_meta(
self.conn.borrow(),
ShieldedPool::Sapling,
target_height,
account_id,
selector,
exclude,
lock_filter,
)?;
#[cfg(feature = "orchard")]
let orchard_pool_meta = unspent_notes_meta(
self.conn.borrow(),
ShieldedPool::Orchard,
target_height,
account_id,
selector,
exclude,
lock_filter,
)?;
#[cfg(not(feature = "orchard"))]
let orchard_pool_meta = None;
#[cfg(feature = "orchard")]
let ironwood_pool_meta = unspent_notes_meta(
self.conn.borrow(),
ShieldedPool::Ironwood,
target_height,
account_id,
selector,
exclude,
lock_filter,
)?;
#[cfg(not(feature = "orchard"))]
let ironwood_pool_meta = None;
Ok(AccountMeta::new(
sapling_pool_meta,
orchard_pool_meta,
ironwood_pool_meta,
))
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletRead
for WalletDb<C, P, CL, R>
{
type Error = SqliteClientError;
type AccountId = AccountUuid;
type Account = wallet::Account;
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(wallet::get_account_ids(self.conn.borrow())?)
}
fn get_account(
&self,
account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account(self.conn.borrow(), &self.params, account_id)
}
fn get_derived_account(
&self,
derivation: &Zip32Derivation,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_derived_account(
self.conn.borrow(),
&self.params,
derivation.seed_fingerprint(),
derivation.account_index(),
#[cfg(feature = "zcashd-compat")]
derivation.legacy_address_index(),
)
}
fn validate_seed(
&self,
account_id: Self::AccountId,
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
if let Some(account) = self.get_account(account_id)? {
if let AccountSource::Derived { derivation, .. } = account.source() {
wallet::seed_matches_derived_account(
&self.params,
seed,
derivation.seed_fingerprint(),
derivation.account_index(),
&account.uivk(),
)
} else {
Err(SqliteClientError::UnknownZip32Derivation)
}
} else {
Ok(false)
}
}
fn seed_relevance_to_derived_accounts(
&self,
seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
let mut has_accounts = false;
let mut has_derived = false;
let mut relevant_account_ids = vec![];
for account_id in self.get_account_ids()? {
has_accounts = true;
let account = self.get_account(account_id)?.expect("account ID exists");
if let AccountSource::Derived { derivation, .. } = account.source() {
has_derived = true;
if wallet::seed_matches_derived_account(
&self.params,
seed,
derivation.seed_fingerprint(),
derivation.account_index(),
&account.uivk(),
)? {
relevant_account_ids.push(account_id);
}
}
}
Ok(
if let Some(account_ids) = NonEmpty::from_vec(relevant_account_ids) {
SeedRelevance::Relevant { account_ids }
} else if has_derived {
SeedRelevance::NotRelevant
} else if has_accounts {
SeedRelevance::NoDerivedAccounts
} else {
SeedRelevance::NoAccounts
},
)
}
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
}
fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
wallet::list_addresses(self.conn.borrow(), &self.params, account)
}
fn find_account_for_address<Q: consensus::Parameters>(
&self,
params: &Q,
address: &zcash_keys::address::Address,
) -> Result<Option<Self::AccountId>, FindAccountForAddressError<Self::Error>> {
wallet::find_account_for_address(self.conn.borrow(), params, address)
}
fn get_last_generated_address_matching(
&self,
account: Self::AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error> {
wallet::get_last_generated_address_matching(
self.conn.borrow(),
&self.params,
account,
request,
)
.map(|res| res.map(|(addr, _)| addr))
}
fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
wallet::account_birthday(self.conn.borrow(), account)
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::wallet_recover_until(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_wallet_summary(
&self,
confirmations_policy: ConfirmationsPolicy,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
wallet::get_wallet_summary(
&self.conn.borrow().unchecked_transaction()?,
&self.params,
confirmations_policy,
&SubtreeProgressEstimator,
)
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::chain_tip_height(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn anchor_retention_interval(&self) -> AnchorRetentionInterval {
self.anchor_retention_interval
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
}
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_metadata(self.conn.borrow(), &self.params, height)
}
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_fully_scanned(self.conn.borrow(), &self.params)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_max_scanned(self.conn.borrow(), &self.params)
}
fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic)
}
fn get_target_and_anchor_heights(
&self,
min_confirmations: NonZeroU32,
) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error> {
wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations)
}
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
wallet::get_tx_height(self.conn.borrow(), txid)
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
}
fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
if sent_memo.is_some() {
Ok(sent_memo)
} else {
wallet::get_received_memo(self.conn.borrow(), note_id)
}
}
fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error> {
wallet::get_transaction(self.conn.borrow(), &self.params, txid)
.map(|res| res.map(|(_, tx)| tx))
}
fn get_sapling_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error> {
wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query)
}
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query)
}
#[cfg(feature = "orchard")]
fn get_ironwood_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
wallet::orchard::get_ironwood_nullifiers(self.conn.borrow(), query)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
account: Self::AccountId,
include_change: bool,
include_standalone: bool,
) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
let key_scopes = Some(KeyScope::EXTERNAL)
.into_iter()
.chain(include_change.then_some(KeyScope::INTERNAL))
.chain(
(include_standalone && cfg!(feature = "transparent-key-import"))
.then_some(KeyScope::Foreign),
)
.collect::<Vec<_>>();
wallet::transparent::get_transparent_receivers(
self.conn.borrow(),
&self.params,
&self.gap_limits,
account,
&key_scopes[..],
None,
false,
)
}
#[cfg(feature = "transparent-inputs")]
fn get_ephemeral_transparent_receivers(
&self,
account: Self::AccountId,
exposure_depth: u32,
exclude_used: bool,
) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
wallet::transparent::get_transparent_receivers(
self.conn.borrow(),
&self.params,
&self.gap_limits,
account,
&[KeyScope::Ephemeral],
Some(exposure_depth),
exclude_used,
)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
account: Self::AccountId,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
) -> Result<TransparentBalances, Self::Error> {
wallet::transparent::get_transparent_balances(
self.conn.borrow(),
&self.params,
account,
target_height,
confirmations_policy,
)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_address_metadata(
&self,
account: Self::AccountId,
address: &TransparentAddress,
) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
wallet::transparent::get_transparent_address_metadata(
self.conn.borrow(),
&self.params,
&self.gap_limits,
account,
address,
)
}
#[cfg(feature = "transparent-inputs")]
fn utxo_query_height(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
let account_ref = wallet::get_account_ref(self.conn.borrow(), account)?;
wallet::transparent::utxo_query_height(self.conn.borrow(), account_ref, &self.gap_limits)
}
fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
if let Some(_chain_tip_height) = wallet::chain_tip_height(self.conn.borrow())? {
let iter = wallet::transaction_data_requests(self.conn.borrow())?.into_iter();
#[cfg(feature = "transparent-inputs")]
let iter = iter.chain(wallet::transparent::transaction_data_requests(
self.conn.borrow(),
&self.params,
_chain_tip_height,
)?);
Ok(iter.collect())
} else {
Ok(vec![])
}
}
fn get_received_outputs(
&self,
txid: TxId,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
) -> Result<Vec<ReceivedTransactionOutput>, Self::Error> {
wallet::get_received_outputs(
self.conn.borrow(),
txid,
target_height,
confirmations_policy,
)
}
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletTest
for WalletDb<C, P, CL, R>
{
fn get_tx_history(
&self,
) -> Result<Vec<TransactionSummary<<Self as WalletRead>::AccountId>>, <Self as WalletRead>::Error>
{
wallet::testing::get_tx_history(self.conn.borrow())
}
fn get_sent_note_ids(
&self,
txid: &TxId,
protocol: ShieldedPool,
) -> Result<Vec<NoteId>, <Self as WalletRead>::Error> {
let mut stmt_sent_notes = self.conn.borrow().prepare(
"SELECT output_index
FROM sent_notes
JOIN transactions ON transactions.id_tx = sent_notes.transaction_id
WHERE transactions.txid = :txid
AND sent_notes.output_pool = :pool_code",
)?;
let note_ids = stmt_sent_notes
.query_map(
named_params! {
":txid": txid.as_ref(),
":pool_code": pool_code(PoolType::Shielded(protocol)),
},
|row| Ok(NoteId::new(*txid, protocol, row.get(0)?)),
)?
.collect::<Result<_, _>>()?;
Ok(note_ids)
}
fn get_sent_outputs(
&self,
txid: &TxId,
) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error> {
let mut stmt_sent = self.conn.borrow().prepare(
"SELECT value, to_address,
a.cached_transparent_receiver_address, a.transparent_child_index
FROM sent_notes
JOIN transactions t ON t.id_tx = sent_notes.transaction_id
LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx
LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope
WHERE t.txid = :txid
ORDER BY value",
)?;
let sends = stmt_sent
.query_map(
named_params![
":txid": txid.as_ref(),
":key_scope": KeyScope::Ephemeral.encode()
],
|row| {
let v = row.get(0)?;
let to_address = row.get::<_, Option<String>>(1)?;
let ephemeral_address = row.get::<_, Option<String>>(2)?;
let address_index = row.get::<_, Option<u32>>(3)?;
Ok((v, to_address, ephemeral_address.zip(address_index)))
},
)?
.map(|res| {
let (amount, external_recipient, _ephemeral_address) = res?;
Ok::<_, SqliteClientError>(OutputOfSentTx::from_parts(
Zatoshis::from_u64(amount)?,
external_recipient
.map(|s| {
Address::decode(&self.params, &s).ok_or_else(|| {
SqliteClientError::CorruptedData(format!(
"invalid transparent address: {s}"
))
})
})
.transpose()?,
#[cfg(feature = "transparent-inputs")]
_ephemeral_address
.map(|(addr_str, idx)| {
let addr =
Address::decode(&self.params, &addr_str).ok_or_else(|| {
SqliteClientError::CorruptedData(format!(
"invalid transparent address: {addr_str}"
))
})?;
let i = NonHardenedChildIndex::from_index(idx).ok_or_else(|| {
SqliteClientError::CorruptedData(format!(
"invalid non-hardened child index: {idx}"
))
})?;
Ok::<_, SqliteClientError>((addr, i))
})
.transpose()?,
))
})
.collect::<Result<_, _>>()?;
Ok(sends)
}
fn get_checkpoint_history(
&self,
protocol: &ShieldedPool,
) -> Result<
Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
<Self as WalletRead>::Error,
> {
wallet::testing::get_checkpoint_history(self.conn.borrow(), *protocol)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_output(
&self,
outpoint: &OutPoint,
target_height: Option<TargetHeight>,
) -> Result<
Option<WalletTransparentOutput<<Self as InputSource>::AccountId>>,
<Self as InputSource>::Error,
> {
wallet::transparent::get_wallet_transparent_output(
self.conn.borrow(),
outpoint,
target_height,
)
}
fn get_notes(
&self,
protocol: ShieldedPool,
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error> {
let (target_height, _) = self
.get_target_and_anchor_heights(NonZeroU32::MIN)?
.ok_or(SqliteClientError::ChainHeightUnknown)?;
let TableConstants {
table_prefix,
output_index_col,
..
} = wallet::common::table_constants::<<Self as InputSource>::Error>(protocol)?;
let mut stmt_received_notes = self.conn.borrow().prepare(&format!(
"SELECT txid, {output_index_col}
FROM {table_prefix}_received_notes rn
INNER JOIN transactions ON transactions.id_tx = rn.transaction_id
WHERE transactions.block IS NOT NULL
AND recipient_key_scope IS NOT NULL
AND nf IS NOT NULL
AND commitment_tree_position IS NOT NULL"
))?;
let result = stmt_received_notes
.query_map([], |row| {
let txid: [u8; 32] = row.get("txid")?;
let output_index: u32 = row.get(output_index_col)?;
let lock_filter = LockFilter::Unfiltered;
let note = self
.get_spendable_note(
&TxId::from_bytes(txid),
protocol,
output_index,
target_height,
lock_filter,
)
.unwrap()
.unwrap();
Ok(note)
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(result)
}
#[cfg(feature = "transparent-inputs")]
fn get_known_ephemeral_addresses(
&self,
account: <Self as WalletRead>::AccountId,
index_range: Option<Range<NonHardenedChildIndex>>,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
let account_id = wallet::get_account_ref(self.conn.borrow(), account)?;
wallet::transparent::ephemeral::get_known_ephemeral_addresses(
self.conn.borrow(),
&self.params,
&self.gap_limits,
account_id,
index_range,
)
}
#[cfg(feature = "transparent-inputs")]
fn find_account_for_ephemeral_address(
&self,
address: &TransparentAddress,
) -> Result<Option<<Self as WalletRead>::AccountId>, <Self as WalletRead>::Error> {
wallet::transparent::ephemeral::find_account_for_ephemeral_address_str(
self.conn.borrow(),
&address.encode(&self.params),
)
}
}
impl<C, P, CL, R> OutputLockStore for WalletDb<C, P, CL, R>
where
C: BorrowMut<rusqlite::Connection>,
P: consensus::Parameters,
CL: Clock,
R: RngCore,
{
type Error = SqliteClientError;
type AccountId = AccountUuid;
fn lock_outputs(
&mut self,
outputs: &[OutputRef],
owner: LockOwner,
lock_expiry_height: BlockHeight,
) -> Result<usize, LockError<Self::Error>> {
Ok(self.transactionally(|wdb| {
wallet::locking::lock_outputs(wdb.conn.0, outputs, owner, lock_expiry_height)
})?)
}
fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
self.transactionally(|wdb| wallet::locking::unlock_output(wdb.conn.0, output, owner))
}
fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
self.transactionally(|wdb| wallet::locking::clear_locked_outputs(wdb.conn.0, account))
}
fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
wallet::locking::get_locked_outputs(self.conn.borrow(), account)
}
}
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R: RngCore>
WalletWrite for WalletDb<C, P, CL, R>
{
type UtxoRef = UtxoId;
fn create_account(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
{
self.borrow_mut()
.transactionally(|wdb| wdb.create_account(account_name, seed, birthday, key_source))
}
fn import_account_hd(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
account_index: zip32::AccountId,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
self.transactionally(|wdb| {
wdb.import_account_hd(account_name, seed, account_index, birthday, key_source)
})
}
fn import_account_ufvk(
&mut self,
account_name: &str,
ufvk: &UnifiedFullViewingKey,
birthday: &AccountBirthday,
purpose: AccountPurpose,
key_source: Option<&str>,
) -> Result<Self::Account, <Self as WalletRead>::Error> {
self.transactionally(|wdb| {
wdb.import_account_ufvk(account_name, ufvk, birthday, purpose, key_source)
})
}
fn delete_account(
&mut self,
account_uuid: <Self as WalletRead>::AccountId,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.delete_account(account_uuid))
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkey(
&mut self,
account: <Self as WalletRead>::AccountId,
pubkey: secp256k1::PublicKey,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.import_standalone_transparent_pubkey(account, pubkey))
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkeys(
&mut self,
account: <Self as WalletRead>::AccountId,
pubkeys: &[secp256k1::PublicKey],
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.import_standalone_transparent_pubkeys(account, pubkeys))
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_script(
&mut self,
account: <Self as WalletRead>::AccountId,
script: zcash_script::script::Redeem,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.import_standalone_transparent_script(account, script))
}
fn get_next_available_address(
&mut self,
account_uuid: <Self as WalletRead>::AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.get_next_available_address(account_uuid, request))
}
fn get_address_for_index(
&mut self,
account: <Self as WalletRead>::AccountId,
diversifier_index: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.get_address_for_index(account, diversifier_index, request))
}
fn update_chain_tip(
&mut self,
tip_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.update_chain_tip(tip_height))
}
fn prune_scan_queue_below(
&mut self,
height: BlockHeight,
retain_with_priority: Option<ScanPriority>,
) -> Result<u64, <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.prune_scan_queue_below(height, retain_with_priority))
}
#[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))]
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
from_state: &ChainState,
blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.put_blocks(from_state, blocks))
}
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
#[cfg(feature = "transparent-inputs")]
return self.transactionally(|wdb| wdb.put_received_transparent_utxo(_output));
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn store_decrypted_tx(
&mut self,
d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.store_decrypted_tx(d_tx))
}
fn set_tx_trust(
&mut self,
txid: TxId,
trusted: bool,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.set_tx_trust(txid, trusted))
}
fn store_transactions_to_be_sent(
&mut self,
transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.store_transactions_to_be_sent(transactions))
}
fn truncate_to_height(
&mut self,
max_height: BlockHeight,
) -> Result<BlockHeight, <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.truncate_to_height(max_height))
}
fn truncate_to_chain_state(
&mut self,
chain_state: ChainState,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.truncate_to_chain_state(chain_state))
}
fn rewind_to_chain_state(
&mut self,
chain_state: ChainState,
reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
let result = wallet::rewind_to_chain_state(
&tx,
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
&chain_state,
reset_account_birthdays,
);
if result.is_ok() {
tx.commit()
.map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
}
result
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_ephemeral_addresses(
&mut self,
account_id: <Self as WalletRead>::AccountId,
n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
self.transactionally(|wdb| wdb.reserve_next_n_ephemeral_addresses(account_id, n))
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_internal_addresses(
&mut self,
account_id: <Self as WalletRead>::AccountId,
n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
self.transactionally(|wdb| wdb.reserve_next_n_internal_addresses(account_id, n))
}
fn set_transaction_status(
&mut self,
txid: TxId,
status: data_api::TransactionStatus,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| WalletWrite::set_transaction_status(wdb, txid, status))
}
#[cfg(feature = "transparent-inputs")]
fn schedule_next_check(
&mut self,
address: &TransparentAddress,
offset_seconds: u32,
) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.schedule_next_check(address, offset_seconds))
}
#[cfg(feature = "transparent-inputs")]
fn mark_transparent_addresses_exposed(
&mut self,
exposures: &[(TransparentAddress, BlockHeight)],
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.mark_transparent_addresses_exposed(exposures))
}
#[cfg(feature = "transparent-inputs")]
fn notify_address_checked(
&mut self,
request: TransactionsInvolvingAddress,
as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.notify_address_checked(request, as_of_height))
}
#[cfg(feature = "spend-index")]
fn notify_output_verified_unspent(
&mut self,
outpoint: OutPoint,
as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
self.transactionally(|wdb| wdb.notify_output_verified_unspent(outpoint, as_of_height))
}
}
impl<P, CL, R> OutputLockStore for WalletDb<SqlTransaction<'_>, P, CL, R>
where
P: consensus::Parameters,
CL: Clock,
R: RngCore,
{
type Error = SqliteClientError;
type AccountId = AccountUuid;
fn lock_outputs(
&mut self,
outputs: &[OutputRef],
owner: LockOwner,
lock_expiry_height: BlockHeight,
) -> Result<usize, LockError<Self::Error>> {
Ok(wallet::locking::lock_outputs(
self.conn.0,
outputs,
owner,
lock_expiry_height,
)?)
}
fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
wallet::locking::unlock_output(self.conn.0, output, owner)
}
fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
wallet::locking::clear_locked_outputs(self.conn.0, account)
}
fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
wallet::locking::get_locked_outputs(self.conn.0, account)
}
}
impl<P: consensus::Parameters, CL: Clock, R: RngCore> WalletWrite
for WalletDb<SqlTransaction<'_>, P, CL, R>
{
type UtxoRef = UtxoId;
fn create_account(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
{
let seed_fingerprint =
SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
SqliteClientError::BadAccountData(
"Seed must be between 32 and 252 bytes in length.".to_owned(),
)
})?;
let zip32_account_index = wallet::max_zip32_account_index(self.conn.0, &seed_fingerprint)?
.map(|a| {
a.next()
.ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
})
.transpose()?
.unwrap_or(zip32::AccountId::ZERO);
let usk =
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), zip32_account_index)
.map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?;
let ufvk = usk.to_unified_full_viewing_key();
let account = wallet::add_account(
self.conn.0,
&self.params,
account_name,
&AccountSource::Derived {
derivation: Zip32Derivation::new(
seed_fingerprint,
zip32_account_index,
#[cfg(feature = "zcashd-compat")]
None,
),
key_source: key_source.map(|s| s.to_owned()),
},
wallet::ViewingKey::Full(Box::new(ufvk)),
birthday,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
)?;
Ok((account.id(), usk))
}
fn import_account_hd(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
account_index: zip32::AccountId,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
let seed_fingerprint =
SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
SqliteClientError::BadAccountData(
"Seed must be between 32 and 252 bytes in length.".to_owned(),
)
})?;
let usk = UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account_index)
.map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
let ufvk = usk.to_unified_full_viewing_key();
let account = wallet::add_account(
self.conn.0,
&self.params,
account_name,
&AccountSource::Derived {
derivation: Zip32Derivation::new(
seed_fingerprint,
account_index,
#[cfg(feature = "zcashd-compat")]
None,
),
key_source: key_source.map(|s| s.to_owned()),
},
wallet::ViewingKey::Full(Box::new(ufvk)),
birthday,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
)?;
Ok((account, usk))
}
fn import_account_ufvk(
&mut self,
account_name: &str,
ufvk: &UnifiedFullViewingKey,
birthday: &AccountBirthday,
purpose: AccountPurpose,
key_source: Option<&str>,
) -> Result<Self::Account, <Self as WalletRead>::Error> {
wallet::add_account(
self.conn.0,
&self.params,
account_name,
&AccountSource::Imported {
purpose,
key_source: key_source.map(|s| s.to_owned()),
},
wallet::ViewingKey::Full(Box::new(ufvk.to_owned())),
birthday,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
)
}
fn delete_account(
&mut self,
account_uuid: <Self as WalletRead>::AccountId,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::delete_account(self.conn.0, account_uuid)
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkey(
&mut self,
account: <Self as WalletRead>::AccountId,
pubkey: secp256k1::PublicKey,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::import_standalone_transparent_pubkey(self.conn.0, &self.params, account, pubkey)
.map(|_inserted| ())
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkeys(
&mut self,
account: <Self as WalletRead>::AccountId,
pubkeys: &[secp256k1::PublicKey],
) -> Result<(), <Self as WalletRead>::Error> {
wallet::import_standalone_transparent_pubkeys(self.conn.0, &self.params, account, pubkeys)
.map(|_inserted| ())
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_script(
&mut self,
account: <Self as WalletRead>::AccountId,
script: zcash_script::script::Redeem,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::import_standalone_transparent_script(self.conn.0, &self.params, account, script)
}
fn get_next_available_address(
&mut self,
account_uuid: <Self as WalletRead>::AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
wallet::get_next_available_address(
self.conn.0,
&self.params,
&self.clock,
account_uuid,
request,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
)
}
fn get_address_for_index(
&mut self,
account: <Self as WalletRead>::AccountId,
diversifier_index: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
if let Some(account) = self.get_account(account)? {
match account.uivk().address(diversifier_index, request) {
Ok(address) => {
let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?;
upsert_address(
self.conn.borrow(),
&self.params,
account.internal_id(),
diversifier_index,
&address,
Some(chain_tip_height.unwrap_or(account.birthday())),
true,
)?;
Ok(Some(address))
}
#[cfg(feature = "transparent-inputs")]
Err(InvalidTransparentChildIndex(_)) => Ok(None),
Err(InvalidSaplingDiversifierIndex(_)) => Ok(None),
Err(e) => Err(SqliteClientError::AddressGeneration(e)),
}
} else {
Err(SqliteClientError::AccountUnknown)
}
}
fn update_chain_tip(
&mut self,
tip_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::scanning::update_chain_tip(self.conn.0, &self.params, tip_height)?;
Ok(())
}
fn prune_scan_queue_below(
&mut self,
height: BlockHeight,
retain_with_priority: Option<ScanPriority>,
) -> Result<u64, <Self as WalletRead>::Error> {
wallet::scanning::prune_scan_queue_below(self.conn.0, height, retain_with_priority)
}
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
from_state: &ChainState,
blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
) -> Result<(), <Self as WalletRead>::Error> {
let anchor_retention = self
.params
.activation_height(consensus::NetworkUpgrade::Nu6_3)
.map(|from_height| {
Ok::<_, SqliteClientError>(AnchorRetention::union(
from_height,
core::iter::once(self.anchor_retention_interval),
))
})
.transpose()?
.flatten();
ll::wallet::put_blocks::<_, SqliteClientError, commitment_tree::Error>(
self,
#[cfg(feature = "transparent-inputs")]
self.gap_limits,
from_state,
blocks,
anchor_retention.as_ref(),
)
.map_err(SqliteClientError::from)
}
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
#[cfg(feature = "transparent-inputs")]
return {
let (account_id, _, key_scope, utxo_id) =
wallet::transparent::put_received_transparent_utxo(
self.conn.0,
&self.params,
&self.gap_limits,
_output,
)?;
if let Some(t_key_scope) = <Option<TransparentKeyScope>>::from(key_scope) {
wallet::transparent::generate_gap_addresses(
self.conn.0,
&self.params,
&self.gap_limits,
account_id,
t_key_scope,
UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
true,
)?;
}
Ok(utxo_id)
};
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn store_decrypted_tx(
&mut self,
d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
) -> Result<(), <Self as WalletRead>::Error> {
let chain_tip = wallet::chain_tip_height(self.conn.borrow())?
.ok_or(SqliteClientError::ChainHeightUnknown)?;
store_decrypted_tx(
self,
&self.params.clone(),
#[cfg(feature = "transparent-inputs")]
self.gap_limits,
chain_tip,
d_tx,
)
}
fn set_tx_trust(
&mut self,
txid: TxId,
trusted: bool,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::set_tx_trust(self.conn.0, txid, trusted)
}
fn store_transactions_to_be_sent(
&mut self,
transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
) -> Result<(), <Self as WalletRead>::Error> {
for sent_tx in transactions {
wallet::store_transaction_to_be_sent(
self.conn.0,
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
sent_tx,
)?;
}
Ok(())
}
fn truncate_to_height(
&mut self,
max_height: BlockHeight,
) -> Result<BlockHeight, <Self as WalletRead>::Error> {
wallet::truncate_to_height(
self.conn.0,
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
max_height,
)
}
fn truncate_to_chain_state(
&mut self,
chain_state: ChainState,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::truncate_to_chain_state(self, chain_state)
}
fn rewind_to_chain_state(
&mut self,
chain_state: ChainState,
reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
wallet::rewind_to_chain_state(
self.conn.0,
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
&chain_state,
reset_account_birthdays,
)
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_ephemeral_addresses(
&mut self,
account_id: <Self as WalletRead>::AccountId,
n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
let reserved = wallet::transparent::reserve_next_n_addresses(
self.conn.0,
&self.params,
account_id,
TransparentKeyScope::EPHEMERAL,
self.gap_limits.ephemeral(),
n,
)?;
Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_internal_addresses(
&mut self,
account_id: <Self as WalletRead>::AccountId,
n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
let reserved = wallet::transparent::reserve_next_n_addresses(
self.conn.0,
&self.params,
account_id,
TransparentKeyScope::INTERNAL,
self.gap_limits.internal(),
n,
)?;
Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
}
fn set_transaction_status(
&mut self,
txid: TxId,
status: data_api::TransactionStatus,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::set_transaction_status(
self.conn.0,
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
txid,
status,
)
}
#[cfg(feature = "transparent-inputs")]
fn schedule_next_check(
&mut self,
address: &TransparentAddress,
offset_seconds: u32,
) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
wallet::transparent::schedule_next_check(
self.conn.0,
&self.params,
&self.clock,
&mut self.rng,
address,
offset_seconds,
)
}
#[cfg(feature = "transparent-inputs")]
fn mark_transparent_addresses_exposed(
&mut self,
exposures: &[(TransparentAddress, BlockHeight)],
) -> Result<(), <Self as WalletRead>::Error> {
wallet::transparent::mark_transparent_addresses_exposed(
self.conn.0,
&self.params,
exposures,
)
}
#[cfg(feature = "transparent-inputs")]
fn notify_address_checked(
&mut self,
request: TransactionsInvolvingAddress,
as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
if let Some(requested_end) = request.block_range_end() {
if as_of_height != requested_end - 1 {
return Err(SqliteClientError::NotificationMismatch {
expected: requested_end - 1,
actual: as_of_height,
});
}
}
wallet::transparent::update_observed_unspent_heights(
self.conn.0,
&self.params,
request.address(),
as_of_height,
)
}
#[cfg(feature = "spend-index")]
fn notify_output_verified_unspent(
&mut self,
outpoint: OutPoint,
as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
wallet::transparent::update_observed_unspent_height_for_outpoint(
self.conn.0,
&outpoint,
as_of_height,
)
}
}
impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
LowLevelWalletRead for WalletDb<C, P, CL, R>
{
type AccountId = AccountUuid;
type AccountRef = AccountRef;
type Account = wallet::Account;
type Error = SqliteClientError;
type TxRef = TxRef;
fn block_fully_scanned_height(
&self,
) -> Result<Option<zcash_protocol::consensus::BlockHeight>, Self::Error> {
Ok(
wallet::block_fully_scanned(self.conn.borrow(), &self.params)?
.map(|meta| meta.block_height()),
)
}
fn select_receiving_address(
&self,
account: Self::AccountId,
receiver: &zcash_keys::address::Receiver,
) -> Result<Option<zcash_address::ZcashAddress>, Self::Error> {
wallet::select_receiving_address(self.conn.borrow(), &self.params, account, receiver)
}
#[cfg(feature = "transparent-inputs")]
fn find_involved_accounts(
&self,
tx_refs: impl IntoIterator<Item = Self::TxRef>,
) -> Result<HashSet<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
Ok(wallet::involved_accounts(self.conn.borrow(), tx_refs)?
.into_iter()
.map(|(_, uuid, scope)| (uuid, scope))
.collect())
}
#[cfg(feature = "transparent-inputs")]
fn find_account_for_transparent_address(
&self,
address: &TransparentAddress,
) -> Result<Option<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
wallet::transparent::find_account_uuid_for_transparent_address(
self.conn.borrow(),
&self.params,
address,
)
.map(|opt| opt.map(|(a, s)| (a, s.as_transparent())))
}
#[cfg(feature = "transparent-inputs")]
fn detect_accounts_transparent<'t>(
&self,
spends: impl Iterator<Item = &'t transparent::bundle::OutPoint>,
) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
wallet::transparent::detect_spending_accounts(self.conn.borrow(), spends)
.map_err(SqliteClientError::from)
}
fn detect_accounts_sapling<'t>(
&self,
spends: impl Iterator<Item = &'t sapling::Nullifier>,
) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
wallet::sapling::detect_spending_accounts(self.conn.borrow(), spends)
.map_err(SqliteClientError::from)
}
#[cfg(feature = "orchard")]
fn detect_accounts_orchard<'t>(
&self,
spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
wallet::orchard::detect_spending_accounts(self.conn.borrow(), ORCHARD_TABLES_PREFIX, spends)
.map_err(SqliteClientError::from)
}
#[cfg(feature = "orchard")]
fn detect_accounts_ironwood<'t>(
&self,
spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
wallet::orchard::detect_spending_accounts(
self.conn.borrow(),
IRONWOOD_TABLES_PREFIX,
spends,
)
.map_err(SqliteClientError::from)
}
#[cfg(feature = "transparent-inputs")]
fn get_wallet_transparent_output(
&self,
outpoint: &OutPoint,
target_height: Option<TargetHeight>,
) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
wallet::transparent::get_wallet_transparent_output(
self.conn.borrow(),
outpoint,
target_height,
)
}
fn get_txs_spending_transparent_outputs_of(
&self,
tx_ref: Self::TxRef,
) -> Result<Vec<(Self::TxRef, Transaction)>, Self::Error> {
wallet::get_txs_spending_transparent_outputs_of(self.conn.borrow(), &self.params, tx_ref)
}
fn detect_sapling_spend(
&self,
nf: &::sapling::Nullifier,
) -> Result<Option<Self::TxRef>, Self::Error> {
wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Sapling, nf)
}
#[cfg(feature = "orchard")]
fn detect_orchard_spend(
&self,
nf: &::orchard::note::Nullifier,
) -> Result<Option<Self::TxRef>, Self::Error> {
wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Orchard, &nf.to_bytes())
}
#[cfg(feature = "orchard")]
fn detect_ironwood_spend(
&self,
nf: &::orchard::note::Nullifier,
) -> Result<Option<Self::TxRef>, Self::Error> {
wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Ironwood, &nf.to_bytes())
}
#[cfg(feature = "transparent-inputs")]
fn get_account_ref(
&self,
account_uuid: Self::AccountId,
) -> Result<Self::AccountRef, Self::Error> {
wallet::get_account_ref(self.conn.borrow(), account_uuid)
}
#[cfg(feature = "transparent-inputs")]
fn get_account_internal(
&self,
account_id: Self::AccountRef,
) -> Result<Option<wallet::Account>, SqliteClientError> {
wallet::get_account_internal(self.conn.borrow(), &self.params, account_id)
}
}
impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
LowLevelWalletWrite for WalletDb<C, P, CL, R>
{
fn put_block_meta(
&mut self,
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
sapling_commitment_tree_size: u32,
sapling_output_count: u32,
#[cfg(feature = "orchard")] orchard_commitment_tree_size: u32,
#[cfg(feature = "orchard")] orchard_action_count: u32,
#[cfg(feature = "orchard")] ironwood_commitment_tree_size: u32,
#[cfg(feature = "orchard")] ironwood_action_count: u32,
) -> Result<(), Self::Error> {
wallet::put_block(
self.conn.borrow(),
block_height,
block_hash,
block_time,
sapling_commitment_tree_size,
sapling_output_count,
#[cfg(feature = "orchard")]
orchard_commitment_tree_size,
#[cfg(feature = "orchard")]
orchard_action_count,
#[cfg(feature = "orchard")]
ironwood_commitment_tree_size,
#[cfg(feature = "orchard")]
ironwood_action_count,
)
}
fn put_tx_meta(
&mut self,
tx: &WalletTx<Self::AccountId>,
height: BlockHeight,
) -> Result<Self::TxRef, Self::Error> {
wallet::put_tx_meta(self.conn.borrow(), tx, height)
}
fn put_tx_data(
&mut self,
tx: &Transaction,
fee: Option<zcash_protocol::value::Zatoshis>,
created_at: Option<time::OffsetDateTime>,
target_height: Option<TargetHeight>,
observed_height: BlockHeight,
) -> Result<Self::TxRef, Self::Error> {
wallet::put_tx_data(
self.conn.borrow(),
tx,
fee,
created_at,
target_height,
observed_height,
)
}
fn set_transaction_status(
&mut self,
txid: TxId,
status: data_api::TransactionStatus,
) -> Result<(), Self::Error> {
wallet::set_transaction_status(
self.conn.borrow(),
&self.params,
#[cfg(feature = "transparent-inputs")]
&self.gap_limits,
txid,
status,
)
}
fn put_zip318_classification(
&mut self,
tx_ref: Self::TxRef,
classification: zcash_protocol::zip318::Zip318Classification,
) -> Result<(), Self::Error> {
wallet::put_zip318_classification(self.conn.borrow(), tx_ref, classification)
}
fn put_received_sapling_note<T: ReceivedSaplingOutput<AccountId = Self::AccountId>>(
&mut self,
output: &T,
tx_ref: Self::TxRef,
target_or_mined_height: Option<BlockHeight>,
spent_in: Option<Self::TxRef>,
) -> Result<(), Self::Error> {
wallet::sapling::put_received_note(
self.conn.borrow(),
&self.params,
output,
tx_ref,
target_or_mined_height,
spent_in,
)?;
Ok(())
}
fn mark_sapling_note_spent(
&mut self,
nf: &::sapling::Nullifier,
tx_ref: Self::TxRef,
) -> Result<bool, Self::Error> {
wallet::sapling::mark_sapling_note_spent(self.conn.borrow(), tx_ref, nf)
}
fn track_block_sapling_nullifiers(
&mut self,
block_height: BlockHeight,
nfs: &[(TxIndex, TxId, Vec<::sapling::Nullifier>)],
) -> Result<(), Self::Error> {
wallet::insert_nullifier_map(self.conn.borrow(), block_height, ShieldedPool::Sapling, nfs)
}
#[cfg(feature = "orchard")]
fn put_received_orchard_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
&mut self,
output: &T,
tx_ref: Self::TxRef,
target_or_mined_height: Option<BlockHeight>,
spent_in: Option<Self::TxRef>,
) -> Result<(), Self::Error> {
wallet::orchard::put_received_note(
self.conn.borrow(),
&self.params,
ShieldedPool::Orchard,
output,
tx_ref,
target_or_mined_height,
spent_in,
)?;
Ok(())
}
#[cfg(feature = "orchard")]
fn put_received_ironwood_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
&mut self,
output: &T,
tx_ref: Self::TxRef,
target_or_mined_height: Option<BlockHeight>,
spent_in: Option<Self::TxRef>,
) -> Result<(), Self::Error> {
wallet::orchard::put_received_note(
self.conn.borrow(),
&self.params,
ShieldedPool::Ironwood,
output,
tx_ref,
target_or_mined_height,
spent_in,
)?;
Ok(())
}
#[cfg(feature = "orchard")]
fn mark_orchard_note_spent(
&mut self,
nf: &::orchard::note::Nullifier,
tx_ref: Self::TxRef,
) -> Result<bool, Self::Error> {
wallet::orchard::mark_orchard_note_spent(self.conn.borrow(), tx_ref, nf)
}
#[cfg(feature = "orchard")]
fn mark_ironwood_note_spent(
&mut self,
nf: &::orchard::note::Nullifier,
tx_ref: Self::TxRef,
) -> Result<bool, Self::Error> {
wallet::orchard::mark_ironwood_note_spent(self.conn.borrow(), tx_ref, nf)
}
#[cfg(feature = "orchard")]
fn track_block_orchard_nullifiers(
&mut self,
block_height: BlockHeight,
nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
) -> Result<(), Self::Error> {
wallet::insert_nullifier_map(
self.conn.borrow(),
block_height,
ShieldedPool::Orchard,
&nfs.iter()
.map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
.collect::<Vec<_>>(),
)
}
#[cfg(feature = "orchard")]
fn track_block_ironwood_nullifiers(
&mut self,
block_height: BlockHeight,
nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
) -> Result<(), Self::Error> {
wallet::insert_nullifier_map(
self.conn.borrow(),
block_height,
ShieldedPool::Ironwood,
&nfs.iter()
.map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
.collect::<Vec<_>>(),
)
}
fn prune_tracked_nullifiers(&mut self, pruning_depth: u32) -> Result<(), Self::Error> {
if let Some(meta) = wallet::block_fully_scanned(self.conn.borrow(), &self.params)? {
wallet::prune_nullifier_map(
self.conn.borrow(),
meta.block_height().saturating_sub(pruning_depth),
)?;
}
Ok(())
}
fn put_sent_output(
&mut self,
from_account_uuid: Self::AccountId,
tx_ref: Self::TxRef,
output_index: usize,
recipient: &zcash_client_backend::wallet::Recipient<Self::AccountId>,
value: zcash_protocol::value::Zatoshis,
memo: Option<&zcash_protocol::memo::MemoBytes>,
) -> Result<(), Self::Error> {
wallet::put_sent_output(
self.conn.borrow(),
&self.params,
from_account_uuid,
tx_ref,
output_index,
recipient,
value,
memo,
)
}
fn update_tx_fee(
&mut self,
tx_ref: Self::TxRef,
fee: zcash_protocol::value::Zatoshis,
) -> Result<(), Self::Error> {
wallet::update_tx_fee(self.conn.borrow(), tx_ref, fee)
}
#[cfg(feature = "transparent-inputs")]
fn put_transparent_output(
&mut self,
output: &WalletTransparentOutput<Self::AccountId>,
observation_height: BlockHeight,
known_unspent: bool,
) -> Result<(Self::AccountId, Option<TransparentKeyScope>), Self::Error> {
let (_, account_uuid, key_scope, _) = wallet::transparent::put_transparent_output(
self.conn.borrow(),
&self.params,
&self.gap_limits,
output,
observation_height,
known_unspent,
)?;
Ok((account_uuid, key_scope.as_transparent()))
}
#[cfg(feature = "transparent-inputs")]
fn mark_transparent_utxo_spent(
&mut self,
outpoint: &OutPoint,
spent_in_tx: Self::TxRef,
) -> Result<bool, Self::Error> {
wallet::transparent::mark_transparent_utxo_spent(self.conn.borrow(), spent_in_tx, outpoint)
}
#[cfg(feature = "transparent-inputs")]
fn generate_transparent_gap_addresses(
&mut self,
account_id: Self::AccountId,
key_scope: TransparentKeyScope,
request: UnifiedAddressRequest,
) -> Result<(), Self::Error> {
generate_transparent_gap_addresses(self, self.gap_limits, account_id, key_scope, request)?;
Ok(())
}
#[cfg(feature = "transparent-inputs")]
fn queue_transparent_spend_detection(
&mut self,
receiving_address: TransparentAddress,
tx_ref: Self::TxRef,
output_index: u32,
) -> Result<(), Self::Error> {
wallet::transparent::queue_transparent_spend_detection(
self.conn.borrow(),
&self.params,
receiving_address,
tx_ref,
output_index,
)
}
#[cfg(feature = "transparent-inputs")]
fn queue_transparent_input_retrieval(
&mut self,
tx_ref: Self::TxRef,
d_tx: &DecryptedTransaction<Transaction, Self::AccountId>,
) -> Result<(), Self::Error> {
wallet::queue_transparent_input_retrieval(self.conn.borrow(), tx_ref, d_tx)
}
fn queue_tx_retrieval(
&mut self,
txids: impl Iterator<Item = TxId>,
dependent_tx_ref: Option<Self::TxRef>,
) -> Result<(), Self::Error> {
wallet::queue_tx_retrieval(self.conn.borrow(), txids, dependent_tx_ref)
}
fn queue_tx_status(&mut self, txid: TxId) -> Result<(), Self::Error> {
wallet::queue_tx_status(self.conn.borrow(), txid)
}
fn delete_retrieval_queue_entries(&mut self, txid: TxId) -> Result<(), Self::Error> {
wallet::delete_retrieval_queue_entries(self.conn.borrow(), txid)
}
fn notify_scan_complete(
&mut self,
range: Range<BlockHeight>,
wallet_note_positions: &[(ShieldedPool, Position)],
) -> Result<(), Self::Error> {
wallet::scanning::scan_complete(
self.conn.borrow(),
&self.params,
range,
wallet_note_positions,
)
}
#[cfg(feature = "transparent-inputs")]
fn update_gap_limits(
&mut self,
gap_limits: &GapLimits,
txid: TxId,
observation_height: BlockHeight,
) -> Result<(), Self::Error> {
wallet::transparent::update_gap_limits(
self.conn.borrow(),
&self.params,
gap_limits,
txid,
observation_height,
)
}
}
pub(crate) type SaplingShardStore<C> = SqliteShardStore<C, sapling::Node, SAPLING_SHARD_HEIGHT>;
pub(crate) type SaplingCommitmentTree<C> =
ShardTree<SaplingShardStore<C>, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>;
pub(crate) fn sapling_tree<C>(
conn: C,
) -> Result<SaplingCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
where
SaplingShardStore<C>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight>,
{
Ok(ShardTree::new(
SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
PRUNING_DEPTH.try_into().unwrap(),
))
}
#[cfg(feature = "orchard")]
pub(crate) type OrchardShardStore<C> =
SqliteShardStore<C, orchard::tree::MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>;
#[cfg(feature = "orchard")]
pub(crate) type OrchardCommitmentTree<C> = ShardTree<
OrchardShardStore<C>,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
ORCHARD_SHARD_HEIGHT,
>;
#[cfg(feature = "orchard")]
pub(crate) fn orchard_tree<C>(
conn: C,
) -> Result<OrchardCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
where
OrchardShardStore<C>:
ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
{
Ok(ShardTree::new(
SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
PRUNING_DEPTH.try_into().unwrap(),
))
}
#[cfg(feature = "orchard")]
pub(crate) type IronwoodShardStore<C> =
SqliteShardStore<C, orchard::tree::MerkleHashOrchard, IRONWOOD_SHARD_HEIGHT>;
#[cfg(feature = "orchard")]
pub(crate) type IronwoodCommitmentTree<C> = ShardTree<
IronwoodShardStore<C>,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
IRONWOOD_SHARD_HEIGHT,
>;
#[cfg(feature = "orchard")]
pub(crate) fn ironwood_tree<C>(
conn: C,
) -> Result<IronwoodCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
where
IronwoodShardStore<C>:
ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
{
Ok(ShardTree::new(
SqliteShardStore::from_connection(conn, IRONWOOD_TABLES_PREFIX)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
PRUNING_DEPTH.try_into().unwrap(),
))
}
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
for WalletDb<C, P, CL, R>
{
type Error = commitment_tree::Error;
type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F:
FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
let result = {
let mut shardtree = sapling_tree(&tx)?;
callback(&mut shardtree)?
};
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(result)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
&tx,
SAPLING_TABLES_PREFIX,
start_index,
roots,
)?;
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(())
}
fn get_sapling_subtree_root(
&mut self,
index: u64,
) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.borrow(), SAPLING_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> = SqliteShardStore<
&'a rusqlite::Transaction<'a>,
orchard::tree::MerkleHashOrchard,
ORCHARD_SHARD_HEIGHT,
>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F:
FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
let result = {
let mut shardtree = orchard_tree(&tx)?;
callback(&mut shardtree)?
};
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(result)
}
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
&tx,
ORCHARD_TABLES_PREFIX,
start_index,
roots,
)?;
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(())
}
#[cfg(feature = "orchard")]
fn get_orchard_subtree_root(
&mut self,
index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.borrow(), ORCHARD_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
fn put_ironwood_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
&tx,
IRONWOOD_TABLES_PREFIX,
start_index,
roots,
)?;
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(())
}
#[cfg(feature = "orchard")]
fn get_ironwood_subtree_root(
&mut self,
index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.borrow(), IRONWOOD_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
where
for<'a> F:
FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let tx = self
.conn
.borrow_mut()
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
let result = {
let mut shardtree = ironwood_tree(&tx)?;
callback(&mut shardtree)?
};
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(Some(result))
}
}
impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
for WalletDb<SqlTransaction<'_>, P, CL, R>
{
type Error = commitment_tree::Error;
type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F:
FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<commitment_tree::Error>>,
{
let mut shardtree = sapling_tree(self.conn.0)?;
let result = callback(&mut shardtree)?;
Ok(result)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
self.conn.0,
SAPLING_TABLES_PREFIX,
start_index,
roots,
)
}
fn get_sapling_subtree_root(
&mut self,
index: u64,
) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.0, SAPLING_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F:
FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let mut shardtree = orchard_tree(self.conn.0)?;
let result = callback(&mut shardtree)?;
Ok(result)
}
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
self.conn.0,
ORCHARD_TABLES_PREFIX,
start_index,
roots,
)
}
#[cfg(feature = "orchard")]
fn get_orchard_subtree_root(
&mut self,
index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.0, ORCHARD_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
fn put_ironwood_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
self.conn.0,
IRONWOOD_TABLES_PREFIX,
start_index,
roots,
)
}
#[cfg(feature = "orchard")]
fn get_ironwood_subtree_root(
&mut self,
index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
wallet::commitment_tree::get_subtree_root(self.conn.0, IRONWOOD_TABLES_PREFIX, index)
.map_err(ShardTreeError::Storage)
}
#[cfg(feature = "orchard")]
fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
where
for<'a> F:
FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let mut shardtree = ironwood_tree(self.conn.0)?;
let result = callback(&mut shardtree)?;
Ok(Some(result))
}
}
#[cfg(feature = "transparent-inputs")]
impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
AddressStore for WalletDb<C, P, CL, R>
{
type Error = SqliteClientError;
type AccountRef = AccountRef;
fn find_gap_start(
&self,
account_ref: Self::AccountRef,
key_scope: TransparentKeyScope,
gap_limit: u32,
) -> Result<Option<NonHardenedChildIndex>, Self::Error> {
wallet::transparent::find_gap_start(self.conn.borrow(), account_ref, key_scope, gap_limit)
}
fn store_address_range(
&mut self,
account_id: Self::AccountRef,
key_scope: TransparentKeyScope,
list: Vec<(Address, TransparentAddress, NonHardenedChildIndex)>,
) -> Result<(), Self::Error> {
wallet::transparent::store_address_range(
self.conn.borrow(),
&self.params,
account_id,
key_scope,
list,
)
}
}
#[cfg(feature = "orchard")]
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletDb<C, P, CL, R> {
pub fn get_unspent_orchard_notes_at_historical_height(
&self,
account: AccountUuid,
height: BlockHeight,
) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
wallet::orchard::get_unspent_orchard_notes_at_historical_height(
self.conn.borrow(),
&self.params,
account,
height,
)
}
pub fn get_unspent_ironwood_notes_at_historical_height(
&self,
account: AccountUuid,
height: BlockHeight,
) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
wallet::orchard::get_unspent_ironwood_notes_at_historical_height(
self.conn.borrow(),
&self.params,
account,
height,
)
}
pub fn generate_orchard_witnesses_at_historical_height(
&self,
note_positions: &[Position],
frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
orchard::tree::MerkleHashOrchard,
>,
height: BlockHeight,
) -> Result<
Vec<
incrementalmerkletree::MerklePath<
orchard::tree::MerkleHashOrchard,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
>,
>,
SqliteClientError,
> {
wallet::commitment_tree::generate_orchard_witnesses_at_historical_height(
self.conn.borrow(),
note_positions,
frontier_at_height,
height,
)
}
pub fn generate_ironwood_witnesses_at_historical_height(
&self,
note_positions: &[Position],
frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
orchard::tree::MerkleHashOrchard,
>,
height: BlockHeight,
) -> Result<
Vec<
incrementalmerkletree::MerklePath<
orchard::tree::MerkleHashOrchard,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
>,
>,
SqliteClientError,
> {
wallet::commitment_tree::generate_ironwood_witnesses_at_historical_height(
self.conn.borrow(),
note_positions,
frontier_at_height,
height,
)
}
}
pub struct BlockDb(rusqlite::Connection);
impl BlockDb {
pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
rusqlite::Connection::open(path).map(BlockDb)
}
#[cfg(any(test, feature = "test-dependencies"))]
pub(crate) fn from_connection(conn: rusqlite::Connection) -> Self {
Self(conn)
}
}
impl BlockSource for BlockDb {
type Error = SqliteClientError;
fn with_blocks<F, DbErrT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<usize>,
with_row: F,
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
where
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
{
chain::blockdb_with_blocks(self, from_height, limit, with_row)
}
}
#[cfg(feature = "unstable")]
pub struct FsBlockDb {
conn: rusqlite::Connection,
blocks_dir: PathBuf,
}
#[derive(Debug)]
#[cfg(feature = "unstable")]
#[non_exhaustive]
pub enum FsBlockDbError {
Fs(io::Error),
Db(rusqlite::Error),
Protobuf(prost::DecodeError),
MissingBlockPath(PathBuf),
InvalidBlockstoreRoot(PathBuf),
InvalidBlockPath(PathBuf),
CorruptedData(String),
CacheMiss(BlockHeight),
}
#[cfg(feature = "unstable")]
impl From<io::Error> for FsBlockDbError {
fn from(err: io::Error) -> Self {
FsBlockDbError::Fs(err)
}
}
#[cfg(feature = "unstable")]
impl From<rusqlite::Error> for FsBlockDbError {
fn from(err: rusqlite::Error) -> Self {
FsBlockDbError::Db(err)
}
}
#[cfg(feature = "unstable")]
impl From<prost::DecodeError> for FsBlockDbError {
fn from(e: prost::DecodeError) -> Self {
FsBlockDbError::Protobuf(e)
}
}
#[cfg(feature = "unstable")]
impl FsBlockDb {
pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
if meta.is_dir() {
let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
let blocks_dir = fsblockdb_root.as_ref().join("blocks");
fs::create_dir_all(&blocks_dir)?;
Ok(FsBlockDb {
conn: rusqlite::Connection::open(db_path).map_err(FsBlockDbError::Db)?,
blocks_dir,
})
} else {
Err(FsBlockDbError::InvalidBlockstoreRoot(
fsblockdb_root.as_ref().to_path_buf(),
))
}
}
pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
}
pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
for m in block_meta {
let block_path = m.block_file_path(&self.blocks_dir);
match fs::metadata(&block_path) {
Err(e) => {
return Err(match e.kind() {
io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
_ => FsBlockDbError::Fs(e),
});
}
Ok(meta) => {
if !meta.is_file() {
return Err(FsBlockDbError::InvalidBlockPath(block_path));
}
}
}
}
Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
}
pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
Ok(chain::blockmetadb_find_block(&self.conn, height)?)
}
pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
Ok(chain::blockmetadb_truncate_to_height(
&self.conn,
block_height,
)?)
}
}
#[cfg(feature = "unstable")]
impl BlockSource for FsBlockDb {
type Error = FsBlockDbError;
fn with_blocks<F, DbErrT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<usize>,
with_row: F,
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
where
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
{
fsblockdb_with_blocks(self, from_height, limit, with_row)
}
}
#[cfg(feature = "unstable")]
impl std::fmt::Display for FsBlockDbError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FsBlockDbError::Fs(io_error) => {
write!(f, "Failed to access the file system: {io_error}")
}
FsBlockDbError::Db(e) => {
write!(f, "There was a problem with the sqlite db: {e}")
}
FsBlockDbError::Protobuf(e) => {
write!(f, "Failed to parse protobuf-encoded record: {e}")
}
FsBlockDbError::MissingBlockPath(block_path) => {
write!(
f,
"CompactBlock file expected but not found at {}",
block_path.display(),
)
}
FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
write!(
f,
"The block storage root {} is not a directory",
fsblockdb_root.display(),
)
}
FsBlockDbError::InvalidBlockPath(block_path) => {
write!(
f,
"CompactBlock path {} is not a file",
block_path.display(),
)
}
FsBlockDbError::CorruptedData(e) => {
write!(
f,
"The block cache has corrupted data and this caused an error: {e}",
)
}
FsBlockDbError::CacheMiss(height) => {
write!(
f,
"Requested height {height} does not exist in the block cache"
)
}
}
}
}
#[cfg(test)]
#[macro_use]
extern crate assert_matches;
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use secrecy::{ExposeSecret, Secret, SecretVec};
use uuid::Uuid;
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::error::FindAccountForAddressError;
use zcash_client_backend::data_api::{
Account, AccountBirthday, AccountPurpose, AccountSource, SAPLING_SHARD_HEIGHT,
WalletCommitmentTrees, WalletRead, WalletTest, WalletWrite,
chain::{ChainState, CommitmentTreeRoot},
testing::{TestBuilder, TestState},
};
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::{
ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
UnifiedIncomingViewingKey, UnifiedSpendingKey,
},
};
use zcash_primitives::block::BlockHash;
use zcash_protocol::{consensus, local_consensus::LocalNetwork};
use zip32::DiversifierIndex;
use crate::{
AccountUuid,
error::SqliteClientError,
testing::db::{TestDb, TestDbFactory},
util::Clock as _,
wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET,
};
use incrementalmerkletree::Hashable as _;
#[cfg(feature = "unstable")]
use {
crate::testing::FsBlockCache,
zcash_client_backend::data_api::testing::AddressType,
zcash_keys::keys::sapling,
zcash_protocol::{consensus::NetworkConstants, value::Zatoshis},
};
#[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
use {
crate::{AccountRef, wallet::transparent},
::transparent::keys::{NonHardenedChildIndex, TransparentKeyScope},
rusqlite::named_params,
};
#[cfg(feature = "transparent-inputs")]
use {
crate::{GapLimits, testing::BlockCache, wallet::transparent::transaction_data_requests},
std::collections::BTreeSet,
zcash_client_backend::data_api::TransactionDataRequest,
};
#[test]
fn get_wallet_recover_until_is_max_across_accounts() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
assert_eq!(st.wallet().get_wallet_recover_until().unwrap(), None);
st.wallet_mut()
.conn_mut()
.execute("UPDATE accounts SET recover_until_height = 123456", [])
.unwrap();
assert_eq!(
st.wallet().get_wallet_recover_until().unwrap(),
Some(zcash_protocol::consensus::BlockHeight::from_u32(123456))
);
}
#[test]
fn get_subtree_root_round_trips_put_subtree_roots() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let root = ::sapling::Node::empty_root(SAPLING_SHARD_HEIGHT.into());
st.wallet_mut()
.db_mut()
.put_sapling_subtree_roots(
0,
&[CommitmentTreeRoot::from_parts(
zcash_protocol::consensus::BlockHeight::from_u32(500_000),
root,
)],
)
.unwrap();
assert_eq!(
st.wallet_mut()
.db_mut()
.get_sapling_subtree_root(0)
.unwrap(),
Some(root)
);
assert_eq!(
st.wallet_mut()
.db_mut()
.get_sapling_subtree_root(1)
.unwrap(),
None
);
}
fn ext_test_state() -> TestState<(), TestDb, LocalNetwork> {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
st.wallet_mut()
.conn_mut()
.execute_batch(
"CREATE TABLE ext_test_notes (account_uuid BLOB NOT NULL, note TEXT NOT NULL);",
)
.unwrap();
st
}
fn account_creation_inputs(
st: &TestState<(), TestDb, LocalNetwork>,
) -> (SecretVec<u8>, AccountBirthday) {
let birthday = st.test_account().unwrap().birthday().clone();
let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().to_vec());
(seed, birthday)
}
#[test]
fn transactionally_with_extension_commits_both() {
let mut st = ext_test_state();
let (seed, birthday) = account_creation_inputs(&st);
let new_account = st
.wallet_mut()
.db_mut()
.transactionally_with_extension::<_, _, SqliteClientError>(|wdb, ext| {
let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
ext.execute(
"INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
(account_id.expose_uuid(), "hello"),
)?;
Ok(account_id)
})
.unwrap();
let account_exists: bool = st
.wallet()
.conn()
.query_row(
"SELECT EXISTS(SELECT 1 FROM accounts WHERE uuid = ?1)",
[new_account.expose_uuid()],
|row| row.get(0),
)
.unwrap();
assert!(account_exists);
let note: String = st
.wallet()
.conn()
.query_row(
"SELECT note FROM ext_test_notes WHERE account_uuid = ?1",
[new_account.expose_uuid()],
|row| row.get(0),
)
.unwrap();
assert_eq!(note, "hello");
}
#[test]
fn transactionally_with_extension_rolls_back_on_error() {
let mut st = ext_test_state();
let (seed, birthday) = account_creation_inputs(&st);
let accounts_before: i64 = st
.wallet()
.conn()
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
.unwrap();
let result: Result<(), SqliteClientError> = st
.wallet_mut()
.db_mut()
.transactionally_with_extension(|wdb, ext| {
let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
ext.execute(
"INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
(account_id.expose_uuid(), "hello"),
)?;
Err(SqliteClientError::AccountUnknown)
});
assert_matches!(result, Err(SqliteClientError::AccountUnknown));
let accounts_after: i64 = st
.wallet()
.conn()
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
.unwrap();
assert_eq!(accounts_before, accounts_after);
let ext_rows: i64 = st
.wallet()
.conn()
.query_row("SELECT COUNT(*) FROM ext_test_notes", [], |row| row.get(0))
.unwrap();
assert_eq!(ext_rows, 0);
}
#[test]
fn transactionally_with_extension_denies_wallet_table_write() {
let mut st = ext_test_state();
let accounts_before: i64 = st
.wallet()
.conn()
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
.unwrap();
let result: Result<(), SqliteClientError> = st
.wallet_mut()
.db_mut()
.transactionally_with_extension(|_wdb, ext| {
ext.execute("DELETE FROM accounts", [])?;
Ok(())
});
assert!(result.is_err());
let accounts_after: i64 = st
.wallet()
.conn()
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
.unwrap();
assert_eq!(accounts_before, accounts_after);
assert!(!st.wallet().get_account_ids().unwrap().is_empty());
}
#[test]
fn transactionally_with_extension_denies_transaction_control() {
let mut st = ext_test_state();
let result: Result<(), SqliteClientError> = st
.wallet_mut()
.db_mut()
.transactionally_with_extension(|_wdb, ext| {
ext.execute("COMMIT", [])?;
Ok(())
});
assert!(result.is_err());
assert!(!st.wallet().get_account_ids().unwrap().is_empty());
}
#[test]
fn validate_seed() {
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().unwrap();
assert!({
st.wallet()
.validate_seed(account.id(), st.test_seed().unwrap())
.unwrap()
});
assert!({
let wrong_account_uuid = AccountUuid(Uuid::nil());
!st.wallet()
.validate_seed(wrong_account_uuid, st.test_seed().unwrap())
.unwrap()
});
assert!({
!st.wallet()
.validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
.unwrap()
});
}
#[test]
pub(crate) fn get_next_available_address() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
st.wallet_mut()
.update_chain_tip(account.birthday().height())
.unwrap();
let current_addr = st
.wallet()
.get_last_generated_address_matching(
account.id(),
UnifiedAddressRequest::AllAvailableKeys,
)
.unwrap();
assert!(current_addr.is_some());
let addr2 = st
.wallet_mut()
.get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.map(|(a, _)| a);
assert!(addr2.is_some());
assert_ne!(current_addr, addr2);
let addr2_cur = st
.wallet()
.get_last_generated_address_matching(
account.id(),
UnifiedAddressRequest::AllAvailableKeys,
)
.unwrap();
assert_eq!(addr2, addr2_cur);
#[cfg(feature = "orchard")]
let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit);
#[cfg(not(feature = "orchard"))]
let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit);
let cur_shielded_only = st
.wallet()
.get_last_generated_address_matching(account.id(), shielded_only_request)
.unwrap();
#[cfg(not(feature = "transparent-inputs"))]
assert_eq!(cur_shielded_only, addr2);
#[cfg(feature = "transparent-inputs")]
assert!(cur_shielded_only.is_none());
let di_lower = st
.wallet()
.db()
.clock
.now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("current time is valid")
.as_secs()
.saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET);
let (shielded_only, di) = st
.wallet_mut()
.get_next_available_address(account.id(), shielded_only_request)
.unwrap()
.expect("generated a shielded-only address");
assert!(u128::from(di) >= u128::from(di_lower));
let cur_shielded_only = st
.wallet()
.get_last_generated_address_matching(account.id(), shielded_only_request)
.unwrap()
.expect("retrieved the last-generated shielded-only address");
assert_eq!(cur_shielded_only, shielded_only);
let collision_offset = 32;
st.wallet_mut()
.db_mut()
.clock
.tick(Duration::from_secs(collision_offset));
let (shielded_only_2, di_2) = st
.wallet_mut()
.get_next_available_address(account.id(), shielded_only_request)
.unwrap()
.expect("generated a shielded-only address");
assert_ne!(shielded_only_2, shielded_only);
assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset));
}
#[test]
pub(crate) fn import_account_hd_0() {
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.set_account_index(zip32::AccountId::ZERO)
.build();
assert_matches!(
st.test_account().unwrap().account().source(),
AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO);
}
#[test]
pub(crate) fn import_account_hd_1_then_2() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = Secret::new(vec![0u8; 32]);
let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
let first = st
.wallet_mut()
.import_account_hd("", &seed, zip32_index_1, &birthday, None)
.unwrap();
assert_matches!(
first.0.source(),
AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1);
let zip32_index_2 = zip32_index_1.next().unwrap();
let second = st
.wallet_mut()
.import_account_hd("", &seed, zip32_index_2, &birthday, None)
.unwrap();
assert_matches!(
second.0.source(),
AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2);
}
fn check_collisions<C, DbT: WalletTest + WalletWrite, P: consensus::Parameters>(
st: &mut TestState<C, DbT, P>,
ufvk: &UnifiedFullViewingKey,
birthday: &AccountBirthday,
is_account_collision: impl Fn(&<DbT as WalletRead>::Error) -> bool,
) where
DbT::Account: core::fmt::Debug,
{
assert_matches!(
st.wallet_mut()
.import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None),
Err(e) if is_account_collision(&e)
);
#[cfg(feature = "transparent-inputs")]
{
assert!(ufvk.transparent().is_some());
let subset_ufvk = UnifiedFullViewingKey::new(
None,
ufvk.sapling().cloned(),
#[cfg(feature = "orchard")]
ufvk.orchard().cloned(),
)
.unwrap();
assert_matches!(
st.wallet_mut().import_account_ufvk(
"",
&subset_ufvk,
birthday,
AccountPurpose::Spending { derivation: None },
None,
),
Err(e) if is_account_collision(&e)
);
}
#[cfg(feature = "orchard")]
{
assert!(ufvk.orchard().is_some());
let subset_ufvk = UnifiedFullViewingKey::new(
#[cfg(feature = "transparent-inputs")]
ufvk.transparent().cloned(),
ufvk.sapling().cloned(),
None,
)
.unwrap();
assert_matches!(
st.wallet_mut().import_account_ufvk(
"",
&subset_ufvk,
birthday,
AccountPurpose::Spending { derivation: None },
None,
),
Err(e) if is_account_collision(&e)
);
}
}
#[test]
pub(crate) fn import_account_hd_1_then_conflicts() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = Secret::new(vec![0u8; 32]);
let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
let (first_account, _) = st
.wallet_mut()
.import_account_hd("", &seed, zip32_index_1, &birthday, None)
.unwrap();
let ufvk = first_account.ufvk().unwrap();
assert_matches!(
st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None),
Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
check_collisions(
&mut st,
ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
);
}
#[test]
pub(crate) fn import_account_ufvk_then_conflicts() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = Secret::new(vec![0u8; 32]);
let zip32_index_0 = zip32::AccountId::ZERO;
let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
.unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let account = st
.wallet_mut()
.import_account_ufvk(
"",
&ufvk,
&birthday,
AccountPurpose::Spending { derivation: None },
None,
)
.unwrap();
assert_eq!(
ufvk.encode(st.network()),
account.ufvk().unwrap().encode(st.network())
);
assert_matches!(
account.source(),
AccountSource::Imported {
purpose: AccountPurpose::Spending { .. },
..
}
);
assert_matches!(
st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
Err(SqliteClientError::AccountCollision(id)) if id == account.id());
check_collisions(
&mut st,
&ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
);
}
#[test]
pub(crate) fn create_account_then_conflicts() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = Secret::new(vec![0u8; 32]);
let zip32_index_0 = zip32::AccountId::ZERO;
let seed_based = st
.wallet_mut()
.create_account("", &seed, &birthday, None)
.unwrap();
let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap();
let ufvk = seed_based_account.ufvk().unwrap();
assert_matches!(
st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
check_collisions(
&mut st,
ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
);
}
#[test]
pub(crate) fn ivk_only_account_upgrade_paths() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = vec![0u8; 32];
let usk =
UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let full_uivk = ufvk.to_unified_incoming_viewing_key();
let sapling_only_uivk = UnifiedIncomingViewingKey::new(
#[cfg(feature = "transparent-inputs")]
None,
full_uivk.sapling().clone(),
#[cfg(feature = "orchard")]
None,
);
let network = *st.network();
let ivk_account = st
.wallet_mut()
.db_mut()
.transactionally(|wdb| {
crate::wallet::add_account(
wdb.conn.0,
&wdb.params,
"ivk-only",
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
},
crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
&birthday,
#[cfg(feature = "transparent-inputs")]
&crate::GapLimits::default(),
)
})
.unwrap();
assert_matches!(
st.wallet_mut().db_mut().transactionally(|wdb| {
crate::wallet::add_account(
wdb.conn.0,
&wdb.params,
"duplicate",
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
},
crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
&birthday,
#[cfg(feature = "transparent-inputs")]
&crate::GapLimits::default(),
)
}),
Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
);
let ufvk_upgraded = st
.wallet_mut()
.import_account_ufvk(
"",
&ufvk,
&birthday,
AccountPurpose::Spending { derivation: None },
None,
)
.unwrap();
assert_eq!(ufvk_upgraded.id(), ivk_account.id());
assert!(ufvk_upgraded.ufvk().is_some());
assert_eq!(
ufvk_upgraded.ufvk().unwrap().encode(&network),
ufvk.encode(&network),
);
assert_matches!(
st.wallet_mut().db_mut().transactionally(|wdb| {
crate::wallet::add_account(
wdb.conn.0,
&wdb.params,
"downgrade",
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
},
crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
&birthday,
#[cfg(feature = "transparent-inputs")]
&crate::GapLimits::default(),
)
}),
Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
);
}
#[cfg(feature = "orchard")]
#[test]
pub(crate) fn ivk_over_ivk_additive_upgrade() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = vec![0u8; 32];
let usk =
UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let full_uivk = ufvk.to_unified_incoming_viewing_key();
let network = *st.network();
let sapling_only_uivk = UnifiedIncomingViewingKey::new(
#[cfg(feature = "transparent-inputs")]
None,
full_uivk.sapling().clone(),
None, );
let ivk_account = st
.wallet_mut()
.db_mut()
.transactionally(|wdb| {
crate::wallet::add_account(
wdb.conn.0,
&wdb.params,
"sapling-only",
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
},
crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk)),
&birthday,
#[cfg(feature = "transparent-inputs")]
&crate::GapLimits::default(),
)
})
.unwrap();
let upgraded = st
.wallet_mut()
.db_mut()
.transactionally(|wdb| {
crate::wallet::add_account(
wdb.conn.0,
&wdb.params,
"upgraded",
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
},
crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
&birthday,
#[cfg(feature = "transparent-inputs")]
&crate::GapLimits::default(),
)
})
.unwrap();
assert_eq!(upgraded.id(), ivk_account.id());
assert!(upgraded.ufvk().is_none());
assert!(upgraded.uivk().encode(&network) != ivk_account.uivk().encode(&network));
}
#[cfg(feature = "transparent-inputs")]
#[test]
fn transparent_receivers() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().unwrap();
let ufvk = account.usk().to_unified_full_viewing_key();
let (taddr, _) = account.usk().default_transparent_address();
let birthday = account.birthday().height();
let account_id = account.id();
let receivers = st
.wallet()
.get_transparent_receivers(account.id(), false, true)
.unwrap();
assert!(
receivers.contains_key(
ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("A valid default address exists for the UFVK")
.0
.transparent()
.unwrap()
)
);
assert!(receivers.contains_key(&taddr));
st.wallet_mut().update_chain_tip(birthday).unwrap();
let ephemeral_addrs = st
.wallet()
.get_known_ephemeral_addresses(account_id, None)
.unwrap();
assert_eq!(
ephemeral_addrs.len(),
GapLimits::default().ephemeral() as usize
);
st.wallet_mut()
.db_mut()
.schedule_ephemeral_address_checks()
.unwrap();
let data_requests =
transaction_data_requests(st.wallet().conn(), &st.wallet().db().params, birthday)
.unwrap();
let base_time = st.wallet().db().clock.now();
let day = Duration::from_secs(60 * 60 * 24);
let mut check_times = BTreeSet::new();
for (addr, _) in ephemeral_addrs {
let has_valid_request = data_requests.iter().any(|req| match req {
TransactionDataRequest::TransactionsInvolvingAddress(req) => {
if let Some(t) = req.request_at() {
req.address() == addr && t > base_time && {
let t_delta = t.duration_since(base_time).unwrap();
let result = t_delta < 2 * day && !check_times.contains(&t);
check_times.insert(t);
result
}
} else {
false
}
}
_ => false,
});
assert!(has_valid_request);
}
}
#[cfg(feature = "unstable")]
#[test]
pub(crate) fn fsblockdb_api() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_block_cache(FsBlockCache::new())
.build();
assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
let seed = [0u8; 32];
let hd_account_index = zip32::AccountId::ZERO;
let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let (h1, meta1, _) = st.generate_next_block(
&dfvk,
AddressType::DefaultExternal,
Zatoshis::const_from_u64(5),
);
let (h2, meta2, _) = st.generate_next_block(
&dfvk,
AddressType::DefaultExternal,
Zatoshis::const_from_u64(10),
);
assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
st.cache()
.write_block_metadata(&[meta1.block_meta, meta2.block_meta])
.unwrap();
assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2.block_meta));
assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
st.cache().truncate_to_height(h1).unwrap();
assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
assert_eq!(st.cache().find_block(h2).unwrap(), None);
assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
}
#[test]
fn find_account_for_address_returns_matching_account_for_own_ua() {
let mut state = create_test_wallet_with_one_account();
let account = state.test_account().cloned().unwrap();
state
.wallet_mut()
.update_chain_tip(account.birthday().height())
.unwrap();
let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
let result = state
.wallet()
.find_account_for_address(state.network(), &Address::Unified(ua));
assert_eq!(result.unwrap(), Some(account.id()));
}
#[test]
fn find_account_for_address_returns_none_for_unknown_address() {
let st = create_test_wallet_with_one_account();
let unknown_address = Address::Transparent(
::transparent::address::TransparentAddress::PublicKeyHash([0u8; 20]),
);
assert_eq!(
st.wallet()
.find_account_for_address(st.network(), &unknown_address)
.unwrap(),
None
);
}
#[test]
fn find_account_for_address_returns_matching_account_for_receivers_of_own_ua() {
let mut state = create_test_wallet_with_one_account();
let account = state.test_account().cloned().unwrap();
state
.wallet_mut()
.update_chain_tip(account.birthday().height())
.unwrap();
let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
if let Some(taddr) = ua.transparent() {
let result = state
.wallet()
.find_account_for_address(state.network(), &Address::Transparent(*taddr));
assert_eq!(result.unwrap(), Some(account.id()));
}
if let Some(pa) = ua.sapling() {
let result = state
.wallet()
.find_account_for_address(state.network(), &Address::Sapling(*pa));
assert_eq!(result.unwrap(), Some(account.id()));
}
}
#[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
#[test]
fn find_account_for_ua_finds_via_transparent_receiver_cache() {
let mut state = create_test_wallet_with_one_account();
let account = state.test_account().cloned().unwrap();
let acc1_id = account.id();
let account_rowid = delete_account_addresses(&mut state, acc1_id);
let transparent_address =
UnifiedSpendingKey::from_seed(&state.network(), &[7u8; 32], zip32::AccountId::ZERO)
.expect("valid seed")
.to_unified_full_viewing_key()
.default_address(UnifiedAddressRequest::unsafe_custom(Omit, Require, Require))
.unwrap()
.0
.transparent()
.cloned()
.expect("UA must have transparent receiver");
state
.wallet_mut()
.update_chain_tip(account.birthday().height())
.unwrap();
state
.wallet_mut()
.db_mut()
.transactionally(|wdb| {
transparent::store_address_range(
wdb.conn.0,
wdb.params(),
AccountRef(account_rowid),
TransparentKeyScope::EXTERNAL,
vec![(
Address::Transparent(transparent_address),
transparent_address,
NonHardenedChildIndex::ZERO,
)],
)?;
transparent::reserve_next_n_addresses(
wdb.conn.0,
wdb.params(),
AccountRef(account_rowid),
TransparentKeyScope::EXTERNAL,
20,
1,
)?;
Ok::<_, SqliteClientError>(())
})
.unwrap();
let usk_external =
UnifiedSpendingKey::from_seed(&state.network(), &[99u8; 32], zip32::AccountId::ZERO)
.expect("valid seed");
let o_external = usk_external
.to_unified_full_viewing_key()
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable")
.0
.orchard()
.cloned()
.expect("orchard receiver must be present");
let address = Address::Unified(
UnifiedAddress::from_receivers(Some(o_external), None, Some(transparent_address))
.expect("orchard+transparent UA must be valid"),
);
let result = state
.wallet()
.find_account_for_address(state.network(), &address);
assert_eq!(result.unwrap(), Some(acc1_id));
}
#[test]
fn find_account_for_ua_finds_via_sapling() {
let mut state = create_test_wallet_with_one_account();
let birthday = AccountBirthday::from_parts(
ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let sapling_activation = state.network().sapling.unwrap();
let (acc1_id, _) = state
.wallet_mut()
.create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
.unwrap();
state
.wallet_mut()
.update_chain_tip(sapling_activation)
.unwrap();
let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
let sapling_receiver = ua1
.sapling()
.cloned()
.expect("UA must have sapling receiver");
let address = Address::Unified(
{
#[cfg(feature = "orchard")]
{
UnifiedAddress::from_receivers(None, Some(sapling_receiver), None)
}
#[cfg(not(feature = "orchard"))]
{
UnifiedAddress::from_receivers(Some(sapling_receiver), None)
}
}
.expect("sapling-only UA must be valid"),
);
let result = state
.wallet()
.find_account_for_address(state.network(), &address);
assert_eq!(result.unwrap(), Some(acc1_id));
}
#[cfg(feature = "orchard")]
#[test]
fn find_account_for_ua_finds_via_orchard() {
let mut state = create_test_wallet_with_one_account();
let birthday = AccountBirthday::from_parts(
ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let sapling_activation = state.network().sapling.unwrap();
let (acc1_id, _) = state
.wallet_mut()
.create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
.unwrap();
state
.wallet_mut()
.update_chain_tip(sapling_activation)
.unwrap();
let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
let orchard_receiver = ua1
.orchard()
.cloned()
.expect("UA must have orchard receiver");
let address = Address::Unified(
UnifiedAddress::from_receivers(Some(orchard_receiver), None, None)
.expect("orchard-only UA must be valid"),
);
let result = state
.wallet()
.find_account_for_address(state.network(), &address);
assert_eq!(result.unwrap(), Some(acc1_id));
}
#[cfg(feature = "orchard")]
#[test]
fn find_account_for_ua_errors_when_receivers_map_to_different_accounts() {
let mut state = create_test_wallet_with_one_account();
let birthday = AccountBirthday::from_parts(
ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let sapling_activation = state.network().sapling.unwrap();
let seed1 = Secret::new(vec![0u8; 32]);
let seed2 = Secret::new(vec![1u8; 32]);
let (acc1_id, _) = state
.wallet_mut()
.create_account("", &seed1, &birthday, None)
.unwrap();
let (acc2_id, _) = state
.wallet_mut()
.create_account("", &seed2, &birthday, None)
.unwrap();
state
.wallet_mut()
.update_chain_tip(sapling_activation)
.unwrap();
let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
let (ua2, _) = generate_unified_address_with_all_available_keys(&mut state, acc2_id);
let sapling_receiver_1 = ua1.sapling().cloned().unwrap();
let orchard_receiver_2 = ua2.orchard().cloned().unwrap();
let invalid_address = Address::Unified(
UnifiedAddress::from_receivers(
Some(orchard_receiver_2),
Some(sapling_receiver_1),
None,
)
.expect("sapling+orchard UA must be valid"),
);
let result = state
.wallet()
.find_account_for_address(state.network(), &invalid_address);
assert!(matches!(
result,
Err(FindAccountForAddressError::UnifiedAddressConflict)
));
}
fn create_test_wallet_with_one_account() -> TestState<(), TestDb, LocalNetwork> {
TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build()
}
fn generate_unified_address_with_all_available_keys(
state: &mut TestState<(), TestDb, LocalNetwork>,
account_id: AccountUuid,
) -> (UnifiedAddress, DiversifierIndex) {
state
.wallet_mut()
.get_next_available_address(account_id, UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.expect("address generation for account 1 must succeed")
}
#[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
fn delete_account_addresses(
state: &mut TestState<(), TestDb, LocalNetwork>,
account_id: AccountUuid,
) -> i64 {
let account_rowid: i64 = state
.wallet()
.conn()
.query_row(
"SELECT id FROM accounts WHERE uuid = :uuid",
named_params![":uuid": account_id.expose_uuid()],
|row| row.get(0),
)
.unwrap();
state
.wallet()
.conn()
.execute(
"DELETE FROM addresses WHERE account_id = :account_id",
named_params![":account_id": account_rowid],
)
.unwrap();
account_rowid
}
}