use std::collections::HashSet;
use rusqlite::{Transaction, named_params};
use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_keys::{
address::{Address, UnifiedAddress},
encoding::AddressCodec,
keys::{ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey},
};
use zcash_protocol::consensus;
use zip32::{AccountId, DiversifierIndex};
use crate::{UA_TRANSPARENT, wallet::init::WalletMigrationError};
#[cfg(feature = "transparent-inputs")]
use ::transparent::keys::IncomingViewingKey;
use super::ufvk_support;
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0xd956978c_9c87_4d6e_815d_fb8f088d094c);
const DEPENDENCIES: &[Uuid] = &[ufvk_support::MIGRATION_ID];
pub(crate) struct Migration<P: consensus::Parameters> {
pub(crate) params: P,
}
impl<P: consensus::Parameters> 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 {
"Adds the addresses table for tracking diversified UAs"
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(
"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 accounts_new (
account INTEGER PRIMARY KEY,
ufvk TEXT NOT NULL
);",
)?;
let mut stmt_fetch_accounts = transaction
.prepare("SELECT account, ufvk, address, transparent_address FROM accounts")?;
let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? {
let account = AccountId::try_from(row.get::<_, u32>(0)?).map_err(|_| {
WalletMigrationError::CorruptedData("Invalid ZIP-32 account index.".to_owned())
})?;
let ufvk_str: String = row.get(1)?;
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
.map_err(WalletMigrationError::CorruptedData)?;
let address: String = row.get(2)?;
let decoded = Address::decode(&self.params, &address).ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {address} as a valid Zcash address."
))
})?;
let decoded_address = if let Address::Unified(ua) = decoded {
ua
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in accounts table was not a Unified Address.".to_string(),
));
};
let (expected_address, idx) = ufvk.default_address(
UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT),
)?;
if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData(format!(
"Decoded UA {} does not match the UFVK's default address {} at {:?}.",
address,
Address::Unified(expected_address).encode(&self.params),
idx,
)));
}
if let Some(transparent_address) = row.get::<_, Option<String>>(3)? {
let decoded_transparent = Address::decode(&self.params, &transparent_address)
.ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {address} as a valid Zcash address."
))
})?;
let decoded_transparent_address = if let Address::Transparent(addr) =
decoded_transparent
{
addr
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in transparent_address column of accounts table was not a transparent address.".to_string(),
));
};
#[cfg(feature = "transparent-inputs")]
{
let expected_address = ufvk
.transparent()
.and_then(|k| k.derive_external_ivk().ok().map(|k| k.default_address().0));
if Some(decoded_transparent_address) != expected_address {
return Err(WalletMigrationError::CorruptedData(format!(
"Decoded transparent address {transparent_address} is not the default transparent address.",
)));
}
}
#[cfg(not(feature = "transparent-inputs"))]
{
let _ = decoded_transparent_address;
return Err(WalletMigrationError::CorruptedData(
"Database needs transparent-inputs feature flag enabled to migrate"
.to_string(),
));
}
}
transaction.execute(
"INSERT INTO accounts_new (account, ufvk)
VALUES (:account, :ufvk)",
named_params![
":account": u32::from(account),
":ufvk": ufvk.encode(&self.params),
],
)?;
let (address, d_idx) = ufvk.default_address(UnifiedAddressRequest::unsafe_custom(
Omit,
Require,
UA_TRANSPARENT,
))?;
insert_address(transaction, &self.params, account, d_idx, &address)?;
}
transaction.execute_batch(
"DROP TABLE accounts;
ALTER TABLE accounts_new RENAME TO accounts;",
)?;
Ok(())
}
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}
fn insert_address<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
account: AccountId,
diversifier_index: DiversifierIndex,
address: &UnifiedAddress,
) -> Result<(), rusqlite::Error> {
let mut stmt = conn.prepare_cached(
"INSERT INTO addresses (
account,
diversifier_index_be,
address,
cached_transparent_receiver_address
)
VALUES (
:account,
:diversifier_index_be,
:address,
:cached_transparent_receiver_address
)",
)?;
let mut di_be = *diversifier_index.as_bytes();
di_be.reverse();
stmt.execute(named_params![
":account": u32::from(account),
":diversifier_index_be": &di_be[..],
":address": &address.encode(params),
":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)),
])?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::wallet::init::migrations::tests::test_migrate;
#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}
}