378 lines
12 KiB
Rust
378 lines
12 KiB
Rust
use crate::orchard::derive_orchard_keys;
|
|
use rusqlite::{params, Connection, OptionalExtension};
|
|
use zcash_primitives::consensus::{Network, Parameters};
|
|
|
|
pub fn get_schema_version(connection: &Connection) -> anyhow::Result<u32> {
|
|
let version: Option<u32> = connection
|
|
.query_row(
|
|
"SELECT version FROM schema_version WHERE id = 1",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.optional()?;
|
|
Ok(version.unwrap_or(0))
|
|
}
|
|
|
|
pub fn update_schema_version(connection: &Connection, version: u32) -> anyhow::Result<()> {
|
|
connection.execute(
|
|
"INSERT INTO schema_version(id, version) VALUES (1, ?1) \
|
|
ON CONFLICT (id) DO UPDATE SET version = excluded.version",
|
|
params![version],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn reset_db(connection: &Connection) -> anyhow::Result<()> {
|
|
// don't drop account data: accounts, taddrs, secret_shares
|
|
connection.execute("DROP TABLE blocks", [])?;
|
|
connection.execute("DROP TABLE transactions", [])?;
|
|
connection.execute("DROP TABLE received_notes", [])?;
|
|
connection.execute("DROP TABLE sapling_witnesses", [])?;
|
|
connection.execute("DROP TABLE diversifiers", [])?;
|
|
connection.execute("DROP TABLE historical_prices", [])?;
|
|
update_schema_version(connection, 0)?;
|
|
Ok(())
|
|
}
|
|
|
|
const LATEST_VERSION: u32 = 8;
|
|
|
|
pub fn init_db(connection: &Connection, network: &Network, has_ua: bool) -> anyhow::Result<()> {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS schema_version (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
version INTEGER NOT NULL)",
|
|
[],
|
|
)?;
|
|
|
|
let version = get_schema_version(connection)?;
|
|
|
|
if version < 1 {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS accounts (
|
|
id_account INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
seed TEXT,
|
|
aindex INTEGER NOT NULL,
|
|
sk TEXT,
|
|
ivk TEXT NOT NULL UNIQUE,
|
|
address TEXT NOT NULL)",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS blocks (
|
|
height INTEGER PRIMARY KEY,
|
|
hash BLOB NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
sapling_tree BLOB NOT NULL)",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS transactions (
|
|
id_tx INTEGER PRIMARY KEY,
|
|
account INTEGER NOT NULL,
|
|
txid BLOB NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
address TEXT,
|
|
memo TEXT,
|
|
tx_index INTEGER,
|
|
CONSTRAINT tx_account UNIQUE (height, tx_index, account))",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
account INTEGER NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
tx INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB NOT NULL UNIQUE,
|
|
spent INTEGER,
|
|
excluded BOOL,
|
|
CONSTRAINT tx_output UNIQUE (tx, output_index))",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS sapling_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
CONSTRAINT witness_height UNIQUE (note, height))",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS diversifiers (
|
|
account INTEGER PRIMARY KEY NOT NULL,
|
|
diversifier_index BLOB NOT NULL)",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS taddrs (
|
|
account INTEGER PRIMARY KEY NOT NULL,
|
|
sk TEXT NOT NULL,
|
|
address TEXT NOT NULL)",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS historical_prices (
|
|
currency TEXT NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
price REAL NOT NULL,
|
|
PRIMARY KEY (currency, timestamp))",
|
|
[],
|
|
)?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS contacts (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
dirty BOOL NOT NULL)",
|
|
[],
|
|
)?;
|
|
}
|
|
|
|
if version < 2 {
|
|
connection.execute(
|
|
"CREATE INDEX i_received_notes ON received_notes(account)",
|
|
[],
|
|
)?;
|
|
connection.execute("CREATE INDEX i_account ON accounts(address)", [])?;
|
|
connection.execute("CREATE INDEX i_contact ON contacts(address)", [])?;
|
|
connection.execute("CREATE INDEX i_transaction ON transactions(account)", [])?;
|
|
connection.execute("CREATE INDEX i_witness ON sapling_witnesses(height)", [])?;
|
|
}
|
|
|
|
if version < 3 {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY,
|
|
account INTEGER NOT NULL,
|
|
sender TEXT,
|
|
recipient TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
timestamp INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
read BOOL NOT NULL)",
|
|
[],
|
|
)?;
|
|
// Don't index because it *really* slows down inserts
|
|
// connection.execute(
|
|
// "CREATE INDEX i_messages ON messages(account)",
|
|
// [],
|
|
// )?;
|
|
}
|
|
|
|
if version < 4 {
|
|
connection.execute("ALTER TABLE messages ADD id_tx INTEGER", [])?;
|
|
}
|
|
|
|
if version < 5 {
|
|
connection.execute(
|
|
"CREATE TABLE orchard_addrs(
|
|
account INTEGER PRIMARY KEY,
|
|
sk BLOB,
|
|
fvk BLOB NOT NULL)",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"CREATE TABLE ua_settings(
|
|
account INTEGER PRIMARY KEY,
|
|
transparent BOOL NOT NULL,
|
|
sapling BOOL NOT NULL,
|
|
orchard BOOL NOT NULL)",
|
|
[],
|
|
)?;
|
|
if has_ua {
|
|
upgrade_accounts(&connection, network)?;
|
|
}
|
|
connection.execute(
|
|
"CREATE TABLE sapling_tree(
|
|
height INTEGER PRIMARY KEY,
|
|
tree BLOB NOT NULL)",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"CREATE TABLE orchard_tree(
|
|
height INTEGER PRIMARY KEY,
|
|
tree BLOB NOT NULL)",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"INSERT INTO sapling_tree SELECT height, sapling_tree FROM blocks",
|
|
[],
|
|
)?;
|
|
connection.execute("ALTER TABLE blocks DROP sapling_tree", [])?;
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS new_received_notes (
|
|
id_note INTEGER PRIMARY KEY,
|
|
account INTEGER NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
tx INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
diversifier BLOB NOT NULL,
|
|
value INTEGER NOT NULL,
|
|
rcm BLOB NOT NULL,
|
|
nf BLOB NOT NULL UNIQUE,
|
|
rho BLOB,
|
|
orchard BOOL NOT NULL DEFAULT false,
|
|
spent INTEGER,
|
|
excluded BOOL,
|
|
CONSTRAINT tx_output UNIQUE (tx, orchard, output_index))",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"INSERT INTO new_received_notes(
|
|
id_note, account, position, tx, height, output_index, diversifier, value,
|
|
rcm, nf, spent, excluded
|
|
) SELECT * FROM received_notes",
|
|
[],
|
|
)?;
|
|
connection.execute("DROP TABLE received_notes", [])?;
|
|
connection.execute(
|
|
"ALTER TABLE new_received_notes RENAME TO received_notes",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS orchard_witnesses (
|
|
id_witness INTEGER PRIMARY KEY,
|
|
note INTEGER NOT NULL,
|
|
height INTEGER NOT NULL,
|
|
witness BLOB NOT NULL,
|
|
CONSTRAINT witness_height UNIQUE (note, height))",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"CREATE INDEX IF NOT EXISTS i_orchard_witness ON orchard_witnesses(height)",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"ALTER TABLE messages ADD incoming BOOL NOT NULL DEFAULT true",
|
|
[],
|
|
)?;
|
|
}
|
|
|
|
if version < 6 {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS send_templates (
|
|
id_send_template INTEGER PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
address TEXT NOT NULL,
|
|
amount INTEGER NOT NULL,
|
|
fiat_amount DECIMAL NOT NULL,
|
|
fee_included BOOL NOT NULL,
|
|
fiat TEXT,
|
|
include_reply_to BOOL NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
body TEXT NOT NULL)",
|
|
[],
|
|
)?;
|
|
}
|
|
|
|
if version < 7 {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS properties (
|
|
name TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL)",
|
|
[],
|
|
)?;
|
|
}
|
|
|
|
if version < 8 {
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS new_taddrs (
|
|
account INTEGER PRIMARY KEY NOT NULL,
|
|
sk TEXT,
|
|
address TEXT NOT NULL)",
|
|
[],
|
|
)?;
|
|
connection.execute(
|
|
"INSERT INTO new_taddrs(
|
|
account, sk, address
|
|
) SELECT * FROM taddrs",
|
|
[],
|
|
)?;
|
|
connection.execute("DROP TABLE taddrs", [])?;
|
|
connection.execute("ALTER TABLE new_taddrs RENAME TO taddrs", [])?;
|
|
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS hw_wallets(
|
|
account INTEGER PRIMARY KEY NOT NULL,
|
|
ledger BOOL NOT NULL)",
|
|
[],
|
|
)?;
|
|
}
|
|
|
|
if version != LATEST_VERSION {
|
|
update_schema_version(connection, LATEST_VERSION)?;
|
|
connection.cache_flush()?;
|
|
log::info!("Database migrated");
|
|
}
|
|
|
|
// We may get a database that has no valid schema version from a version of single currency Z/YWallet
|
|
// because we kept the same app name in Google/Apple Stores. The upgraded app fails to recognize the db tables
|
|
// At least we monkey patch the accounts table to let the user access the backup page and recover his seed phrase
|
|
let c = connection.query_row(
|
|
"SELECT COUNT(*) FROM pragma_table_info('accounts') WHERE name = 'aindex'",
|
|
params![],
|
|
|row| {
|
|
let c: u32 = row.get(0)?;
|
|
Ok(c)
|
|
},
|
|
)?;
|
|
if c == 0 {
|
|
connection.execute(
|
|
"ALTER TABLE accounts ADD aindex INTEGER NOT NULL DEFAULT (0)",
|
|
params![],
|
|
)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn upgrade_accounts(connection: &Connection, network: &Network) -> anyhow::Result<()> {
|
|
let mut statement = connection.prepare("SELECT a.id_account, a.seed, a.aindex, t.address FROM accounts a LEFT JOIN taddrs t ON a.id_account = t.account")?;
|
|
let rows = statement.query_map([], |row| {
|
|
let id_account: u32 = row.get(0)?;
|
|
let seed: Option<String> = row.get(1)?;
|
|
let aindex: u32 = row.get(2)?;
|
|
let transparent_address: Option<String> = row.get(3)?;
|
|
Ok((id_account, seed, aindex, transparent_address.is_some()))
|
|
})?;
|
|
let mut res = vec![];
|
|
for row in rows {
|
|
res.push(row?);
|
|
}
|
|
|
|
for (id_account, seed, aindex, has_transparent) in res {
|
|
let has_orchard = seed.is_some();
|
|
if let Some(seed) = seed {
|
|
let orchard_keys = derive_orchard_keys(network.coin_type(), &seed, aindex);
|
|
connection.execute(
|
|
"INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1,?2,?3)",
|
|
params![id_account, &orchard_keys.sk, &orchard_keys.fvk],
|
|
)?;
|
|
}
|
|
connection.execute(
|
|
"INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1,?2,?3,?4)",
|
|
params![id_account, has_transparent, true, has_orchard],
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|