use std::{collections::HashSet, rc::Rc};
use rusqlite::{named_params, params};
use schemerz_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid;
use zcash_keys::{
address::Address,
keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedSpendingKey},
};
use zcash_protocol::{PoolType, consensus};
use zip32::AccountId;
#[cfg(feature = "transparent-inputs")]
use ::transparent::keys::IncomingViewingKey;
#[cfg(feature = "transparent-inputs")]
use zcash_keys::encoding::AddressCodec;
use crate::{
UA_TRANSPARENT,
wallet::{
init::{WalletMigrationError, migrations::initial_setup},
pool_code,
},
};
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0xbe57ef3b_388e_42ea_97e2_678dafcf9754);
const DEPENDENCIES: &[Uuid] = &[initial_setup::MIGRATION_ID];
pub(super) struct Migration<P> {
pub(super) params: P,
pub(super) seed: Option<Rc<SecretVec<u8>>>,
}
impl<P> schemerz::Migration<Uuid> for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
DEPENDENCIES.iter().copied().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> {
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 seed_is_relevant = false;
let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? {
if let Some(seed) = &self.seed {
let account: u32 = row.get(0)?;
let account = AccountId::try_from(account).map_err(|_| {
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
})?;
let usk =
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
.map_err(|_| {
if seed_is_relevant {
WalletMigrationError::CorruptedData(
"Unable to derive spending key from seed.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
}
})?;
let ufvk = usk.to_unified_full_viewing_key();
let address: String = row.get(1)?;
let decoded = Address::decode(&self.params, &address).ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {address} as a valid Zcash address."
))
})?;
match decoded {
Address::Sapling(decoded_address) => {
let dfvk = ufvk.sapling().ok_or_else(||
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
let (idx, expected_address) = dfvk.default_address();
if decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(format!(
"Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
address,
Address::Sapling(expected_address).encode(&self.params),
idx
))
} else {
WalletMigrationError::SeedNotRelevant
});
}
}
Address::Transparent(_) | Address::Tex(_) => {
return Err(WalletMigrationError::CorruptedData(
"Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
}
Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?;
if decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(format!(
"Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address,
Address::Unified(expected_address).encode(&self.params),
idx
))
} else {
WalletMigrationError::SeedNotRelevant
});
}
}
}
seed_is_relevant = true;
let ufvk_str: String = ufvk.encode(&self.params);
let address_str: String = ufvk.default_address(ua_request)?.0.encode(&self.params);
#[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(
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
VALUES (:account, :ufvk, :address, :transparent_address)",
named_params![
":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;",
)?;
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)
);",
)?;
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([], |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([])?;
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 = Address::decode(&self.params, &address).ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {address} as a valid Zcash address."
))
})?;
let output_pool = match decoded_address {
Address::Sapling(_) => Ok(pool_code(PoolType::SAPLING)),
Address::Transparent(_) | Address::Tex(_) => {
Ok(pool_code(PoolType::TRANSPARENT))
}
Address::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> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
#[cfg(test)]
mod tests {
use crate::wallet::init::migrations::tests::test_migrate;
#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}
}