1278 lines
50 KiB
Rust
1278 lines
50 KiB
Rust
//! Functions for initializing the various databases.
|
|
use either::Either;
|
|
use incrementalmerkletree::Retention;
|
|
use std::{collections::HashMap, fmt, io};
|
|
use tracing::debug;
|
|
|
|
use rusqlite::{self, types::ToSql};
|
|
use schemer::{Migrator, MigratorError};
|
|
use schemer_rusqlite::RusqliteAdapter;
|
|
use secrecy::SecretVec;
|
|
use shardtree::{ShardTree, ShardTreeError};
|
|
use uuid::Uuid;
|
|
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
consensus::{self, BlockHeight},
|
|
merkle_tree::read_commitment_tree,
|
|
sapling,
|
|
transaction::components::amount::BalanceError,
|
|
zip32::AccountId,
|
|
};
|
|
|
|
use zcash_client_backend::{data_api::SAPLING_SHARD_HEIGHT, keys::UnifiedFullViewingKey};
|
|
|
|
use crate::{error::SqliteClientError, wallet, WalletDb, PRUNING_DEPTH, SAPLING_TABLES_PREFIX};
|
|
|
|
use super::commitment_tree::SqliteShardStore;
|
|
|
|
mod migrations;
|
|
|
|
#[derive(Debug)]
|
|
pub enum WalletMigrationError {
|
|
/// The seed is required for the migration.
|
|
SeedRequired,
|
|
|
|
/// Decoding of an existing value from its serialized form has failed.
|
|
CorruptedData(String),
|
|
|
|
/// Wrapper for rusqlite errors.
|
|
DbError(rusqlite::Error),
|
|
|
|
/// Wrapper for amount balance violations
|
|
BalanceError(BalanceError),
|
|
|
|
/// Wrapper for commitment tree invariant violations
|
|
CommitmentTree(ShardTreeError<Either<io::Error, rusqlite::Error>>),
|
|
}
|
|
|
|
impl From<rusqlite::Error> for WalletMigrationError {
|
|
fn from(e: rusqlite::Error) -> Self {
|
|
WalletMigrationError::DbError(e)
|
|
}
|
|
}
|
|
|
|
impl From<BalanceError> for WalletMigrationError {
|
|
fn from(e: BalanceError) -> Self {
|
|
WalletMigrationError::BalanceError(e)
|
|
}
|
|
}
|
|
|
|
impl From<ShardTreeError<Either<io::Error, rusqlite::Error>>> for WalletMigrationError {
|
|
fn from(e: ShardTreeError<Either<io::Error, rusqlite::Error>>) -> Self {
|
|
WalletMigrationError::CommitmentTree(e)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for WalletMigrationError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match &self {
|
|
WalletMigrationError::SeedRequired => {
|
|
write!(
|
|
f,
|
|
"The wallet seed is required in order to update the database."
|
|
)
|
|
}
|
|
WalletMigrationError::CorruptedData(reason) => {
|
|
write!(f, "Wallet database is corrupted: {}", reason)
|
|
}
|
|
WalletMigrationError::DbError(e) => write!(f, "{}", e),
|
|
WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e),
|
|
WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {:?}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for WalletMigrationError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match &self {
|
|
WalletMigrationError::DbError(e) => Some(e),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets up the internal structure of the data database.
|
|
///
|
|
/// This procedure will automatically perform migration operations to update the wallet database to
|
|
/// the database structure required by the current version of this library, and should be invoked
|
|
/// at least once any time a client program upgrades to a new version of this library. The
|
|
/// operation of this procedure is idempotent, so it is safe (though not required) to invoke this
|
|
/// operation every time the wallet is opened.
|
|
///
|
|
/// It is safe to use a wallet database previously created without the ability to create
|
|
/// transparent spends with a build that enables transparent spends (via use of the
|
|
/// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance calculations would
|
|
/// ignore the transparent UTXOs already controlled by the wallet.
|
|
///
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use secrecy::Secret;
|
|
/// use tempfile::NamedTempFile;
|
|
/// use zcash_primitives::consensus::Network;
|
|
/// use zcash_client_sqlite::{
|
|
/// WalletDb,
|
|
/// wallet::init::init_wallet_db,
|
|
/// };
|
|
///
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
|
/// init_wallet_db(&mut db, Some(Secret::new(vec![]))).unwrap();
|
|
/// ```
|
|
// TODO: It would be possible to make the transition from providing transparent support to no
|
|
// longer providing transparent support safe, by including a migration that verifies that no
|
|
// unspent transparent outputs exist in the wallet at the time of upgrading to a version of
|
|
// the library that does not support transparent use. It might be a good idea to add an explicit
|
|
// check for unspent transparent outputs whenever running initialization with a version of the
|
|
// library *not* compiled with the `transparent-inputs` feature flag, and fail if any are present.
|
|
pub fn init_wallet_db<P: consensus::Parameters + 'static>(
|
|
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
|
seed: Option<SecretVec<u8>>,
|
|
) -> Result<(), MigratorError<WalletMigrationError>> {
|
|
init_wallet_db_internal(wdb, seed, &[])
|
|
}
|
|
|
|
fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
|
|
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
|
seed: Option<SecretVec<u8>>,
|
|
target_migrations: &[Uuid],
|
|
) -> Result<(), MigratorError<WalletMigrationError>> {
|
|
// Turn off foreign keys, and ensure that table replacement/modification
|
|
// does not break views
|
|
wdb.conn
|
|
.execute_batch(
|
|
"PRAGMA foreign_keys = OFF;
|
|
PRAGMA legacy_alter_table = TRUE;",
|
|
)
|
|
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
|
|
let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string()));
|
|
adapter.init().expect("Migrations table setup succeeds.");
|
|
|
|
let mut migrator = Migrator::new(adapter);
|
|
migrator
|
|
.register_multiple(migrations::all_migrations(&wdb.params, seed))
|
|
.expect("Wallet migration registration should have been successful.");
|
|
if target_migrations.is_empty() {
|
|
migrator.up(None)?;
|
|
} else {
|
|
for target_migration in target_migrations {
|
|
migrator.up(Some(*target_migration))?;
|
|
}
|
|
}
|
|
wdb.conn
|
|
.execute("PRAGMA foreign_keys = ON", [])
|
|
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Initialises the data database with the given set of account [`UnifiedFullViewingKey`]s.
|
|
///
|
|
/// **WARNING** This method should be used with care, and should ordinarily be unnecessary.
|
|
/// Prefer to use [`WalletWrite::create_account`] instead.
|
|
///
|
|
/// [`WalletWrite::create_account`]: zcash_client_backend::data_api::WalletWrite::create_account
|
|
///
|
|
/// The [`UnifiedFullViewingKey`]s are stored internally and used by other APIs such as
|
|
/// [`scan_cached_blocks`], and [`create_spend_to_address`]. Account identifiers in `keys` **MUST**
|
|
/// form a consecutive sequence beginning at account 0, and the [`UnifiedFullViewingKey`]
|
|
/// corresponding to a given account identifier **MUST** be derived from the wallet's mnemonic seed
|
|
/// at the BIP-44 `account` path level as described by [ZIP
|
|
/// 316](https://zips.z.cash/zip-0316)
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # #[cfg(feature = "transparent-inputs")]
|
|
/// # {
|
|
/// use tempfile::NamedTempFile;
|
|
/// use secrecy::Secret;
|
|
/// use std::collections::HashMap;
|
|
///
|
|
/// use zcash_primitives::{
|
|
/// consensus::{Network, Parameters},
|
|
/// zip32::{AccountId, ExtendedSpendingKey}
|
|
/// };
|
|
///
|
|
/// use zcash_client_backend::{
|
|
/// keys::{
|
|
/// sapling,
|
|
/// UnifiedFullViewingKey
|
|
/// },
|
|
/// };
|
|
///
|
|
/// use zcash_client_sqlite::{
|
|
/// WalletDb,
|
|
/// wallet::init::{init_accounts_table, init_wallet_db}
|
|
/// };
|
|
///
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
/// let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
|
/// init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
|
///
|
|
/// let seed = [0u8; 32]; // insecure; replace with a strong random seed
|
|
/// let account = AccountId::from(0);
|
|
/// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account);
|
|
/// let dfvk = extsk.to_diversifiable_full_viewing_key();
|
|
/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
|
|
/// let ufvks = HashMap::from([(account, ufvk)]);
|
|
/// init_accounts_table(&mut db_data, &ufvks).unwrap();
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// [`get_address`]: crate::wallet::get_address
|
|
/// [`scan_cached_blocks`]: zcash_client_backend::data_api::chain::scan_cached_blocks
|
|
/// [`create_spend_to_address`]: zcash_client_backend::data_api::wallet::create_spend_to_address
|
|
pub fn init_accounts_table<P: consensus::Parameters>(
|
|
wallet_db: &mut WalletDb<rusqlite::Connection, P>,
|
|
keys: &HashMap<AccountId, UnifiedFullViewingKey>,
|
|
) -> Result<(), SqliteClientError> {
|
|
wallet_db.transactionally(|wdb| {
|
|
let mut empty_check = wdb.conn.0.prepare("SELECT * FROM accounts LIMIT 1")?;
|
|
if empty_check.exists([])? {
|
|
return Err(SqliteClientError::TableNotEmpty);
|
|
}
|
|
|
|
// Ensure that the account identifiers are sequential and begin at zero.
|
|
if let Some(account_id) = keys.keys().max() {
|
|
if usize::try_from(u32::from(*account_id)).unwrap() >= keys.len() {
|
|
return Err(SqliteClientError::AccountIdDiscontinuity);
|
|
}
|
|
}
|
|
|
|
// Insert accounts atomically
|
|
for (account, key) in keys.iter() {
|
|
wallet::add_account(wdb.conn.0, &wdb.params, *account, key)?;
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
/// Initialises the data database with the given block.
|
|
///
|
|
/// This enables a newly-created database to be immediately-usable, without needing to
|
|
/// synchronise historic blocks.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use tempfile::NamedTempFile;
|
|
/// use zcash_primitives::{
|
|
/// block::BlockHash,
|
|
/// consensus::{BlockHeight, Network},
|
|
/// };
|
|
/// use zcash_client_sqlite::{
|
|
/// WalletDb,
|
|
/// wallet::init::init_blocks_table,
|
|
/// };
|
|
///
|
|
/// // The block height.
|
|
/// let height = BlockHeight::from_u32(500_000);
|
|
/// // The hash of the block header.
|
|
/// let hash = BlockHash([0; 32]);
|
|
/// // The nTime field from the block header.
|
|
/// let time = 12_3456_7890;
|
|
/// // The serialized Sapling commitment tree as of this block.
|
|
/// // Pre-compute and hard-code, or obtain from a service.
|
|
/// let sapling_tree = &[];
|
|
///
|
|
/// let data_file = NamedTempFile::new().unwrap();
|
|
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
|
/// init_blocks_table(&mut db, height, hash, time, sapling_tree);
|
|
/// ```
|
|
pub fn init_blocks_table<P: consensus::Parameters>(
|
|
wallet_db: &mut WalletDb<rusqlite::Connection, P>,
|
|
height: BlockHeight,
|
|
hash: BlockHash,
|
|
time: u32,
|
|
sapling_tree: &[u8],
|
|
) -> Result<(), SqliteClientError> {
|
|
wallet_db.transactionally(|wdb| {
|
|
let mut empty_check = wdb.conn.0.prepare("SELECT * FROM blocks LIMIT 1")?;
|
|
if empty_check.exists([])? {
|
|
return Err(SqliteClientError::TableNotEmpty);
|
|
}
|
|
|
|
let block_end_tree =
|
|
read_commitment_tree::<sapling::Node, _, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>(
|
|
sapling_tree,
|
|
)
|
|
.map_err(|e| {
|
|
rusqlite::Error::FromSqlConversionFailure(
|
|
sapling_tree.len(),
|
|
rusqlite::types::Type::Blob,
|
|
Box::new(e),
|
|
)
|
|
})?;
|
|
|
|
wdb.conn.0.execute(
|
|
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
|
VALUES (?, ?, ?, ?)",
|
|
[
|
|
u32::from(height).to_sql()?,
|
|
hash.0.to_sql()?,
|
|
time.to_sql()?,
|
|
sapling_tree.to_sql()?,
|
|
],
|
|
)?;
|
|
|
|
if let Some(nonempty_frontier) = block_end_tree.to_frontier().value() {
|
|
debug!("Inserting frontier into ShardTree: {:?}", nonempty_frontier);
|
|
let shard_store =
|
|
SqliteShardStore::<_, sapling::Node, SAPLING_SHARD_HEIGHT>::from_connection(
|
|
wdb.conn.0,
|
|
SAPLING_TABLES_PREFIX,
|
|
)?;
|
|
let mut shard_tree: ShardTree<
|
|
_,
|
|
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
|
|
SAPLING_SHARD_HEIGHT,
|
|
> = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap());
|
|
shard_tree.insert_frontier_nodes(
|
|
nonempty_frontier.clone(),
|
|
Retention::Checkpoint {
|
|
id: height,
|
|
is_marked: false,
|
|
},
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(deprecated)]
|
|
mod tests {
|
|
use rusqlite::{self, ToSql};
|
|
use secrecy::Secret;
|
|
use std::collections::HashMap;
|
|
use tempfile::NamedTempFile;
|
|
|
|
use zcash_client_backend::{
|
|
address::RecipientAddress,
|
|
data_api::WalletRead,
|
|
encoding::{encode_extended_full_viewing_key, encode_payment_address},
|
|
keys::{sapling, UnifiedFullViewingKey, UnifiedSpendingKey},
|
|
};
|
|
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
consensus::{BlockHeight, BranchId, Parameters},
|
|
transaction::{TransactionData, TxVersion},
|
|
zip32::sapling::ExtendedFullViewingKey,
|
|
};
|
|
|
|
use crate::{
|
|
error::SqliteClientError,
|
|
tests::{self, network},
|
|
AccountId, WalletDb,
|
|
};
|
|
|
|
use super::{init_accounts_table, init_blocks_table, init_wallet_db};
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
use {
|
|
crate::{
|
|
wallet::{self, pool_code, PoolType},
|
|
WalletWrite,
|
|
},
|
|
zcash_address::test_vectors,
|
|
zcash_primitives::{
|
|
consensus::Network, legacy::keys as transparent, zip32::DiversifierIndex,
|
|
},
|
|
};
|
|
|
|
#[test]
|
|
fn verify_schema() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&mut db_data, None).unwrap();
|
|
|
|
use regex::Regex;
|
|
let re = Regex::new(r"\s+").unwrap();
|
|
|
|
let expected_tables = vec![
|
|
"CREATE TABLE \"accounts\" (
|
|
account INTEGER PRIMARY KEY,
|
|
ufvk TEXT NOT NULL
|
|
)",
|
|
"CREATE TABLE addresses (
|
|
account INTEGER NOT NULL,
|
|
diversifier_index_be BLOB NOT NULL,
|
|
address TEXT NOT NULL,
|
|
cached_transparent_receiver_address TEXT,
|
|
FOREIGN KEY (account) REFERENCES accounts(account),
|
|
CONSTRAINT diversification UNIQUE (account, diversifier_index_be)
|
|
)",
|
|
"CREATE TABLE blocks (
|
|
height INTEGER PRIMARY KEY,
|
|
hash BLOB NOT NULL,
|
|
time INTEGER NOT NULL,
|
|
sapling_tree BLOB NOT NULL ,
|
|
sapling_commitment_tree_size INTEGER,
|
|
orchard_commitment_tree_size INTEGER)",
|
|
"CREATE TABLE sapling_received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
account INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB UNIQUE,
|
|
is_change INTEGER NOT NULL,
|
|
memo BLOB,
|
|
spent INTEGER,
|
|
commitment_tree_position INTEGER,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (account) REFERENCES accounts(account),
|
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
"CREATE TABLE sapling_tree_cap (
|
|
-- cap_id exists only to be able to take advantage of `ON CONFLICT`
|
|
-- upsert functionality; the table will only ever contain one row
|
|
cap_id INTEGER PRIMARY KEY,
|
|
cap_data BLOB NOT NULL
|
|
)",
|
|
"CREATE TABLE sapling_tree_checkpoint_marks_removed (
|
|
checkpoint_id INTEGER NOT NULL,
|
|
mark_removed_position INTEGER NOT NULL,
|
|
FOREIGN KEY (checkpoint_id) REFERENCES sapling_tree_checkpoints(checkpoint_id)
|
|
ON DELETE CASCADE
|
|
)",
|
|
"CREATE TABLE sapling_tree_checkpoints (
|
|
checkpoint_id INTEGER PRIMARY KEY,
|
|
position INTEGER
|
|
)",
|
|
"CREATE TABLE sapling_tree_shards (
|
|
shard_index INTEGER PRIMARY KEY,
|
|
subtree_end_height INTEGER,
|
|
root_hash BLOB,
|
|
shard_data BLOB,
|
|
contains_marked INTEGER,
|
|
CONSTRAINT root_unique UNIQUE (root_hash)
|
|
)",
|
|
"CREATE TABLE sapling_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
block INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
FOREIGN KEY (note) REFERENCES sapling_received_notes(id_note),
|
|
FOREIGN KEY (block) REFERENCES blocks(height),
|
|
CONSTRAINT witness_height UNIQUE (note, block)
|
|
)",
|
|
"CREATE TABLE scan_queue (
|
|
block_range_start INTEGER NOT NULL,
|
|
block_range_end INTEGER NOT NULL,
|
|
priority INTEGER NOT NULL,
|
|
CONSTRAINT range_start_uniq UNIQUE (block_range_start),
|
|
CONSTRAINT range_end_uniq UNIQUE (block_range_end),
|
|
CONSTRAINT range_bounds_order CHECK (
|
|
block_range_start < block_range_end
|
|
)
|
|
)",
|
|
"CREATE TABLE schemer_migrations (
|
|
id blob PRIMARY KEY
|
|
)",
|
|
"CREATE TABLE \"sent_notes\" (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_pool INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
from_account INTEGER NOT NULL,
|
|
to_address TEXT,
|
|
to_account INTEGER,
|
|
value INTEGER NOT NULL,
|
|
memo BLOB,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (from_account) REFERENCES accounts(account),
|
|
FOREIGN KEY (to_account) REFERENCES accounts(account),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index),
|
|
CONSTRAINT note_recipient CHECK (
|
|
(to_address IS NOT NULL) != (to_account IS NOT NULL)
|
|
)
|
|
)",
|
|
"CREATE TABLE transactions (
|
|
id_tx INTEGER PRIMARY KEY,
|
|
txid BLOB NOT NULL UNIQUE,
|
|
created TEXT,
|
|
block INTEGER,
|
|
tx_index INTEGER,
|
|
expiry_height INTEGER,
|
|
raw BLOB,
|
|
fee INTEGER,
|
|
FOREIGN KEY (block) REFERENCES blocks(height)
|
|
)",
|
|
"CREATE TABLE \"utxos\" (
|
|
id_utxo INTEGER PRIMARY KEY,
|
|
received_by_account INTEGER NOT NULL,
|
|
address TEXT NOT NULL,
|
|
prevout_txid BLOB NOT NULL,
|
|
prevout_idx INTEGER NOT NULL,
|
|
script BLOB NOT NULL,
|
|
value_zat INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
spent_in_tx INTEGER,
|
|
FOREIGN KEY (received_by_account) REFERENCES accounts(account),
|
|
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
|
)",
|
|
];
|
|
|
|
let mut tables_query = db_data
|
|
.conn
|
|
.prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")
|
|
.unwrap();
|
|
let mut rows = tables_query.query([]).unwrap();
|
|
let mut expected_idx = 0;
|
|
while let Some(row) = rows.next().unwrap() {
|
|
let sql: String = row.get(0).unwrap();
|
|
assert_eq!(
|
|
re.replace_all(&sql, " "),
|
|
re.replace_all(expected_tables[expected_idx], " ")
|
|
);
|
|
expected_idx += 1;
|
|
}
|
|
|
|
let expected_views = vec![
|
|
// v_transactions
|
|
"CREATE VIEW v_transactions AS
|
|
WITH
|
|
notes AS (
|
|
SELECT sapling_received_notes.account AS account_id,
|
|
sapling_received_notes.tx AS id_tx,
|
|
2 AS pool,
|
|
sapling_received_notes.value AS value,
|
|
CASE
|
|
WHEN sapling_received_notes.is_change THEN 1
|
|
ELSE 0
|
|
END AS is_change,
|
|
CASE
|
|
WHEN sapling_received_notes.is_change THEN 0
|
|
ELSE 1
|
|
END AS received_count,
|
|
CASE
|
|
WHEN sapling_received_notes.memo IS NULL THEN 0
|
|
ELSE 1
|
|
END AS memo_present
|
|
FROM sapling_received_notes
|
|
UNION
|
|
SELECT utxos.received_by_account AS account_id,
|
|
transactions.id_tx AS id_tx,
|
|
0 AS pool,
|
|
utxos.value_zat AS value,
|
|
0 AS is_change,
|
|
1 AS received_count,
|
|
0 AS memo_present
|
|
FROM utxos
|
|
JOIN transactions
|
|
ON transactions.txid = utxos.prevout_txid
|
|
UNION
|
|
SELECT sapling_received_notes.account AS account_id,
|
|
sapling_received_notes.spent AS id_tx,
|
|
2 AS pool,
|
|
-sapling_received_notes.value AS value,
|
|
0 AS is_change,
|
|
0 AS received_count,
|
|
0 AS memo_present
|
|
FROM sapling_received_notes
|
|
WHERE sapling_received_notes.spent IS NOT NULL
|
|
),
|
|
sent_note_counts AS (
|
|
SELECT sent_notes.from_account AS account_id,
|
|
sent_notes.tx AS id_tx,
|
|
COUNT(DISTINCT sent_notes.id_note) as sent_notes,
|
|
SUM(
|
|
CASE
|
|
WHEN sent_notes.memo IS NULL THEN 0
|
|
ELSE 1
|
|
END
|
|
) AS memo_count
|
|
FROM sent_notes
|
|
LEFT JOIN sapling_received_notes
|
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
|
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
|
WHERE sapling_received_notes.is_change IS NULL
|
|
OR sapling_received_notes.is_change = 0
|
|
GROUP BY account_id, id_tx
|
|
),
|
|
blocks_max_height AS (
|
|
SELECT MAX(blocks.height) as max_height FROM blocks
|
|
)
|
|
SELECT notes.account_id AS account_id,
|
|
transactions.id_tx AS id_tx,
|
|
transactions.block AS mined_height,
|
|
transactions.tx_index AS tx_index,
|
|
transactions.txid AS txid,
|
|
transactions.expiry_height AS expiry_height,
|
|
transactions.raw AS raw,
|
|
SUM(notes.value) AS account_balance_delta,
|
|
transactions.fee AS fee_paid,
|
|
SUM(notes.is_change) > 0 AS has_change,
|
|
MAX(COALESCE(sent_note_counts.sent_notes, 0)) AS sent_note_count,
|
|
SUM(notes.received_count) AS received_note_count,
|
|
SUM(notes.memo_present) + MAX(COALESCE(sent_note_counts.memo_count, 0)) AS memo_count,
|
|
blocks.time AS block_time,
|
|
(
|
|
blocks.height IS NULL
|
|
AND transactions.expiry_height <= blocks_max_height.max_height
|
|
) AS expired_unmined
|
|
FROM transactions
|
|
JOIN notes ON notes.id_tx = transactions.id_tx
|
|
JOIN blocks_max_height
|
|
LEFT JOIN blocks ON blocks.height = transactions.block
|
|
LEFT JOIN sent_note_counts
|
|
ON sent_note_counts.account_id = notes.account_id
|
|
AND sent_note_counts.id_tx = notes.id_tx
|
|
GROUP BY notes.account_id, transactions.id_tx",
|
|
// v_tx_outputs
|
|
"CREATE VIEW v_tx_outputs AS
|
|
SELECT sapling_received_notes.tx AS id_tx,
|
|
2 AS output_pool,
|
|
sapling_received_notes.output_index AS output_index,
|
|
sent_notes.from_account AS from_account,
|
|
sapling_received_notes.account AS to_account,
|
|
NULL AS to_address,
|
|
sapling_received_notes.value AS value,
|
|
sapling_received_notes.is_change AS is_change,
|
|
sapling_received_notes.memo AS memo
|
|
FROM sapling_received_notes
|
|
LEFT JOIN sent_notes
|
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
|
(sapling_received_notes.tx, 2, sent_notes.output_index)
|
|
UNION
|
|
SELECT transactions.id_tx AS id_tx,
|
|
0 AS output_pool,
|
|
utxos.prevout_idx AS output_index,
|
|
NULL AS from_account,
|
|
utxos.received_by_account AS to_account,
|
|
utxos.address AS to_address,
|
|
utxos.value_zat AS value,
|
|
false AS is_change,
|
|
NULL AS memo
|
|
FROM utxos
|
|
JOIN transactions
|
|
ON transactions.txid = utxos.prevout_txid
|
|
UNION
|
|
SELECT sent_notes.tx AS id_tx,
|
|
sent_notes.output_pool AS output_pool,
|
|
sent_notes.output_index AS output_index,
|
|
sent_notes.from_account AS from_account,
|
|
sapling_received_notes.account AS to_account,
|
|
sent_notes.to_address AS to_address,
|
|
sent_notes.value AS value,
|
|
false AS is_change,
|
|
sent_notes.memo AS memo
|
|
FROM sent_notes
|
|
LEFT JOIN sapling_received_notes
|
|
ON (sent_notes.tx, sent_notes.output_pool, sent_notes.output_index) =
|
|
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
|
|
WHERE sapling_received_notes.is_change IS NULL
|
|
OR sapling_received_notes.is_change = 0"
|
|
];
|
|
|
|
let mut views_query = db_data
|
|
.conn
|
|
.prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
|
|
.unwrap();
|
|
let mut rows = views_query.query([]).unwrap();
|
|
let mut expected_idx = 0;
|
|
while let Some(row) = rows.next().unwrap() {
|
|
let sql: String = row.get(0).unwrap();
|
|
assert_eq!(
|
|
re.replace_all(&sql, " "),
|
|
re.replace_all(expected_views[expected_idx], " ")
|
|
);
|
|
expected_idx += 1;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn init_migrate_from_0_3_0() {
|
|
fn init_0_3_0<P>(
|
|
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
|
extfvk: &ExtendedFullViewingKey,
|
|
account: AccountId,
|
|
) -> Result<(), rusqlite::Error> {
|
|
wdb.conn.execute(
|
|
"CREATE TABLE accounts (
|
|
account INTEGER PRIMARY KEY,
|
|
extfvk TEXT NOT NULL,
|
|
address TEXT NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE blocks (
|
|
height INTEGER PRIMARY KEY,
|
|
hash BLOB NOT NULL,
|
|
time INTEGER NOT NULL,
|
|
sapling_tree BLOB NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE transactions (
|
|
id_tx INTEGER PRIMARY KEY,
|
|
txid BLOB NOT NULL UNIQUE,
|
|
created TEXT,
|
|
block INTEGER,
|
|
tx_index INTEGER,
|
|
expiry_height INTEGER,
|
|
raw BLOB,
|
|
FOREIGN KEY (block) REFERENCES blocks(height)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
account INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB NOT NULL UNIQUE,
|
|
is_change INTEGER NOT NULL,
|
|
memo BLOB,
|
|
spent INTEGER,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (account) REFERENCES accounts(account),
|
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sapling_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
block INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
FOREIGN KEY (note) REFERENCES received_notes(id_note),
|
|
FOREIGN KEY (block) REFERENCES blocks(height),
|
|
CONSTRAINT witness_height UNIQUE (note, block)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sent_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
from_account INTEGER NOT NULL,
|
|
address TEXT NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
memo BLOB,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (from_account) REFERENCES accounts(account),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
|
|
let address = encode_payment_address(
|
|
tests::network().hrp_sapling_payment_address(),
|
|
&extfvk.default_address().1,
|
|
);
|
|
let extfvk = encode_extended_full_viewing_key(
|
|
tests::network().hrp_sapling_extended_full_viewing_key(),
|
|
extfvk,
|
|
);
|
|
wdb.conn.execute(
|
|
"INSERT INTO accounts (account, extfvk, address)
|
|
VALUES (?, ?, ?)",
|
|
[
|
|
u32::from(account).to_sql()?,
|
|
extfvk.to_sql()?,
|
|
address.to_sql()?,
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
let seed = [0xab; 32];
|
|
let account = AccountId::from(0);
|
|
let secret_key = sapling::spending_key(&seed, tests::network().coin_type(), account);
|
|
let extfvk = secret_key.to_extended_full_viewing_key();
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_0_3_0(&mut db_data, &extfvk, account).unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn init_migrate_from_autoshielding_poc() {
|
|
fn init_autoshielding<P>(
|
|
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
|
extfvk: &ExtendedFullViewingKey,
|
|
account: AccountId,
|
|
) -> Result<(), rusqlite::Error> {
|
|
wdb.conn.execute(
|
|
"CREATE TABLE accounts (
|
|
account INTEGER PRIMARY KEY,
|
|
extfvk TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
transparent_address TEXT NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE blocks (
|
|
height INTEGER PRIMARY KEY,
|
|
hash BLOB NOT NULL,
|
|
time INTEGER NOT NULL,
|
|
sapling_tree BLOB NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE transactions (
|
|
id_tx INTEGER PRIMARY KEY,
|
|
txid BLOB NOT NULL UNIQUE,
|
|
created TEXT,
|
|
block INTEGER,
|
|
tx_index INTEGER,
|
|
expiry_height INTEGER,
|
|
raw BLOB,
|
|
FOREIGN KEY (block) REFERENCES blocks(height)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
account INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB NOT NULL UNIQUE,
|
|
is_change INTEGER NOT NULL,
|
|
memo BLOB,
|
|
spent INTEGER,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (account) REFERENCES accounts(account),
|
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sapling_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
block INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
FOREIGN KEY (note) REFERENCES received_notes(id_note),
|
|
FOREIGN KEY (block) REFERENCES blocks(height),
|
|
CONSTRAINT witness_height UNIQUE (note, block)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sent_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
from_account INTEGER NOT NULL,
|
|
address TEXT NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
memo BLOB,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (from_account) REFERENCES accounts(account),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE utxos (
|
|
id_utxo INTEGER PRIMARY KEY,
|
|
address TEXT NOT NULL,
|
|
prevout_txid BLOB NOT NULL,
|
|
prevout_idx INTEGER NOT NULL,
|
|
script BLOB NOT NULL,
|
|
value_zat INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
spent_in_tx INTEGER,
|
|
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
|
)",
|
|
[],
|
|
)?;
|
|
|
|
let address = encode_payment_address(
|
|
tests::network().hrp_sapling_payment_address(),
|
|
&extfvk.default_address().1,
|
|
);
|
|
let extfvk = encode_extended_full_viewing_key(
|
|
tests::network().hrp_sapling_extended_full_viewing_key(),
|
|
extfvk,
|
|
);
|
|
wdb.conn.execute(
|
|
"INSERT INTO accounts (account, extfvk, address, transparent_address)
|
|
VALUES (?, ?, ?, '')",
|
|
[
|
|
u32::from(account).to_sql()?,
|
|
extfvk.to_sql()?,
|
|
address.to_sql()?,
|
|
],
|
|
)?;
|
|
|
|
// add a sapling sent note
|
|
wdb.conn.execute(
|
|
"INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
|
|
[],
|
|
)?;
|
|
|
|
let tx = TransactionData::from_parts(
|
|
TxVersion::Sapling,
|
|
BranchId::Canopy,
|
|
0,
|
|
BlockHeight::from(0),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.freeze()
|
|
.unwrap();
|
|
|
|
let mut tx_bytes = vec![];
|
|
tx.write(&mut tx_bytes).unwrap();
|
|
wdb.conn.execute(
|
|
"INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, '', ?)",
|
|
[&tx_bytes[..]],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"INSERT INTO sent_notes (tx, output_index, from_account, address, value)
|
|
VALUES (0, 0, ?, ?, 0)",
|
|
[u32::from(account).to_sql()?, address.to_sql()?],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
let seed = [0xab; 32];
|
|
let account = AccountId::from(0);
|
|
let secret_key = sapling::spending_key(&seed, tests::network().coin_type(), account);
|
|
let extfvk = secret_key.to_extended_full_viewing_key();
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_autoshielding(&mut db_data, &extfvk, account).unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn init_migrate_from_main_pre_migrations() {
|
|
fn init_main<P>(
|
|
wdb: &mut WalletDb<rusqlite::Connection, P>,
|
|
ufvk: &UnifiedFullViewingKey,
|
|
account: AccountId,
|
|
) -> Result<(), rusqlite::Error> {
|
|
wdb.conn.execute(
|
|
"CREATE TABLE accounts (
|
|
account INTEGER PRIMARY KEY,
|
|
ufvk TEXT,
|
|
address TEXT,
|
|
transparent_address TEXT
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE blocks (
|
|
height INTEGER PRIMARY KEY,
|
|
hash BLOB NOT NULL,
|
|
time INTEGER NOT NULL,
|
|
sapling_tree BLOB NOT NULL
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE transactions (
|
|
id_tx INTEGER PRIMARY KEY,
|
|
txid BLOB NOT NULL UNIQUE,
|
|
created TEXT,
|
|
block INTEGER,
|
|
tx_index INTEGER,
|
|
expiry_height INTEGER,
|
|
raw BLOB,
|
|
FOREIGN KEY (block) REFERENCES blocks(height)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
account INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB NOT NULL UNIQUE,
|
|
is_change INTEGER NOT NULL,
|
|
memo BLOB,
|
|
spent INTEGER,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (account) REFERENCES accounts(account),
|
|
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sapling_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
block INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
FOREIGN KEY (note) REFERENCES received_notes(id_note),
|
|
FOREIGN KEY (block) REFERENCES blocks(height),
|
|
CONSTRAINT witness_height UNIQUE (note, block)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE sent_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
tx INTEGER NOT NULL,
|
|
output_pool INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
from_account INTEGER NOT NULL,
|
|
address TEXT NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
memo BLOB,
|
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
|
FOREIGN KEY (from_account) REFERENCES accounts(account),
|
|
CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index)
|
|
)",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"CREATE TABLE utxos (
|
|
id_utxo INTEGER PRIMARY KEY,
|
|
address TEXT NOT NULL,
|
|
prevout_txid BLOB NOT NULL,
|
|
prevout_idx INTEGER NOT NULL,
|
|
script BLOB NOT NULL,
|
|
value_zat INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
spent_in_tx INTEGER,
|
|
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
|
)",
|
|
[],
|
|
)?;
|
|
|
|
let ufvk_str = ufvk.encode(&tests::network());
|
|
let address_str =
|
|
RecipientAddress::Unified(ufvk.default_address().0).encode(&tests::network());
|
|
wdb.conn.execute(
|
|
"INSERT INTO accounts (account, ufvk, address, transparent_address)
|
|
VALUES (?, ?, ?, '')",
|
|
[
|
|
u32::from(account).to_sql()?,
|
|
ufvk_str.to_sql()?,
|
|
address_str.to_sql()?,
|
|
],
|
|
)?;
|
|
|
|
// add a transparent "sent note"
|
|
#[cfg(feature = "transparent-inputs")]
|
|
{
|
|
let taddr =
|
|
RecipientAddress::Transparent(*ufvk.default_address().0.transparent().unwrap())
|
|
.encode(&tests::network());
|
|
wdb.conn.execute(
|
|
"INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')",
|
|
[],
|
|
)?;
|
|
wdb.conn.execute(
|
|
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
|
|
VALUES (0, ?, 0, ?, ?, 0)",
|
|
[pool_code(PoolType::Transparent).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
let seed = [0xab; 32];
|
|
let account = AccountId::from(0);
|
|
let secret_key = UnifiedSpendingKey::from_seed(&tests::network(), &seed, account).unwrap();
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_main(
|
|
&mut db_data,
|
|
&secret_key.to_unified_full_viewing_key(),
|
|
account,
|
|
)
|
|
.unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn init_accounts_table_only_works_once() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
|
|
|
// We can call the function as many times as we want with no data
|
|
init_accounts_table(&mut db_data, &HashMap::new()).unwrap();
|
|
init_accounts_table(&mut db_data, &HashMap::new()).unwrap();
|
|
|
|
let seed = [0u8; 32];
|
|
let account = AccountId::from(0);
|
|
|
|
// First call with data should initialise the accounts table
|
|
let extsk = sapling::spending_key(&seed, network().coin_type(), account);
|
|
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(
|
|
Some(
|
|
transparent::AccountPrivKey::from_seed(&network(), &seed, account)
|
|
.unwrap()
|
|
.to_account_pubkey(),
|
|
),
|
|
Some(dfvk),
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
|
|
let ufvks = HashMap::from([(account, ufvk)]);
|
|
|
|
init_accounts_table(&mut db_data, &ufvks).unwrap();
|
|
|
|
// Subsequent calls should return an error
|
|
init_accounts_table(&mut db_data, &HashMap::new()).unwrap_err();
|
|
init_accounts_table(&mut db_data, &ufvks).unwrap_err();
|
|
}
|
|
|
|
#[test]
|
|
fn init_accounts_table_allows_no_gaps() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
|
|
|
// allow sequential initialization
|
|
let seed = [0u8; 32];
|
|
let ufvks = |ids: &[u32]| {
|
|
ids.iter()
|
|
.map(|a| {
|
|
let account = AccountId::from(*a);
|
|
UnifiedSpendingKey::from_seed(&network(), &seed, account)
|
|
.map(|k| (account, k.to_unified_full_viewing_key()))
|
|
.unwrap()
|
|
})
|
|
.collect::<HashMap<_, _>>()
|
|
};
|
|
|
|
// should fail if we have a gap
|
|
assert_matches!(
|
|
init_accounts_table(&mut db_data, &ufvks(&[0, 2])),
|
|
Err(SqliteClientError::AccountIdDiscontinuity)
|
|
);
|
|
|
|
// should succeed if there are no gaps
|
|
assert!(init_accounts_table(&mut db_data, &ufvks(&[0, 1, 2])).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn init_blocks_table_only_works_once() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
|
|
|
|
// First call with data should initialise the blocks table
|
|
init_blocks_table(
|
|
&mut db_data,
|
|
BlockHeight::from(1u32),
|
|
BlockHash([1; 32]),
|
|
1,
|
|
&[0x0, 0x0, 0x0],
|
|
)
|
|
.unwrap();
|
|
|
|
// Subsequent calls should return an error
|
|
init_blocks_table(
|
|
&mut db_data,
|
|
BlockHeight::from(2u32),
|
|
BlockHash([2; 32]),
|
|
2,
|
|
&[0x0, 0x0, 0x0],
|
|
)
|
|
.unwrap_err();
|
|
}
|
|
|
|
#[test]
|
|
fn init_accounts_table_stores_correct_address() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&mut db_data, None).unwrap();
|
|
|
|
let seed = [0u8; 32];
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let usk = UnifiedSpendingKey::from_seed(&tests::network(), &seed, account_id).unwrap();
|
|
let ufvk = usk.to_unified_full_viewing_key();
|
|
let expected_address = ufvk.sapling().unwrap().default_address().1;
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&mut db_data, &ufvks).unwrap();
|
|
|
|
// The account's address should be in the data DB
|
|
let ua = db_data.get_current_address(AccountId::from(0)).unwrap();
|
|
assert_eq!(ua.unwrap().sapling().unwrap(), &expected_address);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "transparent-inputs")]
|
|
fn account_produces_expected_ua_sequence() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let mut db_data = WalletDb::for_path(data_file.path(), Network::MainNetwork).unwrap();
|
|
init_wallet_db(&mut db_data, None).unwrap();
|
|
|
|
let seed = test_vectors::UNIFIED[0].root_seed;
|
|
let (account, _usk) = db_data.create_account(&Secret::new(seed.to_vec())).unwrap();
|
|
assert_eq!(account, AccountId::from(0u32));
|
|
|
|
for tv in &test_vectors::UNIFIED[..3] {
|
|
if let Some(RecipientAddress::Unified(tvua)) =
|
|
RecipientAddress::decode(&Network::MainNetwork, tv.unified_addr)
|
|
{
|
|
let (ua, di) = wallet::get_current_address(&db_data.conn, &db_data.params, account)
|
|
.unwrap()
|
|
.expect("create_account generated the first address");
|
|
assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
|
|
assert_eq!(tvua.transparent(), ua.transparent());
|
|
assert_eq!(tvua.sapling(), ua.sapling());
|
|
assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
|
|
|
|
db_data
|
|
.get_next_available_address(account)
|
|
.unwrap()
|
|
.expect("get_next_available_address generated an address");
|
|
} else {
|
|
panic!(
|
|
"{} did not decode to a valid unified address",
|
|
tv.unified_addr
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|