Merge pull request #1258 from nuttycom/sqlite_wallet/fuzzy_key_matching
Return partial matches when using `WalletRead::get_account_for_ufvk`.
This commit is contained in:
commit
5e810d3689
|
@ -13,6 +13,7 @@ and this library adheres to Rust's notion of
|
|||
changes related to `Orchard` below are introduced under this feature
|
||||
flag.
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `Account`
|
||||
- `AccountBalance::with_orchard_balance_mut`
|
||||
- `AccountBirthday::orchard_frontier`
|
||||
- `BlockMetadata::orchard_tree_size`
|
||||
|
@ -49,7 +50,10 @@ and this library adheres to Rust's notion of
|
|||
- Arguments to `BlockMetadata::from_parts` have changed.
|
||||
- Arguments to `ScannedBlock::from_parts` have changed.
|
||||
- Changes to the `WalletRead` trait:
|
||||
- Added `Account` associated type.
|
||||
- Added `get_orchard_nullifiers`
|
||||
- `get_account_for_ufvk` now returns an `Self::Account` instead of a bare
|
||||
`AccountId`
|
||||
- Changes to the `InputSource` trait:
|
||||
- `select_spendable_notes` now takes its `target_value` argument as a
|
||||
`NonNegativeAmount`. Also, the values of the returned map are also
|
||||
|
|
|
@ -311,6 +311,35 @@ impl AccountBalance {
|
|||
}
|
||||
}
|
||||
|
||||
/// A set of capabilities that a client account must provide.
|
||||
pub trait Account<AccountId: Copy> {
|
||||
/// Returns the unique identifier for the account.
|
||||
fn id(&self) -> AccountId;
|
||||
|
||||
/// Returns the UFVK that the wallet backend has stored for the account, if any.
|
||||
fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
|
||||
}
|
||||
|
||||
impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
|
||||
fn id(&self) -> A {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
|
||||
Some(&self.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) {
|
||||
fn id(&self) -> A {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
|
||||
self.1.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A polymorphic ratio type, usually used for rational numbers.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Ratio<T> {
|
||||
|
@ -507,6 +536,9 @@ pub trait WalletRead {
|
|||
/// will be interpreted as belonging to that account.
|
||||
type AccountId: Copy + Debug + Eq + Hash;
|
||||
|
||||
/// The concrete account type used by this wallet backend.
|
||||
type Account: Account<Self::AccountId>;
|
||||
|
||||
/// Verifies that the given seed corresponds to the viewing key for the specified account.
|
||||
///
|
||||
/// Returns:
|
||||
|
@ -617,11 +649,11 @@ pub trait WalletRead {
|
|||
&self,
|
||||
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>;
|
||||
|
||||
/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], if any.
|
||||
/// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
|
||||
fn get_account_for_ufvk(
|
||||
&self,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<Self::AccountId>, Self::Error>;
|
||||
) -> Result<Option<Self::Account>, Self::Error>;
|
||||
|
||||
/// Returns the wallet balances and sync status for an account given the specified minimum
|
||||
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
|
||||
|
@ -1466,6 +1498,7 @@ pub mod testing {
|
|||
impl WalletRead for MockWalletDb {
|
||||
type Error = ();
|
||||
type AccountId = u32;
|
||||
type Account = (Self::AccountId, UnifiedFullViewingKey);
|
||||
|
||||
fn validate_seed(
|
||||
&self,
|
||||
|
@ -1551,7 +1584,7 @@ pub mod testing {
|
|||
fn get_account_for_ufvk(
|
||||
&self,
|
||||
_ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<Self::AccountId>, Self::Error> {
|
||||
) -> Result<Option<Self::Account>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ use super::InputSource;
|
|||
use crate::{
|
||||
address::Address,
|
||||
data_api::{
|
||||
error::Error, SentTransaction, SentTransactionOutput, WalletCommitmentTrees, WalletRead,
|
||||
WalletWrite,
|
||||
error::Error, Account, SentTransaction, SentTransactionOutput, WalletCommitmentTrees,
|
||||
WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{self, DustOutputPolicy},
|
||||
|
@ -269,7 +269,7 @@ where
|
|||
wallet_db,
|
||||
params,
|
||||
StandardFeeRule::PreZip313,
|
||||
account,
|
||||
account.id(),
|
||||
min_confirmations,
|
||||
to,
|
||||
amount,
|
||||
|
@ -380,7 +380,7 @@ where
|
|||
let proposal = propose_transfer(
|
||||
wallet_db,
|
||||
params,
|
||||
account,
|
||||
account.id(),
|
||||
input_selector,
|
||||
request,
|
||||
min_confirmations,
|
||||
|
@ -694,7 +694,8 @@ where
|
|||
let account = wallet_db
|
||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
|
||||
.map_err(Error::DataSource)?
|
||||
.ok_or(Error::KeyNotRecognized)?;
|
||||
.ok_or(Error::KeyNotRecognized)?
|
||||
.id();
|
||||
|
||||
let (sapling_anchor, sapling_inputs) =
|
||||
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
|
||||
|
|
|
@ -283,6 +283,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
|
||||
type Error = SqliteClientError;
|
||||
type AccountId = AccountId;
|
||||
type Account = (AccountId, Option<UnifiedFullViewingKey>);
|
||||
|
||||
fn validate_seed(
|
||||
&self,
|
||||
|
@ -400,7 +401,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
|
|||
fn get_account_for_ufvk(
|
||||
&self,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<AccountId>, Self::Error> {
|
||||
) -> Result<Option<Self::Account>, Self::Error> {
|
||||
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
|
||||
}
|
||||
|
||||
|
|
|
@ -295,7 +295,7 @@ struct AccountSqlValues<'a> {
|
|||
account_type: u32,
|
||||
hd_seed_fingerprint: Option<&'a [u8]>,
|
||||
hd_account_index: Option<u32>,
|
||||
ufvk: Option<String>,
|
||||
ufvk: Option<&'a UnifiedFullViewingKey>,
|
||||
uivk: String,
|
||||
}
|
||||
|
||||
|
@ -309,14 +309,14 @@ fn get_sql_values_for_account_parameters<'a, P: consensus::Parameters>(
|
|||
account_type: AccountType::Zip32.into(),
|
||||
hd_seed_fingerprint: Some(hdaccount.hd_seed_fingerprint().as_bytes()),
|
||||
hd_account_index: Some(hdaccount.account_index().into()),
|
||||
ufvk: Some(hdaccount.ufvk().encode(params)),
|
||||
ufvk: Some(hdaccount.ufvk()),
|
||||
uivk: ufvk_to_uivk(hdaccount.ufvk(), params)?,
|
||||
},
|
||||
Account::Imported(ImportedAccount::Full(ufvk)) => AccountSqlValues {
|
||||
account_type: AccountType::Imported.into(),
|
||||
hd_seed_fingerprint: None,
|
||||
hd_account_index: None,
|
||||
ufvk: Some(ufvk.encode(params)),
|
||||
ufvk: Some(ufvk),
|
||||
uivk: ufvk_to_uivk(ufvk, params)?,
|
||||
},
|
||||
Account::Imported(ImportedAccount::Incoming(uivk)) => AccountSqlValues {
|
||||
|
@ -361,21 +361,49 @@ pub(crate) fn add_account<P: consensus::Parameters>(
|
|||
birthday: AccountBirthday,
|
||||
) -> Result<AccountId, SqliteClientError> {
|
||||
let args = get_sql_values_for_account_parameters(&account, params)?;
|
||||
let account_id: AccountId = conn.query_row(r#"
|
||||
INSERT INTO accounts (account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk, birthday_height, recover_until_height)
|
||||
VALUES (:account_type, :hd_seed_fingerprint, :hd_account_index, :ufvk, :uivk, :birthday_height, :recover_until_height)
|
||||
|
||||
let orchard_item = args
|
||||
.ufvk
|
||||
.and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes()));
|
||||
let sapling_item = args
|
||||
.ufvk
|
||||
.and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes()));
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent_item = args
|
||||
.ufvk
|
||||
.and_then(|ufvk| ufvk.transparent().map(|k| k.serialize()));
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_item: Option<Vec<u8>> = None;
|
||||
|
||||
let account_id: AccountId = conn.query_row(
|
||||
r#"
|
||||
INSERT INTO accounts (
|
||||
account_type, hd_seed_fingerprint, hd_account_index,
|
||||
ufvk, uivk,
|
||||
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
|
||||
birthday_height, recover_until_height
|
||||
)
|
||||
VALUES (
|
||||
:account_type, :hd_seed_fingerprint, :hd_account_index,
|
||||
:ufvk, :uivk,
|
||||
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
|
||||
:birthday_height, :recover_until_height
|
||||
)
|
||||
RETURNING id;
|
||||
"#,
|
||||
named_params![
|
||||
":account_type": args.account_type,
|
||||
":hd_seed_fingerprint": args.hd_seed_fingerprint,
|
||||
":hd_account_index": args.hd_account_index,
|
||||
":ufvk": args.ufvk,
|
||||
":ufvk": args.ufvk.map(|ufvk| ufvk.encode(params)),
|
||||
":uivk": args.uivk,
|
||||
":orchard_fvk_item_cache": orchard_item,
|
||||
":sapling_fvk_item_cache": sapling_item,
|
||||
":p2pkh_fvk_item_cache": transparent_item,
|
||||
":birthday_height": u32::from(birthday.height()),
|
||||
":recover_until_height": birthday.recover_until().map(u32::from)
|
||||
],
|
||||
|row| Ok(AccountId(row.get(0)?))
|
||||
|row| Ok(AccountId(row.get(0)?)),
|
||||
)?;
|
||||
|
||||
// If a birthday frontier is available, insert it into the note commitment tree. If the
|
||||
|
@ -703,17 +731,52 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
|
|||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
ufvk: &UnifiedFullViewingKey,
|
||||
) -> Result<Option<AccountId>, SqliteClientError> {
|
||||
conn.query_row(
|
||||
"SELECT id FROM accounts WHERE ufvk = ?",
|
||||
[&ufvk.encode(params)],
|
||||
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent_item = ufvk.transparent().map(|k| k.serialize());
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_item: Option<Vec<u8>> = None;
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, ufvk
|
||||
FROM accounts
|
||||
WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache
|
||||
OR sapling_fvk_item_cache = :sapling_fvk_item_cache
|
||||
OR p2pkh_fvk_item_cache = :p2pkh_fvk_item_cache",
|
||||
)?;
|
||||
|
||||
let accounts = stmt
|
||||
.query_and_then::<_, SqliteClientError, _, _>(
|
||||
named_params![
|
||||
":orchard_fvk_item_cache": ufvk.orchard().map(|k| k.to_bytes()),
|
||||
":sapling_fvk_item_cache": ufvk.sapling().map(|k| k.to_bytes()),
|
||||
":p2pkh_fvk_item_cache": transparent_item,
|
||||
],
|
||||
|row| {
|
||||
let acct = row.get(0)?;
|
||||
Ok(AccountId(acct))
|
||||
let account_id = row.get::<_, u32>(0).map(AccountId)?;
|
||||
Ok((
|
||||
account_id,
|
||||
row.get::<_, Option<String>>(1)?
|
||||
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str))
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
SqliteClientError::CorruptedData(format!(
|
||||
"Could not decode unified full viewing key for account {:?}: {}",
|
||||
account_id, e
|
||||
))
|
||||
})?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if accounts.len() > 1 {
|
||||
Err(SqliteClientError::CorruptedData(
|
||||
"Mutiple account records matched the provided UFVK".to_owned(),
|
||||
))
|
||||
} else {
|
||||
Ok(accounts.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ScanProgress {
|
||||
|
|
|
@ -229,6 +229,9 @@ mod tests {
|
|||
hd_account_index INTEGER,
|
||||
ufvk TEXT,
|
||||
uivk TEXT NOT NULL,
|
||||
orchard_fvk_item_cache BLOB,
|
||||
sapling_fvk_item_cache BLOB,
|
||||
p2pkh_fvk_item_cache BLOB,
|
||||
birthday_height INTEGER NOT NULL,
|
||||
recover_until_height INTEGER,
|
||||
CHECK ( (account_type = 0 AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) OR (account_type = 1 AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) )
|
||||
|
|
|
@ -58,6 +58,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
hd_account_index INTEGER,
|
||||
ufvk TEXT,
|
||||
uivk TEXT NOT NULL,
|
||||
orchard_fvk_item_cache BLOB,
|
||||
sapling_fvk_item_cache BLOB,
|
||||
p2pkh_fvk_item_cache BLOB,
|
||||
birthday_height INTEGER NOT NULL,
|
||||
recover_until_height INTEGER,
|
||||
CHECK (
|
||||
|
@ -116,19 +119,40 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
let uivk = ufvk_to_uivk(&ufvk_parsed, &self.params)
|
||||
.map_err(|e| WalletMigrationError::CorruptedData(e.to_string()))?;
|
||||
|
||||
transaction.execute(r#"
|
||||
INSERT INTO accounts_new (id, account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk, birthday_height, recover_until_height)
|
||||
VALUES (:account_id, :account_type, :seed_id, :account_index, :ufvk, :uivk, :birthday_height, :recover_until_height);
|
||||
"#, named_params![
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize());
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_item: Option<Vec<u8>> = None;
|
||||
|
||||
transaction.execute(
|
||||
r#"
|
||||
INSERT INTO accounts_new (
|
||||
id, account_type, hd_seed_fingerprint, hd_account_index,
|
||||
ufvk, uivk,
|
||||
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
|
||||
birthday_height, recover_until_height
|
||||
)
|
||||
VALUES (
|
||||
:account_id, :account_type, :seed_id, :account_index,
|
||||
:ufvk, :uivk,
|
||||
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
|
||||
:birthday_height, :recover_until_height
|
||||
);
|
||||
"#,
|
||||
named_params![
|
||||
":account_id": account_id,
|
||||
":account_type": account_type,
|
||||
":seed_id": seed_id.as_bytes(),
|
||||
":account_index": account_index,
|
||||
":ufvk": ufvk,
|
||||
":uivk": uivk,
|
||||
":orchard_fvk_item_cache": ufvk_parsed.orchard().map(|k| k.to_bytes()),
|
||||
":sapling_fvk_item_cache": ufvk_parsed.sapling().map(|k| k.to_bytes()),
|
||||
":p2pkh_fvk_item_cache": transparent_item,
|
||||
":birthday_height": birthday_height,
|
||||
":recover_until_height": recover_until_height,
|
||||
])?;
|
||||
],
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
return Err(WalletMigrationError::SeedRequired);
|
||||
|
|
Loading…
Reference in New Issue