Merge pull request #1059 from zcash/1044-extract-zip32
Extract `zip32` crate again
This commit is contained in:
commit
a9d6505148
|
@ -3114,6 +3114,7 @@ dependencies = [
|
|||
"zcash_address",
|
||||
"zcash_encoding",
|
||||
"zcash_note_encryption",
|
||||
"zip32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3177,3 +3178,14 @@ dependencies = [
|
|||
"quote",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip32"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22"
|
||||
dependencies = [
|
||||
"blake2b_simd",
|
||||
"memuse",
|
||||
"subtle",
|
||||
]
|
||||
|
|
|
@ -102,6 +102,7 @@ rand_xorshift = "0.3"
|
|||
# ZIP 32
|
||||
aes = "0.8"
|
||||
fpe = "0.6"
|
||||
zip32 = "0.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
|
|
@ -18,16 +18,11 @@ fn parse_viewing_key(s: &str) -> Result<(ExtendedFullViewingKey, bool), &'static
|
|||
|
||||
fn parse_diversifier_index(s: &str) -> Result<DiversifierIndex, &'static str> {
|
||||
let i: u128 = s.parse().map_err(|_| "Diversifier index is not a number")?;
|
||||
if i >= (1 << 88) {
|
||||
return Err("Diversifier index too large");
|
||||
}
|
||||
Ok(DiversifierIndex(i.to_le_bytes()[..11].try_into().unwrap()))
|
||||
DiversifierIndex::try_from(i).map_err(|_| "Diversifier index too large")
|
||||
}
|
||||
|
||||
fn encode_diversifier_index(di: &DiversifierIndex) -> u128 {
|
||||
let mut bytes = [0; 16];
|
||||
bytes[..11].copy_from_slice(&di.0);
|
||||
u128::from_le_bytes(bytes)
|
||||
(*di).into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
|
|
@ -253,7 +253,7 @@ impl RecipientAddress {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use zcash_address::test_vectors;
|
||||
use zcash_primitives::consensus::MAIN_NETWORK;
|
||||
use zcash_primitives::{consensus::MAIN_NETWORK, zip32::AccountId};
|
||||
|
||||
use super::{RecipientAddress, UnifiedAddress};
|
||||
use crate::keys::sapling;
|
||||
|
@ -267,7 +267,7 @@ mod tests {
|
|||
};
|
||||
|
||||
let sapling = {
|
||||
let extsk = sapling::spending_key(&[0; 32], 0, 0.into());
|
||||
let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO);
|
||||
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
Some(dfvk.default_address().1)
|
||||
};
|
||||
|
|
|
@ -1274,7 +1274,7 @@ pub mod testing {
|
|||
seed: &SecretVec<u8>,
|
||||
_birthday: AccountBirthday,
|
||||
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
|
||||
.map(|k| (account, k))
|
||||
.map_err(|_| ())
|
||||
|
|
|
@ -185,17 +185,17 @@ impl<P: consensus::Parameters> AddressCodec<P> for UnifiedAddress {
|
|||
/// keys::sapling,
|
||||
/// };
|
||||
///
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::from(0));
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO);
|
||||
/// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk);
|
||||
/// ```
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::sapling::zip32::ExtendedSpendingKey
|
||||
pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> String {
|
||||
bech32_encode(hrp, |w| extsk.write(w))
|
||||
}
|
||||
|
||||
/// Decodes an [`ExtendedSpendingKey`] from a Bech32-encoded string.
|
||||
///
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::sapling::zip32::ExtendedSpendingKey
|
||||
pub fn decode_extended_spending_key(
|
||||
hrp: &str,
|
||||
s: &str,
|
||||
|
@ -210,26 +210,26 @@ pub fn decode_extended_spending_key(
|
|||
/// ```
|
||||
/// use zcash_primitives::{
|
||||
/// constants::testnet::{COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY},
|
||||
/// sapling::zip32::ExtendedFullViewingKey,
|
||||
/// zip32::AccountId,
|
||||
/// };
|
||||
/// use zcash_client_backend::{
|
||||
/// encoding::encode_extended_full_viewing_key,
|
||||
/// keys::sapling,
|
||||
/// };
|
||||
/// use zcash_primitives::zip32::ExtendedFullViewingKey;
|
||||
///
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::from(0));
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO);
|
||||
/// let extfvk = extsk.to_extended_full_viewing_key();
|
||||
/// let encoded = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk);
|
||||
/// ```
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::sapling::zip32::ExtendedFullViewingKey
|
||||
pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingKey) -> String {
|
||||
bech32_encode(hrp, |w| extfvk.write(w))
|
||||
}
|
||||
|
||||
/// Decodes an [`ExtendedFullViewingKey`] from a Bech32-encoded string.
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::sapling::zip32::ExtendedFullViewingKey
|
||||
pub fn decode_extended_full_viewing_key(
|
||||
hrp: &str,
|
||||
s: &str,
|
||||
|
|
|
@ -51,9 +51,9 @@ pub mod sapling {
|
|||
/// keys::sapling,
|
||||
/// };
|
||||
///
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::from(0));
|
||||
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, AccountId::ZERO);
|
||||
/// ```
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: zcash_primitives::sapling::zip32::ExtendedSpendingKey
|
||||
pub fn spending_key(seed: &[u8], coin_type: u32, account: AccountId) -> ExtendedSpendingKey {
|
||||
if seed.len() < 32 {
|
||||
panic!("ZIP 32 seeds MUST be at least 32 bytes");
|
||||
|
@ -72,7 +72,7 @@ pub mod sapling {
|
|||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn to_transparent_child_index(j: DiversifierIndex) -> Option<u32> {
|
||||
let (low_4_bytes, rest) = j.0.split_at(4);
|
||||
let (low_4_bytes, rest) = j.as_bytes().split_at(4);
|
||||
let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap());
|
||||
if transparent_j > (0x7FFFFFFF) || rest.iter().any(|b| b != &0) {
|
||||
None
|
||||
|
@ -588,7 +588,11 @@ pub mod testing {
|
|||
prop::array::uniform32(prop::num::u8::ANY).prop_flat_map(move |seed| {
|
||||
prop::num::u32::ANY
|
||||
.prop_map(move |account| {
|
||||
UnifiedSpendingKey::from_seed(¶ms, &seed, AccountId::from(account))
|
||||
UnifiedSpendingKey::from_seed(
|
||||
¶ms,
|
||||
&seed,
|
||||
AccountId::try_from(account & ((1 << 31) - 1)).unwrap(),
|
||||
)
|
||||
})
|
||||
.prop_filter("seeds must generate valid USKs", |v| v.is_ok())
|
||||
.prop_map(|v| v.unwrap())
|
||||
|
@ -632,14 +636,14 @@ mod tests {
|
|||
#[test]
|
||||
#[should_panic]
|
||||
fn spending_key_panics_on_short_seed() {
|
||||
let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::from(0));
|
||||
let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::ZERO);
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
#[test]
|
||||
fn pk_to_taddr() {
|
||||
let taddr =
|
||||
legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::from(0))
|
||||
legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO)
|
||||
.unwrap()
|
||||
.to_account_pubkey()
|
||||
.derive_external_ivk()
|
||||
|
@ -652,7 +656,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn ufvk_round_trip() {
|
||||
let account = 0.into();
|
||||
let account = AccountId::ZERO;
|
||||
|
||||
let orchard = {
|
||||
let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap();
|
||||
|
@ -666,8 +670,7 @@ mod tests {
|
|||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent = {
|
||||
let privkey =
|
||||
AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], AccountId::from(0)).unwrap();
|
||||
let privkey = AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], account).unwrap();
|
||||
Some(privkey.to_account_pubkey())
|
||||
};
|
||||
|
||||
|
@ -726,7 +729,7 @@ mod tests {
|
|||
let usk = UnifiedSpendingKey::from_seed(
|
||||
&MAIN_NETWORK,
|
||||
&tv.root_seed,
|
||||
AccountId::from(tv.account),
|
||||
AccountId::try_from(tv.account).unwrap(),
|
||||
)
|
||||
.expect("seed produced a valid unified spending key");
|
||||
|
||||
|
|
|
@ -244,7 +244,7 @@ impl fmt::Display for ScanError {
|
|||
/// [`WalletSaplingOutput`]s, whereas the implementation for [`SaplingIvk`] cannot
|
||||
/// do so and will return the unit value in those outputs instead.
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::sapling::zip32::ExtendedFullViewingKey
|
||||
/// [`SaplingIvk`]: zcash_primitives::sapling::SaplingIvk
|
||||
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
|
||||
/// [`ScanningKey`]: crate::scanning::ScanningKey
|
||||
|
@ -465,10 +465,9 @@ pub(crate) fn scan_block_with_runner<
|
|||
let spend = nullifiers
|
||||
.iter()
|
||||
.map(|&(account, nf)| CtOption::new(account, nf.ct_eq(&spend_nf)))
|
||||
.fold(
|
||||
CtOption::new(AccountId::from(0), 0.into()),
|
||||
|first, next| CtOption::conditional_select(&next, &first, first.is_some()),
|
||||
)
|
||||
.fold(CtOption::new(AccountId::ZERO, 0.into()), |first, next| {
|
||||
CtOption::conditional_select(&next, &first, first.is_some())
|
||||
})
|
||||
.map(|account| WalletSaplingSpend::from_parts(index, spend_nf, account));
|
||||
|
||||
if spend.is_some().into() {
|
||||
|
@ -808,7 +807,7 @@ mod tests {
|
|||
#[test]
|
||||
fn scan_block_with_my_tx() {
|
||||
fn go(scan_multithreaded: bool) {
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
|
||||
|
@ -893,7 +892,7 @@ mod tests {
|
|||
#[test]
|
||||
fn scan_block_with_txs_after_my_tx() {
|
||||
fn go(scan_multithreaded: bool) {
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
|
||||
|
@ -928,7 +927,7 @@ mod tests {
|
|||
let scanned_block = scan_block_with_runner(
|
||||
&Network::TestNetwork,
|
||||
cb,
|
||||
&[(&AccountId::from(0), &dfvk)],
|
||||
&[(&AccountId::ZERO, &dfvk)],
|
||||
&[],
|
||||
None,
|
||||
batch_runner.as_mut(),
|
||||
|
@ -942,7 +941,7 @@ mod tests {
|
|||
assert_eq!(tx.sapling_spends.len(), 0);
|
||||
assert_eq!(tx.sapling_outputs.len(), 1);
|
||||
assert_eq!(tx.sapling_outputs[0].index(), 0);
|
||||
assert_eq!(tx.sapling_outputs[0].account(), AccountId::from(0));
|
||||
assert_eq!(tx.sapling_outputs[0].account(), AccountId::ZERO);
|
||||
assert_eq!(tx.sapling_outputs[0].note().value().inner(), 5);
|
||||
|
||||
assert_eq!(
|
||||
|
@ -971,7 +970,7 @@ mod tests {
|
|||
let extsk = ExtendedSpendingKey::master(&[]);
|
||||
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
let nf = Nullifier([7; 32]);
|
||||
let account = AccountId::from(12);
|
||||
let account = AccountId::try_from(12).unwrap();
|
||||
|
||||
let cb = fake_compact_block(
|
||||
1u32.into(),
|
||||
|
|
|
@ -333,7 +333,7 @@ pub enum OvkPolicy {
|
|||
/// Transaction outputs will be decryptable by the sender, in addition to the
|
||||
/// recipients.
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::sapling::zip32::ExtendedFullViewingKey
|
||||
Sender,
|
||||
|
||||
/// Use a custom outgoing viewing key. This might for instance be derived from a
|
||||
|
|
|
@ -455,7 +455,7 @@ mod tests {
|
|||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
|
||||
|
@ -466,7 +466,7 @@ mod tests {
|
|||
|
||||
// Account balance should be unaltered
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
|
||||
|
@ -476,14 +476,14 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Account balance should only contain the first received note
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), value);
|
||||
|
||||
// Scan the cache again
|
||||
st.scan_cached_blocks(h, 2);
|
||||
|
||||
// Account balance should again reflect both received notes
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -502,7 +502,7 @@ mod tests {
|
|||
let value = NonNegativeAmount::const_from_u64(50000);
|
||||
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
st.scan_cached_blocks(h1, 1);
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), value);
|
||||
|
||||
// Create blocks to reach SAPLING_ACTIVATION_HEIGHT + 2
|
||||
let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
|
||||
|
@ -514,7 +514,7 @@ mod tests {
|
|||
// Now scan the block of height SAPLING_ACTIVATION_HEIGHT + 1
|
||||
st.scan_cached_blocks(h2, 1);
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
NonNegativeAmount::const_from_u64(150_000)
|
||||
);
|
||||
|
||||
|
@ -567,7 +567,7 @@ mod tests {
|
|||
assert_eq!(summary.received_sapling_note_count(), 1);
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), value);
|
||||
|
||||
// Create a second fake CompactBlock sending more value to the address
|
||||
let value2 = NonNegativeAmount::const_from_u64(7);
|
||||
|
@ -581,7 +581,7 @@ mod tests {
|
|||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value + value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -606,7 +606,7 @@ mod tests {
|
|||
st.scan_cached_blocks(received_height, 1);
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), value);
|
||||
|
||||
// Create a second fake CompactBlock spending value from the address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[0]);
|
||||
|
@ -619,7 +619,7 @@ mod tests {
|
|||
|
||||
// Account balance should equal the change
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value - value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -652,7 +652,7 @@ mod tests {
|
|||
|
||||
// Account balance should equal the change
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value - value2).unwrap()
|
||||
);
|
||||
|
||||
|
@ -661,7 +661,7 @@ mod tests {
|
|||
|
||||
// Account balance should be the same.
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(0)),
|
||||
st.get_total_balance(AccountId::ZERO),
|
||||
(value - value2).unwrap()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -385,12 +385,9 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
|
||||
self.transactionally(|wdb| {
|
||||
let account = wallet::get_max_account_id(wdb.conn.0)?
|
||||
.map(|a| AccountId::from(u32::from(a) + 1))
|
||||
.unwrap_or_else(|| AccountId::from(0));
|
||||
|
||||
if u32::from(account) >= 0x7FFFFFFF {
|
||||
return Err(SqliteClientError::AccountIdOutOfRange);
|
||||
}
|
||||
.map(|a| a.next().ok_or(SqliteClientError::AccountIdOutOfRange))
|
||||
.transpose()?
|
||||
.unwrap_or(AccountId::ZERO);
|
||||
|
||||
let usk = UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account)
|
||||
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
|
||||
|
@ -1132,7 +1129,7 @@ mod tests {
|
|||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
.build();
|
||||
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let current_addr = st.wallet().get_current_address(account).unwrap();
|
||||
assert!(current_addr.is_some());
|
||||
|
||||
|
@ -1157,7 +1154,10 @@ mod tests {
|
|||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
let (taddr, _) = usk.default_transparent_address();
|
||||
|
||||
let receivers = st.wallet().get_transparent_receivers(0.into()).unwrap();
|
||||
let receivers = st
|
||||
.wallet()
|
||||
.get_transparent_receivers(AccountId::ZERO)
|
||||
.unwrap();
|
||||
|
||||
// The receiver for the default UA should be in the set.
|
||||
assert!(receivers.contains_key(ufvk.default_address().0.transparent().unwrap()));
|
||||
|
@ -1176,7 +1176,7 @@ mod tests {
|
|||
|
||||
// Generate some fake CompactBlocks.
|
||||
let seed = [0u8; 32];
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let extsk = sapling::spending_key(&seed, st.wallet().params.coin_type(), account);
|
||||
let dfvk = extsk.to_diversifiable_full_viewing_key();
|
||||
let (h1, meta1, _) = st.generate_next_block(
|
||||
|
|
|
@ -166,11 +166,13 @@ pub(crate) fn get_max_account_id(
|
|||
conn: &rusqlite::Connection,
|
||||
) -> Result<Option<AccountId>, SqliteClientError> {
|
||||
// This returns the most recently generated address.
|
||||
conn.query_row("SELECT MAX(account) FROM accounts", [], |row| {
|
||||
conn.query_row_and_then("SELECT MAX(account) FROM accounts", [], |row| {
|
||||
let account_id: Option<u32> = row.get(0)?;
|
||||
Ok(account_id.map(AccountId::from))
|
||||
account_id
|
||||
.map(AccountId::try_from)
|
||||
.transpose()
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)
|
||||
})
|
||||
.map_err(SqliteClientError::from)
|
||||
}
|
||||
|
||||
pub(crate) fn add_account<P: consensus::Parameters>(
|
||||
|
@ -297,7 +299,7 @@ pub(crate) fn get_current_address<P: consensus::Parameters>(
|
|||
addr_str,
|
||||
))),
|
||||
})
|
||||
.map(|addr| (addr, DiversifierIndex(di_be)))
|
||||
.map(|addr| (addr, DiversifierIndex::from(di_be)))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
@ -309,7 +311,7 @@ pub(crate) fn insert_address<P: consensus::Parameters>(
|
|||
conn: &rusqlite::Connection,
|
||||
params: &P,
|
||||
account: AccountId,
|
||||
mut diversifier_index: DiversifierIndex,
|
||||
diversifier_index: DiversifierIndex,
|
||||
address: &UnifiedAddress,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
let mut stmt = conn.prepare_cached(
|
||||
|
@ -328,10 +330,11 @@ pub(crate) fn insert_address<P: consensus::Parameters>(
|
|||
)?;
|
||||
|
||||
// the diversifier index is stored in big-endian order to allow sorting
|
||||
diversifier_index.0.reverse();
|
||||
let mut di_be = *diversifier_index.as_bytes();
|
||||
di_be.reverse();
|
||||
stmt.execute(named_params![
|
||||
":account": &u32::from(account),
|
||||
":diversifier_index_be": &&diversifier_index.0[..],
|
||||
":diversifier_index_be": &di_be[..],
|
||||
":address": &address.encode(params),
|
||||
":cached_transparent_receiver_address": &address.transparent().map(|r| r.encode(params)),
|
||||
])?;
|
||||
|
@ -377,7 +380,7 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
|
|||
if let Some(taddr) = ua.transparent() {
|
||||
ret.insert(
|
||||
*taddr,
|
||||
AddressMetadata::new(account, DiversifierIndex(di_be)),
|
||||
AddressMetadata::new(account, DiversifierIndex::from(di_be)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -436,7 +439,7 @@ pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
|
|||
|
||||
let rows = stmt_fetch_accounts.query_map([], |row| {
|
||||
let acct: u32 = row.get(0)?;
|
||||
let account = AccountId::from(acct);
|
||||
let account = AccountId::try_from(acct).map_err(|_| SqliteClientError::AccountIdOutOfRange);
|
||||
let ufvk_str: String = row.get(1)?;
|
||||
let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
|
||||
.map_err(SqliteClientError::CorruptedData);
|
||||
|
@ -447,7 +450,7 @@ pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
|
|||
let mut res: HashMap<AccountId, UnifiedFullViewingKey> = HashMap::new();
|
||||
for row in rows {
|
||||
let (account_id, ufvkr) = row?;
|
||||
res.insert(account_id, ufvkr?);
|
||||
res.insert(account_id?, ufvkr?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
|
@ -465,11 +468,12 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
|
|||
[&ufvk.encode(params)],
|
||||
|row| {
|
||||
let acct: u32 = row.get(0)?;
|
||||
Ok(AccountId::from(acct))
|
||||
Ok(AccountId::try_from(acct).map_err(|_| SqliteClientError::AccountIdOutOfRange))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(SqliteClientError::from)
|
||||
.map_err(SqliteClientError::from)?
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub(crate) trait ScanProgress {
|
||||
|
@ -612,9 +616,10 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
let mut stmt_accounts = conn.prepare_cached("SELECT account FROM accounts")?;
|
||||
let mut account_balances = stmt_accounts
|
||||
.query([])?
|
||||
.mapped(|row| {
|
||||
row.get::<_, u32>(0)
|
||||
.map(|a| (AccountId::from(a), AccountBalance::ZERO))
|
||||
.and_then(|row| {
|
||||
AccountId::try_from(row.get::<_, u32>(0)?)
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)
|
||||
.map(|a| (a, AccountBalance::ZERO))
|
||||
})
|
||||
.collect::<Result<BTreeMap<AccountId, AccountBalance>, _>>()?;
|
||||
|
||||
|
@ -636,7 +641,8 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
let mut rows =
|
||||
stmt_select_notes.query(named_params![":summary_height": u32::from(summary_height)])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let account = row.get::<_, u32>(0).map(AccountId::from)?;
|
||||
let account = AccountId::try_from(row.get::<_, u32>(0)?)
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)?;
|
||||
|
||||
let value_raw = row.get::<_, i64>(1)?;
|
||||
let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
|
||||
|
@ -710,7 +716,8 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
|||
])?;
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let account = AccountId::from(row.get::<_, u32>(0)?);
|
||||
let account = AccountId::try_from(row.get::<_, u32>(0)?)
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)?;
|
||||
let raw_value = row.get(1)?;
|
||||
let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| {
|
||||
SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value))
|
||||
|
@ -1612,18 +1619,23 @@ pub(crate) fn put_received_transparent_utxo<P: consensus::Parameters>(
|
|||
.query_row(
|
||||
"SELECT account FROM addresses WHERE cached_transparent_receiver_address = :address",
|
||||
named_params![":address": &address_str],
|
||||
|row| row.get::<_, u32>(0).map(AccountId::from),
|
||||
|row| row.get::<_, u32>(0),
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
let utxoid = if let Some(account) = account_id {
|
||||
put_legacy_transparent_utxo(conn, params, output, account)?
|
||||
put_legacy_transparent_utxo(
|
||||
conn,
|
||||
params,
|
||||
output,
|
||||
AccountId::try_from(account).map_err(|_| SqliteClientError::AccountIdOutOfRange)?,
|
||||
)?
|
||||
} else {
|
||||
// If the UTXO is received at the legacy transparent address, there may be no entry in the
|
||||
// addresses table that can be used to tie the address to a particular account. In this
|
||||
// case, we should look up the legacy address for account 0 and check whether it matches
|
||||
// the address for the received UTXO, and if so then insert/update it directly.
|
||||
let account = AccountId::from(0u32);
|
||||
let account = AccountId::ZERO;
|
||||
get_legacy_transparent_address(params, conn, account).and_then(|legacy_taddr| {
|
||||
if legacy_taddr
|
||||
.iter()
|
||||
|
@ -2014,13 +2026,14 @@ mod tests {
|
|||
|
||||
// The default address is set for the test account
|
||||
assert_matches!(
|
||||
st.wallet().get_current_address(AccountId::from(0)),
|
||||
st.wallet().get_current_address(AccountId::ZERO),
|
||||
Ok(Some(_))
|
||||
);
|
||||
|
||||
// No default address is set for an un-initialized account
|
||||
assert_matches!(
|
||||
st.wallet().get_current_address(AccountId::from(1)),
|
||||
st.wallet()
|
||||
.get_current_address(AccountId::try_from(1).unwrap()),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -720,7 +720,7 @@ mod tests {
|
|||
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
||||
|
||||
let seed = [0xab; 32];
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
|
||||
let extfvk = secret_key.to_extended_full_viewing_key();
|
||||
|
||||
|
@ -891,7 +891,7 @@ mod tests {
|
|||
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
||||
|
||||
let seed = [0xab; 32];
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
|
||||
let extfvk = secret_key.to_extended_full_viewing_key();
|
||||
|
||||
|
@ -1044,7 +1044,7 @@ mod tests {
|
|||
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
|
||||
|
||||
let seed = [0xab; 32];
|
||||
let account = AccountId::from(0);
|
||||
let account = AccountId::ZERO;
|
||||
let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap();
|
||||
|
||||
init_main(
|
||||
|
@ -1077,7 +1077,7 @@ mod tests {
|
|||
let (account, _usk) = db_data
|
||||
.create_account(&Secret::new(seed.to_vec()), birthday)
|
||||
.unwrap();
|
||||
assert_eq!(account, AccountId::from(0u32));
|
||||
assert_eq!(account, AccountId::ZERO);
|
||||
|
||||
for tv in &test_vectors::UNIFIED[..3] {
|
||||
if let Some(RecipientAddress::Unified(tvua)) =
|
||||
|
|
|
@ -308,8 +308,7 @@ mod tests {
|
|||
let data_file = NamedTempFile::new().unwrap();
|
||||
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
|
||||
init_wallet_db_internal(&mut db_data, None, &[addresses_table::MIGRATION_ID]).unwrap();
|
||||
let usk =
|
||||
UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::from(0)).unwrap();
|
||||
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
|
||||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
|
||||
db_data
|
||||
|
@ -436,8 +435,7 @@ mod tests {
|
|||
let mut tx_bytes = vec![];
|
||||
tx.write(&mut tx_bytes).unwrap();
|
||||
|
||||
let usk =
|
||||
UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::from(0)).unwrap();
|
||||
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
|
||||
let ufvk = usk.to_unified_full_viewing_key();
|
||||
let (ua, _) = ufvk.default_address();
|
||||
let taddr = ufvk
|
||||
|
|
|
@ -59,18 +59,21 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
let mut rows = stmt_fetch_accounts.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let account: u32 = row.get(0)?;
|
||||
let taddrs =
|
||||
get_transparent_receivers(transaction, &self._params, AccountId::from(account))
|
||||
.map_err(|e| match e {
|
||||
SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
|
||||
SqliteClientError::CorruptedData(s) => {
|
||||
WalletMigrationError::CorruptedData(s)
|
||||
}
|
||||
other => WalletMigrationError::CorruptedData(format!(
|
||||
"Unexpected error in migration: {}",
|
||||
other
|
||||
)),
|
||||
})?;
|
||||
let taddrs = get_transparent_receivers(
|
||||
transaction,
|
||||
&self._params,
|
||||
AccountId::try_from(account).map_err(|_| {
|
||||
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
|
||||
})?,
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
|
||||
SqliteClientError::CorruptedData(s) => WalletMigrationError::CorruptedData(s),
|
||||
other => WalletMigrationError::CorruptedData(format!(
|
||||
"Unexpected error in migration: {}",
|
||||
other
|
||||
)),
|
||||
})?;
|
||||
|
||||
for (taddr, _) in taddrs {
|
||||
stmt_update_utxo_account.execute(named_params![
|
||||
|
|
|
@ -61,7 +61,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
let mut rows = stmt_fetch_accounts.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
let account: u32 = row.get(0)?;
|
||||
let account = AccountId::from(account);
|
||||
let account = AccountId::try_from(account).map_err(|_| {
|
||||
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
|
||||
})?;
|
||||
|
||||
let ufvk_str: String = row.get(1)?;
|
||||
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
|
||||
|
|
|
@ -242,9 +242,8 @@ mod tests {
|
|||
init_wallet_db_internal(&mut db_data, None, &[v_transactions_net::MIGRATION_ID]).unwrap();
|
||||
|
||||
// Create an account in the wallet
|
||||
let usk0 =
|
||||
UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::from(0))
|
||||
.unwrap();
|
||||
let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO)
|
||||
.unwrap();
|
||||
let ufvk0 = usk0.to_unified_full_viewing_key();
|
||||
db_data
|
||||
.conn
|
||||
|
|
|
@ -71,10 +71,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
))
|
||||
})?;
|
||||
|
||||
tx_sent_notes
|
||||
.entry((id_tx, txid))
|
||||
.or_default()
|
||||
.insert(AccountId::from(account), ufvk);
|
||||
tx_sent_notes.entry((id_tx, txid)).or_default().insert(
|
||||
AccountId::try_from(account).map_err(|_| {
|
||||
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
|
||||
})?,
|
||||
ufvk,
|
||||
);
|
||||
}
|
||||
|
||||
let mut stmt_update_sent_memo = transaction.prepare(
|
||||
|
|
|
@ -71,7 +71,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
|||
// 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 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)
|
||||
.unwrap();
|
||||
|
|
|
@ -222,9 +222,8 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// Create two accounts in the wallet.
|
||||
let usk0 =
|
||||
UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::from(0))
|
||||
.unwrap();
|
||||
let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO)
|
||||
.unwrap();
|
||||
let ufvk0 = usk0.to_unified_full_viewing_key();
|
||||
db_data
|
||||
.conn
|
||||
|
@ -234,9 +233,12 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let usk1 =
|
||||
UnifiedSpendingKey::from_seed(&db_data.params, &[1u8; 32][..], AccountId::from(1))
|
||||
.unwrap();
|
||||
let usk1 = UnifiedSpendingKey::from_seed(
|
||||
&db_data.params,
|
||||
&[1u8; 32][..],
|
||||
AccountId::try_from(1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let ufvk1 = usk1.to_unified_full_viewing_key();
|
||||
db_data
|
||||
.conn
|
||||
|
|
|
@ -178,9 +178,8 @@ mod tests {
|
|||
init_wallet_db_internal(&mut db_data, None, &[v_transactions_net::MIGRATION_ID]).unwrap();
|
||||
|
||||
// Create an account in the wallet
|
||||
let usk0 =
|
||||
UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::from(0))
|
||||
.unwrap();
|
||||
let usk0 = UnifiedSpendingKey::from_seed(&db_data.params, &[0u8; 32][..], AccountId::ZERO)
|
||||
.unwrap();
|
||||
let ufvk0 = usk0.to_unified_full_viewing_key();
|
||||
db_data
|
||||
.conn
|
||||
|
|
|
@ -338,13 +338,12 @@ pub(crate) fn get_sapling_nullifiers(
|
|||
WHERE block IS NULL
|
||||
AND nf IS NOT NULL",
|
||||
)?;
|
||||
let nullifiers = stmt_fetch_nullifiers.query_map([], |row| {
|
||||
let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| {
|
||||
let account: u32 = row.get(1)?;
|
||||
let nf_bytes: Vec<u8> = row.get(2)?;
|
||||
Ok((
|
||||
AccountId::from(account),
|
||||
sapling::Nullifier::from_slice(&nf_bytes).unwrap(),
|
||||
))
|
||||
AccountId::try_from(account)
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)
|
||||
.map(|a| (a, sapling::Nullifier::from_slice(&nf_bytes).unwrap()))
|
||||
})?;
|
||||
|
||||
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||
|
@ -361,13 +360,12 @@ pub(crate) fn get_all_sapling_nullifiers(
|
|||
FROM sapling_received_notes rn
|
||||
WHERE nf IS NOT NULL",
|
||||
)?;
|
||||
let nullifiers = stmt_fetch_nullifiers.query_map([], |row| {
|
||||
let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| {
|
||||
let account: u32 = row.get(1)?;
|
||||
let nf_bytes: Vec<u8> = row.get(2)?;
|
||||
Ok((
|
||||
AccountId::from(account),
|
||||
sapling::Nullifier::from_slice(&nf_bytes).unwrap(),
|
||||
))
|
||||
AccountId::try_from(account)
|
||||
.map_err(|_| SqliteClientError::AccountIdOutOfRange)
|
||||
.map(|a| (a, sapling::Nullifier::from_slice(&nf_bytes).unwrap()))
|
||||
})?;
|
||||
|
||||
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||
|
@ -688,7 +686,7 @@ pub(crate) mod tests {
|
|||
let to = dfvk.default_address().1.into();
|
||||
|
||||
// Create a USK that doesn't exist in the wallet
|
||||
let acct1 = AccountId::from(1);
|
||||
let acct1 = AccountId::try_from(1).unwrap();
|
||||
let usk1 = UnifiedSpendingKey::from_seed(&st.network(), &[1u8; 32], acct1).unwrap();
|
||||
|
||||
// Attempting to spend with a USK that is not in the wallet results in an error
|
||||
|
@ -1263,11 +1261,11 @@ pub(crate) mod tests {
|
|||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
// Spendable balance matches total balance
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), value);
|
||||
assert_eq!(st.get_spendable_balance(AccountId::from(0), 1), value);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), value);
|
||||
assert_eq!(st.get_spendable_balance(AccountId::ZERO, 1), value);
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::from(1)),
|
||||
NonNegativeAmount::ZERO
|
||||
st.get_total_balance(AccountId::try_from(1).unwrap()),
|
||||
NonNegativeAmount::ZERO,
|
||||
);
|
||||
|
||||
let amount_sent = NonNegativeAmount::from_u64(20000).unwrap();
|
||||
|
@ -1317,15 +1315,18 @@ pub(crate) mod tests {
|
|||
let pending_change = (amount_left - amount_legacy_change).unwrap();
|
||||
|
||||
// The "legacy change" is not counted by get_pending_change().
|
||||
assert_eq!(st.get_pending_change(AccountId::from(0), 1), pending_change);
|
||||
assert_eq!(st.get_pending_change(AccountId::ZERO, 1), pending_change);
|
||||
// We spent the only note so we only have pending change.
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), pending_change);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), pending_change);
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
||||
assert_eq!(st.get_total_balance(AccountId::from(1)), amount_sent);
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), amount_left);
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::try_from(1).unwrap()),
|
||||
amount_sent,
|
||||
);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), amount_left);
|
||||
|
||||
st.reset();
|
||||
|
||||
|
@ -1353,8 +1354,11 @@ pub(crate) mod tests {
|
|||
|
||||
st.scan_cached_blocks(st.sapling_activation_height(), 2);
|
||||
|
||||
assert_eq!(st.get_total_balance(AccountId::from(1)), amount_sent);
|
||||
assert_eq!(st.get_total_balance(AccountId::from(0)), amount_left);
|
||||
assert_eq!(
|
||||
st.get_total_balance(AccountId::try_from(1).unwrap()),
|
||||
amount_sent,
|
||||
);
|
||||
assert_eq!(st.get_total_balance(AccountId::ZERO), amount_left);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1575,7 +1579,7 @@ pub(crate) mod tests {
|
|||
let spendable = select_spendable_sapling_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
AccountId::from(0),
|
||||
AccountId::ZERO,
|
||||
Amount::const_from_i64(300000),
|
||||
received_tx_height + 10,
|
||||
&[],
|
||||
|
@ -1591,7 +1595,7 @@ pub(crate) mod tests {
|
|||
let spendable = select_spendable_sapling_notes(
|
||||
&st.wallet().conn,
|
||||
&st.wallet().params,
|
||||
AccountId::from(0),
|
||||
AccountId::ZERO,
|
||||
Amount::const_from_i64(300000),
|
||||
received_tx_height + 10,
|
||||
&[],
|
||||
|
|
|
@ -21,6 +21,7 @@ all-features = true
|
|||
equihash.workspace = true
|
||||
zcash_address.workspace = true
|
||||
zcash_encoding.workspace = true
|
||||
zip32.workspace = true
|
||||
|
||||
# Dependencies exposed in a public API:
|
||||
# (Breaking upgrades to these require a breaking upgrade to this crate.)
|
||||
|
|
|
@ -9,7 +9,7 @@ pub const COIN_TYPE: u32 = 133;
|
|||
///
|
||||
/// Defined in [ZIP 32].
|
||||
///
|
||||
/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey
|
||||
/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst
|
||||
pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main";
|
||||
|
||||
|
@ -17,7 +17,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-main";
|
|||
///
|
||||
/// Defined in [ZIP 32].
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey
|
||||
/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst
|
||||
pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviews";
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ pub const COIN_TYPE: u32 = 1;
|
|||
///
|
||||
/// It is defined in [the `zcashd` codebase].
|
||||
///
|
||||
/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey
|
||||
/// [the `zcashd` codebase]: <https://github.com/zcash/zcash/blob/128d863fb8be39ee294fda397c1ce3ba3b889cb2/src/chainparams.cpp#L496>
|
||||
pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest";
|
||||
|
||||
|
@ -21,7 +21,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-regtest
|
|||
///
|
||||
/// It is defined in [the `zcashd` codebase].
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey
|
||||
/// [the `zcashd` codebase]: <https://github.com/zcash/zcash/blob/128d863fb8be39ee294fda397c1ce3ba3b889cb2/src/chainparams.cpp#L494>
|
||||
pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewregtestsapling";
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ pub const COIN_TYPE: u32 = 1;
|
|||
///
|
||||
/// Defined in [ZIP 32].
|
||||
///
|
||||
/// [`ExtendedSpendingKey`]: crate::zip32::ExtendedSpendingKey
|
||||
/// [`ExtendedSpendingKey`]: crate::sapling::zip32::ExtendedSpendingKey
|
||||
/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst
|
||||
pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test";
|
||||
|
||||
|
@ -17,7 +17,7 @@ pub const HRP_SAPLING_EXTENDED_SPENDING_KEY: &str = "secret-extended-key-test";
|
|||
///
|
||||
/// Defined in [ZIP 32].
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: crate::zip32::ExtendedFullViewingKey
|
||||
/// [`ExtendedFullViewingKey`]: crate::sapling::zip32::ExtendedFullViewingKey
|
||||
/// [ZIP 32]: https://github.com/zcash/zips/blob/master/zip-0032.rst
|
||||
pub const HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY: &str = "zxviewtestsapling";
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ pub mod memo;
|
|||
pub mod merkle_tree;
|
||||
pub mod sapling;
|
||||
pub mod transaction;
|
||||
pub mod zip32;
|
||||
pub use zip32;
|
||||
pub mod zip339;
|
||||
|
||||
#[cfg(feature = "zfuture")]
|
||||
|
|
|
@ -8,6 +8,7 @@ use aes::Aes256;
|
|||
use blake2b_simd::Params as Blake2bParams;
|
||||
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use fpe::ff1::{BinaryNumeralString, FF1};
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
use std::ops::AddAssign;
|
||||
|
||||
|
@ -178,7 +179,7 @@ impl DiversifierKey {
|
|||
fn try_diversifier_internal(ff: &FF1<Aes256>, j: DiversifierIndex) -> Option<Diversifier> {
|
||||
// Generate d_j
|
||||
let enc = ff
|
||||
.encrypt(&[], &BinaryNumeralString::from_bytes_le(&j.0[..]))
|
||||
.encrypt(&[], &BinaryNumeralString::from_bytes_le(j.as_bytes()))
|
||||
.unwrap();
|
||||
let mut d_j = [0; 11];
|
||||
d_j.copy_from_slice(&enc.to_bytes_le());
|
||||
|
@ -205,9 +206,7 @@ impl DiversifierKey {
|
|||
let dec = ff
|
||||
.decrypt(&[], &BinaryNumeralString::from_bytes_le(&d.0[..]))
|
||||
.unwrap();
|
||||
let mut j = DiversifierIndex::new();
|
||||
j.0.copy_from_slice(&dec.to_bytes_le());
|
||||
j
|
||||
DiversifierIndex::from(<[u8; 11]>::try_from(&dec.to_bytes_le()[..]).unwrap())
|
||||
}
|
||||
|
||||
/// Returns the first index starting from j that generates a valid
|
||||
|
@ -854,9 +853,9 @@ mod tests {
|
|||
fn diversifier() {
|
||||
let dk = DiversifierKey([0; 32]);
|
||||
let j_0 = DiversifierIndex::new();
|
||||
let j_1 = DiversifierIndex([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_2 = DiversifierIndex([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_3 = DiversifierIndex([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_1 = DiversifierIndex::from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_2 = DiversifierIndex::from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_3 = DiversifierIndex::from([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
// Computed using this Rust implementation
|
||||
let d_0 = [220, 231, 126, 188, 236, 10, 38, 175, 214, 153, 140];
|
||||
let d_3 = [60, 253, 170, 8, 171, 147, 220, 31, 3, 144, 34];
|
||||
|
@ -883,12 +882,12 @@ mod tests {
|
|||
let di32: u32 = 0xa0b0c0d0;
|
||||
assert_eq!(
|
||||
DiversifierIndex::from(di32),
|
||||
DiversifierIndex([0xd0, 0xc0, 0xb0, 0xa0, 0, 0, 0, 0, 0, 0, 0])
|
||||
DiversifierIndex::from([0xd0, 0xc0, 0xb0, 0xa0, 0, 0, 0, 0, 0, 0, 0])
|
||||
);
|
||||
let di64: u64 = 0x0102030405060708;
|
||||
assert_eq!(
|
||||
DiversifierIndex::from(di64),
|
||||
DiversifierIndex([8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0])
|
||||
DiversifierIndex::from([8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -896,9 +895,9 @@ mod tests {
|
|||
fn find_diversifier() {
|
||||
let dk = DiversifierKey([0; 32]);
|
||||
let j_0 = DiversifierIndex::new();
|
||||
let j_1 = DiversifierIndex([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_2 = DiversifierIndex([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_3 = DiversifierIndex([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_1 = DiversifierIndex::from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_2 = DiversifierIndex::from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_3 = DiversifierIndex::from([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
// Computed using this Rust implementation
|
||||
let d_0 = [220, 231, 126, 188, 236, 10, 38, 175, 214, 153, 140];
|
||||
let d_3 = [60, 253, 170, 8, 171, 147, 220, 31, 3, 144, 34];
|
||||
|
@ -958,7 +957,7 @@ mod tests {
|
|||
[59, 246, 250, 31, 131, 191, 69, 99, 200, 167, 19]
|
||||
);
|
||||
|
||||
let j_1 = DiversifierIndex([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let j_1 = DiversifierIndex::from([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(xfvk_m.address(j_1), None);
|
||||
}
|
||||
|
||||
|
@ -967,7 +966,7 @@ mod tests {
|
|||
let seed = [0; 32];
|
||||
let xsk_m = ExtendedSpendingKey::master(&seed);
|
||||
let (j_m, addr_m) = xsk_m.default_address();
|
||||
assert_eq!(j_m.0, [0; 11]);
|
||||
assert_eq!(j_m.as_bytes(), &[0; 11]);
|
||||
assert_eq!(
|
||||
addr_m.diversifier().0,
|
||||
// Computed using this Rust implementation
|
||||
|
@ -1689,7 +1688,7 @@ mod tests {
|
|||
}
|
||||
|
||||
// dmax
|
||||
let dmax = DiversifierIndex([0xff; 11]);
|
||||
let dmax = DiversifierIndex::from([0xff; 11]);
|
||||
match xfvk.dk.find_diversifier(dmax) {
|
||||
Some((l, d)) if l == dmax => assert_eq!(d.0, tv.dmax.unwrap()),
|
||||
Some((_, _)) => panic!(),
|
||||
|
|
|
@ -784,7 +784,7 @@ mod tests {
|
|||
orchard_saks: Vec::new(),
|
||||
};
|
||||
|
||||
let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::from(0)).unwrap();
|
||||
let tsk = AccountPrivKey::from_seed(&TEST_NETWORK, &[0u8; 32], AccountId::ZERO).unwrap();
|
||||
let prev_coin = TxOut {
|
||||
value: NonNegativeAmount::const_from_u64(50000),
|
||||
script_pubkey: tsk
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
//! Implementation of [ZIP 32] for hierarchical deterministic key management.
|
||||
//!
|
||||
//! [ZIP 32]: https://zips.z.cash/zip-0032
|
||||
|
||||
use memuse::{self, DynamicUsage};
|
||||
use subtle::{Choice, ConditionallySelectable};
|
||||
|
||||
pub mod fingerprint;
|
||||
|
||||
#[deprecated(note = "Please use the types exported from the `zip32::sapling` module instead.")]
|
||||
pub use crate::sapling::zip32::{
|
||||
sapling_address, sapling_default_address, sapling_derive_internal_fvk, sapling_find_address,
|
||||
DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey,
|
||||
ZIP32_SAPLING_FVFP_PERSONALIZATION, ZIP32_SAPLING_INT_PERSONALIZATION,
|
||||
ZIP32_SAPLING_MASTER_PERSONALIZATION,
|
||||
};
|
||||
|
||||
/// A type-safe wrapper for account identifiers.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct AccountId(u32);
|
||||
|
||||
memuse::impl_no_dynamic_usage!(AccountId);
|
||||
|
||||
impl From<u32> for AccountId {
|
||||
fn from(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountId> for u32 {
|
||||
fn from(id: AccountId) -> Self {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountId> for ChildIndex {
|
||||
fn from(id: AccountId) -> Self {
|
||||
// Account IDs are always hardened in derivation paths.
|
||||
ChildIndex::hardened(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConditionallySelectable for AccountId {
|
||||
fn conditional_select(a0: &Self, a1: &Self, c: Choice) -> Self {
|
||||
AccountId(u32::conditional_select(&a0.0, &a1.0, c))
|
||||
}
|
||||
}
|
||||
|
||||
// ZIP 32 structures
|
||||
|
||||
/// A child index for a derived key.
|
||||
///
|
||||
/// Only hardened derivation is supported.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ChildIndex(u32);
|
||||
|
||||
impl ChildIndex {
|
||||
/// Parses the given ZIP 32 child index.
|
||||
///
|
||||
/// Returns `None` if the hardened bit is not set.
|
||||
pub fn from_index(i: u32) -> Option<Self> {
|
||||
if i >= (1 << 31) {
|
||||
Some(ChildIndex(i))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a hardened `ChildIndex` from the given value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `value >= (1 << 31)`.
|
||||
pub const fn hardened(value: u32) -> Self {
|
||||
assert!(value < (1 << 31));
|
||||
Self(value + (1 << 31))
|
||||
}
|
||||
|
||||
/// Returns the index as a 32-bit integer, including the hardened bit.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that is needed, in addition to a spending key, in order to derive descendant
|
||||
/// keys and addresses of that key.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ChainCode([u8; 32]);
|
||||
|
||||
impl ChainCode {
|
||||
/// Constructs a `ChainCode` from the given array.
|
||||
pub fn new(c: [u8; 32]) -> Self {
|
||||
Self(c)
|
||||
}
|
||||
|
||||
/// Returns the byte representation of the chain code, as required for
|
||||
/// [ZIP 32](https://zips.z.cash/zip-0032) encoding.
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DiversifierIndex(pub [u8; 11]);
|
||||
|
||||
impl Default for DiversifierIndex {
|
||||
fn default() -> Self {
|
||||
DiversifierIndex::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for DiversifierIndex {
|
||||
fn from(i: u32) -> Self {
|
||||
u64::from(i).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for DiversifierIndex {
|
||||
fn from(i: u64) -> Self {
|
||||
let mut result = DiversifierIndex([0; 11]);
|
||||
result.0[..8].copy_from_slice(&i.to_le_bytes());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DiversifierIndex> for u32 {
|
||||
type Error = std::num::TryFromIntError;
|
||||
|
||||
fn try_from(di: DiversifierIndex) -> Result<u32, Self::Error> {
|
||||
let mut u128_bytes = [0u8; 16];
|
||||
u128_bytes[0..11].copy_from_slice(&di.0[..]);
|
||||
u128::from_le_bytes(u128_bytes).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DiversifierIndex {
|
||||
pub fn new() -> Self {
|
||||
DiversifierIndex([0; 11])
|
||||
}
|
||||
|
||||
pub fn increment(&mut self) -> Result<(), ()> {
|
||||
for k in 0..11 {
|
||||
self.0[k] = self.0[k].wrapping_add(1);
|
||||
if self.0[k] != 0 {
|
||||
// No overflow
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Overflow
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The scope of a viewing key or address.
|
||||
///
|
||||
/// A "scope" narrows the visibility or usage to a level below "full".
|
||||
///
|
||||
/// Consistent usage of `Scope` enables the user to provide consistent views over a wallet
|
||||
/// to other people. For example, a user can give an external [SaplingIvk] to a merchant
|
||||
/// terminal, enabling it to only detect "real" transactions from customers and not
|
||||
/// internal transactions from the wallet.
|
||||
///
|
||||
/// [SaplingIvk]: crate::sapling::SaplingIvk
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Scope {
|
||||
/// A scope used for wallet-external operations, namely deriving addresses to give to
|
||||
/// other users in order to receive funds.
|
||||
External,
|
||||
/// A scope used for wallet-internal operations, such as creating change notes,
|
||||
/// auto-shielding, and note management.
|
||||
Internal,
|
||||
}
|
||||
|
||||
memuse::impl_no_dynamic_usage!(Scope);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DiversifierIndex;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
#[test]
|
||||
fn diversifier_index_to_u32() {
|
||||
let two = DiversifierIndex([
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]);
|
||||
assert_eq!(u32::try_from(two), Ok(2));
|
||||
|
||||
let max_u32 = DiversifierIndex([
|
||||
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]);
|
||||
assert_eq!(u32::try_from(max_u32), Ok(u32::MAX));
|
||||
|
||||
let too_big = DiversifierIndex([
|
||||
0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]);
|
||||
assert_matches!(u32::try_from(too_big), Err(_));
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
//! Seed Fingerprints according to ZIP 32
|
||||
//!
|
||||
//! Implements section `Seed Fingerprints` of Shielded Hierarchical Deterministic Wallets (ZIP 32)
|
||||
//!
|
||||
//! [Section Seed Fingerprints]: https://zips.z.cash/zip-0032#seed-fingerprints
|
||||
use blake2b_simd::Params as Blake2bParams;
|
||||
|
||||
pub const ZIP32_SEED_FP_PERSONALIZATION: &[u8; 16] = b"Zcash_HD_Seed_FP";
|
||||
pub struct SeedFingerprint([u8; 32]);
|
||||
|
||||
impl SeedFingerprint {
|
||||
/// Return the seed fingerprint of the wallet as defined in
|
||||
/// <https://zips.z.cash/zip-0032#seed-fingerprints> or None
|
||||
/// if the length of `seed_bytes` is less than 32 or
|
||||
/// greater than 252.
|
||||
pub fn from_seed(seed_bytes: &[u8]) -> Option<SeedFingerprint> {
|
||||
let seed_len = seed_bytes.len();
|
||||
|
||||
if (32..=252).contains(&seed_len) {
|
||||
let seed_len: u8 = seed_len.try_into().unwrap();
|
||||
Some(SeedFingerprint(
|
||||
Blake2bParams::new()
|
||||
.hash_length(32)
|
||||
.personal(ZIP32_SEED_FP_PERSONALIZATION)
|
||||
.to_state()
|
||||
.update(&[seed_len])
|
||||
.update(seed_bytes)
|
||||
.finalize()
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("hash length should be 32 bytes"),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the fingerprint as a byte array.
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seed_fingerprint() {
|
||||
struct TestVector {
|
||||
root_seed: Vec<u8>,
|
||||
fingerprint: Vec<u8>,
|
||||
}
|
||||
|
||||
let test_vectors = vec![TestVector {
|
||||
root_seed: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
|
||||
0x1c, 0x1d, 0x1e, 0x1f,
|
||||
],
|
||||
fingerprint: vec![
|
||||
0xde, 0xff, 0x60, 0x4c, 0x24, 0x67, 0x10, 0xf7, 0x17, 0x6d, 0xea, 0xd0, 0x2a, 0xa7,
|
||||
0x46, 0xf2, 0xfd, 0x8d, 0x53, 0x89, 0xf7, 0x7, 0x25, 0x56, 0xdc, 0xb5, 0x55, 0xfd,
|
||||
0xbe, 0x5e, 0x3a, 0xe3,
|
||||
],
|
||||
}];
|
||||
|
||||
for tv in test_vectors {
|
||||
let fp = SeedFingerprint::from_seed(&tv.root_seed).expect("root_seed has valid length");
|
||||
assert_eq!(&fp.to_bytes(), &tv.fingerprint[..]);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_seed_fingerprint_is_none() {
|
||||
let odd_seed = vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f,
|
||||
];
|
||||
|
||||
assert!(
|
||||
SeedFingerprint::from_seed(&odd_seed).is_none(),
|
||||
"fingerprint from short seed should be `None`"
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue