use zcash_protocol::value::Zatoshis;
use super::params::MigrationParams;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Denominations {
outputs: Vec<Zatoshis>,
remainder: Zatoshis,
}
impl Denominations {
pub fn outputs(&self) -> &[Zatoshis] {
&self.outputs
}
pub fn remainder(&self) -> Zatoshis {
self.remainder
}
pub fn total(&self) -> Zatoshis {
Zatoshis::const_from_u64(self.outputs.iter().map(|z| u64::from(*z)).sum())
}
}
pub fn decompose(value: Zatoshis, params: &MigrationParams) -> Denominations {
let mut remaining = u64::from(value);
let mut outputs = Vec::new();
for &denomination in ¶ms.denominations {
let count = remaining / denomination;
remaining -= count * denomination;
for _ in 0..count {
outputs.push(Zatoshis::const_from_u64(denomination));
}
}
Denominations {
outputs,
remainder: Zatoshis::const_from_u64(remaining),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ChainType;
use proptest::prelude::*;
use zcash_protocol::value::COIN;
use zcash_protocol::value::MAX_MONEY;
fn params() -> MigrationParams {
MigrationParams::provisional(ChainType::Mainnet)
}
fn values(outputs: &[Zatoshis]) -> Vec<u64> {
outputs.iter().map(|z| u64::from(*z)).collect()
}
#[test]
fn zero_decomposes_to_nothing() {
let d = decompose(Zatoshis::ZERO, ¶ms());
assert!(d.outputs().is_empty());
assert_eq!(d.remainder(), Zatoshis::ZERO);
assert_eq!(d.total(), Zatoshis::ZERO);
}
#[test]
fn worked_example() {
let d = decompose(Zatoshis::const_from_u64(123_456_789), ¶ms());
assert_eq!(
values(d.outputs()),
vec![
100_000_000, 20_000_000, 2_000_000, 1_000_000, ]
);
assert_eq!(u64::from(d.remainder()), 456_789);
}
#[test]
fn value_above_largest_denomination_uses_many_top_notes() {
let d = decompose(Zatoshis::const_from_u64(25_000 * COIN), ¶ms());
assert_eq!(
values(d.outputs()),
vec![10_000 * COIN, 10_000 * COIN, 5_000 * COIN]
);
assert_eq!(d.remainder(), Zatoshis::ZERO);
}
#[test]
fn sub_denomination_value_is_all_remainder() {
let d = decompose(Zatoshis::const_from_u64(90_000), ¶ms());
assert!(d.outputs().is_empty());
assert_eq!(u64::from(d.remainder()), 90_000);
}
proptest! {
#[test]
fn outputs_are_canonical_denominations(value in 0u64..=MAX_MONEY) {
let params = params();
let d = decompose(Zatoshis::const_from_u64(value), ¶ms);
for output in d.outputs() {
prop_assert!(
params.denominations.contains(&u64::from(*output)),
"non-canonical output: {output:?}"
);
}
}
#[test]
fn conserves_value(value in 0u64..=MAX_MONEY) {
let d = decompose(Zatoshis::const_from_u64(value), ¶ms());
prop_assert_eq!(
u64::from(d.total()) + u64::from(d.remainder()),
value
);
}
#[test]
fn remainder_below_max_residual_value(value in 0u64..=MAX_MONEY) {
let params = params();
let d = decompose(Zatoshis::const_from_u64(value), ¶ms);
prop_assert!(u64::from(d.remainder()) < params.max_residual_value);
}
}
}