Merge pull request #656 from nuttycom/sqlite/migration_organization
Move wallet migrations into individual modules.
This commit is contained in:
commit
81e0d482d0
|
@ -1,10 +1,11 @@
|
||||||
//! Functions for initializing the various databases.
|
//! Functions for initializing the various databases.
|
||||||
use rusqlite::{self, params, types::ToSql, NO_PARAMS};
|
use std::collections::HashMap;
|
||||||
use schemer::{migration, Migration, Migrator, MigratorError};
|
|
||||||
use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration};
|
|
||||||
use secrecy::{ExposeSecret, SecretVec};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use rusqlite::{self, types::ToSql, NO_PARAMS};
|
||||||
|
use schemer::{Migrator, MigratorError};
|
||||||
|
use schemer_rusqlite::RusqliteAdapter;
|
||||||
|
use secrecy::SecretVec;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
|
@ -14,22 +15,9 @@ use zcash_primitives::{
|
||||||
zip32::AccountId,
|
zip32::AccountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::keys::UnifiedFullViewingKey;
|
||||||
address::RecipientAddress,
|
|
||||||
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{error::SqliteClientError, wallet, WalletDb};
|
||||||
error::SqliteClientError,
|
|
||||||
wallet::{self, PoolType},
|
|
||||||
WalletDb,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
|
||||||
use {
|
|
||||||
zcash_client_backend::encoding::AddressCodec,
|
|
||||||
zcash_primitives::legacy::keys::IncomingViewingKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod migrations;
|
mod migrations;
|
||||||
|
|
||||||
|
@ -87,375 +75,6 @@ impl std::error::Error for WalletMigrationError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WalletMigration0;
|
|
||||||
|
|
||||||
migration!(
|
|
||||||
WalletMigration0,
|
|
||||||
"bc4f5e57-d600-4b6c-990f-b3538f0bfce1",
|
|
||||||
[],
|
|
||||||
"Initialize the wallet database."
|
|
||||||
);
|
|
||||||
|
|
||||||
impl RusqliteMigration for WalletMigration0 {
|
|
||||||
type Error = WalletMigrationError;
|
|
||||||
|
|
||||||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
transaction.execute_batch(
|
|
||||||
// We set the user_version field of the database to a constant value of 8 to allow
|
|
||||||
// correct integration with the Android SDK with versions of the database that were
|
|
||||||
// created prior to the introduction of migrations in this crate. This constant should
|
|
||||||
// remain fixed going forward, and should not be altered by migrations; migration
|
|
||||||
// status is maintained exclusively by the schemer_migrations table.
|
|
||||||
"PRAGMA user_version = 8;
|
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
|
||||||
account INTEGER PRIMARY KEY,
|
|
||||||
extfvk TEXT NOT NULL,
|
|
||||||
address TEXT NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS blocks (
|
|
||||||
height INTEGER PRIMARY KEY,
|
|
||||||
hash BLOB NOT NULL,
|
|
||||||
time INTEGER NOT NULL,
|
|
||||||
sapling_tree BLOB NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS 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)
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS 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)
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS 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)
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS 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)
|
|
||||||
);",
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
// We should never down-migrate the first migration, as that can irreversibly
|
|
||||||
// destroy data.
|
|
||||||
panic!("Cannot revert the initial migration.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WalletMigration1;
|
|
||||||
|
|
||||||
migration!(
|
|
||||||
WalletMigration1,
|
|
||||||
"a2e0ed2e-8852-475e-b0a4-f154b15b9dbe",
|
|
||||||
["bc4f5e57-d600-4b6c-990f-b3538f0bfce1"],
|
|
||||||
"Add support for receiving transparent UTXOs."
|
|
||||||
);
|
|
||||||
|
|
||||||
impl RusqliteMigration for WalletMigration1 {
|
|
||||||
type Error = WalletMigrationError;
|
|
||||||
|
|
||||||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
transaction.execute_batch(
|
|
||||||
"CREATE TABLE IF NOT EXISTS 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)
|
|
||||||
);",
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
transaction.execute_batch("DROP TABLE utxos;")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WalletMigration2<P> {
|
|
||||||
params: P,
|
|
||||||
seed: Option<SecretVec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P> WalletMigration2<P> {
|
|
||||||
fn id() -> Uuid {
|
|
||||||
Uuid::parse_str("be57ef3b-388e-42ea-97e2-678dafcf9754").unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P> Migration for WalletMigration2<P> {
|
|
||||||
fn id(&self) -> Uuid {
|
|
||||||
WalletMigration2::<P>::id()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dependencies(&self) -> HashSet<Uuid> {
|
|
||||||
["a2e0ed2e-8852-475e-b0a4-f154b15b9dbe"]
|
|
||||||
.iter()
|
|
||||||
.map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
|
||||||
"Add support for unified full viewing keys"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
|
|
||||||
type Error = WalletMigrationError;
|
|
||||||
|
|
||||||
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
//
|
|
||||||
// Update the accounts table to store ufvks rather than extfvks
|
|
||||||
//
|
|
||||||
|
|
||||||
transaction.execute_batch(
|
|
||||||
"CREATE TABLE accounts_new (
|
|
||||||
account INTEGER PRIMARY KEY,
|
|
||||||
ufvk TEXT NOT NULL,
|
|
||||||
address TEXT,
|
|
||||||
transparent_address TEXT
|
|
||||||
);",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut stmt_fetch_accounts =
|
|
||||||
transaction.prepare("SELECT account, address FROM accounts")?;
|
|
||||||
|
|
||||||
let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
|
|
||||||
while let Some(row) = rows.next()? {
|
|
||||||
// We only need to check for the presence of the seed if we have keys that
|
|
||||||
// need to be migrated; otherwise, it's fine to not supply the seed if this
|
|
||||||
// migration is being used to initialize an empty database.
|
|
||||||
if let Some(seed) = &self.seed {
|
|
||||||
let account: u32 = row.get(0)?;
|
|
||||||
let account = AccountId::from(account);
|
|
||||||
let usk =
|
|
||||||
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
|
|
||||||
.unwrap();
|
|
||||||
let ufvk = usk.to_unified_full_viewing_key();
|
|
||||||
|
|
||||||
let address: String = row.get(1)?;
|
|
||||||
let decoded =
|
|
||||||
RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
|
|
||||||
WalletMigrationError::CorruptedData(format!(
|
|
||||||
"Could not decode {} as a valid Zcash address.",
|
|
||||||
address
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
match decoded {
|
|
||||||
RecipientAddress::Shielded(decoded_address) => {
|
|
||||||
let dfvk = ufvk.sapling().expect(
|
|
||||||
"Derivation should have produced a UFVK containing a Sapling component.",
|
|
||||||
);
|
|
||||||
let (idx, expected_address) = dfvk.default_address();
|
|
||||||
if decoded_address != expected_address {
|
|
||||||
return Err(WalletMigrationError::CorruptedData(
|
|
||||||
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
|
|
||||||
address,
|
|
||||||
RecipientAddress::Shielded(expected_address).encode(&self.params),
|
|
||||||
idx)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RecipientAddress::Transparent(_) => {
|
|
||||||
return Err(WalletMigrationError::CorruptedData(
|
|
||||||
"Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
|
|
||||||
}
|
|
||||||
RecipientAddress::Unified(decoded_address) => {
|
|
||||||
let (expected_address, idx) = ufvk.default_address();
|
|
||||||
if decoded_address != expected_address {
|
|
||||||
return Err(WalletMigrationError::CorruptedData(
|
|
||||||
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
|
|
||||||
address,
|
|
||||||
RecipientAddress::Unified(expected_address).encode(&self.params),
|
|
||||||
idx)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ufvk_str: String = ufvk.encode(&self.params);
|
|
||||||
let address_str: String = ufvk.default_address().0.encode(&self.params);
|
|
||||||
|
|
||||||
// This migration, and the wallet behaviour before it, stored the default
|
|
||||||
// transparent address in the `accounts` table. This does not necessarily
|
|
||||||
// match the transparent receiver in the default Unified Address. Starting
|
|
||||||
// from `AddressesTableMigration` below, we no longer store transparent
|
|
||||||
// addresses directly, but instead extract them from the Unified Address
|
|
||||||
// (or from the UFVK if the UA was derived without a transparent receiver,
|
|
||||||
// which is not the case for UAs generated by this crate).
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
|
||||||
let taddress_str: Option<String> = ufvk.transparent().and_then(|k| {
|
|
||||||
k.derive_external_ivk()
|
|
||||||
.ok()
|
|
||||||
.map(|k| k.default_address().0.encode(&self.params))
|
|
||||||
});
|
|
||||||
#[cfg(not(feature = "transparent-inputs"))]
|
|
||||||
let taddress_str: Option<String> = None;
|
|
||||||
|
|
||||||
transaction.execute_named(
|
|
||||||
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
|
|
||||||
VALUES (:account, :ufvk, :address, :transparent_address)",
|
|
||||||
&[
|
|
||||||
(":account", &<u32>::from(account)),
|
|
||||||
(":ufvk", &ufvk_str),
|
|
||||||
(":address", &address_str),
|
|
||||||
(":transparent_address", &taddress_str),
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
return Err(WalletMigrationError::SeedRequired);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.execute_batch(
|
|
||||||
"DROP TABLE accounts;
|
|
||||||
ALTER TABLE accounts_new RENAME TO accounts;",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Update the sent_notes table to inclue an output_pool column that
|
|
||||||
// is respected by the uniqueness constraint
|
|
||||||
//
|
|
||||||
|
|
||||||
transaction.execute_batch(
|
|
||||||
"CREATE TABLE sent_notes_new (
|
|
||||||
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)
|
|
||||||
);",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// we query in a nested scope so that the col_names iterator is correctly
|
|
||||||
// dropped and doesn't maintain a lock on the table.
|
|
||||||
let has_output_pool = {
|
|
||||||
let mut stmt_fetch_columns = transaction.prepare("PRAGMA TABLE_INFO('sent_notes')")?;
|
|
||||||
let mut col_names = stmt_fetch_columns.query_map(NO_PARAMS, |row| {
|
|
||||||
let col_name: String = row.get(1)?;
|
|
||||||
Ok(col_name)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
col_names.any(|cname| cname == Ok("output_pool".to_string()))
|
|
||||||
};
|
|
||||||
|
|
||||||
if has_output_pool {
|
|
||||||
transaction.execute_batch(
|
|
||||||
"INSERT INTO sent_notes_new
|
|
||||||
(id_note, tx, output_pool, output_index, from_account, address, value, memo)
|
|
||||||
SELECT id_note, tx, output_pool, output_index, from_account, address, value, memo
|
|
||||||
FROM sent_notes;"
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
let mut stmt_fetch_sent_notes = transaction.prepare(
|
|
||||||
"SELECT id_note, tx, output_index, from_account, address, value, memo
|
|
||||||
FROM sent_notes",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut stmt_insert_sent_note = transaction.prepare(
|
|
||||||
"INSERT INTO sent_notes_new
|
|
||||||
(id_note, tx, output_pool, output_index, from_account, address, value, memo)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut rows = stmt_fetch_sent_notes.query(NO_PARAMS)?;
|
|
||||||
while let Some(row) = rows.next()? {
|
|
||||||
let id_note: i64 = row.get(0)?;
|
|
||||||
let tx_ref: i64 = row.get(1)?;
|
|
||||||
let output_index: i64 = row.get(2)?;
|
|
||||||
let account_id: u32 = row.get(3)?;
|
|
||||||
let address: String = row.get(4)?;
|
|
||||||
let value: i64 = row.get(5)?;
|
|
||||||
let memo: Option<Vec<u8>> = row.get(6)?;
|
|
||||||
|
|
||||||
let decoded_address =
|
|
||||||
RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
|
|
||||||
WalletMigrationError::CorruptedData(format!(
|
|
||||||
"Could not decode {} as a valid Zcash address.",
|
|
||||||
address
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let output_pool = match decoded_address {
|
|
||||||
RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()),
|
|
||||||
RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()),
|
|
||||||
RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData(
|
|
||||||
"Unified addresses should not yet appear in the sent_notes table."
|
|
||||||
.to_string(),
|
|
||||||
)),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
stmt_insert_sent_note.execute(params![
|
|
||||||
id_note,
|
|
||||||
tx_ref,
|
|
||||||
output_pool,
|
|
||||||
output_index,
|
|
||||||
account_id,
|
|
||||||
address,
|
|
||||||
value,
|
|
||||||
memo
|
|
||||||
])?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.execute_batch(
|
|
||||||
"DROP TABLE sent_notes;
|
|
||||||
ALTER TABLE sent_notes_new RENAME TO sent_notes;",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
|
||||||
// TODO: something better than just panic?
|
|
||||||
panic!("Cannot revert this migration.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets up the internal structure of the data database.
|
/// Sets up the internal structure of the data database.
|
||||||
///
|
///
|
||||||
/// This procedure will automatically perform migration operations to update the wallet database to
|
/// This procedure will automatically perform migration operations to update the wallet database to
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
mod addresses_table;
|
|
||||||
pub(super) use addresses_table::AddressesTableMigration;
|
|
||||||
|
|
||||||
mod add_transaction_views;
|
mod add_transaction_views;
|
||||||
|
mod addresses_table;
|
||||||
|
mod initial_setup;
|
||||||
|
mod ufvk_support;
|
||||||
|
mod utxos_table;
|
||||||
|
|
||||||
use schemer_rusqlite::RusqliteMigration;
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
use secrecy::SecretVec;
|
use secrecy::SecretVec;
|
||||||
use zcash_primitives::consensus;
|
use zcash_primitives::consensus;
|
||||||
|
|
||||||
use super::{WalletMigration0, WalletMigration1, WalletMigration2, WalletMigrationError};
|
use super::WalletMigrationError;
|
||||||
|
|
||||||
pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
|
||||||
params: &P,
|
params: &P,
|
||||||
seed: Option<SecretVec<u8>>,
|
seed: Option<SecretVec<u8>>,
|
||||||
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
|
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
|
||||||
vec![
|
vec![
|
||||||
Box::new(WalletMigration0 {}),
|
Box::new(initial_setup::Migration {}),
|
||||||
Box::new(WalletMigration1 {}),
|
Box::new(utxos_table::Migration {}),
|
||||||
Box::new(WalletMigration2 {
|
Box::new(ufvk_support::Migration {
|
||||||
params: params.clone(),
|
params: params.clone(),
|
||||||
seed,
|
seed,
|
||||||
}),
|
}),
|
||||||
Box::new(AddressesTableMigration {
|
Box::new(addresses_table::Migration {
|
||||||
params: params.clone(),
|
params: params.clone(),
|
||||||
}),
|
}),
|
||||||
Box::new(add_transaction_views::Migration {
|
Box::new(add_transaction_views::Migration {
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
//! Functions for initializing the various databases.
|
//! Migration that adds transaction summary views & add fee information to transactions.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use rusqlite::{self, types::ToSql, OptionalExtension, NO_PARAMS};
|
use rusqlite::{self, types::ToSql, OptionalExtension, NO_PARAMS};
|
||||||
use schemer::{self};
|
use schemer::{self};
|
||||||
use schemer_rusqlite::RusqliteMigration;
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
|
@ -12,11 +11,15 @@ use zcash_primitives::{
|
||||||
transaction::{components::amount::Amount, Transaction},
|
transaction::{components::amount::Amount, Transaction},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::{WalletMigration2, WalletMigrationError};
|
use super::{ufvk_support, utxos_table};
|
||||||
|
use crate::wallet::init::WalletMigrationError;
|
||||||
|
|
||||||
pub(crate) fn migration_id() -> Uuid {
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
|
||||||
Uuid::parse_str("282fad2e-8372-4ca0-8bed-71821320909f").unwrap()
|
0x282fad2e,
|
||||||
}
|
0x8372,
|
||||||
|
0x4ca0,
|
||||||
|
b"\x8b\xed\x71\x82\x13\x20\x90\x9f",
|
||||||
|
);
|
||||||
|
|
||||||
pub(crate) struct Migration<P> {
|
pub(crate) struct Migration<P> {
|
||||||
pub(super) params: P,
|
pub(super) params: P,
|
||||||
|
@ -24,13 +27,13 @@ pub(crate) struct Migration<P> {
|
||||||
|
|
||||||
impl<P> schemer::Migration for Migration<P> {
|
impl<P> schemer::Migration for Migration<P> {
|
||||||
fn id(&self) -> Uuid {
|
fn id(&self) -> Uuid {
|
||||||
migration_id()
|
MIGRATION_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependencies(&self) -> HashSet<Uuid> {
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
let mut deps = HashSet::new();
|
[ufvk_support::MIGRATION_ID, utxos_table::MIGRATION_ID]
|
||||||
deps.insert(WalletMigration2::<P>::id());
|
.into_iter()
|
||||||
deps
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
|
@ -216,11 +219,11 @@ mod tests {
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use {
|
use {
|
||||||
crate::wallet::init::WalletMigration2,
|
crate::wallet::init::migrations::ufvk_support,
|
||||||
rusqlite::params,
|
rusqlite::params,
|
||||||
zcash_client_backend::{encoding::AddressCodec, keys::UnifiedSpendingKey},
|
zcash_client_backend::{encoding::AddressCodec, keys::UnifiedSpendingKey},
|
||||||
zcash_primitives::{
|
zcash_primitives::{
|
||||||
consensus::{BlockHeight, BranchId, Network},
|
consensus::{BlockHeight, BranchId},
|
||||||
legacy::{keys::IncomingViewingKey, Script},
|
legacy::{keys::IncomingViewingKey, Script},
|
||||||
transaction::{
|
transaction::{
|
||||||
components::{
|
components::{
|
||||||
|
@ -235,10 +238,7 @@ mod tests {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
tests,
|
tests,
|
||||||
wallet::init::{
|
wallet::init::{init_wallet_db, init_wallet_db_internal, migrations::addresses_table},
|
||||||
init_wallet_db, init_wallet_db_internal,
|
|
||||||
migrations::addresses_table::ADDRESSES_TABLE_MIGRATION,
|
|
||||||
},
|
|
||||||
WalletDb,
|
WalletDb,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ mod tests {
|
||||||
fn transaction_views() {
|
fn transaction_views() {
|
||||||
let data_file = NamedTempFile::new().unwrap();
|
let data_file = NamedTempFile::new().unwrap();
|
||||||
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||||
init_wallet_db_internal(&mut db_data, None, Some(ADDRESSES_TABLE_MIGRATION)).unwrap();
|
init_wallet_db_internal(&mut db_data, None, Some(addresses_table::MIGRATION_ID)).unwrap();
|
||||||
|
|
||||||
db_data.conn.execute_batch(
|
db_data.conn.execute_batch(
|
||||||
"INSERT INTO accounts (account, ufvk) VALUES (0, '');
|
"INSERT INTO accounts (account, ufvk) VALUES (0, '');
|
||||||
|
@ -327,8 +327,7 @@ mod tests {
|
||||||
fn migrate_from_wm2() {
|
fn migrate_from_wm2() {
|
||||||
let data_file = NamedTempFile::new().unwrap();
|
let data_file = NamedTempFile::new().unwrap();
|
||||||
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
||||||
init_wallet_db_internal(&mut db_data, None, Some(WalletMigration2::<Network>::id()))
|
init_wallet_db_internal(&mut db_data, None, Some(ufvk_support::MIGRATION_ID)).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// create a UTXO to spend
|
// create a UTXO to spend
|
||||||
let tx = TransactionData::from_parts(
|
let tx = TransactionData::from_parts(
|
||||||
|
|
|
@ -1,43 +1,41 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use rusqlite::{Transaction, NO_PARAMS};
|
use rusqlite::{Transaction, NO_PARAMS};
|
||||||
use schemer::Migration;
|
use schemer;
|
||||||
use schemer_rusqlite::RusqliteMigration;
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey};
|
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey};
|
||||||
use zcash_primitives::{consensus, zip32::AccountId};
|
use zcash_primitives::{consensus, zip32::AccountId};
|
||||||
|
|
||||||
use super::super::WalletMigrationError;
|
use crate::wallet::{add_account_internal, init::WalletMigrationError};
|
||||||
use crate::wallet::add_account_internal;
|
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use zcash_primitives::legacy::keys::IncomingViewingKey;
|
use zcash_primitives::legacy::keys::IncomingViewingKey;
|
||||||
|
|
||||||
|
use super::ufvk_support;
|
||||||
|
|
||||||
/// The migration that removed the address columns from the `accounts` table, and created
|
/// The migration that removed the address columns from the `accounts` table, and created
|
||||||
/// the `accounts` table.
|
/// the `accounts` table.
|
||||||
///
|
///
|
||||||
/// d956978c-9c87-4d6e-815d-fb8f088d094c
|
/// d956978c-9c87-4d6e-815d-fb8f088d094c
|
||||||
pub(super) const ADDRESSES_TABLE_MIGRATION: Uuid = Uuid::from_fields(
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
|
||||||
0xd956978c,
|
0xd956978c,
|
||||||
0x9c87,
|
0x9c87,
|
||||||
0x4d6e,
|
0x4d6e,
|
||||||
b"\x81\x5d\xfb\x8f\x08\x8d\x09\x4c",
|
b"\x81\x5d\xfb\x8f\x08\x8d\x09\x4c",
|
||||||
);
|
);
|
||||||
|
|
||||||
pub(crate) struct AddressesTableMigration<P: consensus::Parameters> {
|
pub(crate) struct Migration<P: consensus::Parameters> {
|
||||||
pub(crate) params: P,
|
pub(crate) params: P,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: consensus::Parameters> Migration for AddressesTableMigration<P> {
|
impl<P: consensus::Parameters> schemer::Migration for Migration<P> {
|
||||||
fn id(&self) -> Uuid {
|
fn id(&self) -> Uuid {
|
||||||
ADDRESSES_TABLE_MIGRATION
|
MIGRATION_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dependencies(&self) -> HashSet<Uuid> {
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
["be57ef3b-388e-42ea-97e2-678dafcf9754"]
|
[ufvk_support::MIGRATION_ID].into_iter().collect()
|
||||||
.iter()
|
|
||||||
.map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
|
@ -45,7 +43,7 @@ impl<P: consensus::Parameters> Migration for AddressesTableMigration<P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: consensus::Parameters> RusqliteMigration for AddressesTableMigration<P> {
|
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
type Error = WalletMigrationError;
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
|
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
//! The migration that performs the initial setup of the wallet database.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rusqlite;
|
||||||
|
use schemer;
|
||||||
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::wallet::init::WalletMigrationError;
|
||||||
|
|
||||||
|
/// Identifier for the migration that performs the initial setup of the wallet database.
|
||||||
|
///
|
||||||
|
/// bc4f5e57-d600-4b6c-990f-b3538f0bfce1,
|
||||||
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
|
||||||
|
0xbc4f5e57,
|
||||||
|
0xd600,
|
||||||
|
0x4b6c,
|
||||||
|
b"\x99\x0f\xb3\x53\x8f\x0b\xfc\xe1",
|
||||||
|
);
|
||||||
|
|
||||||
|
pub(super) struct Migration;
|
||||||
|
|
||||||
|
impl schemer::Migration for Migration {
|
||||||
|
fn id(&self) -> Uuid {
|
||||||
|
MIGRATION_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
|
HashSet::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Initialize the wallet database."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RusqliteMigration for Migration {
|
||||||
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
|
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
transaction.execute_batch(
|
||||||
|
// We set the user_version field of the database to a constant value of 8 to allow
|
||||||
|
// correct integration with the Android SDK with versions of the database that were
|
||||||
|
// created prior to the introduction of migrations in this crate. This constant should
|
||||||
|
// remain fixed going forward, and should not be altered by migrations; migration
|
||||||
|
// status is maintained exclusively by the schemer_migrations table.
|
||||||
|
"PRAGMA user_version = 8;
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
account INTEGER PRIMARY KEY,
|
||||||
|
extfvk TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS blocks (
|
||||||
|
height INTEGER PRIMARY KEY,
|
||||||
|
hash BLOB NOT NULL,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
sapling_tree BLOB NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS 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)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS 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)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS 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)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS 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)
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
// We should never down-migrate the first migration, as that can irreversibly
|
||||||
|
// destroy data.
|
||||||
|
panic!("Cannot revert the initial migration.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
//! Migration that adds support for unified full viewing keys.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rusqlite::{self, params, NO_PARAMS};
|
||||||
|
use schemer;
|
||||||
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
use secrecy::{ExposeSecret, SecretVec};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedSpendingKey};
|
||||||
|
use zcash_primitives::{consensus, zip32::AccountId};
|
||||||
|
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
use zcash_primitives::legacy::keys::IncomingViewingKey;
|
||||||
|
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
use zcash_client_backend::encoding::AddressCodec;
|
||||||
|
|
||||||
|
use crate::wallet::{
|
||||||
|
init::{migrations::utxos_table, WalletMigrationError},
|
||||||
|
PoolType,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
|
||||||
|
0xbe57ef3b,
|
||||||
|
0x388e,
|
||||||
|
0x42ea,
|
||||||
|
b"\x97\xe2\x67\x8d\xaf\xcf\x97\x54",
|
||||||
|
);
|
||||||
|
|
||||||
|
pub(super) struct Migration<P> {
|
||||||
|
pub(super) params: P,
|
||||||
|
pub(super) seed: Option<SecretVec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> schemer::Migration for Migration<P> {
|
||||||
|
fn id(&self) -> Uuid {
|
||||||
|
MIGRATION_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
|
[utxos_table::MIGRATION_ID].into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Add support for unified full viewing keys"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
|
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
//
|
||||||
|
// Update the accounts table to store ufvks rather than extfvks
|
||||||
|
//
|
||||||
|
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE accounts_new (
|
||||||
|
account INTEGER PRIMARY KEY,
|
||||||
|
ufvk TEXT NOT NULL,
|
||||||
|
address TEXT,
|
||||||
|
transparent_address TEXT
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut stmt_fetch_accounts =
|
||||||
|
transaction.prepare("SELECT account, address FROM accounts")?;
|
||||||
|
|
||||||
|
let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
// We only need to check for the presence of the seed if we have keys that
|
||||||
|
// need to be migrated; otherwise, it's fine to not supply the seed if this
|
||||||
|
// migration is being used to initialize an empty database.
|
||||||
|
if let Some(seed) = &self.seed {
|
||||||
|
let account: u32 = row.get(0)?;
|
||||||
|
let account = AccountId::from(account);
|
||||||
|
let usk =
|
||||||
|
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
|
||||||
|
.unwrap();
|
||||||
|
let ufvk = usk.to_unified_full_viewing_key();
|
||||||
|
|
||||||
|
let address: String = row.get(1)?;
|
||||||
|
let decoded =
|
||||||
|
RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
|
||||||
|
WalletMigrationError::CorruptedData(format!(
|
||||||
|
"Could not decode {} as a valid Zcash address.",
|
||||||
|
address
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
match decoded {
|
||||||
|
RecipientAddress::Shielded(decoded_address) => {
|
||||||
|
let dfvk = ufvk.sapling().expect(
|
||||||
|
"Derivation should have produced a UFVK containing a Sapling component.",
|
||||||
|
);
|
||||||
|
let (idx, expected_address) = dfvk.default_address();
|
||||||
|
if decoded_address != expected_address {
|
||||||
|
return Err(WalletMigrationError::CorruptedData(
|
||||||
|
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
|
||||||
|
address,
|
||||||
|
RecipientAddress::Shielded(expected_address).encode(&self.params),
|
||||||
|
idx)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecipientAddress::Transparent(_) => {
|
||||||
|
return Err(WalletMigrationError::CorruptedData(
|
||||||
|
"Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
|
||||||
|
}
|
||||||
|
RecipientAddress::Unified(decoded_address) => {
|
||||||
|
let (expected_address, idx) = ufvk.default_address();
|
||||||
|
if decoded_address != expected_address {
|
||||||
|
return Err(WalletMigrationError::CorruptedData(
|
||||||
|
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
|
||||||
|
address,
|
||||||
|
RecipientAddress::Unified(expected_address).encode(&self.params),
|
||||||
|
idx)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ufvk_str: String = ufvk.encode(&self.params);
|
||||||
|
let address_str: String = ufvk.default_address().0.encode(&self.params);
|
||||||
|
|
||||||
|
// This migration, and the wallet behaviour before it, stored the default
|
||||||
|
// transparent address in the `accounts` table. This does not necessarily
|
||||||
|
// match the transparent receiver in the default Unified Address. Starting
|
||||||
|
// from `AddressesTableMigration` below, we no longer store transparent
|
||||||
|
// addresses directly, but instead extract them from the Unified Address
|
||||||
|
// (or from the UFVK if the UA was derived without a transparent receiver,
|
||||||
|
// which is not the case for UAs generated by this crate).
|
||||||
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
let taddress_str: Option<String> = ufvk.transparent().and_then(|k| {
|
||||||
|
k.derive_external_ivk()
|
||||||
|
.ok()
|
||||||
|
.map(|k| k.default_address().0.encode(&self.params))
|
||||||
|
});
|
||||||
|
#[cfg(not(feature = "transparent-inputs"))]
|
||||||
|
let taddress_str: Option<String> = None;
|
||||||
|
|
||||||
|
transaction.execute_named(
|
||||||
|
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
|
||||||
|
VALUES (:account, :ufvk, :address, :transparent_address)",
|
||||||
|
&[
|
||||||
|
(":account", &<u32>::from(account)),
|
||||||
|
(":ufvk", &ufvk_str),
|
||||||
|
(":address", &address_str),
|
||||||
|
(":transparent_address", &taddress_str),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
return Err(WalletMigrationError::SeedRequired);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.execute_batch(
|
||||||
|
"DROP TABLE accounts;
|
||||||
|
ALTER TABLE accounts_new RENAME TO accounts;",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Update the sent_notes table to inclue an output_pool column that
|
||||||
|
// is respected by the uniqueness constraint
|
||||||
|
//
|
||||||
|
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE sent_notes_new (
|
||||||
|
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)
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// we query in a nested scope so that the col_names iterator is correctly
|
||||||
|
// dropped and doesn't maintain a lock on the table.
|
||||||
|
let has_output_pool = {
|
||||||
|
let mut stmt_fetch_columns = transaction.prepare("PRAGMA TABLE_INFO('sent_notes')")?;
|
||||||
|
let mut col_names = stmt_fetch_columns.query_map(NO_PARAMS, |row| {
|
||||||
|
let col_name: String = row.get(1)?;
|
||||||
|
Ok(col_name)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
col_names.any(|cname| cname == Ok("output_pool".to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_output_pool {
|
||||||
|
transaction.execute_batch(
|
||||||
|
"INSERT INTO sent_notes_new
|
||||||
|
(id_note, tx, output_pool, output_index, from_account, address, value, memo)
|
||||||
|
SELECT id_note, tx, output_pool, output_index, from_account, address, value, memo
|
||||||
|
FROM sent_notes;"
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
let mut stmt_fetch_sent_notes = transaction.prepare(
|
||||||
|
"SELECT id_note, tx, output_index, from_account, address, value, memo
|
||||||
|
FROM sent_notes",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut stmt_insert_sent_note = transaction.prepare(
|
||||||
|
"INSERT INTO sent_notes_new
|
||||||
|
(id_note, tx, output_pool, output_index, from_account, address, value, memo)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut rows = stmt_fetch_sent_notes.query(NO_PARAMS)?;
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let id_note: i64 = row.get(0)?;
|
||||||
|
let tx_ref: i64 = row.get(1)?;
|
||||||
|
let output_index: i64 = row.get(2)?;
|
||||||
|
let account_id: u32 = row.get(3)?;
|
||||||
|
let address: String = row.get(4)?;
|
||||||
|
let value: i64 = row.get(5)?;
|
||||||
|
let memo: Option<Vec<u8>> = row.get(6)?;
|
||||||
|
|
||||||
|
let decoded_address =
|
||||||
|
RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
|
||||||
|
WalletMigrationError::CorruptedData(format!(
|
||||||
|
"Could not decode {} as a valid Zcash address.",
|
||||||
|
address
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let output_pool = match decoded_address {
|
||||||
|
RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()),
|
||||||
|
RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()),
|
||||||
|
RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData(
|
||||||
|
"Unified addresses should not yet appear in the sent_notes table."
|
||||||
|
.to_string(),
|
||||||
|
)),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
stmt_insert_sent_note.execute(params![
|
||||||
|
id_note,
|
||||||
|
tx_ref,
|
||||||
|
output_pool,
|
||||||
|
output_index,
|
||||||
|
account_id,
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
memo
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.execute_batch(
|
||||||
|
"DROP TABLE sent_notes;
|
||||||
|
ALTER TABLE sent_notes_new RENAME TO sent_notes;",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
// TODO: something better than just panic?
|
||||||
|
panic!("Cannot revert this migration.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
//! The migration that adds initial support for transparent UTXOs to the wallet.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use rusqlite;
|
||||||
|
use schemer;
|
||||||
|
use schemer_rusqlite::RusqliteMigration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::wallet::init::{migrations::initial_setup, WalletMigrationError};
|
||||||
|
|
||||||
|
pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
|
||||||
|
0xa2e0ed2e,
|
||||||
|
0x8852,
|
||||||
|
0x475e,
|
||||||
|
b"\xb0\xa4\xf1\x54\xb1\x5b\x9d\xbe",
|
||||||
|
);
|
||||||
|
|
||||||
|
pub(super) struct Migration;
|
||||||
|
|
||||||
|
impl schemer::Migration for Migration {
|
||||||
|
fn id(&self) -> Uuid {
|
||||||
|
MIGRATION_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> HashSet<Uuid> {
|
||||||
|
[initial_setup::MIGRATION_ID].into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Add support for receiving transparent UTXOs."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RusqliteMigration for Migration {
|
||||||
|
type Error = WalletMigrationError;
|
||||||
|
|
||||||
|
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
transaction.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS 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)
|
||||||
|
);",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
|
||||||
|
transaction.execute_batch("DROP TABLE utxos;")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue