diff --git a/masq_lib/src/percentage.rs b/masq_lib/src/percentage.rs index 1e89bb045..f290fb74c 100644 --- a/masq_lib/src/percentage.rs +++ b/masq_lib/src/percentage.rs @@ -6,28 +6,6 @@ use num::{CheckedDiv, CheckedMul, Integer}; use std::any::type_name; use std::fmt::Debug; use std::ops::Rem; -// Designed to store values from 0 to 100 and offer a set of handy methods for PurePercentage -// operations over a wide variety of integer types. It is also to look after the least significant -// digit on the resulted number in order to avoid the effect of a loss on precision that genuinely -// comes with division on integers if a remainder is left over. The percents are always represented -// by an unsigned integer. On the contrary, the numbers that it is applied on can take on both -// positive and negative values. - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PurePercentage { - degree: u8, -} - -// This is a wider type that allows to specify cumulative percents of more than only 100. -// The expected use of this would look like requesting percents meaning possibly multiples of 100%, -// roughly, of a certain base number. Similarly to the PurePercentage type, also signed numbers -// would be accepted. - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LoosePercentage { - multiples_of_100_percent: u32, - degrees_from_remainder: PurePercentage, -} pub trait PercentageInteger: TryFrom @@ -51,51 +29,18 @@ macro_rules! impl_percentage_integer { impl_percentage_integer!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); -impl LoosePercentage { - pub fn new(percents: u32) -> Self { - let multiples_of_100_percent = percents / 100; - let remainder = (percents % 100) as u8; - let degrees_from_remainder = - PurePercentage::try_from(remainder).expect("should never happen"); - Self { - multiples_of_100_percent, - degrees_from_remainder, - } - } - - // If this overflows you probably want to precede the operation by converting your base number - // to a larger integer type - pub fn of(&self, num: N) -> Result - where - N: PercentageInteger, - >::Error: Debug, - N: TryFrom, - >::Error: Debug, - i16: TryFrom, - >::Error: Debug, - { - let multiples = match N::try_from(self.multiples_of_100_percent) { - Ok(num) => num, - Err(_) => return Err(BaseTypeOverflow {}), - }; - - let by_wholes = match num.checked_mul(&multiples) { - Some(num) => num, - None => return Err(BaseTypeOverflow {}), - }; - - let by_remainder = self.degrees_from_remainder.of(num); +// Designed to store values from 0 to 100 and offer a set of handy methods for PurePercentage +// operations over a wide variety of integer types. It is also to look after the least significant +// digit on the resulted number in order to avoid the effect of a loss on precision that genuinely +// comes with division on integers if a remainder is left over. The percents are always represented +// by an unsigned integer. On the contrary, the numbers that it is applied on can take on both +// positive and negative values. - match by_wholes.checked_add(&by_remainder) { - Some(res) => Ok(res), - None => Err(BaseTypeOverflow {}), - } - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PurePercentage { + degree: u8, } -#[derive(Debug, PartialEq, Eq)] -pub struct BaseTypeOverflow {} - impl TryFrom for PurePercentage { type Error = String; @@ -103,122 +48,91 @@ impl TryFrom for PurePercentage { match degree { 0..=100 => Ok(Self { degree }), x => Err(format!( - "Accepts only range from 0 to 100 but {} was supplied", + "Accepts only range from 0 to 100, but {} was supplied", x )), } } } -impl PurePercentage { - pub fn of(&self, num: N) -> N - where - N: PercentageInteger, - >::Error: Debug, - i16: TryFrom, - >::Error: Debug, - { - if let Some(zero) = self.return_zero(num) { +trait PurePercentageInternalMethods +where + Self: Sized, +{ + fn _of(&self, num: N) -> N; + fn __check_zero_and_maybe_return_it(&self, num: N) -> Option; + fn __abs(num: N, is_signed: bool) -> N; + fn __derive_rounding_increment(remainder: N) -> N; + fn _increase_by_percent_for(&self, num: N) -> N; + fn _decrease_by_percent_for(&self, num: N) -> N; + fn __handle_upper_overflow(&self, num: N) -> N; +} + +impl PurePercentageInternalMethods for PurePercentage +where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, +{ + fn _of(&self, num: N) -> N { + if let Some(zero) = self.__check_zero_and_maybe_return_it(num) { return zero; } let product_before_final_div = match N::try_from(self.degree as i8) - .expect("Each type has 100") + .expect("Each integer has 100") .checked_mul(&num) { Some(num) => num, - None => return self.handle_upper_overflow(num), + None => return self.__handle_upper_overflow(num), }; - Self::div_by_100_and_round(product_before_final_div) + let (base, remainder) = base_and_rem_from_div_100(product_before_final_div); + + base + Self::__derive_rounding_increment(remainder) } - fn return_zero(&self, num: N) -> Option - where - N: PercentageInteger, - >::Error: Debug, - { - let zero = N::try_from(0).expect("Each type has 0"); - if num == zero || N::try_from(self.degree as i8).expect("Each type has 100") == zero { + fn __check_zero_and_maybe_return_it(&self, num: N) -> Option { + let zero = N::try_from(0).expect("Each integer has 0"); + if num == zero || N::try_from(self.degree as i8).expect("Each integer has 100") == zero { Some(zero) } else { None } } - fn div_by_100_and_round(num: N) -> N - where - N: PercentageInteger, - >::Error: Debug, - { - let divisor = N::try_from(100).expect("Each type has 100"); - let desired_rounding = Self::should_be_rounded_to(num, divisor); - let significant_digits_only = num.checked_div(&divisor).expect("Division failed"); - - macro_rules! adjust_num { - ($significant_digits: expr, $method_add_or_sub: ident, $msg_in_expect: literal) => { - $significant_digits - .$method_add_or_sub(&N::try_from(1).expect("Each type has 1")) - .expect($msg_in_expect) - }; - } - - match desired_rounding { - RoundingTo::BiggerPositive => { - adjust_num!(significant_digits_only, checked_add, "Addition failed") - } - RoundingTo::BiggerNegative => { - adjust_num!(significant_digits_only, checked_sub, "Subtraction failed") - } - RoundingTo::SmallerNegative | RoundingTo::SmallerPositive => significant_digits_only, - } - } - - fn should_be_rounded_to(num: N, divisor: N) -> RoundingTo - where - N: PercentageInteger, - >::Error: Debug, - { - let least_significant_digits: N = num % divisor; - let is_signed = num < N::try_from(0).expect("Each type has 0"); - let divider = N::try_from(50).expect("Each type has 50"); - let abs_of_significant_digits = - Self::abs_of_least_significant_digits(least_significant_digits, is_signed); - let is_minor = abs_of_significant_digits < divider; - match (is_signed, is_minor) { - (false, true) => RoundingTo::SmallerPositive, - (false, false) => RoundingTo::BiggerPositive, - (true, true) => RoundingTo::SmallerNegative, - (true, false) => RoundingTo::BiggerNegative, - } - } - - fn abs_of_least_significant_digits(least_significant_digits: N, is_signed: bool) -> N - where - N: TryFrom + CheckedMul, - >::Error: Debug, - { - if is_signed { + fn __abs(num: N, is_negative: bool) -> N { + if is_negative { N::try_from(-1) .expect("Negative 1 must be possible for a confirmed signed integer") - .checked_mul(&least_significant_digits) - .expect("Must be possible in these low values") + .checked_mul(&num) + .expect("Must be possible for these low values") } else { - least_significant_digits + num } } - pub fn add_percent_to(&self, num: N) -> N - where - N: PercentageInteger, - >::Error: Debug, - i16: TryFrom, - >::Error: Debug, - { - let to_add = self.of(num); + // This function helps to correct the last digit of the resulting integer to be as close + // as possible to the hypothetical fractional number, if we could go beyond the decimal point. + fn __derive_rounding_increment(remainder: N) -> N { + let is_negative = remainder < N::try_from(0).expect("Each integer has 0"); + let is_minor = + Self::__abs(remainder, is_negative) < N::try_from(50).expect("Each integer has 50"); + let addition = match (is_negative, is_minor) { + (false, true) => 0, + (false, false) => 1, + (true, true) => 0, + (true, false) => -1, + }; + N::try_from(addition).expect("Each integer has 1, or -1 if signed") + } + + fn _increase_by_percent_for(&self, num: N) -> N { + let to_add = self._of(num); num.checked_add(&to_add).unwrap_or_else(|| { panic!( - "Overflowed during addition of {} percent, that is {:?}, to {:?} of type {}.", + "Overflowed during addition of {} percent, that is an extra {:?} for {:?} of type {}.", self.degree, to_add, num, @@ -227,61 +141,180 @@ impl PurePercentage { }) } - pub fn subtract_percent_from(&self, num: N) -> N + fn _decrease_by_percent_for(&self, num: N) -> N { + let to_subtract = self._of(num); + num.checked_sub(&to_subtract) + .expect("Mathematically impossible") + } + + fn __handle_upper_overflow(&self, num: N) -> N { + let (base, remainder) = base_and_rem_from_div_100(num); + let percents = N::try_from(self.degree as i8).expect("Each integer has 100"); + let percents_of_base = base * percents; + let (percents_of_remainder, nearly_lost_tail) = + base_and_rem_for_ensured_i16(remainder, percents); + let final_rounding_element = Self::__derive_rounding_increment(nearly_lost_tail); + + percents_of_base + percents_of_remainder + final_rounding_element + } +} + +impl PurePercentage { + pub fn of(&self, num: N) -> N where - N: PercentageInteger + CheckedSub, + N: PercentageInteger, >::Error: Debug, i16: TryFrom, >::Error: Debug, { - let to_subtract = self.of(num); - num.checked_sub(&to_subtract) - .expect("should never happen by its principle") + self._of(num) } - fn handle_upper_overflow(&self, num: N) -> N + pub fn increase_by_percent_for(&self, num: N) -> N where N: PercentageInteger, >::Error: Debug, i16: TryFrom, >::Error: Debug, { - let hundred = N::try_from(100).expect("Each type has 100"); - let modulo = num % hundred; - let percent = N::try_from(self.degree as i8).expect("Each type has 100"); - - let without_treated_remainder = (num / hundred) * percent; - let final_remainder_treatment = Self::treat_remainder(modulo, percent); - without_treated_remainder + final_remainder_treatment + self._increase_by_percent_for(num) } - fn treat_remainder(modulo: N, percent: N) -> N + pub fn decrease_by_percent_for(&self, num: N) -> N where N: PercentageInteger, >::Error: Debug, i16: TryFrom, >::Error: Debug, { - let extended_remainder_prepared_for_rounding = i16::try_from(modulo) - .unwrap_or_else(|_| panic!("u16 from -100..=100 failed at modulo {:?}", modulo)) - * i16::try_from(percent).expect("i16 from within 0..=100 failed at multiplier"); - let rounded = Self::div_by_100_and_round(extended_remainder_prepared_for_rounding); - N::try_from(rounded as i8).expect("Each type has 0 up to 100") + self._decrease_by_percent_for(num) + } +} + +fn base_and_rem_for_ensured_i16(a: N, b: N) -> (N, N) +where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, +{ + let num = i16::try_from(a) + .expect("Remainder: Each integer can go up to 100, or down to -100 if signed") + * i16::try_from(b) + .expect("Percents: Each integer can go up to 100, or down to -100 if signed"); + + let (base, remainder) = base_and_rem_from_div_100(num); + + ( + N::try_from(base as i8) + .expect("Base: Each integer can go up to 100, or down to -100 if signed"), + N::try_from(remainder as i8) + .expect("Remainder: Each integer can go up to 100, or down to -100 if signed"), + ) +} + +fn base_and_rem_from_div_100(num: N) -> (N, N) +where + N: PercentageInteger, + >::Error: Debug, +{ + let hundred = N::try_from(100i8).expect("Each integer has 100"); + let modulo = num % hundred; + (num / hundred, modulo) +} + +// This is a wider type that allows to specify cumulative percents of more than only 100. +// The expected use of this would look like requesting percents meaning possibly multiples of 100%, +// roughly, of a certain base number. Similarly to the PurePercentage type, also signed numbers +// would be accepted. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LoosePercentage { + multiplier_of_100_percent: u32, + degrees_from_remainder: PurePercentage, +} + +impl LoosePercentage { + pub fn new(percents: u32) -> Self { + let multiples_of_100_percent = percents / 100; + let remainder = (percents % 100) as u8; + let degrees_from_remainder = + PurePercentage::try_from(remainder).expect("Should never happen."); + Self { + multiplier_of_100_percent: multiples_of_100_percent, + degrees_from_remainder, + } + } + + // If this returns an overflow error, you may want to precede this by converting the base + // number to a larger integer + pub fn of(&self, num: N) -> Result + where + N: PercentageInteger + TryFrom, + >::Error: Debug, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + let multiplier = match N::try_from(self.multiplier_of_100_percent) { + Ok(n) => n, + Err(e) => { + return Err(BaseTypeOverflow { + msg: format!( + "Couldn't init multiplier {} to type {} due to {:?}.", + self.multiplier_of_100_percent, + type_name::(), + e + ), + }) + } + }; + + let wholes = match num.checked_mul(&multiplier) { + Some(n) => n, + None => { + return Err(BaseTypeOverflow { + msg: format!( + "Multiplication failed between {:?} and {:?} for type {}.", + num, + multiplier, + type_name::() + ), + }) + } + }; + + let remainder = self.degrees_from_remainder.of(num); + + match wholes.checked_add(&remainder) { + Some(res) => Ok(res), + None => Err(BaseTypeOverflow { + msg: format!( + "Final addition failed on {:?} and {:?} for type {}.", + wholes, + remainder, + type_name::() + ), + }), + } } + + // Note that functions like 'add_percents_to' or 'subtract_percents_from' don't need to be + // implemented here, even though they are at the 'PurePercentage'. You can substitute them + // simply by querying 100 + or 100 - } #[derive(Debug, PartialEq, Eq)] -enum RoundingTo { - BiggerPositive, - BiggerNegative, - SmallerPositive, - SmallerNegative, +pub struct BaseTypeOverflow { + msg: String, } #[cfg(test)] mod tests { use crate::percentage::{ - BaseTypeOverflow, LoosePercentage, PercentageInteger, PurePercentage, RoundingTo, + BaseTypeOverflow, LoosePercentage, PercentageInteger, PurePercentage, + PurePercentageInternalMethods, }; use std::fmt::Debug; @@ -289,29 +322,90 @@ mod tests { fn percentage_is_implemented_for_all_rust_integers() { let subject = PurePercentage::try_from(50).unwrap(); - assert_integer_compatibility(&subject, u8::MAX, 128); - assert_integer_compatibility(&subject, u16::MAX, 32768); - assert_integer_compatibility(&subject, u32::MAX, 2147483648); - assert_integer_compatibility(&subject, u64::MAX, 9223372036854775808); - assert_integer_compatibility(&subject, u128::MAX, 170141183460469231731687303715884105728); - assert_integer_compatibility(&subject, i8::MIN, -64); - assert_integer_compatibility(&subject, i16::MIN, -16384); - assert_integer_compatibility(&subject, i32::MIN, -1073741824); - assert_integer_compatibility(&subject, i64::MIN, -4611686018427387904); - assert_integer_compatibility(&subject, i128::MIN, -85070591730234615865843651857942052864); + assert_positive_integer_compatibility(&subject, u8::MAX, 128); + assert_positive_integer_compatibility(&subject, u16::MAX, 32768); + assert_positive_integer_compatibility(&subject, u32::MAX, 2147483648); + assert_positive_integer_compatibility(&subject, u64::MAX, 9223372036854775808); + assert_positive_integer_compatibility( + &subject, + u128::MAX, + 170141183460469231731687303715884105728, + ); + assert_negative_integer_compatibility(&subject, i8::MIN, -64); + assert_negative_integer_compatibility(&subject, i16::MIN, -16384); + assert_negative_integer_compatibility(&subject, i32::MIN, -1073741824); + assert_negative_integer_compatibility(&subject, i64::MIN, -4611686018427387904); + assert_negative_integer_compatibility( + &subject, + i128::MIN, + -85070591730234615865843651857942052864, + ); + } + + fn assert_positive_integer_compatibility( + subject: &PurePercentage, + num: N, + expected_literal_num: N, + ) where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + assert_against_literal_value(subject, num, expected_literal_num); + + let trivially_calculated_half = num / N::try_from(2).unwrap(); + // Widening the bounds to compensate the extra rounding + let one = N::try_from(1).unwrap(); + assert!( + trivially_calculated_half <= expected_literal_num + && expected_literal_num <= (trivially_calculated_half + one), + "We expected {:?} to be {:?} or {:?}", + expected_literal_num, + trivially_calculated_half, + trivially_calculated_half + one + ) } - fn assert_integer_compatibility(subject: &PurePercentage, num: N, expected: N) - where + fn assert_negative_integer_compatibility( + subject: &PurePercentage, + num: N, + expected_literal_num: N, + ) where N: PercentageInteger, >::Error: Debug, i16: TryFrom, >::Error: Debug, { - assert_eq!(subject.of(num), expected); - let half = num / N::try_from(2).unwrap(); + assert_against_literal_value(subject, num, expected_literal_num); + + let trivially_calculated_half = num / N::try_from(2).unwrap(); + // Widening the bounds to compensate the extra rounding let one = N::try_from(1).unwrap(); - assert!((half - one) <= half && half <= (half + one)) + assert!( + trivially_calculated_half >= expected_literal_num + && expected_literal_num >= trivially_calculated_half - one, + "We expected {:?} to be {:?} or {:?}", + expected_literal_num, + trivially_calculated_half, + trivially_calculated_half - one + ) + } + + fn assert_against_literal_value(subject: &PurePercentage, num: N, expected_literal_num: N) + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + let percents_of_num = subject.of(num); + + assert_eq!( + percents_of_num, expected_literal_num, + "Expected {:?}, but was {:?}", + expected_literal_num, percents_of_num + ); } #[test] @@ -322,34 +416,31 @@ mod tests { #[test] fn pure_percentage_end_to_end_test_for_unsigned() { + let base_value = 100; + let act = |percent, base| PurePercentage::try_from(percent).unwrap().of(base); let expected_values = (0..=100).collect::>(); - test_end_to_end(100, expected_values, |percent, base| { - PurePercentage::try_from(percent).unwrap().of(base) - }) + test_end_to_end(act, base_value, expected_values) } #[test] fn pure_percentage_end_to_end_test_for_signed() { + let base_value = -100; + let act = |percent, base| PurePercentage::try_from(percent).unwrap().of(base); let expected_values = (-100..=0).rev().collect::>(); - test_end_to_end(-100, expected_values, |percent, base| { - PurePercentage::try_from(percent).unwrap().of(base) - }) + test_end_to_end(act, base_value, expected_values) } - fn test_end_to_end( - base: i8, - expected_values: Vec, - create_percentage_and_apply_it_on_number: F, - ) where + fn test_end_to_end(act: F, base: i8, expected_values: Vec) + where F: Fn(u8, i8) -> i8, { let range = 0_u8..=100; let round_returned_range = range .into_iter() - .map(|percent| create_percentage_and_apply_it_on_number(percent, base)) + .map(|percent| act(percent, base)) .collect::>(); assert_eq!(round_returned_range, expected_values) @@ -363,7 +454,7 @@ mod tests { assert_eq!( res, Err(format!( - "Accepts only range from 0 to 100 but {} was supplied", + "Accepts only range from 0 to 100, but {} was supplied", num )) ) @@ -412,7 +503,7 @@ mod tests { .of(case.examined_base_number); assert_eq!( result, case.expected_result, - "For {} percent and number {} the expected result was {} but we got {}", + "For {} percent and number {} the expected result was {}, but we got {}", case.requested_percent, case.examined_base_number, case.expected_result, result ) }) @@ -421,58 +512,46 @@ mod tests { #[test] fn should_be_rounded_to_works_for_last_but_one_digit() { [ - (49, RoundingTo::SmallerPositive, RoundingTo::SmallerNegative), - (50, RoundingTo::BiggerPositive, RoundingTo::BiggerNegative), - (51, RoundingTo::BiggerPositive, RoundingTo::BiggerNegative), - (5, RoundingTo::SmallerPositive, RoundingTo::SmallerNegative), - ( - 100, - RoundingTo::SmallerPositive, - RoundingTo::SmallerNegative, - ), - ( - 787879, - RoundingTo::BiggerPositive, - RoundingTo::BiggerNegative, - ), - ( - 898784545, - RoundingTo::SmallerPositive, - RoundingTo::SmallerNegative, - ), + (49, 0), + (50, 1), + (51, 1), + (5, 0), + (99,1), + (0,0) ] .into_iter() .for_each( - |(num, expected_result_for_unsigned_base, expected_result_for_signed_base)| { - let result = PurePercentage::should_be_rounded_to(num, 100); + |(num, expected_abs_result)| { + let result = PurePercentage::__derive_rounding_increment(num); assert_eq!( - result, - expected_result_for_unsigned_base, - "Unsigned number {} was identified for rounding as {:?} but it should've been {:?}", - num, - result, - expected_result_for_unsigned_base + result, + expected_abs_result, + "Unsigned number {} was identified for rounding as {:?}, but it should've been {:?}", + num, + result, + expected_abs_result ); let signed = num as i64 * -1; - let result = PurePercentage::should_be_rounded_to(signed, 100); + let result = PurePercentage::__derive_rounding_increment(signed); + let expected_neg_result = expected_abs_result * -1; assert_eq!( result, - expected_result_for_signed_base, - "Signed number {} was identified for rounding as {:?} but it should've been {:?}", + expected_neg_result, + "Signed number {} was identified for rounding as {:?}, but it should've been {:?}", signed, result, - expected_result_for_signed_base + expected_neg_result ) }, ) } #[test] - fn add_percent_to_works() { + fn increase_by_percent_for_works() { let subject = PurePercentage::try_from(13).unwrap(); - let unsigned = subject.add_percent_to(100); - let signed = subject.add_percent_to(-100); + let unsigned = subject.increase_by_percent_for(100); + let signed = subject.increase_by_percent_for(-100); assert_eq!(unsigned, 113); assert_eq!(signed, -113) @@ -480,19 +559,19 @@ mod tests { #[test] #[should_panic(expected = "Overflowed during addition of 1 percent, that is \ - 184467440737095516, to 18446744073709551615 of type u64.")] - fn add_percent_to_hits_overflow() { + an extra 184467440737095516 for 18446744073709551615 of type u64.")] + fn increase_by_percent_for_hits_overflow() { let _ = PurePercentage::try_from(1) .unwrap() - .add_percent_to(u64::MAX); + .increase_by_percent_for(u64::MAX); } #[test] - fn subtract_percent_from_works() { + fn decrease_by_percent_for_works() { let subject = PurePercentage::try_from(55).unwrap(); - let unsigned = subject.subtract_percent_from(100); - let signed = subject.subtract_percent_from(-100); + let unsigned = subject.decrease_by_percent_for(100); + let signed = subject.decrease_by_percent_for(-100); assert_eq!(unsigned, 45); assert_eq!(signed, -45) @@ -524,7 +603,7 @@ mod tests { assert_eq!(case_one, 187541898082713775); assert_eq!(case_two, 77); assert_eq!(case_three, 76) - //Note: Interestingly, this isn't a threat on the negative numbers, even the extremes. + // Note: Interestingly, this isn't a threat on the negative numbers, even the extremes. } #[test] @@ -535,20 +614,20 @@ mod tests { #[test] fn loose_percentage_end_to_end_test_for_standard_values_unsigned() { + let base_value = 100; + let act = |percent, base| LoosePercentage::new(percent as u32).of(base).unwrap(); let expected_values = (0..=100).collect::>(); - test_end_to_end(100, expected_values, |percent, base| { - LoosePercentage::new(percent as u32).of(base).unwrap() - }) + test_end_to_end(act, base_value, expected_values) } #[test] fn loose_percentage_end_to_end_test_for_standard_values_signed() { + let base_value = -100; + let act = |percent, base| LoosePercentage::new(percent as u32).of(base).unwrap(); let expected_values = (-100..=0).rev().collect::>(); - test_end_to_end(-100, expected_values, |percent, base| { - LoosePercentage::new(percent as u32).of(base).unwrap() - }) + test_end_to_end(act, base_value, expected_values) } const TEST_SET: [Case; 5] = [ @@ -621,26 +700,42 @@ mod tests { let result: Result = subject.of(1); - assert_eq!(result, Err(BaseTypeOverflow {})) + assert_eq!( + result, + Err(BaseTypeOverflow { + msg: "Couldn't init multiplier 256 to type u8 due to TryFromIntError(())." + .to_string() + }) + ) } #[test] - fn loose_percentage_multiplying_input_number_hits_limit() { + fn loose_percentage_hits_limit_at_multiplication() { let percents = 200; let subject = LoosePercentage::new(percents); let result: Result = subject.of(u8::MAX); - assert_eq!(result, Err(BaseTypeOverflow {})) + assert_eq!( + result, + Err(BaseTypeOverflow { + msg: "Multiplication failed between 255 and 2 for type u8.".to_string() + }) + ) } #[test] - fn loose_percentage_adding_portion_from_remainder_hits_limit() { + fn loose_percentage_hits_limit_at_addition_from_remainder() { let percents = 101; let subject = LoosePercentage::new(percents); let result: Result = subject.of(u8::MAX); - assert_eq!(result, Err(BaseTypeOverflow {})) + assert_eq!( + result, + Err(BaseTypeOverflow { + msg: "Final addition failed on 255 and 3 for type u8.".to_string() + }) + ) } } diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index a250b8ed3..b121d4148 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -2,7 +2,7 @@ use crate::verify_bill_payment_utils::utils::{ test_body, to_wei, AssertionsValues, Debt, DebtsSpecs, FinalServiceFeeBalancesByServingNodes, - NodeProfile, Ports, TestInputsBuilder, WholesomeConfig, + NodeProfile, TestInputBuilder, UiPorts, WholesomeConfig, }; use itertools::Itertools; use masq_lib::blockchains::chains::Chain; @@ -23,7 +23,7 @@ use std::u128; mod verify_bill_payment_utils; #[test] -fn full_payments_were_processed_for_sufficient_balances() { +fn payments_processed_fully_as_balances_were_sufficient() { // Note: besides the main objectives of this test, it relies on (and so it proves) the premise // that each Node, after it achieves an effective connectivity as making a route is enabled, // activates the accountancy module whereas the first cycle of scanners is unleashed. That's @@ -46,7 +46,7 @@ fn full_payments_were_processed_for_sufficient_balances() { let owed_to_serving_node_2_minor = debt_threshold_wei + 456_789; let owed_to_serving_node_3_minor = debt_threshold_wei + 789_012; let consuming_node_initial_service_fee_balance_minor = debt_threshold_wei * 4; - let test_inputs = TestInputsBuilder::default() + let test_input = TestInputBuilder::default() .consuming_node_initial_service_fee_balance_minor( consuming_node_initial_service_fee_balance_minor, ) @@ -75,7 +75,7 @@ fn full_payments_were_processed_for_sufficient_balances() { }; test_body( - test_inputs, + test_input, assertions_values, stimulate_consuming_node_to_pay_for_test_with_sufficient_funds, activating_serving_nodes_for_test_with_sufficient_funds, @@ -87,7 +87,8 @@ fn stimulate_consuming_node_to_pay_for_test_with_sufficient_funds( real_consuming_node: &MASQRealNode, _wholesome_config: &WholesomeConfig, ) { - for _ in 0..6 { + // 1 + 4 Nodes should be enough to compose a route, right? + for _ in 0..4 { cluster.start_real_node( NodeStartupConfigBuilder::standard() .chain(Chain::Dev) @@ -145,7 +146,8 @@ fn set_old_debts( .into_iter() .map(|balance_minor| Debt::new(balance_minor, quite_long_ago)) .collect_vec(); - DebtsSpecs::new(debts[0], debts[1], debts[2]) + let debt_array = debts.try_into().unwrap(); + DebtsSpecs::new(debt_array) } #[test] @@ -161,14 +163,15 @@ fn payments_were_adjusted_due_to_insufficient_balances() { // Assuming all Nodes rely on the same set of payment thresholds let owed_to_serv_node_1_minor = to_wei(payment_thresholds.debt_threshold_gwei + 5_000_000); let owed_to_serv_node_2_minor = to_wei(payment_thresholds.debt_threshold_gwei + 20_000_000); - // Account of Node 3 will be a victim of tx fee insufficiency and will fall away, as its debt - // is the heaviest, implying the smallest weight evaluated and the last priority compared to - // those two others. + // Account of Node 3 will be a victim of tx fee insufficiency and will drop out, as its debt + // is the heaviest, implying the smallest weight evaluated and the smallest priority compared to + // the two others. let owed_to_serv_node_3_minor = to_wei(payment_thresholds.debt_threshold_gwei + 60_000_000); let enough_balance_for_serving_node_1_and_2 = owed_to_serv_node_1_minor + owed_to_serv_node_2_minor; + let missing_portion_to_the_full_amount = to_wei(2_345_678); let consuming_node_initial_service_fee_balance_minor = - enough_balance_for_serving_node_1_and_2 - to_wei(2_345_678); + enough_balance_for_serving_node_1_and_2 - missing_portion_to_the_full_amount; let gas_price_major = 60; let tx_fee_needed_to_pay_for_one_payment_major = { // We'll need littler funds, but we can stand mild inaccuracy from assuming the use of @@ -183,15 +186,15 @@ fn payments_were_adjusted_due_to_insufficient_balances() { .unwrap() }; let transaction_fee_margin = PurePercentage::try_from(15).unwrap(); - transaction_fee_margin.add_percent_to(gas_limit_dev_chain * gas_price_major) + transaction_fee_margin.increase_by_percent_for(gas_limit_dev_chain * gas_price_major) }; - const AFFORDABLE_PAYMENTS_COUNT: u128 = 2; + let affordable_payments_count_by_tx_fee = 2; let tx_fee_needed_to_pay_for_one_payment_minor: u128 = to_wei(tx_fee_needed_to_pay_for_one_payment_major); let consuming_node_transaction_fee_balance_minor = - AFFORDABLE_PAYMENTS_COUNT * tx_fee_needed_to_pay_for_one_payment_minor; - let test_inputs = TestInputsBuilder::default() - .ui_ports(Ports::new( + affordable_payments_count_by_tx_fee * tx_fee_needed_to_pay_for_one_payment_minor; + let test_input = TestInputBuilder::default() + .ui_ports(UiPorts::new( find_free_port(), find_free_port(), find_free_port(), @@ -202,7 +205,7 @@ fn payments_were_adjusted_due_to_insufficient_balances() { .consuming_node_initial_service_fee_balance_minor( consuming_node_initial_service_fee_balance_minor, ) - .debts_config(DebtsSpecs::new( + .debts_config(DebtsSpecs::new([ // This account will be the most significant and will deserve the full balance Debt::new( owed_to_serv_node_1_minor, @@ -219,7 +222,7 @@ fn payments_were_adjusted_due_to_insufficient_balances() { owed_to_serv_node_3_minor, payment_thresholds.maturity_threshold_sec + 30_000, ), - )) + ])) .payment_thresholds_all_nodes(payment_thresholds) .consuming_node_gas_price_major(gas_price_major) .build(); @@ -240,7 +243,7 @@ fn payments_were_adjusted_due_to_insufficient_balances() { }; test_body( - test_inputs, + test_input, assertions_values, stimulate_consuming_node_to_pay_for_test_with_insufficient_funds, activating_serving_nodes_for_test_with_insufficient_funds, @@ -286,7 +289,7 @@ fn activating_serving_nodes_for_test_with_insufficient_funds( node_config, ); let ui_port = serving_node_attributes - .serving_node_profile + .node_profile .ui_port() .expect("ui port missing"); diff --git a/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs b/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs index 18eee8ae7..cdace9d32 100644 --- a/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs +++ b/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs @@ -1,6 +1,7 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use bip39::{Language, Mnemonic, Seed}; +use ethereum_types::H256; use futures::Future; use itertools::Itertools; use lazy_static::lazy_static; @@ -20,9 +21,7 @@ use node_lib::blockchain::bip32::Bip32EncryptionKeyProvider; use node_lib::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; -use node_lib::blockchain::blockchain_interface::lower_level_interface::{ - LowBlockchainInt, ResultForBalance, -}; +use node_lib::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; use node_lib::blockchain::blockchain_interface::BlockchainInterface; use node_lib::database::db_initializer::{ DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, @@ -31,7 +30,7 @@ use node_lib::sub_lib::accountant::PaymentThresholds; use node_lib::sub_lib::blockchain_interface_web3::transaction_data_web3; use node_lib::sub_lib::wallet::Wallet; use node_lib::test_utils; -use node_lib::test_utils::standard_dir_for_test_input_data; +use node_lib::test_utils::test_input_data_standard_dir; use rustc_hex::{FromHex, ToHex}; use std::cell::RefCell; use std::fs::File; @@ -41,46 +40,39 @@ use std::thread; use std::time::{Duration, Instant, SystemTime}; use tiny_hderive::bip32::ExtendedPrivKey; use web3::transports::Http; -use web3::types::{Address, Bytes, SignedTransaction, TransactionParameters, TransactionRequest}; +use web3::types::{ + Address, Bytes, SignedTransaction, TransactionParameters, TransactionReceipt, + TransactionRequest, +}; use web3::Web3; pub type StimulateConsumingNodePayments = fn(&mut MASQNodeCluster, &MASQRealNode, &WholesomeConfig); -pub type StartServingNodesAndLetThemPerformReceivablesCheck = +pub type StartServingNodesAndLetThemActivateTheirAccountancy = fn(&mut MASQNodeCluster, &WholesomeConfig) -> [MASQRealNode; 3]; pub fn test_body( - test_inputs: TestInputs, + test_inputs: TestInput, assertions_values: AssertionsValues, - stimulate_consuming_node_to_pay: StimulateConsumingNodePayments, - start_serving_nodes_and_activate_their_accountancy: StartServingNodesAndLetThemPerformReceivablesCheck, + stimulate_consuming_node_payments: StimulateConsumingNodePayments, + start_serving_nodes_and_let_them_activate_their_accountancy: StartServingNodesAndLetThemActivateTheirAccountancy, ) { // It's important to prevent the blockchain server handle being dropped too early let (mut cluster, global_values, _blockchain_server) = establish_test_frame(test_inputs); - let consuming_node = - global_values.prepare_consuming_node(&mut cluster, &global_values.blockchain_interfaces); + let consuming_node = global_values.prepare_consuming_node(&mut cluster); let serving_nodes_array = global_values.prepare_serving_nodes(&mut cluster); global_values.set_up_consuming_node_db(&serving_nodes_array, &consuming_node); global_values.set_up_serving_nodes_databases(&serving_nodes_array, &consuming_node); let wholesome_config = WholesomeConfig::new(global_values, consuming_node, serving_nodes_array); wholesome_config.assert_expected_wallet_addresses(); + let cn_common = &wholesome_config.consuming_node.common; let real_consuming_node = cluster.start_named_real_node( - &wholesome_config - .consuming_node - .common - .prepared_node - .node_docker_name, - wholesome_config.consuming_node.common.prepared_node.index, - wholesome_config - .consuming_node - .common - .startup_config_opt - .borrow_mut() - .take() - .unwrap(), + &cn_common.prepared_node.node_docker_name, + cn_common.prepared_node.index, + cn_common.startup_config_opt.borrow_mut().take().unwrap(), ); - stimulate_consuming_node_to_pay(&mut cluster, &real_consuming_node, &wholesome_config); + stimulate_consuming_node_payments(&mut cluster, &real_consuming_node, &wholesome_config); let timeout_start = Instant::now(); while !wholesome_config @@ -94,7 +86,7 @@ pub fn test_body( } wholesome_config.assert_payments_via_direct_blockchain_scanning(&assertions_values); - let _ = start_serving_nodes_and_activate_their_accountancy( + let _ = start_serving_nodes_and_let_them_activate_their_accountancy( &mut cluster, // So that individual Configs can be pulled out and used &wholesome_config, @@ -103,11 +95,7 @@ pub fn test_body( wholesome_config.assert_serving_nodes_addressed_received_payments(&assertions_values) } -const MNEMONIC_PHRASE: &str = - "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle \ - lamp absent write kind term toddler sphere ripple idle dragon curious hold"; - -pub struct TestInputs { +pub struct TestInput { // The contract owner wallet is populated with 100 ETH as defined in the set of commands with // which we start up the Ganache server. // @@ -119,17 +107,17 @@ pub struct TestInputs { } #[derive(Default)] -pub struct TestInputsBuilder { - ui_ports_opt: Option, +pub struct TestInputBuilder { + ui_ports_opt: Option, consuming_node_initial_tx_fee_balance_minor_opt: Option, consuming_node_initial_service_fee_balance_minor_opt: Option, debts_config_opt: Option, payment_thresholds_all_nodes_opt: Option, - consuming_node_gas_price_opt: Option, + consuming_node_gas_price_major_opt: Option, } -impl TestInputsBuilder { - pub fn ui_ports(mut self, ports: Ports) -> Self { +impl TestInputBuilder { + pub fn ui_ports(mut self, ports: UiPorts) -> Self { self.ui_ports_opt = Some(ports); self } @@ -155,11 +143,11 @@ impl TestInputsBuilder { } pub fn consuming_node_gas_price_major(mut self, gas_price: u64) -> Self { - self.consuming_node_gas_price_opt = Some(gas_price); + self.consuming_node_gas_price_major_opt = Some(gas_price); self } - pub fn build(self) -> TestInputs { + pub fn build(self) -> TestInput { let mut debts = self .debts_config_opt .expect("You forgot providing a mandatory input: debts config") @@ -170,7 +158,7 @@ impl TestInputsBuilder { let mut serving_nodes_ui_ports_opt = serving_nodes_ui_ports_opt.to_vec(); let consuming_node = ConsumingNodeProfile { ui_port_opt: consuming_node_ui_port_opt, - gas_price_opt: self.consuming_node_gas_price_opt, + gas_price_opt: self.consuming_node_gas_price_major_opt, initial_tx_fee_balance_minor_opt: self.consuming_node_initial_tx_fee_balance_minor_opt, initial_service_fee_balance_minor: self .consuming_node_initial_service_fee_balance_minor_opt @@ -197,7 +185,7 @@ impl TestInputsBuilder { serving_nodes: core::array::from_fn(|_| serving_nodes.remove(0)), }; - TestInputs { + TestInput { payment_thresholds_all_nodes: self .payment_thresholds_all_nodes_opt .expect("Mandatory input not provided: payment thresholds"), @@ -205,13 +193,11 @@ impl TestInputsBuilder { } } - fn resolve_ports(ui_ports_opt: Option) -> (Option, [Option; 3]) { + fn resolve_ports(ui_ports_opt: Option) -> (Option, [Option; 3]) { match ui_ports_opt { Some(ui_ports) => { - let mut ui_ports_as_opt = - ui_ports.serving_nodes.into_iter().map(Some).collect_vec(); - let serving_nodes_array: [Option; 3] = - core::array::from_fn(|_| ui_ports_as_opt.remove(0)); + let ui_ports_as_opt = ui_ports.serving_nodes.into_iter().map(Some).collect_vec(); + let serving_nodes_array: [Option; 3] = ui_ports_as_opt.try_into().unwrap(); (Some(ui_ports.consuming_node), serving_nodes_array) } None => Default::default(), @@ -259,21 +245,277 @@ impl FinalServiceFeeBalancesByServingNodes { pub struct BlockchainParams { chain: Chain, server_url: String, - contract_owner_addr: Address, contract_owner_wallet: Wallet, seed: Seed, } -struct BlockchainInterfaces { - standard_blockchain_interface: Box, - web3: Web3, +impl BlockchainParams { + fn new( + chain: Chain, + server_url: String, + blockchain_interface: &ExtendedBlockchainInterface, + ) -> Self { + let seed = make_seed(); + let (contract_owner_wallet, _) = + make_node_wallet_and_private_key(&seed, &derivation_path(0, 0)); + let contract_owner_addr = + blockchain_interface.deploy_smart_contract(&contract_owner_wallet, chain); + + assert_eq!( + contract_owner_addr, + chain.rec().contract, + "Either the contract has been modified or Ganache is not accurately mimicking Ethereum. \ + Resulted contact addr {:?} doesn't much what's expected: {:?}", + contract_owner_addr, + chain.rec().contract + ); + + BlockchainParams { + chain, + server_url, + contract_owner_wallet, + seed, + } + } +} + +struct ExtendedBlockchainInterface { + node_standard_interface: Box, + raw_interface: RawBlockchainInterface, +} + +impl ExtendedBlockchainInterface { + fn new(chain: Chain, server_url: &str) -> Self { + let (event_loop_handle, http) = + Http::with_max_parallel(&server_url, REQUESTS_IN_PARALLEL).unwrap(); + let web3 = Web3::new(http.clone()); + let raw_interface = RawBlockchainInterface::new(web3); + let node_standard_interface = + Box::new(BlockchainInterfaceWeb3::new(http, event_loop_handle, chain)); + Self { + node_standard_interface, + raw_interface, + } + } + + fn deploy_smart_contract(&self, wallet: &Wallet, chain: Chain) -> Address { + let contract = load_contract_in_bytes(); + let tx = TransactionParameters { + nonce: Some(ethereum_types::U256::zero()), + to: None, + gas: *GAS_LIMIT, + gas_price: Some(*GAS_PRICE), + value: ethereum_types::U256::zero(), + data: Bytes(contract), + chain_id: Some(chain.rec().num_chain_id), + }; + let signed_tx = self.raw_interface.await_sign_transaction(tx, wallet); + match self.raw_interface.await_send_raw_transaction(signed_tx) { + Ok(tx_hash) => match self.raw_interface.await_transaction_receipt(tx_hash) { + Ok(Some(tx_receipt)) => tx_receipt.contract_address.unwrap(), + Ok(None) => panic!("Contract deployment failed Ok(None)"), + Err(e) => panic!("Contract deployment failed {:?}", e), + }, + Err(e) => panic!("Contract deployment failed {:?}", e), + } + } + + fn transfer_transaction_fee_amount_to_address( + &self, + from_wallet: &Wallet, + to_wallet: &Wallet, + amount_minor: u128, + transaction_nonce: u64, + ) { + let tx = TransactionRequest { + from: from_wallet.address(), + to: Some(to_wallet.address()), + gas: Some(*GAS_LIMIT), + gas_price: Some(*GAS_PRICE), + value: Some(ethereum_types::U256::from(amount_minor)), + data: None, + nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), + condition: None, + }; + match self + .raw_interface + .await_unlock_account(from_wallet.address(), "", None) + { + Ok(was_successful) => { + if was_successful { + eprintln!("Account {} unlocked for a single transaction", from_wallet) + } else { + panic!( + "Couldn't unlock account {} for the purpose of signing the next transaction", + from_wallet + ) + } + } + Err(e) => panic!( + "Attempt to unlock account {:?} failed at {:?}", + from_wallet.address(), + e + ), + } + match self.raw_interface.await_send_transaction(tx) { + Ok(tx_hash) => eprintln!( + "Transaction {:?} of {} wei of ETH was sent from wallet {:?} to {:?}", + tx_hash, amount_minor, from_wallet, to_wallet + ), + Err(e) => panic!("Transaction for token transfer failed {:?}", e), + } + } + + fn transfer_service_fee_amount_to_address( + &self, + contract_addr: Address, + from_wallet: &Wallet, + to_wallet: &Wallet, + amount_minor: u128, + transaction_nonce: u64, + chain: Chain, + ) { + let data = transaction_data_web3(to_wallet, amount_minor); + let tx = TransactionParameters { + nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), + to: Some(contract_addr), + gas: *GAS_LIMIT, + gas_price: Some(*GAS_PRICE), + value: ethereum_types::U256::zero(), + data: Bytes(data.to_vec()), + chain_id: Some(chain.rec().num_chain_id), + }; + let signed_tx = self.raw_interface.await_sign_transaction(tx, from_wallet); + match &self.raw_interface.await_send_raw_transaction(signed_tx) { + Ok(tx_hash) => eprintln!( + "Transaction {:?} of {} wei of MASQ was sent from wallet {} to {}", + tx_hash, amount_minor, from_wallet, to_wallet + ), + Err(e) => panic!("Transaction for token transfer failed {:?}", e), + } + } + + fn single_balance_assertion( + &self, + wallet: &Wallet, + expected_balance: u128, + asserted_balance: AssertedBalance, + ) { + let balance_fetcher = match asserted_balance { + AssertedBalance::TransactionFee => LowBlockchainInt::get_transaction_fee_balance, + AssertedBalance::ServiceFee => LowBlockchainInt::get_service_fee_balance, + }; + let lower_blockchain_int = self.node_standard_interface.lower_interface(); + let actual_balance = balance_fetcher(lower_blockchain_int, &wallet) + .unwrap_or_else(|_| panic!("Failed to retrieve {:?} for {}", asserted_balance, wallet)); + assert_eq!( + actual_balance, + web3::types::U256::from(expected_balance), + "Actual {:?} {} doesn't much with expected {} for {}", + asserted_balance, + actual_balance, + expected_balance, + wallet + ); + } + + fn assert_balances( + &self, + wallet: &Wallet, + expected_tx_fee_balance: u128, + expected_service_fee_balance: u128, + ) { + self.single_balance_assertion( + wallet, + expected_tx_fee_balance, + AssertedBalance::TransactionFee, + ); + + self.single_balance_assertion( + wallet, + expected_service_fee_balance, + AssertedBalance::ServiceFee, + ); + } +} + +#[derive(Debug, Clone, Copy)] +enum AssertedBalance { + TransactionFee, + ServiceFee, +} + +lazy_static! { + static ref GAS_PRICE: ethereum_types::U256 = + 50_u64.try_into().expect("Gas price, internal error"); + static ref GAS_LIMIT: ethereum_types::U256 = + 1_000_000_u64.try_into().expect("Gas limit, internal error"); +} + +struct RawBlockchainInterface { + web3_transport: Web3, +} + +impl RawBlockchainInterface { + fn new(web3_transport: Web3) -> Self { + Self { web3_transport } + } + fn await_unlock_account( + &self, + address: Address, + password: &str, + duration: Option, + ) -> Result { + self.web3_transport + .personal() + .unlock_account(address, password, duration) + .wait() + } + fn await_send_transaction(&self, tx: TransactionRequest) -> Result { + self.web3_transport.eth().send_transaction(tx).wait() + } + + fn await_sign_transaction( + &self, + tx: TransactionParameters, + signing_wallet: &Wallet, + ) -> SignedTransaction { + let secret = &signing_wallet + .prepare_secp256k1_secret() + .expect("wallet without secret"); + self.web3_transport + .accounts() + .sign_transaction(tx, secret) + .wait() + .expect("transaction preparation failed") + } + + fn await_send_raw_transaction( + &self, + tx: SignedTransaction, + ) -> Result { + self.web3_transport + .eth() + .send_raw_transaction(tx.raw_transaction) + .wait() + } + + fn await_transaction_receipt( + &self, + tx_hash: H256, + ) -> Result, web3::error::Error> { + self.web3_transport + .eth() + .transaction_receipt(tx_hash) + .wait() + } } pub struct GlobalValues { - pub test_inputs: TestInputs, + pub test_inputs: TestInput, pub blockchain_params: BlockchainParams, pub now_in_common: SystemTime, - blockchain_interfaces: BlockchainInterfaces, + blockchain_interface: ExtendedBlockchainInterface, } pub struct WholesomeConfig { @@ -287,8 +529,7 @@ pub struct DebtsSpecs { } impl DebtsSpecs { - pub fn new(node_1: Debt, node_2: Debt, node_3: Debt) -> Self { - let debts = [node_1, node_2, node_3]; + pub fn new(debts: [Debt; 3]) -> Self { Self { debts } } } @@ -376,56 +617,38 @@ impl NodeProfile for ServingNodeProfile { } pub fn establish_test_frame( - test_inputs: TestInputs, + test_inputs: TestInput, ) -> (MASQNodeCluster, GlobalValues, BlockchainServer) { let now = SystemTime::now(); let cluster = match MASQNodeCluster::start() { Ok(cluster) => cluster, Err(e) => panic!("{}", e), }; - let blockchain_server = BlockchainServer::new("ganache-cli"); - blockchain_server.start(); - blockchain_server.wait_until_ready(); - let server_url = blockchain_server.url().to_string(); - let (event_loop_handle, http) = - Http::with_max_parallel(&server_url, REQUESTS_IN_PARALLEL).unwrap(); - let web3 = Web3::new(http.clone()); - let seed = make_seed(); - let (contract_owner_wallet, _) = - make_node_wallet_and_private_key(&seed, &derivation_path(0, 0)); + let (blockchain_server, server_url) = start_blockchain_server(); let chain = cluster.chain(); - let contract_owner_addr = deploy_smart_contract(&contract_owner_wallet, &web3, chain); - let blockchain_interface = - Box::new(BlockchainInterfaceWeb3::new(http, event_loop_handle, chain)); - let blockchain_params = BlockchainParams { - chain, - server_url, - contract_owner_addr, - contract_owner_wallet, - seed, - }; - let blockchain_interfaces = BlockchainInterfaces { - standard_blockchain_interface: blockchain_interface, - web3, - }; + let blockchain_interface = ExtendedBlockchainInterface::new(chain, &server_url); + let blockchain_params = BlockchainParams::new(chain, server_url, &blockchain_interface); let global_values = GlobalValues { test_inputs, blockchain_params, - blockchain_interfaces, + blockchain_interface, now_in_common: now, }; - assert_eq!( - contract_owner_addr, - chain.rec().contract, - "Either the contract has been modified or Ganache is not accurately mimicking Ethereum. \ - Resulted contact addr {:?} doesn't much what's expected: {:?}", - contract_owner_addr, - chain.rec().contract - ); - (cluster, global_values, blockchain_server) } +fn start_blockchain_server() -> (BlockchainServer, String) { + let blockchain_server = BlockchainServer::new("ganache-cli"); + blockchain_server.start(); + blockchain_server.wait_until_ready(); + let url = blockchain_server.url().to_string(); + (blockchain_server, url) +} + +const MNEMONIC_PHRASE: &str = + "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle \ + lamp absent write kind term toddler sphere ripple idle dragon curious hold"; + fn make_seed() -> Seed { let mnemonic = Mnemonic::from_phrase(MNEMONIC_PHRASE, Language::English).unwrap(); Seed::new(&mnemonic, "") @@ -444,9 +667,8 @@ fn make_db_init_config(chain: Chain) -> DbInitializationConfig { } fn load_contract_in_bytes() -> Vec { - let file_path = - standard_dir_for_test_input_data().join("smart_contract_for_on_blockchain_test"); - let mut file = File::open(file_path).expect("couldn't acquire a handle to the data file"); + let file_path = test_input_data_standard_dir().join("verify_bill_payments_smart_contract"); + let mut file = File::open(file_path).expect("couldn't acquire a file handle"); let mut data = String::new(); file.read_to_string(&mut data).unwrap(); let data = data @@ -454,184 +676,7 @@ fn load_contract_in_bytes() -> Vec { .filter(|char| !char.is_whitespace()) .collect::(); data.from_hex::>() - .expect("bad contract: contains non-hexadecimal characters") -} - -lazy_static! { - static ref GAS_PRICE: ethereum_types::U256 = - 50_u64.try_into().expect("Gas price, internal error"); - static ref GAS_LIMIT: ethereum_types::U256 = - 1_000_000_u64.try_into().expect("Gas limit, internal error"); -} - -fn deploy_smart_contract(wallet: &Wallet, web3: &Web3, chain: Chain) -> Address { - let contract = load_contract_in_bytes(); - let tx = TransactionParameters { - nonce: Some(ethereum_types::U256::zero()), - to: None, - gas: *GAS_LIMIT, - gas_price: Some(*GAS_PRICE), - value: ethereum_types::U256::zero(), - data: Bytes(contract), - chain_id: Some(chain.rec().num_chain_id), - }; - let signed_tx = primitive_sign_transaction(web3, tx, wallet); - match web3 - .eth() - .send_raw_transaction(signed_tx.raw_transaction) - .wait() - { - Ok(tx_hash) => match web3.eth().transaction_receipt(tx_hash).wait() { - Ok(Some(tx_receipt)) => tx_receipt.contract_address.unwrap(), - Ok(None) => panic!("Contract deployment failed Ok(None)"), - Err(e) => panic!("Contract deployment failed {:?}", e), - }, - Err(e) => panic!("Contract deployment failed {:?}", e), - } -} - -fn transfer_service_fee_amount_to_address( - contract_addr: Address, - from_wallet: &Wallet, - to_wallet: &Wallet, - amount_minor: u128, - transaction_nonce: u64, - web3: &Web3, - chain: Chain, -) { - let data = transaction_data_web3(to_wallet, amount_minor); - let tx = TransactionParameters { - nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), - to: Some(contract_addr), - gas: *GAS_LIMIT, - gas_price: Some(*GAS_PRICE), - value: ethereum_types::U256::zero(), - data: Bytes(data.to_vec()), - chain_id: Some(chain.rec().num_chain_id), - }; - let signed_tx = primitive_sign_transaction(web3, tx, from_wallet); - match web3 - .eth() - .send_raw_transaction(signed_tx.raw_transaction) - .wait() - { - Ok(tx_hash) => eprintln!( - "Transaction {:?} of {} wei of MASQ was sent from wallet {} to {}", - tx_hash, amount_minor, from_wallet, to_wallet - ), - Err(e) => panic!("Transaction for token transfer failed {:?}", e), - } -} - -fn primitive_sign_transaction( - web3: &Web3, - tx: TransactionParameters, - signing_wallet: &Wallet, -) -> SignedTransaction { - let secret = &signing_wallet - .prepare_secp256k1_secret() - .expect("wallet without secret"); - web3.accounts() - .sign_transaction(tx, secret) - .wait() - .expect("transaction preparation failed") -} - -fn transfer_transaction_fee_amount_to_address( - from_wallet: &Wallet, - to_wallet: &Wallet, - amount_minor: u128, - transaction_nonce: u64, - web3: &Web3, -) { - let tx = TransactionRequest { - from: from_wallet.address(), - to: Some(to_wallet.address()), - gas: Some(*GAS_LIMIT), - gas_price: Some(*GAS_PRICE), - value: Some(ethereum_types::U256::from(amount_minor)), - data: None, - nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), - condition: None, - }; - match web3 - .personal() - .unlock_account(from_wallet.address(), "", None) - .wait() - { - Ok(was_successful) => { - if was_successful { - eprintln!("Account {} unlocked for a single transaction", from_wallet) - } else { - panic!( - "Couldn't unlock account {} for the purpose of signing the next transaction", - from_wallet - ) - } - } - Err(e) => panic!( - "Attempt to unlock account {:?} failed at {:?}", - from_wallet.address(), - e - ), - } - match web3.eth().send_transaction(tx).wait() { - Ok(tx_hash) => eprintln!( - "Transaction {:?} of {} wei of ETH was sent from wallet {:?} to {:?}", - tx_hash, amount_minor, from_wallet, to_wallet - ), - Err(e) => panic!("Transaction for token transfer failed {:?}", e), - } -} - -fn assert_balances( - wallet: &Wallet, - blockchain_interface: &dyn BlockchainInterface, - expected_eth_balance: u128, - expected_token_balance: u128, -) { - single_balance_assertion( - blockchain_interface, - wallet, - expected_eth_balance, - "ETH balance", - |blockchain_interface, wallet| blockchain_interface.get_transaction_fee_balance(wallet), - ); - - single_balance_assertion( - blockchain_interface, - wallet, - expected_token_balance, - "MASQ balance", - |blockchain_interface, wallet| blockchain_interface.get_service_fee_balance(wallet), - ); -} - -fn single_balance_assertion( - blockchain_interface: &dyn BlockchainInterface, - wallet: &Wallet, - expected_balance: u128, - balance_specification: &str, - balance_fetcher: fn(&dyn LowBlockchainInt, &Wallet) -> ResultForBalance, -) { - let actual_balance = { - let lower_blockchain_int = blockchain_interface.lower_interface(); - balance_fetcher(lower_blockchain_int, &wallet).unwrap_or_else(|_| { - panic!( - "Failed to retrieve {} for {}", - balance_specification, wallet - ) - }) - }; - assert_eq!( - actual_balance, - web3::types::U256::from(expected_balance), - "Actual {} {} doesn't much with expected {} for {}", - balance_specification, - actual_balance, - expected_balance, - wallet - ); + .expect("contract contains non-hexadecimal characters") } fn make_node_wallet_and_private_key(seed: &Seed, derivation_path: &str) -> (Wallet, String) { @@ -668,11 +713,7 @@ impl GlobalValues { (config_builder.build(), node_wallet) } - fn prepare_consuming_node( - &self, - cluster: &mut MASQNodeCluster, - blockchain_interfaces: &BlockchainInterfaces, - ) -> ConsumingNode { + fn prepare_consuming_node(&self, cluster: &mut MASQNodeCluster) -> ConsumingNode { let consuming_node_profile = self.test_inputs.node_profiles.consuming_node.clone(); let initial_service_fee_balance_minor = consuming_node_profile.initial_service_fee_balance_minor; @@ -681,26 +722,25 @@ impl GlobalValues { let (consuming_node_config, consuming_node_wallet) = self.get_node_config_and_wallet(&consuming_node_profile); let initial_transaction_fee_balance = initial_tx_fee_balance_opt.unwrap_or(ONE_ETH_IN_WEI); - transfer_transaction_fee_amount_to_address( - &self.blockchain_params.contract_owner_wallet, - &consuming_node_wallet, - initial_transaction_fee_balance, - 1, - &blockchain_interfaces.web3, - ); - transfer_service_fee_amount_to_address( - self.blockchain_params.contract_owner_addr, - &self.blockchain_params.contract_owner_wallet, - &consuming_node_wallet, - initial_service_fee_balance_minor, - 2, - &blockchain_interfaces.web3, - self.blockchain_params.chain, - ); + self.blockchain_interface + .transfer_transaction_fee_amount_to_address( + &self.blockchain_params.contract_owner_wallet, + &consuming_node_wallet, + initial_transaction_fee_balance, + 1, + ); + self.blockchain_interface + .transfer_service_fee_amount_to_address( + self.blockchain_params.contract_owner_wallet.address(), + &self.blockchain_params.contract_owner_wallet, + &consuming_node_wallet, + initial_service_fee_balance_minor, + 2, + self.blockchain_params.chain, + ); - assert_balances( + self.blockchain_interface.assert_balances( &consuming_node_wallet, - blockchain_interfaces.standard_blockchain_interface.as_ref(), initial_transaction_fee_balance, initial_service_fee_balance_minor, ); @@ -773,14 +813,8 @@ impl GlobalValues { .receivable_dao .more_money_receivable(timestamp, &consuming_node.consuming_wallet, balance) .unwrap(); - assert_balances( - &serving_node.earning_wallet, - self.blockchain_interfaces - .standard_blockchain_interface - .as_ref(), - 0, - 0, - ); + self.blockchain_interface + .assert_balances(&serving_node.earning_wallet, 0, 0); Self::set_start_block_to_zero(&serving_node.common.prepared_node.db_path) }) } @@ -836,7 +870,7 @@ impl WholesomeConfig { &serving_node_actual, expected_wallet_addr, "{:?} wallet {} mismatched with expected {}", - serving_node.serving_node_profile.serving_node_by_name, + serving_node.node_profile.serving_node_by_name, serving_node_actual, expected_wallet_addr ); @@ -844,14 +878,9 @@ impl WholesomeConfig { } fn assert_payments_via_direct_blockchain_scanning(&self, assertions_values: &AssertionsValues) { - let blockchain_interface = self - .global_values - .blockchain_interfaces - .standard_blockchain_interface - .as_ref(); - assert_balances( + let blockchain_interfaces = &self.global_values.blockchain_interface; + blockchain_interfaces.assert_balances( &self.consuming_node.consuming_wallet, - blockchain_interface, assertions_values.final_consuming_node_transaction_fee_balance_minor, assertions_values.final_consuming_node_service_fee_balance_minor, ); @@ -861,9 +890,8 @@ impl WholesomeConfig { .into_iter() .zip(self.serving_nodes.iter()) .for_each(|(expected_remaining_owed_value, serving_node)| { - assert_balances( + blockchain_interfaces.assert_balances( &serving_node.earning_wallet, - blockchain_interface, 0, expected_remaining_owed_value, ); @@ -882,7 +910,7 @@ impl WholesomeConfig { .iter() .zip(actually_received_payments.into_iter()) .for_each(|(serving_node, received_payment)| { - let original_debt = serving_node.serving_node_profile.debt_specs().balance_minor; + let original_debt = serving_node.node_profile.debt_specs().balance_minor; let expected_final_balance = original_debt - received_payment; Self::wait_for_exact_balance_in_receivables( &serving_node.receivable_dao, @@ -909,12 +937,12 @@ impl WholesomeConfig { pub const ONE_ETH_IN_WEI: u128 = 10_u128.pow(18); -pub struct Ports { +pub struct UiPorts { consuming_node: u16, serving_nodes: [u16; 3], } -impl Ports { +impl UiPorts { pub fn new( consuming_node: u16, serving_node_1: u16, @@ -953,7 +981,7 @@ pub struct ConsumingNode { #[derive(Debug)] pub struct ServingNode { - pub serving_node_profile: ServingNodeProfile, + pub node_profile: ServingNodeProfile, pub common: NodeAttributesCommon, pub earning_wallet: Wallet, pub receivable_dao: ReceivableDaoReal, @@ -961,7 +989,7 @@ pub struct ServingNode { impl ServingNode { fn debt_balance_and_timestamp(&self, now: SystemTime) -> (u128, SystemTime) { - let debt_specs = self.serving_node_profile.debt_specs(); + let debt_specs = self.node_profile.debt_specs(); (debt_specs.balance_minor, debt_specs.proper_timestamp(now)) } } @@ -986,7 +1014,7 @@ impl ConsumingNode { impl ServingNode { fn new( - serving_node_profile: ServingNodeProfile, + node_profile: ServingNodeProfile, prepared_node: PreparedNodeInfo, config: NodeStartupConfig, earning_wallet: Wallet, @@ -994,7 +1022,7 @@ impl ServingNode { ) -> Self { let common = NodeAttributesCommon::new(prepared_node, config); Self { - serving_node_profile, + node_profile, common, earning_wallet, receivable_dao, diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index c9551598a..a3a27257e 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1078,15 +1078,13 @@ mod tests { }; use crate::accountant::db_access_objects::receivable_dao::ReceivableAccount; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; - use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_into_analyzed_payables_in_test; + use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysisReport, PaymentAdjusterError, + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, TransactionFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ - PayableInspector, PayableThresholdsGaugeReal, - }; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGaugeReal; use crate::accountant::scanners::test_utils::protect_qualified_payables_in_test; use crate::accountant::scanners::BeginScanError; use crate::accountant::test_utils::DaoWithDestination::{ @@ -1585,7 +1583,7 @@ mod tests { let prepare_unadjusted_and_adjusted_payable = |n: u64| { let unadjusted_account = make_meaningless_qualified_payable(n); let adjusted_account = PayableAccount { - balance_wei: gwei_to_wei(n / 3), + balance_wei: gwei_to_wei::(n) / 3, ..unadjusted_account.bare_account.clone() }; (unadjusted_account, adjusted_account) @@ -1621,7 +1619,7 @@ mod tests { response_skeleton_opt: Some(response_skeleton), }; let analyzed_accounts = - convert_qualified_into_analyzed_payables_in_test(unadjusted_qualified_accounts.clone()); + convert_qualified_p_into_analyzed_p(unadjusted_qualified_accounts.clone()); let adjustment_analysis = AdjustmentAnalysisReport::new(Adjustment::ByServiceFee, analyzed_accounts.clone()); let payment_adjuster = PaymentAdjusterMock::default() @@ -1739,13 +1737,15 @@ mod tests { let test_name = "payment_adjuster_throws_out_an_error_during_stage_one_the_insolvency_check"; let payment_adjuster = PaymentAdjusterMock::default().consider_adjustment_result(Err( - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 1, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor: gwei_to_wei(60_u64 * 55_000), - cw_transaction_fee_balance_minor: gwei_to_wei(123_u64), - }), - service_fee_opt: None, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: gwei_to_wei(60_u64 * 55_000), + cw_transaction_fee_balance_minor: gwei_to_wei(123_u64), + }), + service_fee_opt: None, + }, }, )); @@ -1753,17 +1753,17 @@ mod tests { let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Add more funds into your consuming wallet in order to become able \ - to repay already expired liabilities as the creditors would respond by delinquency bans \ - otherwise. Details: Current transaction fee balance is not enough to pay a single payment. \ - Number of canceled payments: 1. Transaction fee per payment: 3,300,000,000,000,000 wei, \ - while the wallet contains: 123,000,000,000 wei." + "WARN: {test_name}: Add more funds into your consuming wallet to become able to repay \ + already matured debts as the creditors would respond by a delinquency ban otherwise. \ + Details: Current transaction fee balance is not enough to pay a single payment. Number \ + of canceled payments: 1. Transaction fee per payment: 3,300,000,000,000,000 wei, while \ + the wallet contains: 123,000,000,000 wei." )); log_handler .exists_log_containing(&format!("INFO: {test_name}: The Payables scan ended in")); log_handler.exists_log_containing(&format!( - "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for payments. \ - The cause appears to be in competence of the user." + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." )); } @@ -1777,23 +1777,23 @@ mod tests { Adjustment::ByServiceFee, vec![make_meaningless_analyzed_account(123)], )))) - .adjust_payments_result(Err(PaymentAdjusterError::RecursionDrainedAllAccounts)); + .adjust_payments_result(Err(PaymentAdjusterError::RecursionEliminatedAllAccounts)); test_payment_adjuster_error_during_different_stages(test_name, payment_adjuster); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( "WARN: {test_name}: Payment adjustment has not produced any executable payments. Add \ - more funds into your consuming wallet in order to become able to repay already expired \ - liabilities as the creditors would respond by delinquency bans otherwise. Details: The \ + more funds into your consuming wallet to become able to repay already matured debts as \ + the creditors would respond by a delinquency ban otherwise. Details: The \ payments adjusting process failed to find any combination of payables that can be paid \ immediately with the finances provided" )); log_handler .exists_log_containing(&format!("INFO: {test_name}: The Payables scan ended in")); log_handler.exists_log_containing(&format!( - "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for \ - payments. The cause appears to be in competence of the user" + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." )); } @@ -1804,13 +1804,15 @@ mod tests { "payment_adjuster_error_is_not_reported_to_ui_if_scan_not_manually_requested"; let mut subject = AccountantBuilder::default().build(); let payment_adjuster = PaymentAdjusterMock::default().consider_adjustment_result(Err( - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 20, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor: 40_000_000_000, - cw_transaction_fee_balance_minor: U256::from(123), - }), - service_fee_opt: None, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 40_000_000_000, + cw_transaction_fee_balance_minor: U256::from(123), + }), + service_fee_opt: None, + }, }, )); let payable_scanner = PayableScannerBuilder::new() @@ -1832,8 +1834,8 @@ mod tests { // No NodeUiMessage was sent because there is no `response_skeleton`. It is evident by // the fact that the test didn't blow up even though UIGateway is unbound TestLogHandler::new().exists_log_containing(&format!( - "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for payments. \ - The cause appears to be in competence of the user" + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." )); } @@ -3626,9 +3628,7 @@ mod tests { let payable_scanner = PayableScannerBuilder::new() .payable_dao(payable_dao_for_payable_scanner) .pending_payable_dao(pending_payable_dao_for_payable_scanner) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .payment_adjuster(payment_adjuster) .build(); subject.scanners.payable = Box::new(payable_scanner); diff --git a/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs b/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs index 77f086bd4..45226e199 100644 --- a/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs +++ b/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs @@ -62,7 +62,7 @@ mod tests { payment_adjuster_inner.initialize_guts(None, 123456789, largest_exceeding_balance); let subject = BalanceCriterionCalculator::default(); - let computed_criteria = analyzed_accounts + let calculated_values = analyzed_accounts .iter() .map(|analyzed_account| { subject.calculate(&analyzed_account.qualified_as, &payment_adjuster_inner) @@ -70,7 +70,7 @@ mod tests { .collect::>(); let expected_values = vec![4_384_000_000_000, 4_336_000_000_000, 2_216_000_000_000]; - computed_criteria + calculated_values .into_iter() .zip(expected_values.into_iter()) .for_each(|(actual_criterion, expected_criterion)| { diff --git a/node/src/accountant/payment_adjuster/disqualification_arbiter.rs b/node/src/accountant/payment_adjuster/disqualification_arbiter.rs index 119b9c0ba..1992b9667 100644 --- a/node/src/accountant/payment_adjuster/disqualification_arbiter.rs +++ b/node/src/accountant/payment_adjuster/disqualification_arbiter.rs @@ -177,13 +177,13 @@ impl DisqualificationGaugeReal { let considered_forgiven = threshold_intercept_minor - permanent_debt_allowed_minor; let minimal_acceptable_payment = exceeding_threshold + permanent_debt_allowed_minor; - let condition_of_debt_fast_growth = minimal_acceptable_payment + let is_debt_growing_fast = minimal_acceptable_payment >= Self::FIRST_QUALIFICATION_CONDITION_COEFFICIENT * considered_forgiven; - let condition_of_position_on_rather_the_left_half_of_the_slope = considered_forgiven + let situated_on_the_left_half_of_the_slope = considered_forgiven >= Self::SECOND_QUALIFICATION_CONDITION_COEFFICIENT * permanent_debt_allowed_minor; - condition_of_debt_fast_growth && condition_of_position_on_rather_the_left_half_of_the_slope + is_debt_growing_fast && situated_on_the_left_half_of_the_slope } fn determine_adequate_minimal_payment( @@ -207,6 +207,9 @@ impl DisqualificationGaugeReal { // This schema shows the conditions used to determine the disqualification limit // (or minimal acceptable payment) // + // Y axis - debt size + // + // | // | A + // | | P -----------+ // | | P | @@ -237,7 +240,7 @@ impl DisqualificationGaugeReal { // | U U \ U \ | P // | U U \U \ | P // | U U U \|D' P E' - // +---------------------------+---+---------------------+ + // +---------------------------+---+---------------------+ X axis - time // 3 4 2 1 // // This diagram presents computation of the disqualification limit which differs by four cases. @@ -280,7 +283,7 @@ mod tests { }; use crate::accountant::payment_adjuster::miscellaneous::data_structures::UnconfirmedAdjustment; use crate::accountant::payment_adjuster::test_utils::local_utils::{ - make_meaningless_weighed_account, make_non_guaranteed_unconfirmed_adjustment, + make_meaningless_unconfirmed_adjustment, make_meaningless_weighed_account, }; use itertools::Itertools; use masq_lib::logger::Logger; @@ -442,7 +445,7 @@ mod tests { #[test] fn list_accounts_nominated_for_disqualification_ignores_adjustment_even_to_the_dsq_limit() { - let mut account = make_non_guaranteed_unconfirmed_adjustment(444); + let mut account = make_meaningless_unconfirmed_adjustment(444); account.proposed_adjusted_balance_minor = 1_000_000_000; account .weighed_account @@ -525,24 +528,6 @@ mod tests { ); assert_eq!(result, wallet_3); - // Hardening of the test with more formal checks - let all_wallets = unconfirmed_adjustments - .iter() - .map(|unconfirmed_adjustment| { - &unconfirmed_adjustment - .weighed_account - .analyzed_account - .qualified_as - .bare_account - .wallet - }) - .collect_vec(); - assert_eq!(all_wallets.len(), 4); - let wallets_same_as_wallet_3 = all_wallets - .iter() - .filter(|wallet| wallet.address() == wallet_3) - .collect_vec(); - assert_eq!(wallets_same_as_wallet_3.len(), 1); } fn make_unconfirmed_adjustments(weights: Vec) -> Vec { @@ -550,7 +535,7 @@ mod tests { .into_iter() .enumerate() .map(|(idx, weight)| { - let mut account = make_non_guaranteed_unconfirmed_adjustment(idx as u64); + let mut account = make_meaningless_unconfirmed_adjustment(idx as u64); account.weighed_account.weight = weight; account }) diff --git a/node/src/accountant/payment_adjuster/inner.rs b/node/src/accountant/payment_adjuster/inner.rs index 99bafd080..24daf4910 100644 --- a/node/src/accountant/payment_adjuster/inner.rs +++ b/node/src/accountant/payment_adjuster/inner.rs @@ -78,7 +78,7 @@ impl PaymentAdjusterInner { }) } pub fn subtract_from_remaining_cw_service_fee_balance_minor(&self, subtrahend: u128) { - let updated_thought_cw_balance = self.get_value( + let updated_cw_balance = self.get_value( "subtract_from_remaining_cw_service_fee_balance_minor", |guts_ref| { guts_ref @@ -89,7 +89,7 @@ impl PaymentAdjusterInner { ); self.set_value( "subtract_from_remaining_cw_service_fee_balance_minor", - |guts_mut| guts_mut.remaining_cw_service_fee_balance_minor = updated_thought_cw_balance, + |guts_mut| guts_mut.remaining_cw_service_fee_balance_minor = updated_cw_balance, ) } @@ -182,7 +182,7 @@ mod tests { } #[test] - fn reducing_remaining_cw_service_fee_balance_works() { + fn subtracting_remaining_cw_service_fee_balance_works() { let initial_cw_service_fee_balance_minor = 123_123_678_678; let subject = PaymentAdjusterInner::default(); subject.initialize_guts(None, initial_cw_service_fee_balance_minor, 12345); diff --git a/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs b/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs index 45d7d1015..f6dabec53 100644 --- a/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs +++ b/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs @@ -30,9 +30,9 @@ macro_rules! diagnostics { ) }; // Displays an account by wallet address, brief description and formatted literal with arguments - ($wallet: expr, $description: expr, $($formatted_values: tt)*) => { + ($wallet_address: expr, $description: expr, $($formatted_values: tt)*) => { diagnostics( - Some(||format!("{:?}", $wallet)), + Some(||format!("{:?}", $wallet_address)), $description, Some(|| format!($($formatted_values)*)) ) @@ -100,7 +100,7 @@ pub mod ordinary_diagnostic_functions { use thousands::Separable; use web3::types::Address; - pub fn thriving_competitor_found_diagnostics( + pub fn diagnostics_for_accounts_above_disqualification_limit( account_info: &UnconfirmedAdjustment, disqualification_limit: u128, ) { diff --git a/node/src/accountant/payment_adjuster/logging_and_diagnostics/log_functions.rs b/node/src/accountant/payment_adjuster/logging_and_diagnostics/log_functions.rs index d8dbb7740..ed3d5934e 100644 --- a/node/src/accountant/payment_adjuster/logging_and_diagnostics/log_functions.rs +++ b/node/src/accountant/payment_adjuster/logging_and_diagnostics/log_functions.rs @@ -2,7 +2,6 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationSuspectedAccount; -use crate::masq_lib::utils::ExpectValue; use itertools::Itertools; use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; @@ -12,8 +11,8 @@ use thousands::Separable; use web3::types::{Address, U256}; const REFILL_RECOMMENDATION: &str = "\ -Please be aware that abandoning your debts is going to result in delinquency bans. In order to \ -consume services without limitations, you will need to place more funds into your consuming wallet."; +Please be aware that abandoning your debts is going to result in delinquency bans. To consume \ +services without limitations, you will need to place more funds into your consuming wallet."; pub const LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY: &str = "\ Passed successfully adjustment by transaction fee, then rechecked the service fee balance to be \ applied on the adjusted set, but discovered a shortage of MASQ not to suffice even for a single \ @@ -27,17 +26,17 @@ pub fn accounts_before_and_after_debug( ) -> String { let excluded_wallets_and_balances = preprocess_excluded_accounts(&original_account_balances_mapped, adjusted_accounts); - let excluded_accounts_summary = excluded_wallets_and_balances.is_empty().not().then(|| { + let excluded_accounts_summary_opt = excluded_wallets_and_balances.is_empty().not().then(|| { write_title_and_summary( &excluded_accounts_title(), &format_summary_for_excluded_accounts(&excluded_wallets_and_balances), ) }); - let included_accounts = write_title_and_summary( + let included_accounts_summary = write_title_and_summary( &included_accounts_title(), &format_summary_for_included_accounts(&original_account_balances_mapped, adjusted_accounts), ); - concatenate_summaries(included_accounts, excluded_accounts_summary) + concatenate_summaries(included_accounts_summary, excluded_accounts_summary_opt) } fn included_accounts_title() -> String { @@ -66,19 +65,18 @@ fn format_summary_for_included_accounts( // Sorting in descending order Ord::cmp(&account_b.balance_wei, &account_a.balance_wei) }) - .map(|account| { - let original_balance = original_account_balances_mapped - .get(&account.wallet.address()) - .expectv(""); - (account, *original_balance) - }) - .map(format_single_included_account) + .map(|account| format_single_included_account(account, original_account_balances_mapped)) .join("\n") } fn format_single_included_account( - (processed_account, original_balance): (&PayableAccount, u128), + processed_account: &PayableAccount, + original_account_balances_mapped: &HashMap, ) -> String { + let original_balance = original_account_balances_mapped + .get(&processed_account.wallet.address()) + .expect("The hashmap should contain every wallet"); + format!( "{} {}\n{:^length$} {}", processed_account.wallet, @@ -174,20 +172,19 @@ pub fn log_adjustment_by_service_fee_is_required( pub fn log_insufficient_transaction_fee_balance( logger: &Logger, - cw_required_transactions_count: u16, + requested_tx_count: u16, + feasible_tx_count: u16, txn_fee_required_per_txn_minor: u128, transaction_fee_minor: U256, - limiting_count: u16, ) { warning!( logger, "Transaction fee balance of {} wei cannot cover the anticipated {} wei for {} \ transactions. Maximal count is set to {}. Adjustment must be performed.", transaction_fee_minor.separate_with_commas(), - (cw_required_transactions_count as u128 * txn_fee_required_per_txn_minor) - .separate_with_commas(), - cw_required_transactions_count, - limiting_count + (requested_tx_count as u128 * txn_fee_required_per_txn_minor).separate_with_commas(), + requested_tx_count, + feasible_tx_count ); info!(logger, "{}", REFILL_RECOMMENDATION) } @@ -206,8 +203,8 @@ mod tests { fn constants_are_correct() { assert_eq!( REFILL_RECOMMENDATION, - "Please be aware that abandoning your debts is going to result in delinquency bans. In \ - order to consume services without limitations, you will need to place more funds into \ + "Please be aware that abandoning your debts is going to result in delinquency bans. \ + To consume services without limitations, you will need to place more funds into \ your consuming wallet." ); assert_eq!( diff --git a/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs b/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs index 2007975d1..dbd17bffb 100644 --- a/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs +++ b/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs @@ -66,11 +66,11 @@ impl From for AdjustedAccountBeforeFinalization { // an amount that equals to their disqualification limit (and can be later provided with even more) impl From for AdjustedAccountBeforeFinalization { fn from(weighed_account: WeighedPayable) -> Self { - let limited_adjusted_balance = weighed_account.disqualification_limit(); - minimal_acceptable_balance_assigned_diagnostics(&weighed_account, limited_adjusted_balance); + let adjusted_balance = weighed_account.disqualification_limit(); + minimal_acceptable_balance_assigned_diagnostics(&weighed_account, adjusted_balance); let weight = weighed_account.weight; let original_account = weighed_account.analyzed_account.qualified_as.bare_account; - AdjustedAccountBeforeFinalization::new(original_account, weight, limited_adjusted_balance) + AdjustedAccountBeforeFinalization::new(original_account, weight, adjusted_balance) } } diff --git a/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs b/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs index 597aaaf61..23156e7ae 100644 --- a/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs +++ b/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs @@ -101,6 +101,11 @@ impl AffordableAndRequiredTxCounts { } } +pub enum AccountsByFinalization { + Unexhausted(Vec), + Finalized(Vec), +} + #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::miscellaneous::data_structures::AffordableAndRequiredTxCounts; diff --git a/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs b/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs index c372d5108..0a276cf17 100644 --- a/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs +++ b/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs @@ -6,16 +6,14 @@ use crate::accountant::payment_adjuster::diagnostics; use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::{ exhausting_cw_balance_diagnostics, not_exhausting_cw_balance_diagnostics, }; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AdjustedAccountBeforeFinalization, WeighedPayable}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AccountsByFinalization, AdjustedAccountBeforeFinalization, WeighedPayable}; use crate::accountant::{AnalyzedPayableAccount}; -use itertools::{Either, Itertools}; +use itertools::{Itertools}; -pub fn no_affordable_accounts_found( - accounts: &Either, Vec>, -) -> bool { +pub fn no_affordable_accounts_found(accounts: &AccountsByFinalization) -> bool { match accounts { - Either::Left(vector) => vector.is_empty(), - Either::Right(vector) => vector.is_empty(), + AccountsByFinalization::Finalized(vector) => vector.is_empty(), + AccountsByFinalization::Unexhausted(vector) => vector.is_empty(), } } @@ -185,7 +183,9 @@ impl ConsumingWalletExhaustingStatus { #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; - use crate::accountant::payment_adjuster::miscellaneous::data_structures::AdjustedAccountBeforeFinalization; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + AccountsByFinalization, AdjustedAccountBeforeFinalization, + }; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ compute_mul_coefficient_preventing_fractional_numbers, eliminate_accounts_by_tx_fee_limit, exhaust_cw_balance_entirely, find_largest_exceeding_balance, no_affordable_accounts_found, @@ -195,19 +195,19 @@ mod tests { use crate::accountant::test_utils::{make_meaningless_analyzed_account, make_payable_account}; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; - use itertools::{Either, Itertools}; + use itertools::Itertools; use std::time::SystemTime; #[test] - fn no_affordable_accounts_found_found_returns_true_for_non_finalized_accounts() { - let result = no_affordable_accounts_found(&Either::Left(vec![])); + fn no_affordable_accounts_found_returns_true_for_non_finalized_accounts() { + let result = no_affordable_accounts_found(&AccountsByFinalization::Unexhausted(vec![])); assert_eq!(result, true) } #[test] fn no_affordable_accounts_found_returns_false_for_non_finalized_accounts() { - let result = no_affordable_accounts_found(&Either::Left(vec![ + let result = no_affordable_accounts_found(&AccountsByFinalization::Unexhausted(vec![ AdjustedAccountBeforeFinalization::new(make_payable_account(456), 5678, 1234), ])); @@ -216,14 +216,16 @@ mod tests { #[test] fn no_affordable_accounts_found_returns_true_for_finalized_accounts() { - let result = no_affordable_accounts_found(&Either::Right(vec![])); + let result = no_affordable_accounts_found(&AccountsByFinalization::Finalized(vec![])); assert_eq!(result, true) } #[test] fn no_affordable_accounts_found_returns_false_for_finalized_accounts() { - let result = no_affordable_accounts_found(&Either::Right(vec![make_payable_account(123)])); + let result = no_affordable_accounts_found(&AccountsByFinalization::Finalized(vec![ + make_payable_account(123), + ])); assert_eq!(result, false) } diff --git a/node/src/accountant/payment_adjuster/mod.rs b/node/src/accountant/payment_adjuster/mod.rs index 77307b8aa..1a9a7c6bc 100644 --- a/node/src/accountant/payment_adjuster/mod.rs +++ b/node/src/accountant/payment_adjuster/mod.rs @@ -28,13 +28,13 @@ use crate::accountant::payment_adjuster::inner::{ use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::{ accounts_before_and_after_debug, }; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AdjustedAccountBeforeFinalization, WeighedPayable}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AccountsByFinalization, AdjustedAccountBeforeFinalization, WeighedPayable}; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ eliminate_accounts_by_tx_fee_limit, exhaust_cw_balance_entirely, find_largest_exceeding_balance, sum_as, no_affordable_accounts_found, }; -use crate::accountant::payment_adjuster::preparatory_analyser::{LateServiceFeeSingleTxErrorFactory, PreparatoryAnalyzer}; +use crate::accountant::payment_adjuster::preparatory_analyser::{LaterServiceFeeErrorFactory, PreparatoryAnalyzer}; use crate::accountant::payment_adjuster::service_fee_adjuster::{ ServiceFeeAdjuster, ServiceFeeAdjusterReal, }; @@ -54,15 +54,43 @@ use masq_lib::utils::convert_collection; use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::DisqualificationLimitProvidingAccount; // PaymentAdjuster is a recursive and scalable algorithm that inspects payments under conditions -// of an acute insolvency. You can easily expand the range of evaluated parameters to determine -// an optimized allocation of scarce assets by writing your own CriterionCalculator. The calculator -// is supposed to be dedicated to a single parameter that can be tracked for each payable account. +// of acute insolvency. Each parameter that participates in the determination of the optimized +// asset allocation should have its own calculator. You can easily maintain the range of evaluated +// parameters by removing or adding your own calculator. These calculators, placed in a vector, +// make the heart of the algorithm. // -// For parameters that can't be derived from each account, or even one at all, there is a way to -// provide such data up into the calculator. This can be achieved via the PaymentAdjusterInner. +// For parameters that can't be derived from an account, there is still a way to provide such values +// up into the calculator. This can be achieved via the PaymentAdjusterInner. + +// Algorithm description: +// +// It begins with accounts getting weights from the criteria calculators. The weighting is inverse +// to the debt size, accounts with smaller debts are prioritized so that we can satisfy as many +// accounts as possible and avoid the same number of bans. // -// Once the new calculator exists, its place belongs in the vector of calculators which is the heart -// of this module. +// If it is necessary to adjust the set by the transaction fee, the accounts are sorted +// by the weights and only accounts that fit together under the limit are kept in. The adjustment +// by service fee follows up (or it may take the first place if the need for the previous step was +// missing). + +// Here comes the recursive part. The algorithm iterates through the weighted accounts and assigns +// a proportional portion of the available means to them. Since the initial stage of the Payment- +// Adjuster, where accounts were tested on causing insolvency, they've remained equipped with +// a computed parameter of the so-called disqualification limit. This limit determines +// if the weight-derived assignment of the money is too low or enough. If it is below the limit, +// the account is removed from consideration. However, only a single account with the smallest +// weight is eliminated per each recursion. + +// The pool of money remains the same, but the set of accounts is reduced. The recursion repeats. +// If none of the accounts disqualifies, it means all accounts were proposed with enough money. +// Although the proposals may exceed the disqualification limits, only the value of the limit is +// dedicated to the selected accounts. + +// The remaining assets are later allocated to the accounts based on the order by their weights, +// exhausting them fully one account after another up their 100% allocation until there is any +// money that can be distributed. + +// In the end, the accounts are shaped back as a PayableAccount and returned. pub type AdjustmentAnalysisResult = Result, PaymentAdjusterError>; @@ -184,35 +212,29 @@ impl PaymentAdjusterReal { let processed_accounts = self.resolve_initial_adjustment_dispatch(weighed_accounts)?; if no_affordable_accounts_found(&processed_accounts) { - return Err(PaymentAdjusterError::RecursionDrainedAllAccounts); + return Err(PaymentAdjusterError::RecursionEliminatedAllAccounts); } match processed_accounts { - Either::Left(non_exhausted_accounts) => { - let original_cw_service_fee_balance_minor = - self.inner.original_cw_service_fee_balance_minor(); - let exhaustive_affordable_accounts = exhaust_cw_balance_entirely( - non_exhausted_accounts, - original_cw_service_fee_balance_minor, - ); - Ok(exhaustive_affordable_accounts) + AccountsByFinalization::Unexhausted(unexhausted_accounts) => { + Ok(exhaust_cw_balance_entirely( + unexhausted_accounts, + self.inner.original_cw_service_fee_balance_minor(), + )) } - Either::Right(finalized_accounts) => Ok(finalized_accounts), + AccountsByFinalization::Finalized(accounts) => Ok(accounts), } } fn resolve_initial_adjustment_dispatch( &self, weighed_payables: Vec, - ) -> Result< - Either, Vec>, - PaymentAdjusterError, - > { + ) -> Result { if let Some(limit) = self.inner.transaction_count_limit_opt() { return self.begin_with_adjustment_by_transaction_fee(weighed_payables, limit); } - Ok(Either::Left( + Ok(AccountsByFinalization::Unexhausted( self.propose_possible_adjustment_recursively(weighed_payables), )) } @@ -221,16 +243,13 @@ impl PaymentAdjusterReal { &self, weighed_accounts: Vec, transaction_count_limit: u16, - ) -> Result< - Either, Vec>, - PaymentAdjusterError, - > { + ) -> Result { diagnostics!( "\nBEGINNING WITH ADJUSTMENT BY TRANSACTION FEE FOR ACCOUNTS:", &weighed_accounts ); - let error_factory = LateServiceFeeSingleTxErrorFactory::new(&weighed_accounts); + let error_factory = LaterServiceFeeErrorFactory::new(&weighed_accounts); let weighed_accounts_affordable_by_transaction_fee = eliminate_accounts_by_tx_fee_limit(weighed_accounts, transaction_count_limit); @@ -248,12 +267,16 @@ impl PaymentAdjusterReal { weighed_accounts_affordable_by_transaction_fee, ); - Ok(Either::Left(final_set_before_exhausting_cw_balance)) + Ok(AccountsByFinalization::Unexhausted( + final_set_before_exhausting_cw_balance, + )) } else { let accounts_not_needing_adjustment = convert_collection(weighed_accounts_affordable_by_transaction_fee); - Ok(Either::Right(accounts_not_needing_adjustment)) + Ok(AccountsByFinalization::Finalized( + accounts_not_needing_adjustment, + )) } } @@ -285,21 +308,20 @@ impl PaymentAdjusterReal { } if !decided_accounts.is_empty() { - self.adjust_remaining_remaining_cw_balance_down(&decided_accounts) + self.adjust_remaining_cw_balance_down(&decided_accounts) } - let merged = - if self.is_cw_balance_enough_to_remaining_accounts(&remaining_undecided_accounts) { - Self::merge_accounts( - decided_accounts, - convert_collection(remaining_undecided_accounts), - ) - } else { - Self::merge_accounts( - decided_accounts, - self.propose_possible_adjustment_recursively(remaining_undecided_accounts), - ) - }; + let merged = if self.is_cw_balance_enough(&remaining_undecided_accounts) { + Self::merge_accounts( + decided_accounts, + convert_collection(remaining_undecided_accounts), + ) + } else { + Self::merge_accounts( + decided_accounts, + self.propose_possible_adjustment_recursively(remaining_undecided_accounts), + ) + }; diagnostics!( "\nFINAL SET OF ADJUSTED ACCOUNTS IN CURRENT ITERATION:", @@ -309,10 +331,7 @@ impl PaymentAdjusterReal { merged } - fn is_cw_balance_enough_to_remaining_accounts( - &self, - remaining_undecided_accounts: &[WeighedPayable], - ) -> bool { + fn is_cw_balance_enough(&self, remaining_undecided_accounts: &[WeighedPayable]) -> bool { let remaining_cw_service_fee_balance = self.inner.remaining_cw_service_fee_balance_minor(); let minimum_sum_required: u128 = sum_as(remaining_undecided_accounts, |weighed_account| { weighed_account.disqualification_limit() @@ -364,7 +383,7 @@ impl PaymentAdjusterReal { .collect() } - fn adjust_remaining_remaining_cw_balance_down( + fn adjust_remaining_cw_balance_down( &self, decided_accounts: &[AdjustedAccountBeforeFinalization], ) { @@ -437,18 +456,35 @@ impl AdjustmentAnalysisReport { #[derive(Debug, PartialEq, Eq, VariantCount)] pub enum PaymentAdjusterError { - AbsolutelyInsufficientBalance { + AbsoluteFeeInsufficiency { number_of_accounts: usize, + detection_phase: DetectionPhase, + }, + // AbsolutelyInsufficientBalance { + // number_of_accounts: usize, + // transaction_fee_opt: Option, + // service_fee_opt: Option, + // }, + // AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { + // original_number_of_accounts: usize, + // number_of_accounts: usize, + // original_total_service_fee_required_minor: u128, + // cw_service_fee_balance_minor: u128, + // }, + RecursionEliminatedAllAccounts, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum DetectionPhase { + InitialCheck { transaction_fee_opt: Option, service_fee_opt: Option, }, - AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { + PostTxFeeAdjustment { original_number_of_accounts: usize, - number_of_accounts: usize, original_total_service_fee_required_minor: u128, cw_service_fee_balance_minor: u128, }, - RecursionDrainedAllAccounts, } #[derive(Debug, PartialEq, Eq)] @@ -466,12 +502,14 @@ pub struct ServiceFeeImmoderateInsufficiency { impl PaymentAdjusterError { pub fn insolvency_detected(&self) -> bool { match self { - PaymentAdjusterError::AbsolutelyInsufficientBalance { .. } => true, - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - .. - } => true, - PaymentAdjusterError::RecursionDrainedAllAccounts => true, - // We haven't needed to worry in this matter yet, this is rather a future alarm that + PaymentAdjusterError::AbsoluteFeeInsufficiency { + detection_phase, .. + } => match detection_phase { + DetectionPhase::InitialCheck { .. } => true, + DetectionPhase::PostTxFeeAdjustment { .. } => true, + }, + PaymentAdjusterError::RecursionEliminatedAllAccounts => true, + // We haven't needed to worry about this matter, yet this is rather a future alarm that // will draw attention after somebody adds a possibility for an error not necessarily // implying that an insolvency was detected before. At the moment, each error occurs // only alongside an actual insolvency. (Hint: There might be consequences for @@ -484,12 +522,16 @@ impl PaymentAdjusterError { impl Display for PaymentAdjusterError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - transaction_fee_opt, - service_fee_opt, + detection_phase } => { - match (transaction_fee_opt, service_fee_opt) { + match detection_phase { + DetectionPhase::InitialCheck { + transaction_fee_opt, + service_fee_opt + } => + match (transaction_fee_opt, service_fee_opt) { (Some(transaction_fee_check_summary), None) => write!( f, @@ -522,14 +564,9 @@ impl Display for PaymentAdjusterError { service_fee_check_summary.cw_service_fee_balance_minor.separate_with_commas() ), (None, None) => unreachable!("This error contains no specifications") - } - }, - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts, - number_of_accounts, - original_total_service_fee_required_minor, - cw_service_fee_balance_minor, - } => write!(f, "The original set with {} accounts was adjusted down to {} due to \ + }, + DetectionPhase::PostTxFeeAdjustment { original_number_of_accounts, original_total_service_fee_required_minor, cw_service_fee_balance_minor } + => write!(f, "The original set with {} accounts was adjusted down to {} due to \ transaction fee. The new set was tested on service fee later again and did not \ pass. Original required amount of service fee: {} wei, while the wallet \ contains {} wei.", @@ -537,8 +574,8 @@ impl Display for PaymentAdjusterError { number_of_accounts, original_total_service_fee_required_minor.separate_with_commas(), cw_service_fee_balance_minor.separate_with_commas() - ), - PaymentAdjusterError::RecursionDrainedAllAccounts => write!( + )}}, + PaymentAdjusterError::RecursionEliminatedAllAccounts => write!( f, "The payments adjusting process failed to find any combination of payables that \ can be paid immediately with the finances provided." @@ -553,13 +590,13 @@ mod tests { use crate::accountant::payment_adjuster::inner::PaymentAdjusterInner; use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustmentIterationResult, WeighedPayable, + AccountsByFinalization, AdjustmentIterationResult, WeighedPayable, }; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ find_largest_exceeding_balance, sum_as, }; use crate::accountant::payment_adjuster::service_fee_adjuster::illustrative_util::illustrate_why_we_need_to_prevent_exceeding_the_original_value; - use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_into_analyzed_payables_in_test; + use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; use crate::accountant::payment_adjuster::test_utils::local_utils::{ make_mammoth_payables, make_meaningless_analyzed_account_by_wallet, multiply_by_billion, multiply_by_billion_concise, multiply_by_quintillion, multiply_by_quintillion_concise, @@ -567,8 +604,8 @@ mod tests { MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, PRESERVED_TEST_PAYMENT_THRESHOLDS, }; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysisReport, PaymentAdjuster, PaymentAdjusterError, - PaymentAdjusterReal, ServiceFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjuster, + PaymentAdjusterError, PaymentAdjusterReal, ServiceFeeImmoderateInsufficiency, TransactionFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; @@ -636,7 +673,7 @@ mod tests { ); let transaction_fee_balance_exactly_required_minor: u128 = { let base_value = (100 * 6 * 53_000) as u128; - let with_margin = TX_FEE_MARGIN_IN_PERCENT.add_percent_to(base_value); + let with_margin = TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(base_value); multiply_by_billion(with_margin) }; // Transaction fee balance > payments @@ -690,15 +727,14 @@ mod tests { number_of_accounts, tx_computation_units: 55_000, cw_transaction_fee_balance_minor: TX_FEE_MARGIN_IN_PERCENT - .add_percent_to(multiply_by_billion(100 * 3 * 55_000)) + .increase_by_percent_for(multiply_by_billion(100 * 3 * 55_000)) - 1, }), ); let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); - let analyzed_payables = - convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); assert_eq!( result, Ok(Either::Right(AdjustmentAnalysisReport::new( @@ -716,7 +752,7 @@ mod tests { )); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Please be aware that abandoning your debts is going to result in \ - delinquency bans. In order to consume services without limitations, you will need to \ + delinquency bans. To consume services without limitations, you will need to \ place more funds into your consuming wallet." )); } @@ -741,8 +777,7 @@ mod tests { let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); - let analyzed_payables = - convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); assert_eq!( result, Ok(Either::Right(AdjustmentAnalysisReport::new( @@ -758,7 +793,7 @@ mod tests { )); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Please be aware that abandoning your debts is going to result in \ - delinquency bans. In order to consume services without limitations, you will need to \ + delinquency bans. To consume services without limitations, you will need to \ place more funds into your consuming wallet." )); } @@ -769,7 +804,7 @@ mod tests { let number_of_accounts = 3; let tx_fee_exactly_required_for_single_tx = { let base_minor = multiply_by_billion(55_000 * 100); - TX_FEE_MARGIN_IN_PERCENT.add_percent_to(base_minor) + TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(base_minor) }; let cw_transaction_fee_balance_minor = tx_fee_exactly_required_for_single_tx - 1; let (qualified_payables, agent) = make_input_for_initial_check_tests( @@ -789,17 +824,19 @@ mod tests { let per_transaction_requirement_minor = { let base_minor = multiply_by_billion(55_000 * 100); - TX_FEE_MARGIN_IN_PERCENT.add_percent_to(base_minor) + TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(base_minor) }; assert_eq!( result, - Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor, - cw_transaction_fee_balance_minor: cw_transaction_fee_balance_minor.into(), - }), - service_fee_opt: None + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor, + cw_transaction_fee_balance_minor: cw_transaction_fee_balance_minor.into(), + }), + service_fee_opt: None + } }) ); } @@ -818,8 +855,7 @@ mod tests { }); let (qualified_payables, boxed_agent) = make_input_for_initial_check_tests(service_fee_balances_config_opt, None); - let analyzed_accounts = - convert_qualified_into_analyzed_payables_in_test(qualified_payables.clone()); + let analyzed_accounts = convert_qualified_p_into_analyzed_p(qualified_payables.clone()); let minimal_disqualification_limit = analyzed_accounts .iter() .map(|account| account.disqualification_limit_minor) @@ -839,13 +875,15 @@ mod tests { assert_eq!( result, - Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 3, - transaction_fee_opt: None, - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { - total_service_fee_required_minor: multiply_by_billion(920), - cw_service_fee_balance_minor: actual_insufficient_cw_service_fee_balance - }) + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: multiply_by_billion(920), + cw_service_fee_balance_minor: actual_insufficient_cw_service_fee_balance + }) + } }) ); } @@ -873,19 +911,21 @@ mod tests { let result = subject.consider_adjustment(qualified_payables, &*agent); let per_transaction_requirement_minor = - TX_FEE_MARGIN_IN_PERCENT.add_percent_to(55_000 * multiply_by_billion(123)); + TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(55_000 * multiply_by_billion(123)); assert_eq!( result, - Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor, - cw_transaction_fee_balance_minor: U256::zero(), - }), - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { - total_service_fee_required_minor: multiply_by_billion(500), - cw_service_fee_balance_minor: 0 - }) + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor, + cw_transaction_fee_balance_minor: U256::zero(), + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: multiply_by_billion(500), + cw_service_fee_balance_minor: 0 + }) + } }) ); } @@ -894,42 +934,48 @@ mod tests { fn payment_adjuster_error_implements_display() { let inputs = vec![ ( - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 4, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency{ - per_transaction_requirement_minor: multiply_by_billion(70_000), - cw_transaction_fee_balance_minor: U256::from(90_000), - }), - service_fee_opt: None + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: multiply_by_billion(70_000), + cw_transaction_fee_balance_minor: U256::from(90_000), + }), + service_fee_opt: None + } }, "Current transaction fee balance is not enough to pay a single payment. Number of \ canceled payments: 4. Transaction fee per payment: 70,000,000,000,000 wei, while \ the wallet contains: 90,000 wei", ), ( - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 5, - transaction_fee_opt: None, - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency{ - total_service_fee_required_minor: 6_000_000_000, - cw_service_fee_balance_minor: 333_000_000, - }) + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 6_000_000_000, + cw_service_fee_balance_minor: 333_000_000, + }) + } }, "Current service fee balance is not enough to pay a single payment. Number of \ canceled payments: 5. Total amount required: 6,000,000,000 wei, while the wallet \ contains: 333,000,000 wei", ), ( - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 5, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency{ - per_transaction_requirement_minor: 5_000_000_000, - cw_transaction_fee_balance_minor: U256::from(3_000_000_000_u64) - }), - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency{ - total_service_fee_required_minor: 7_000_000_000, - cw_service_fee_balance_minor: 100_000_000 - }) + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 5_000_000_000, + cw_transaction_fee_balance_minor: U256::from(3_000_000_000_u64) + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 7_000_000_000, + cw_service_fee_balance_minor: 100_000_000 + }) + } }, "Neither transaction fee nor service fee balance is enough to pay a single payment. \ Number of payments considered: 5. Transaction fee per payment: 5,000,000,000 wei, \ @@ -937,18 +983,20 @@ mod tests { while in wallet: 100,000,000 wei", ), ( - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts: 6, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 3, - original_total_service_fee_required_minor: 1234567891011, - cw_service_fee_balance_minor: 333333, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 6, + original_total_service_fee_required_minor: 1234567891011, + cw_service_fee_balance_minor: 333333, + } }, "The original set with 6 accounts was adjusted down to 3 due to transaction fee. \ The new set was tested on service fee later again and did not pass. Original \ required amount of service fee: 1,234,567,891,011 wei, while the wallet contains \ 333,333 wei."), ( - PaymentAdjusterError::RecursionDrainedAllAccounts, + PaymentAdjusterError::RecursionEliminatedAllAccounts, "The payments adjusting process failed to find any combination of payables that \ can be paid immediately with the finances provided.", ), @@ -957,7 +1005,7 @@ mod tests { inputs .into_iter() .for_each(|(error, expected_msg)| assert_eq!(error.to_string(), expected_msg)); - assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 2) + assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 3) } #[test] @@ -966,10 +1014,12 @@ mod tests { specifications" )] fn error_message_for_input_referring_to_no_issues_cannot_be_made() { - let _ = PaymentAdjusterError::AbsolutelyInsufficientBalance { + let _ = PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 0, - transaction_fee_opt: None, - service_fee_opt: None, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: None, + }, } .to_string(); } @@ -977,39 +1027,47 @@ mod tests { #[test] fn we_can_say_if_error_occurred_after_insolvency_was_detected() { let inputs = vec![ - PaymentAdjusterError::RecursionDrainedAllAccounts, - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::RecursionEliminatedAllAccounts, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 0, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor: 0, - cw_transaction_fee_balance_minor: Default::default(), - }), - service_fee_opt: None, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 0, + cw_transaction_fee_balance_minor: Default::default(), + }), + service_fee_opt: None, + }, }, - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 0, - transaction_fee_opt: None, - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { - total_service_fee_required_minor: 0, - cw_service_fee_balance_minor: 0, - }), + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }), + }, }, - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 0, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor: 0, - cw_transaction_fee_balance_minor: Default::default(), - }), - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { - total_service_fee_required_minor: 0, - cw_service_fee_balance_minor: 0, - }), + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 0, + cw_transaction_fee_balance_minor: Default::default(), + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }), + }, }, - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts: 0, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 0, - original_total_service_fee_required_minor: 0, - cw_service_fee_balance_minor: 0, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 0, + original_total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }, }, ]; let inputs_count = inputs.len(); @@ -1018,7 +1076,7 @@ mod tests { .map(|err| err.insolvency_detected()) .collect::>(); assert_eq!(results, vec![true, true, true, true, true]); - assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 2) + assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 3) } #[test] @@ -1050,12 +1108,13 @@ mod tests { WeighedPayable::new(account_2, weighed_account_2), ]; - let mut result = subject - .resolve_initial_adjustment_dispatch(weighed_payables.clone()) - .unwrap() - .left() - .unwrap(); + let result = subject.resolve_initial_adjustment_dispatch(weighed_payables.clone()); + let mut accounts = if let AccountsByFinalization::Unexhausted(accounts) = result.unwrap() { + accounts + } else { + panic!("We expected unexhausted accounts but got those already finalized") + }; // This shows how the weights can turn tricky for which it's important to have a hard upper // limit, chosen quite down, as the disqualification limit, for optimisation. In its // extremity, the naked algorithm of the reallocation of funds could have granted a value @@ -1074,19 +1133,19 @@ mod tests { .analyzed_account .qualified_as .bare_account; - let first_returned_account = result.remove(0); + let first_returned_account = accounts.remove(0); assert_eq!(&first_returned_account.original_account, payable_account_2); assert_eq!( first_returned_account.proposed_adjusted_balance_minor, disqualification_limit_2 ); - let second_returned_account = result.remove(0); + let second_returned_account = accounts.remove(0); assert_eq!(&second_returned_account.original_account, payable_account_1); assert_eq!( second_returned_account.proposed_adjusted_balance_minor, disqualification_limit_1 ); - assert!(result.is_empty()); + assert!(accounts.is_empty()); } #[test] @@ -1190,7 +1249,7 @@ mod tests { ok.affordable_accounts ), }; - assert_eq!(err, PaymentAdjusterError::RecursionDrainedAllAccounts) + assert_eq!(err, PaymentAdjusterError::RecursionEliminatedAllAccounts) } #[test] @@ -1326,7 +1385,7 @@ mod tests { Ok(_) => panic!("we expected err but got ok"), Err(e) => e, }; - assert_eq!(err, PaymentAdjusterError::RecursionDrainedAllAccounts); + assert_eq!(err, PaymentAdjusterError::RecursionEliminatedAllAccounts); let expected_log = |wallet: &str| { format!( "INFO: {test_name}: Ready payment to {wallet} was eliminated to spare MASQ for \ @@ -1380,7 +1439,7 @@ mod tests { initial_disqualification_limit_for_each_account; let weighed_payables = vec![payable_1, payable_2]; - let result = subject.is_cw_balance_enough_to_remaining_accounts(&weighed_payables); + let result = subject.is_cw_balance_enough(&weighed_payables); assert_eq!(result, expected_result) } @@ -1988,8 +2047,7 @@ mod tests { ) }) .collect(); - let analyzed_accounts = - convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_accounts = convert_qualified_p_into_analyzed_p(qualified_payables); let analyzed_accounts: [AnalyzedPayableAccount; 3] = analyzed_accounts.try_into().unwrap(); let disqualification_limits: QuantifiedDisqualificationLimits = (&analyzed_accounts).into(); (analyzed_accounts, disqualification_limits) @@ -2073,13 +2131,15 @@ mod tests { }; assert_eq!( err, - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts: 3, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts: 2, - original_total_service_fee_required_minor: balance_account_1 - + balance_account_2 - + balance_account_3, - cw_service_fee_balance_minor + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 3, + original_total_service_fee_required_minor: balance_account_1 + + balance_account_2 + + balance_account_3, + cw_service_fee_balance_minor + } } ); TestLogHandler::new().assert_logs_contain_in_order(vec![ @@ -2090,7 +2150,7 @@ mod tests { ), &format!( "INFO: {test_name}: Please be aware that abandoning your debts is going to \ - result in delinquency bans. In order to consume services without limitations, you \ + result in delinquency bans. To consume services without limitations, you \ will need to place more funds into your consuming wallet.", ), &format!( @@ -2269,12 +2329,15 @@ mod tests { .into_iter() .map(|calculator| calculator.calculate(&qualified_payable, &context)) .fold(0, |previous_result, current_result| { + // Testing a bigger gap between the values of the different calculators (we don't + // want to use previous_result != current_result because that could also mean + // a difference by one or a similarly negligible value) let slightly_less_than_current = (current_result * 97) / 100; let slightly_more_than_current = (current_result * 103) / 100; assert_ne!(current_result, 0); assert!( previous_result <= slightly_less_than_current - || slightly_more_than_current <= previous_result + || previous_result >= slightly_more_than_current ); current_result }); @@ -2429,8 +2492,7 @@ mod tests { qualified_payables: Vec, cw_service_fee_balance_minor: u128, ) -> Vec { - let analyzed_payables = - convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); let max_debt_above_threshold_in_qualified_payables_minor = find_largest_exceeding_balance(&analyzed_payables); let mut subject = PaymentAdjusterBuilder::default() @@ -2469,7 +2531,7 @@ mod tests { // It allows to halt the code executions without a dive in the recursion assert_eq!( actual_result, - Err(PaymentAdjusterError::RecursionDrainedAllAccounts) + Err(PaymentAdjusterError::RecursionEliminatedAllAccounts) ); let mut perform_adjustment_by_service_fee_params = perform_adjustment_by_service_fee_params_arc.lock().unwrap(); diff --git a/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs b/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs index 32346cd7f..49bcddcce 100644 --- a/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs +++ b/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs @@ -6,10 +6,10 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::sum_as; use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::BalanceProvidingAccount; -use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_into_analyzed_payables_in_test; +use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; use crate::accountant::payment_adjuster::test_utils::local_utils::PRESERVED_TEST_PAYMENT_THRESHOLDS; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysisReport, PaymentAdjuster, PaymentAdjusterError, + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjuster, PaymentAdjusterError, PaymentAdjusterReal, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; @@ -18,7 +18,7 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ PayableInspector, PayableThresholdsGaugeReal, }; use crate::accountant::test_utils::{ - make_single_qualified_payable_opt, try_to_make_guaranteed_qualified_payables, + make_single_qualified_payable_opt, try_to_make_qualified_payables, }; use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; @@ -44,8 +44,8 @@ use web3::types::{Address, U256}; // TODO If an option for "occasional tests" is added, this is a good adept #[ignore] fn loading_test_with_randomized_params() { - // This is a fuzz test. It generates possibly an overwhelming amount of scenarios that - // the PaymentAdjuster could be given sort them out, as realistic as it can get, while its + // This is a fuzz test. It generates possibly an overwhelming number of scenarios that + // the PaymentAdjuster could be given to sort them out, as realistic as it can get, while its // nature of randomness offers chances to have a dense range of combinations that a human fails // to even try imagining. The hypothesis is that some of those might be corner cases whose // trickiness wasn't recognized when the functionality was still at design. This test is to @@ -64,8 +64,8 @@ fn loading_test_with_randomized_params() { // we allow this always implied waste than trying to invent an algorithm whose randomness would // be exercised within strictly controlled boundaries. - // Some other are lost quite early as legitimate errors that the PaymentAdjuster can detect, - // which would prevent finishing the search for given scenario. + // Some others are lost quite early as legitimate errors that the PaymentAdjuster can detect, + // which would prevent finishing the search for a given scenario. // When the test reaches its end, it produces important output in a text file, located: // node/generated/test/payment_adjuster/tests/home/loading_test_output.txt @@ -81,16 +81,16 @@ fn loading_test_with_randomized_params() { // payables. See those percentages. They may not excel at explaining themselves when it comes to // their inconsistent proportionality towards the balances. These percents represent a payment // coverage of the initial debts. But why don't they correspond with ascending balances? There's - // a principle to equip accounts low balances with the biggest weights. True. However, it doesn't + // a principle to equip account low balances with the biggest weights. True. However, it doesn't // need to be reflected so clearly, though. The adjustment depends heavily on a so-called // "disqualification limit". Besides other purposes, this value affects that the payment won't // require the entire amount but only its portion. That inherently will do for the payer to stay // unbanned. In bulky accounts, this until-some-time forgiven portion stands only as a fraction // of a whole. Small accounts, however, if it can be applied (as opposed to the account having // to be excluded) might get shrunk a lot, and therefore many percents are to be reported as - // missing. This is what the numbers like 99% and 90% illustrates. That said, the letter account - // comes across as it should take precedence for its expectedly larger weight, and gain at the - // expanse of the other, but the percents speak otherwise. Yet, it's correct. The interpretation + // missing. This is what the numbers like 99% and 90% illustrate. That said, the letter account + // comes across as it should take precedence for its expectedly larger weight and gain at the + // expanse of the other, but the percents speak otherwise. Yet it's correct. The interpretation // is the key. (Caution: this test displays its output with those accounts sorted). // CW service fee balance: 32,041,461,894,055,482 wei @@ -100,9 +100,9 @@ fn loading_test_with_randomized_params() { // 2000000|1000|1000|1000000|500000|1000000 // _____________________________________________________________________________________________ // 1,988,742,049,305,843 wei | 236,766 s | 100 % - // 21,971,010,542,100,729 wei | 472,884 s | 99 % # # # # # # # # - // 4,726,030,753,976,563 wei | 395,377 s | 95 % # # # # # # # # - // 3,995,577,830,314,875 wei | 313,396 s | 90 % # # # # # # # # + // 21,971,010,542,100,729 wei | 472,884 s | 99 % << << << << + // 4,726,030,753,976,563 wei | 395,377 s | 95 % << << << << + // 3,995,577,830,314,875 wei | 313,396 s | 90 % << << << << // 129,594,971,536,673,815 wei | 343,511 s | X // In the code, we select and pale up accounts so that the picked balance isn't the full range, @@ -171,27 +171,35 @@ fn loading_test_with_randomized_params() { let second_stage_scenarios = first_stage_output.allowed_scenarios; let test_overall_output_collector = first_stage_output.output_collector; - let scenario_adjustment_results = second_stage_scenarios - .into_iter() - .map(|scenario| { - let prepared_adjustment = scenario.prepared_adjustment; - let account_infos = - preserve_account_infos(&prepared_adjustment.adjustment_analysis.accounts, now); - let required_adjustment = prepared_adjustment.adjustment_analysis.adjustment.clone(); - let cw_service_fee_balance_minor = - prepared_adjustment.agent.service_fee_balance_minor(); - - let payment_adjuster_result = subject.adjust_payments(prepared_adjustment); - - administrate_single_scenario_result( - payment_adjuster_result, - account_infos, - scenario.applied_thresholds, - required_adjustment, - cw_service_fee_balance_minor, - ) - }) - .collect(); + let (test_overall_output_collector, scenario_adjustment_results) = + second_stage_scenarios.into_iter().fold( + (test_overall_output_collector, vec![]), + |(test_overall_output_collector_in_fold, mut scenario_results), scenario| { + let prepared_adjustment = scenario.prepared_adjustment; + let account_infos = + preserve_account_infos(&prepared_adjustment.adjustment_analysis.accounts, now); + + let required_adjustment = + prepared_adjustment.adjustment_analysis.adjustment.clone(); + let cw_service_fee_balance_minor = + prepared_adjustment.agent.service_fee_balance_minor(); + + let payment_adjuster_result = subject.adjust_payments(prepared_adjustment); + + let (t_o_c_i_f, scenario_result) = administrate_single_scenario_result( + test_overall_output_collector_in_fold, + payment_adjuster_result, + account_infos, + scenario.applied_thresholds, + required_adjustment, + cw_service_fee_balance_minor, + ); + + scenario_results.push(scenario_result); + + (t_o_c_i_f, scenario_results) + }, + ); render_results_to_file_and_attempt_basic_assertions( scenario_adjustment_results, @@ -219,7 +227,7 @@ fn try_making_single_valid_scenario( let (cw_service_fee_balance, qualified_payables, applied_thresholds) = try_generating_qualified_payables_and_cw_balance(gn, accounts_count, now)?; - let analyzed_accounts = convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_accounts = convert_qualified_p_into_analyzed_p(qualified_payables); let agent = make_agent(cw_service_fee_balance); let adjustment = make_adjustment(gn, analyzed_accounts.len()); let prepared_adjustment = PreparedAdjustment::new( @@ -256,21 +264,28 @@ fn generate_debt_age(gn: &mut ThreadRng, thresholds: &PaymentThresholds) -> u64 gn, thresholds.maturity_threshold_sec, thresholds.maturity_threshold_sec + thresholds.threshold_interval_sec, - ) / 2 + ) } fn generate_highly_randomized_payable_account_balance( gn: &mut ThreadRng, thresholds: &PaymentThresholds, ) -> u128 { - // This seems overcomplicated, damn. As a result of simple intentions though. I wanted to ensure - // occurrence of accounts with balances having different magnitudes in the frame of a single - // scenario. This was crucial to me so much that I was ready to write even this piece of code - // a bit crazy by look. - // This setup worked well to stress the randomness I needed, a lot more significant compared to - // what the naked number generator can put for you. Using some nesting, it broke the rigid - // pattern and gave an existence to accounts with diverse balances. - let mut generate_u128 = || generate_non_zero_usize(gn, 100) as u128; + // This seems overcomplicated, damn. Yet it's a result of good simple intentions. I wanted + // to ensure the occurrence of accounts with balances of different magnitudes to be generated + // for a single scenario. This was crucial to me so much that I didn't stop myself from writing + // this fishy-looking piece of code. + // This setup worked well for the randomness I needed, a lot significantly more compared to + // what the default number generator from this library seemed to be able to provide only. + // Using some nesting, it scattered the distribution better and allowed me to have accounts + // with diverse balances. + const COEFFICIENT_A: usize = 100; + const COEFFICIENT_B: usize = 2; + const COEFFICIENT_C: usize = 3; + const COEFFICIENT_D: usize = 4; + const COEFFICIENT_E: usize = 5; + + let mut generate_u128 = || generate_non_zero_usize(gn, COEFFICIENT_A) as u128; let parameter_a = generate_u128(); let parameter_b = generate_u128(); @@ -279,29 +294,17 @@ fn generate_highly_randomized_payable_account_balance( let parameter_e = generate_u128(); let parameter_f = generate_u128(); - let mut use_variable_exponent = + let mut apply_arbitrary_variable_exponent = |parameter: u128, up_to: usize| parameter.pow(generate_non_zero_usize(gn, up_to) as u32); - let a_b_c_d_e = parameter_a - * use_variable_exponent(parameter_b, 2) - * use_variable_exponent(parameter_c, 3) - * use_variable_exponent(parameter_d, 4) - * use_variable_exponent(parameter_e, 5); - let addition = (0..6).fold(a_b_c_d_e, |so_far, subtrahend| { - if so_far != a_b_c_d_e { - so_far - } else { - if let Some(num) = - a_b_c_d_e.checked_sub(use_variable_exponent(parameter_f, 6 - subtrahend)) - { - num - } else { - so_far - } - } - }); + let a_b_c_d_e_f = parameter_a + * apply_arbitrary_variable_exponent(parameter_b, COEFFICIENT_B) + * apply_arbitrary_variable_exponent(parameter_c, COEFFICIENT_C) + * apply_arbitrary_variable_exponent(parameter_d, COEFFICIENT_D) + * apply_arbitrary_variable_exponent(parameter_e, COEFFICIENT_E) + * parameter_f; - thresholds.permanent_debt_allowed_gwei as u128 + addition + thresholds.permanent_debt_allowed_gwei as u128 + a_b_c_d_e_f } fn try_make_qualified_payables_by_applied_thresholds( @@ -311,40 +314,47 @@ fn try_make_qualified_payables_by_applied_thresholds( ) -> Vec { let payment_inspector = PayableInspector::new(Box::new(PayableThresholdsGaugeReal::default())); match applied_thresholds { - AppliedThresholds::Defaulted => try_to_make_guaranteed_qualified_payables( + AppliedThresholds::Defaulted => try_to_make_qualified_payables( payable_accounts, - &PRESERVED_TEST_PAYMENT_THRESHOLDS, + &PaymentThresholds::default(), now, false, ), AppliedThresholds::CommonButRandomized { common_thresholds } => { - try_to_make_guaranteed_qualified_payables( - payable_accounts, - common_thresholds, - now, - false, - ) + try_to_make_qualified_payables(payable_accounts, common_thresholds, now, false) } AppliedThresholds::RandomizedForEachAccount { individual_thresholds, - } => { - let vec_of_thresholds = individual_thresholds.values().collect_vec(); - let zipped = payable_accounts.into_iter().zip(vec_of_thresholds.iter()); - zipped - .flat_map(|(qualified_payable, thresholds)| { - make_single_qualified_payable_opt( - qualified_payable, - &payment_inspector, - &thresholds, - false, - now, - ) - }) - .collect() - } + } => make_qualified_payables_for_individualized_thresholds( + payable_accounts, + now, + &payment_inspector, + individual_thresholds, + ), } } +fn make_qualified_payables_for_individualized_thresholds( + payable_accounts: Vec, + now: SystemTime, + payment_inspector: &PayableInspector, + individual_thresholds: &HashMap, +) -> Vec { + let vec_of_thresholds = individual_thresholds.values().collect_vec(); + let zipped = payable_accounts.into_iter().zip(vec_of_thresholds.iter()); + zipped + .flat_map(|(qualified_payable, thresholds)| { + make_single_qualified_payable_opt( + qualified_payable, + &payment_inspector, + &thresholds, + false, + now, + ) + }) + .collect() +} + fn try_generating_qualified_payables_and_cw_balance( gn: &mut ThreadRng, accounts_count: usize, @@ -378,14 +388,19 @@ fn pick_appropriate_cw_service_fee_balance( qualified_payables: &[QualifiedPayableAccount], accounts_count: usize, ) -> u128 { - // Value picked empirically - const COEFFICIENT: usize = 1000; + // Values picked empirically + const COEFFICIENT_A: usize = 1000; + const COEFFICIENT_B: usize = 2; + let balance_average = sum_as(qualified_payables, |account| { account.initial_balance_minor() }) / accounts_count as u128; - let max_pieces = accounts_count * COEFFICIENT; - let number_of_pieces = generate_usize(gn, max_pieces - 2) as u128 + 2; - balance_average / COEFFICIENT as u128 * number_of_pieces + + let max_pieces = accounts_count * COEFFICIENT_A; + let number_of_pieces = + generate_usize(gn, max_pieces - COEFFICIENT_B) as u128 + COEFFICIENT_B as u128; + + balance_average / COEFFICIENT_A as u128 * number_of_pieces } fn make_payables_according_to_thresholds_setup( @@ -398,12 +413,9 @@ fn make_payables_according_to_thresholds_setup( let nominated_thresholds = choose_thresholds(gn, &wallets); let payables = match &nominated_thresholds { - AppliedThresholds::Defaulted => make_payables_with_common_thresholds( - gn, - wallets, - &PRESERVED_TEST_PAYMENT_THRESHOLDS, - now, - ), + AppliedThresholds::Defaulted => { + make_payables_with_common_thresholds(gn, wallets, &PaymentThresholds::default(), now) + } AppliedThresholds::CommonButRandomized { common_thresholds } => { make_payables_with_common_thresholds(gn, wallets, common_thresholds, now) } @@ -424,22 +436,22 @@ fn prepare_account_wallets(accounts_count: usize) -> Vec { fn choose_thresholds(gn: &mut ThreadRng, prepared_wallets: &[Wallet]) -> AppliedThresholds { let be_defaulted = generate_boolean(gn); if be_defaulted { - AppliedThresholds::Defaulted - } else { - let be_same_for_all_accounts = generate_boolean(gn); - if be_same_for_all_accounts { - AppliedThresholds::CommonButRandomized { - common_thresholds: return_single_randomized_thresholds(gn), - } - } else { - let individual_thresholds = prepared_wallets - .iter() - .map(|wallet| (wallet.address(), return_single_randomized_thresholds(gn))) - .collect::>(); - AppliedThresholds::RandomizedForEachAccount { - individual_thresholds, - } - } + return AppliedThresholds::Defaulted; + } + + let be_same_for_all_accounts = generate_boolean(gn); + if be_same_for_all_accounts { + return AppliedThresholds::CommonButRandomized { + common_thresholds: return_single_randomized_thresholds(gn), + }; + } + + let individual_thresholds = prepared_wallets + .iter() + .map(|wallet| (wallet.address(), return_single_randomized_thresholds(gn))) + .collect::>(); + AppliedThresholds::RandomizedForEachAccount { + individual_thresholds, } } @@ -500,7 +512,7 @@ fn make_agent(cw_service_fee_balance: u128) -> BlockchainAgentMock { .service_fee_balance_minor_result(cw_service_fee_balance) // For PaymentAdjuster itself .service_fee_balance_minor_result(cw_service_fee_balance) - .gas_price_margin_result(TX_FEE_MARGIN_IN_PERCENT.clone()) + .gas_price_margin_result(*TX_FEE_MARGIN_IN_PERCENT) } fn make_adjustment(gn: &mut ThreadRng, accounts_count: usize) -> Adjustment { @@ -517,12 +529,13 @@ fn make_adjustment(gn: &mut ThreadRng, accounts_count: usize) -> Adjustment { } fn administrate_single_scenario_result( + mut test_overall_output_collector: TestOverallOutputCollector, payment_adjuster_result: Result, account_infos: Vec, used_thresholds: AppliedThresholds, required_adjustment: Adjustment, cw_service_fee_balance_minor: u128, -) -> ScenarioResult { +) -> (TestOverallOutputCollector, ScenarioResult) { let common = CommonScenarioInfo { cw_service_fee_balance_minor, required_adjustment, @@ -535,17 +548,23 @@ fn administrate_single_scenario_result( PercentPortionOfCWUsed::new(&adjusted_accounts, &common); let merged = merge_information_about_particular_account(account_infos, adjusted_accounts); - let interpretable_adjustments = merged - .into_iter() - .map(InterpretableAccountAdjustmentResult::new) - .collect_vec(); - let (partially_sorted_interpretable_adjustments, were_no_accounts_eliminated) = + + let (interpretable_adjustments, adjusted_accounts_within_this_scenario) = + prepare_interpretable_adjustment_results(merged); + + look_after_adjustment_statistics( + &mut test_overall_output_collector, + adjusted_accounts_within_this_scenario, + ); + + let (partially_sorted_interpretable_adjustments, no_accounts_eliminated) = sort_interpretable_adjustments(interpretable_adjustments); + Ok(SuccessfulAdjustment { common, portion_of_cw_cumulatively_used_percents, partially_sorted_interpretable_adjustments, - no_accounts_eliminated: were_no_accounts_eliminated, + no_accounts_eliminated, }) } Err(adjuster_error) => Err(FailedAdjustment { @@ -555,7 +574,10 @@ fn administrate_single_scenario_result( }), }; - ScenarioResult::new(reinterpreted_result) + ( + test_overall_output_collector, + ScenarioResult::new(reinterpreted_result), + ) } fn merge_information_about_particular_account( @@ -576,6 +598,35 @@ fn merge_information_about_particular_account( .collect() } +fn prepare_interpretable_adjustment_results( + merged: Vec<(AccountInfo, Option)>, +) -> (Vec, usize) { + merged.into_iter().fold( + (vec![], 0), + |(mut interpretable_accounts, adjusted_accounts_so_far), + (info, maybe_eliminated_account)| { + let (interpretable_account, was_this_account_eliminated) = + InterpretableAccountAdjustmentResult::new(info, maybe_eliminated_account); + interpretable_accounts.push(interpretable_account); + ( + interpretable_accounts, + adjusted_accounts_so_far + if was_this_account_eliminated { 1 } else { 0 }, + ) + }, + ) +} + +fn look_after_adjustment_statistics( + test_overall_output_collector: &mut TestOverallOutputCollector, + adjusted_accounts_within_this_scenario: usize, +) { + if adjusted_accounts_within_this_scenario > 1 { + test_overall_output_collector.scenarios_with_more_than_one_adjustment += 1 + } + test_overall_output_collector.adjusted_account_count_total += + adjusted_accounts_within_this_scenario; +} + enum PercentPortionOfCWUsed { Percents(u8), LessThanOnePercent, @@ -585,10 +636,10 @@ impl PercentPortionOfCWUsed { fn new(adjusted_accounts: &[PayableAccount], common: &CommonScenarioInfo) -> Self { let used_absolute: u128 = sum_as(adjusted_accounts, |account| account.balance_wei); let percents = ((100 * used_absolute) / common.cw_service_fee_balance_minor) as u8; - if percents >= 1 { - PercentPortionOfCWUsed::Percents(percents) - } else { + if percents < 1 { PercentPortionOfCWUsed::LessThanOnePercent + } else { + PercentPortionOfCWUsed::Percents(percents) } } @@ -679,7 +730,7 @@ fn render_results_to_file_and_attempt_basic_assertions( total_scenarios_handled_including_invalid_ones, number_of_requested_scenarios ); // Only some of the generated scenarios are acceptable, don't be surprised by the waste. That's - // anticipated given the nature of the generator and the requirements on the payable accounts + // expected given the nature of the generator and the requirements on the payable accounts // so that they are picked up and let in the PaymentAdjuster. We'll be better off truly faithful // to the use case and the expected conditions. Therefore, we insist on making "guaranteed" // QualifiedPayableAccounts out of PayableAccount which is where we take the losses. @@ -715,10 +766,9 @@ fn introduction(file: &mut File) { let page_width = PAGE_WIDTH; file.write_fmt(format_args!( "{:^page_width$}\n", - "A short summary can be found at the tail" + "There is a short summary at the tail" )) .unwrap(); - write_thick_dividing_line(file); write_thick_dividing_line(file) } @@ -742,13 +792,16 @@ fn write_brief_test_summary_at_file_s_tail( Plain service fee adjustments:......... {}\n\ Bills fulfillment distribution:\n\ {}\n\n\ + Individual accounts adjusted:.......... {}\n\ + Scenarios with more than one account\n\ + adjusted:.............................. {}\n\n\ Unsuccessful\n\ Caught by the entry check:............. {}\n\ - With 'RecursionDrainedAllAccounts':.... {}\n\ + With 'RecursionEliminatedAllAccounts':.... {}\n\ With late insufficient balance errors:. {}\n\n\ Legend\n\ - Adjusted balances are highlighted by \ - these marks by the side:............. . {}", + Adjusted balances are highlighted\n\ + by these marks by the side:............ {}", scenarios_requested, scenarios_evaluated, output_collector.oks, @@ -765,6 +818,8 @@ fn write_brief_test_summary_at_file_s_tail( output_collector .fulfillment_distribution_for_service_fee_adjustments .render_in_two_lines(), + output_collector.adjusted_account_count_total, + output_collector.scenarios_with_more_than_one_adjustment, output_collector.scenarios_denied_before_adjustment_started, output_collector.all_accounts_eliminated, output_collector.late_immoderately_insufficient_service_fee_balance, @@ -815,13 +870,18 @@ fn do_final_processing_of_single_scenario( } Err(negative) => { match negative.adjuster_error { - PaymentAdjusterError::AbsolutelyInsufficientBalance { .. } => { - panic!("Such errors should be already filtered out") - } - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { .. } => { - output_collector.late_immoderately_insufficient_service_fee_balance += 1 - } - PaymentAdjusterError::RecursionDrainedAllAccounts => { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + ref detection_phase, + .. + } => match detection_phase { + DetectionPhase::InitialCheck { .. } => { + panic!("Such errors should be already filtered out") + } + DetectionPhase::PostTxFeeAdjustment { .. } => { + output_collector.late_immoderately_insufficient_service_fee_balance += 1 + } + }, + PaymentAdjusterError::RecursionEliminatedAllAccounts => { output_collector.all_accounts_eliminated += 1 } } @@ -976,7 +1036,7 @@ fn single_account_output( .unwrap(); } -const NON_EXHAUSTED_ACCOUNT_MARKER: &str = "# # # # # # # #"; +const NON_EXHAUSTED_ACCOUNT_MARKER: &str = "<< << << <<"; fn resolve_account_fulfilment_status_graphically( bill_coverage_in_percentage_opt: Option, @@ -1080,6 +1140,8 @@ struct TestOverallOutputCollector { // ____________________________________ oks: usize, with_no_accounts_eliminated: usize, + adjusted_account_count_total: usize, + scenarios_with_more_than_one_adjustment: usize, fulfillment_distribution_for_transaction_fee_adjustments: PercentageFulfillmentDistribution, fulfillment_distribution_for_service_fee_adjustments: PercentageFulfillmentDistribution, // Errors @@ -1094,6 +1156,8 @@ impl TestOverallOutputCollector { scenarios_denied_before_adjustment_started: 0, oks: 0, with_no_accounts_eliminated: 0, + adjusted_account_count_total: 0, + scenarios_with_more_than_one_adjustment: 0, fulfillment_distribution_for_transaction_fee_adjustments: Default::default(), fulfillment_distribution_for_service_fee_adjustments: Default::default(), all_accounts_eliminated: 0, @@ -1237,27 +1301,30 @@ impl AccountWithWallet for InterpretableAccountAdjustmentResult { } impl InterpretableAccountAdjustmentResult { - fn new((info, non_eliminated_payable): (AccountInfo, Option)) -> Self { - let bill_coverage_in_percentage_opt = match &non_eliminated_payable { - Some(payable) => { - let bill_coverage_in_percentage = { - let percentage = - (payable.balance_wei * 100) / info.initially_requested_service_fee_minor; - u8::try_from(percentage).unwrap() - }; - Some(bill_coverage_in_percentage) - } - None => None, - }; - InterpretableAccountAdjustmentResult { - info: AccountInfo { - wallet: info.wallet, - debt_age_s: info.debt_age_s, - initially_requested_service_fee_minor: info.initially_requested_service_fee_minor, + fn new(info: AccountInfo, maybe_eliminated_payable: Option) -> (Self, bool) { + let (bill_coverage_in_percentage_opt, was_this_account_adjusted) = + match &maybe_eliminated_payable { + Some(payable) => { + let percents_of_bill_coverage = Self::percents_of_bill_coverage(&info, payable); + ( + Some(percents_of_bill_coverage), + percents_of_bill_coverage != 100, + ) + } + None => (None, false), + }; + ( + InterpretableAccountAdjustmentResult { + info, + bill_coverage_in_percentage_opt, }, + was_this_account_adjusted, + ) + } - bill_coverage_in_percentage_opt, - } + fn percents_of_bill_coverage(info: &AccountInfo, payable: &PayableAccount) -> u8 { + let percentage = (payable.balance_wei * 100) / info.initially_requested_service_fee_minor; + u8::try_from(percentage).unwrap() } } diff --git a/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs index 62ddde7ac..d16d8c3b5 100644 --- a/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs +++ b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs @@ -15,8 +15,8 @@ use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstract BalanceProvidingAccount, DisqualificationLimitProvidingAccount, }; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysisReport, PaymentAdjusterError, ServiceFeeImmoderateInsufficiency, - TransactionFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, + ServiceFeeImmoderateInsufficiency, TransactionFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; @@ -25,6 +25,8 @@ use itertools::Either; use masq_lib::logger::Logger; use masq_lib::percentage::PurePercentage; +type TxCount = u16; + pub struct PreparatoryAnalyzer {} impl PreparatoryAnalyzer { @@ -72,7 +74,7 @@ impl PreparatoryAnalyzer { ); let service_fee_check_result = if is_service_fee_adjustment_needed { - let error_factory = EarlyServiceFeeSingleTXErrorFactory::default(); + let error_factory = EarlyServiceFeeErrorFactory::default(); Self::check_adjustment_possibility( &prepared_accounts, @@ -104,16 +106,21 @@ impl PreparatoryAnalyzer { fn handle_errors_if_present( number_of_accounts: usize, - transaction_fee_check_result: Result, TransactionFeeImmoderateInsufficiency>, + transaction_fee_check_result: Result< + Option, + TransactionFeeImmoderateInsufficiency, + >, service_fee_check_result: Result<(), ServiceFeeImmoderateInsufficiency>, - ) -> Result, PaymentAdjusterError> { + ) -> Result, PaymentAdjusterError> { let construct_error = |tx_fee_check_err_opt: Option, service_fee_check_err_opt: Option| { - PaymentAdjusterError::AbsolutelyInsufficientBalance { + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - transaction_fee_opt: tx_fee_check_err_opt, - service_fee_opt: service_fee_check_err_opt, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: tx_fee_check_err_opt, + service_fee_opt: service_fee_check_err_opt, + }, } }; @@ -138,7 +145,7 @@ impl PreparatoryAnalyzer { &self, weighed_accounts: &[WeighedPayable], cw_service_fee_balance_minor: u128, - error_factory: LateServiceFeeSingleTxErrorFactory, + error_factory: LaterServiceFeeErrorFactory, logger: &Logger, ) -> Result { if Self::is_service_fee_adjustment_needed( @@ -168,9 +175,9 @@ impl PreparatoryAnalyzer { per_transaction_requirement_minor: u128, number_of_qualified_accounts: usize, logger: &Logger, - ) -> Result, TransactionFeeImmoderateInsufficiency> { + ) -> Result, TransactionFeeImmoderateInsufficiency> { let per_txn_requirement_minor_with_margin = - gas_price_margin.add_percent_to(per_transaction_requirement_minor); + gas_price_margin.increase_by_percent_for(per_transaction_requirement_minor); let verified_tx_counts = Self::transaction_counts_verification( cw_transaction_fee_balance_minor, @@ -178,8 +185,8 @@ impl PreparatoryAnalyzer { number_of_qualified_accounts, ); - let max_tx_count_we_can_afford: u16 = verified_tx_counts.affordable; - let required_tx_count: u16 = verified_tx_counts.required; + let max_tx_count_we_can_afford = verified_tx_counts.affordable; + let required_tx_count = verified_tx_counts.required; if max_tx_count_we_can_afford == 0 { Err(TransactionFeeImmoderateInsufficiency { @@ -192,9 +199,9 @@ impl PreparatoryAnalyzer { log_insufficient_transaction_fee_balance( logger, required_tx_count, + max_tx_count_we_can_afford, per_txn_requirement_minor_with_margin, cw_transaction_fee_balance_minor, - max_tx_count_we_can_afford, ); Ok(Some(max_tx_count_we_can_afford)) @@ -219,15 +226,15 @@ impl PreparatoryAnalyzer { ) -> Result<(), Error> where AnalyzableAccounts: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, - ErrorFactory: ServiceFeeSingleTXErrorFactory, + ErrorFactory: ServiceFeeErrorFactory, { let lowest_disqualification_limit = Self::find_lowest_disqualification_limit(prepared_accounts); - // We cannot do much in this area but stepping in if the cw balance is zero or nearly + // We can do little in this area but stepping in if the cw balance is zero or nearly // zero with the assumption that the debt with the lowest disqualification limit in // the set fits in the available balance. If it doesn't, we're not going to bother - // the payment adjuster by that work, so it'll abort and no payments will come out. + // the payment adjuster by that work, so it'll abort, and no payments will come out. if lowest_disqualification_limit <= cw_service_fee_balance_minor { Ok(()) } else { @@ -291,7 +298,7 @@ impl PreparatoryAnalyzer { } } -pub trait ServiceFeeSingleTXErrorFactory +pub trait ServiceFeeErrorFactory where AnalyzableAccount: BalanceProvidingAccount, { @@ -299,10 +306,10 @@ where } #[derive(Default)] -pub struct EarlyServiceFeeSingleTXErrorFactory {} +pub struct EarlyServiceFeeErrorFactory {} -impl ServiceFeeSingleTXErrorFactory - for EarlyServiceFeeSingleTXErrorFactory +impl ServiceFeeErrorFactory + for EarlyServiceFeeErrorFactory { fn make( &self, @@ -319,12 +326,12 @@ impl ServiceFeeSingleTXErrorFactory Self { let original_number_of_accounts = unadjusted_accounts.len(); let original_total_service_fee_required_minor = sum_as(unadjusted_accounts, |account| { @@ -337,21 +344,21 @@ impl LateServiceFeeSingleTxErrorFactory { } } -impl ServiceFeeSingleTXErrorFactory - for LateServiceFeeSingleTxErrorFactory -{ +impl ServiceFeeErrorFactory for LaterServiceFeeErrorFactory { fn make( &self, current_set_of_accounts: &[WeighedPayable], cw_service_fee_balance_minor: u128, ) -> PaymentAdjusterError { let number_of_accounts = current_set_of_accounts.len(); - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts: self.original_number_of_accounts, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - original_total_service_fee_required_minor: self - .original_total_service_fee_required_minor, - cw_service_fee_balance_minor, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + cw_service_fee_balance_minor, + original_number_of_accounts: self.original_number_of_accounts, + original_total_service_fee_required_minor: self + .original_total_service_fee_required_minor, + }, } } } @@ -367,15 +374,15 @@ mod tests { BalanceProvidingAccount, DisqualificationLimitProvidingAccount, }; use crate::accountant::payment_adjuster::preparatory_analyser::{ - EarlyServiceFeeSingleTXErrorFactory, LateServiceFeeSingleTxErrorFactory, - PreparatoryAnalyzer, ServiceFeeSingleTXErrorFactory, + EarlyServiceFeeErrorFactory, LaterServiceFeeErrorFactory, PreparatoryAnalyzer, + ServiceFeeErrorFactory, }; use crate::accountant::payment_adjuster::test_utils::local_utils::{ make_meaningless_weighed_account, multiply_by_billion, multiply_by_billion_concise, DisqualificationGaugeMock, }; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysisReport, PaymentAdjusterError, + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, ServiceFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; @@ -508,7 +515,7 @@ mod tests { ) where EnsureAccountsRightType: FnOnce(Vec) -> Vec, PrepareExpectedError: FnOnce(usize, u128, u128) -> Error, - ErrorFactory: ServiceFeeSingleTXErrorFactory, + ErrorFactory: ServiceFeeErrorFactory, Error: Debug + PartialEq, AnalyzableAccount: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, { @@ -557,7 +564,7 @@ mod tests { #[test] fn not_enough_for_even_the_smallest_account_error_right_after_alarmed_tx_fee_check() { - let error_factory = EarlyServiceFeeSingleTXErrorFactory::default(); + let error_factory = EarlyServiceFeeErrorFactory::default(); let ensure_accounts_right_type = |weighed_payables: Vec| { weighed_payables .into_iter() @@ -588,17 +595,19 @@ mod tests { make_meaningless_weighed_account(1011), ]; let original_number_of_accounts = original_accounts.len(); - let initial_sum = sum_as(&original_accounts, |account| { + let original_total_service_fee_required_minor = sum_as(&original_accounts, |account| { account.initial_balance_minor() }); - let error_factory = LateServiceFeeSingleTxErrorFactory::new(&original_accounts); + let error_factory = LaterServiceFeeErrorFactory::new(&original_accounts); let ensure_accounts_right_type = |accounts| accounts; let prepare_expected_error = |number_of_accounts, _, cw_service_fee_balance_minor| { - PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { - original_number_of_accounts, + PaymentAdjusterError::AbsoluteFeeInsufficiency { number_of_accounts, - original_total_service_fee_required_minor: initial_sum, - cw_service_fee_balance_minor, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts, + cw_service_fee_balance_minor, + original_total_service_fee_required_minor, + }, } }; @@ -610,10 +619,10 @@ mod tests { } #[test] - fn recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighted_payables() { + fn recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighed_payables() { init_test_logging(); let test_name = - "recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighted_payables"; + "recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighed_payables"; let balance_1 = multiply_by_billion(2_000_000); let mut weighed_account_1 = make_meaningless_weighed_account(123); weighed_account_1 @@ -630,33 +639,33 @@ mod tests { .balance_wei = balance_2; let accounts = vec![weighed_account_1, weighed_account_2]; let service_fee_totally_required_minor = balance_1 + balance_2; - // We start at a value being one bigger than required, and in the act, we subtract from it - // so that we also get the exact edge and finally also not enough by one. - let cw_service_fee_balance_minor = service_fee_totally_required_minor + 1; - let error_factory = LateServiceFeeSingleTxErrorFactory::new(&accounts); + let error_factory = LaterServiceFeeErrorFactory::new(&accounts); let logger = Logger::new(test_name); let subject = PreparatoryAnalyzer::new(); - [(0, false), (1, false), (2, true)].iter().for_each( - |(subtrahend_from_cw_balance, adjustment_is_needed_expected)| { - let service_fee_balance = cw_service_fee_balance_minor - subtrahend_from_cw_balance; - let adjustment_is_needed_actual = subject - .recheck_if_service_fee_adjustment_is_needed( - &accounts, - service_fee_balance, - error_factory.clone(), - &logger, - ) - .unwrap(); - assert_eq!(adjustment_is_needed_actual, *adjustment_is_needed_expected); - }, - ); + [ + (service_fee_totally_required_minor - 1, true), + (service_fee_totally_required_minor, false), + (service_fee_totally_required_minor + 1, false), + ] + .iter() + .for_each(|(service_fee_balance, adjustment_is_needed_expected)| { + let adjustment_is_needed_actual = subject + .recheck_if_service_fee_adjustment_is_needed( + &accounts, + *service_fee_balance, + error_factory.clone(), + &logger, + ) + .unwrap(); + assert_eq!(adjustment_is_needed_actual, *adjustment_is_needed_expected); + }); TestLogHandler::new().exists_log_containing(&format!( "WARN: {test_name}: Mature payables amount to {} MASQ wei while the consuming wallet \ holds only {}", service_fee_totally_required_minor.separate_with_commas(), - (cw_service_fee_balance_minor - 2).separate_with_commas() + (service_fee_totally_required_minor - 1).separate_with_commas() )); } @@ -678,11 +687,11 @@ mod tests { .balance_wei = balance_2; let weighed_accounts = vec![account_1, account_2]; - let result = LateServiceFeeSingleTxErrorFactory::new(&weighed_accounts); + let result = LaterServiceFeeErrorFactory::new(&weighed_accounts); assert_eq!( result, - LateServiceFeeSingleTxErrorFactory { + LaterServiceFeeErrorFactory { original_number_of_accounts: 2, original_total_service_fee_required_minor: balance_1 + balance_2 } diff --git a/node/src/accountant/payment_adjuster/service_fee_adjuster.rs b/node/src/accountant/payment_adjuster/service_fee_adjuster.rs index f9e86a7b5..ab1c51d3d 100644 --- a/node/src/accountant/payment_adjuster/service_fee_adjuster.rs +++ b/node/src/accountant/payment_adjuster/service_fee_adjuster.rs @@ -12,7 +12,7 @@ use itertools::Either; use masq_lib::logger::Logger; use masq_lib::utils::convert_collection; use std::vec; -use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::thriving_competitor_found_diagnostics; +use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::diagnostics_for_accounts_above_disqualification_limit; pub trait ServiceFeeAdjuster { fn perform_adjustment_by_service_fee( @@ -41,12 +41,14 @@ impl ServiceFeeAdjuster for ServiceFeeAdjusterReal { let checked_accounts = Self::try_confirm_some_accounts(unconfirmed_adjustments); match checked_accounts { - Either::Left(no_accounts_above_disq_limit) => Self::disqualify_single_account( + Either::Left(only_accounts_below_disq_limit) => Self::disqualify_single_account( disqualification_arbiter, - no_accounts_above_disq_limit, + only_accounts_below_disq_limit, logger, ), - Either::Right(some_accounts_above_disq_limit) => some_accounts_above_disq_limit, + Either::Right(some_accounts_above_or_even_to_disq_limit) => { + some_accounts_above_or_even_to_disq_limit + } } } } @@ -66,24 +68,25 @@ impl ServiceFeeAdjusterReal { // wisely, better redistributed among the rest of accounts, as much as the wider group of them // can be satisfied, even though just partially. // - // However, if it begins to be clear that the remaining money doesn't allow to keep any + // However, if it begins to be clear that the remaining money doesn't allow keeping any // additional account in the selection, there is the next step to come, where the already // selected accounts are reviewed again in the order of their significance resolved from // remembering their weights from the earlier processing, and the unused money is poured into, // until all resources are used. + fn try_confirm_some_accounts( unconfirmed_adjustments: Vec, ) -> Either, AdjustmentIterationResult> { - let (accounts_above_disq_limit, accounts_below_disq_limit) = + let (accounts_above_or_even_to_disq_limit, accounts_below_disq_limit) = Self::filter_and_process_confirmable_accounts(unconfirmed_adjustments); - if accounts_above_disq_limit.is_empty() { + if accounts_above_or_even_to_disq_limit.is_empty() { Either::Left(accounts_below_disq_limit) } else { let remaining_undecided_accounts: Vec = convert_collection(accounts_below_disq_limit); let pre_processed_decided_accounts: Vec = - convert_collection(accounts_above_disq_limit); + convert_collection(accounts_above_or_even_to_disq_limit); Either::Right(AdjustmentIterationResult { decided_accounts: pre_processed_decided_accounts, remaining_undecided_accounts, @@ -119,27 +122,31 @@ impl ServiceFeeAdjusterReal { Vec, ) { let init: (Vec, Vec) = (vec![], vec![]); - let fold_guts = |(mut above_disq_limit, mut below_disq_limit): (Vec<_>, Vec<_>), - current: UnconfirmedAdjustment| { - let disqualification_limit = current.disqualification_limit_minor(); - if current.proposed_adjusted_balance_minor >= disqualification_limit { - thriving_competitor_found_diagnostics(¤t, disqualification_limit); - let mut adjusted = current; - adjusted.proposed_adjusted_balance_minor = disqualification_limit; - above_disq_limit.push(adjusted) - } else { - below_disq_limit.push(current) - } - (above_disq_limit, below_disq_limit) - }; + let fold_guts = + |(mut above_or_even_to_disq_limit, mut below_disq_limit): (Vec<_>, Vec<_>), + current: UnconfirmedAdjustment| { + let disqualification_limit = current.disqualification_limit_minor(); + if current.proposed_adjusted_balance_minor >= disqualification_limit { + diagnostics_for_accounts_above_disqualification_limit( + ¤t, + disqualification_limit, + ); + let mut adjusted = current; + adjusted.proposed_adjusted_balance_minor = disqualification_limit; + above_or_even_to_disq_limit.push(adjusted) + } else { + below_disq_limit.push(current) + } + (above_or_even_to_disq_limit, below_disq_limit) + }; - let (accounts_above_disq_limit, accounts_below_disq_limit) = + let (accounts_above_or_even_to_disq_limit, accounts_below_disq_limit) = unconfirmed_adjustments.into_iter().fold(init, fold_guts); - let decided_accounts = if accounts_above_disq_limit.is_empty() { + let decided_accounts = if accounts_above_or_even_to_disq_limit.is_empty() { vec![] } else { - convert_collection(accounts_above_disq_limit) + convert_collection(accounts_above_or_even_to_disq_limit) }; (decided_accounts, accounts_below_disq_limit) @@ -184,7 +191,7 @@ fn compute_proportional_cw_fragment( multiplication_coefficient: u128, ) -> u128 { cw_service_fee_balance_minor - // Considered safe for the nature of the calculus producing this coefficient + // Considered safe as to the nature of the calculus producing this coefficient .checked_mul(multiplication_coefficient) .unwrap_or_else(|| { panic!( @@ -201,13 +208,13 @@ mod tests { use crate::accountant::payment_adjuster::miscellaneous::data_structures::AdjustedAccountBeforeFinalization; use crate::accountant::payment_adjuster::service_fee_adjuster::ServiceFeeAdjusterReal; use crate::accountant::payment_adjuster::test_utils::local_utils::{ - make_non_guaranteed_unconfirmed_adjustment, multiply_by_quintillion, + make_meaningless_unconfirmed_adjustment, multiply_by_quintillion, multiply_by_quintillion_concise, }; #[test] fn filter_and_process_confirmable_accounts_limits_them_by_their_disqualification_edges() { - let mut account_1 = make_non_guaranteed_unconfirmed_adjustment(111); + let mut account_1 = make_meaningless_unconfirmed_adjustment(111); let weight_1 = account_1.weighed_account.weight; account_1 .weighed_account @@ -220,7 +227,7 @@ mod tests { .analyzed_account .disqualification_limit_minor = multiply_by_quintillion_concise(1.8); account_1.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(3.0); - let mut account_2 = make_non_guaranteed_unconfirmed_adjustment(222); + let mut account_2 = make_meaningless_unconfirmed_adjustment(222); let weight_2 = account_2.weighed_account.weight; account_2 .weighed_account @@ -233,7 +240,7 @@ mod tests { .analyzed_account .disqualification_limit_minor = multiply_by_quintillion_concise(4.2) - 1; account_2.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(4.2); - let mut account_3 = make_non_guaranteed_unconfirmed_adjustment(333); + let mut account_3 = make_meaningless_unconfirmed_adjustment(333); account_3 .weighed_account .analyzed_account @@ -245,7 +252,7 @@ mod tests { .analyzed_account .disqualification_limit_minor = multiply_by_quintillion(2) + 1; account_3.proposed_adjusted_balance_minor = multiply_by_quintillion(2); - let mut account_4 = make_non_guaranteed_unconfirmed_adjustment(444); + let mut account_4 = make_meaningless_unconfirmed_adjustment(444); let weight_4 = account_4.weighed_account.weight; account_4 .weighed_account @@ -258,7 +265,7 @@ mod tests { .analyzed_account .disqualification_limit_minor = multiply_by_quintillion_concise(0.5); account_4.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(0.5); - let mut account_5 = make_non_guaranteed_unconfirmed_adjustment(555); + let mut account_5 = make_meaningless_unconfirmed_adjustment(555); account_5 .weighed_account .analyzed_account @@ -335,18 +342,21 @@ pub mod illustrative_util { unconfirmed_adjustments[1].wallet(), wallet_of_expected_outweighed ); - // To prevent unjust reallocation we used to secure a rule an account could never demand - // more than 100% of its size. + // To prevent unjust reallocation, we secured a rule an account could never demand more + // than 100% of its size. + + // Later it was changed to a different policy, the so-called "outweighed" account is given + // automatically a balance equal to its disqualification limit. Still, it's quite likely + // some accounts will acquire slightly more by a distribution of the last bits of funds + // away out of the consuming wallet. - // Later it was changed to a different policy, the so called "outweighed" account is given - // automatically a balance equal to its disqualification limit. Still, later on, it's quite - // likely to acquire slightly more by a distribution of the last bits of funds away from - // within the consuming wallet. + // Here, though, the assertion illustrates what the latest policy intends to fight off, + // as the unprotected proposed adjusted balance rises over the original balance. let proposed_adjusted_balance = unconfirmed_adjustments[1].proposed_adjusted_balance_minor; assert!( proposed_adjusted_balance > (original_balance_of_outweighed_account * 11 / 10), - "we expected the proposed balance at least 1.1 times bigger than the original balance \ - which is {} but it was {}", + "we expected the proposed balance to be unsound, bigger than the original balance \ + (at least 1.1 times more) which would be {} but it was {}", original_balance_of_outweighed_account.separate_with_commas(), proposed_adjusted_balance.separate_with_commas() ); diff --git a/node/src/accountant/payment_adjuster/test_utils.rs b/node/src/accountant/payment_adjuster/test_utils.rs index e6c1e3eab..f85408b5c 100644 --- a/node/src/accountant/payment_adjuster/test_utils.rs +++ b/node/src/accountant/payment_adjuster/test_utils.rs @@ -113,8 +113,8 @@ pub(super) mod local_utils { .into_iter() .map(|months| (months, constant_balance)) .collect(), - Either::Right(specific_months_and_specific_balance) => { - specific_months_and_specific_balance + Either::Right(specific_months_and_specific_balances) => { + specific_months_and_specific_balances } }; accounts_seeds @@ -143,7 +143,7 @@ pub(super) mod local_utils { unban_below_gwei: 1_000_000, }; - pub fn make_non_guaranteed_unconfirmed_adjustment(n: u64) -> UnconfirmedAdjustment { + pub fn make_meaningless_unconfirmed_adjustment(n: u64) -> UnconfirmedAdjustment { let qualified_account = make_meaningless_qualified_payable(n); let account_balance = qualified_account.bare_account.balance_wei; let proposed_adjusted_balance_minor = (2 * account_balance) / 3; @@ -325,7 +325,8 @@ pub mod exposed_utils { use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; - pub fn convert_qualified_into_analyzed_payables_in_test( + // Refrain from using this fn in the prod code + pub fn convert_qualified_p_into_analyzed_p( qualified_account: Vec, ) -> Vec { qualified_account diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs index ff1f6d3d7..bf6acf656 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs @@ -16,7 +16,7 @@ pub struct BlockchainAgentMock { transaction_fee_balance_minor_results: RefCell>, service_fee_balance_minor_results: RefCell>, gas_price_results: RefCell>, - gas_price_margin: RefCell>, + gas_price_margin_results: RefCell>, consuming_wallet_result_opt: Option, pending_transaction_id_results: RefCell>, arbitrary_id_stamp_opt: Option, @@ -46,7 +46,7 @@ impl BlockchainAgent for BlockchainAgentMock { } fn gas_price_margin(&self) -> PurePercentage { - self.gas_price_margin.borrow_mut().remove(0) + self.gas_price_margin_results.borrow_mut().remove(0) } fn consuming_wallet(&self) -> &Wallet { @@ -92,7 +92,7 @@ impl BlockchainAgentMock { } pub fn gas_price_margin_result(self, result: PurePercentage) -> Self { - self.gas_price_margin.borrow_mut().push(result); + self.gas_price_margin_results.borrow_mut().push(result); self } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 70d7345b8..3b50910af 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -330,8 +330,8 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { fn cancel_scan(&mut self, logger: &Logger) { error!( logger, - "Payable scanner is blocked from preparing instructions for payments. The cause appears \ - to be in competence of the user." + "Payable scanner is unable to generate payment instructions. It looks like only \ + the user can resolve this issue." ); self.mark_as_ended(logger) } @@ -582,9 +582,8 @@ impl PayableScanner { } const ADD_MORE_FUNDS_URGE: &'static str = - "Add more funds into your consuming wallet in order to \ - become able to repay already expired liabilities as the creditors would respond by delinquency \ - bans otherwise"; + "Add more funds into your consuming wallet to become able to repay already matured debts \ + as the creditors would respond by a delinquency ban otherwise"; } pub struct PendingPayableScanner { @@ -1328,9 +1327,7 @@ mod tests { PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let result = subject.begin_scan(now, None, &Logger::new(test_name)); @@ -1364,9 +1361,7 @@ mod tests { PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let _result = subject.begin_scan(now, None, &Logger::new("test")); @@ -1389,9 +1384,7 @@ mod tests { PayableDaoMock::new().non_pending_payables_result(unqualified_payable_accounts); let mut subject = PayableScannerBuilder::new() .payable_dao(payable_dao) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let result = subject.begin_scan(now, None, &Logger::new("test")); @@ -2187,9 +2180,7 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; @@ -2220,9 +2211,7 @@ mod tests { }; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified"; let logger = Logger::new(test_name); @@ -2263,9 +2252,7 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let logger = Logger::new(test_name); @@ -2288,9 +2275,9 @@ mod tests { unban_below_gwei: 0, }; let wallet = make_wallet("abc"); - // It is important to have a payable laying in the declining part of the thresholds, also - // it will be the more believable the steeper we have the slope because then a single second - // can make a certain difference for the intercept value which is the value this test + // It is important to have a payable lying in the declining part of the thresholds, also + // it will be more believable the steeper we have the slope because then a single second + // can make a certain difference for the intercept value, which is the value this test // compares for carrying out the conclusion let debt_age = payment_thresholds.maturity_threshold_sec + (payment_thresholds.threshold_interval_sec / 2); @@ -2302,9 +2289,7 @@ mod tests { }; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) - .payable_inspector(PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), - ))) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let intercept_before = subject .payable_exceeded_threshold(&payable, SystemTime::now()) diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 16c445362..748879593 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -314,6 +314,11 @@ pub mod payable_scanner_utils { .payable_threshold_gauge .calculate_payout_threshold_in_gwei(payment_thresholds, debt_age); + eprintln!( + "Balance wei: {}\n\ + Threshold: {}", + payable.balance_wei, threshold + ); if payable.balance_wei > threshold { Some(threshold) } else { diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index f4a286091..02d5a192a 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -13,7 +13,7 @@ use crate::accountant::db_access_objects::receivable_dao::{ ReceivableAccount, ReceivableDao, ReceivableDaoError, ReceivableDaoFactory, }; use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t, CustomQuery}; -use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_into_analyzed_payables_in_test; +use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; use crate::accountant::payment_adjuster::{ AdjustmentAnalysisResult, PaymentAdjuster, PaymentAdjusterError, }; @@ -1133,8 +1133,11 @@ impl PayableScannerBuilder { self } - pub fn payable_inspector(mut self, payable_inspector: PayableInspector) -> Self { - self.payable_inspector = payable_inspector; + pub fn payable_threshold_gauge( + mut self, + payable_threshold_gauge: Box, + ) -> Self { + self.payable_inspector = PayableInspector::new(payable_threshold_gauge); self } @@ -1747,7 +1750,7 @@ pub fn make_qualified_payables( payment_thresholds: &PaymentThresholds, now: SystemTime, ) -> Vec { - try_to_make_guaranteed_qualified_payables(payables, payment_thresholds, now, true) + try_to_make_qualified_payables(payables, payment_thresholds, now, true) } pub fn make_analyzed_payables( @@ -1755,14 +1758,10 @@ pub fn make_analyzed_payables( payment_thresholds: &PaymentThresholds, now: SystemTime, ) -> Vec { - convert_qualified_into_analyzed_payables_in_test(make_qualified_payables( - payables, - payment_thresholds, - now, - )) + convert_qualified_p_into_analyzed_p(make_qualified_payables(payables, payment_thresholds, now)) } -pub fn try_to_make_guaranteed_qualified_payables( +pub fn try_to_make_qualified_payables( payables: Vec, payment_thresholds: &PaymentThresholds, now: SystemTime, diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 7bf9d4124..493360302 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -7,7 +7,7 @@ use crate::database::db_initializer::ExternalData; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::database::db_migrations::db_migrator::DbMigrator; -use crate::test_utils::standard_dir_for_test_input_data; +use crate::test_utils::test_input_data_standard_dir; use masq_lib::logger::Logger; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::{to_string, NeighborhoodModeLight}; @@ -26,7 +26,7 @@ pub fn bring_db_0_back_to_life_and_return_connection(db_path: &Path) -> Connecti _ => (), }; let conn = Connection::open(&db_path).unwrap(); - let file_path = standard_dir_for_test_input_data().join("database_version_0_sqls.txt"); + let file_path = test_input_data_standard_dir().join("database_version_0_sqls.txt"); let mut file = File::open(file_path).unwrap(); let mut buffer = String::new(); file.read_to_string(&mut buffer).unwrap(); diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index a3b279eca..d3219d800 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -527,11 +527,14 @@ pub struct TestRawTransaction { pub data: Vec, } -pub fn standard_dir_for_test_input_data() -> PathBuf { - let mut working_dir = current_dir().unwrap(); - if !working_dir.ends_with("/node/") { - working_dir = working_dir.parent().unwrap().join("node"); - } +pub fn test_input_data_standard_dir() -> PathBuf { + let working_dir = current_dir().unwrap(); + if !working_dir.ends_with("node") { + panic!( + "Project structure with missing \"node\" directory: {:?}.", + working_dir + ); + }; working_dir .join("src") .join("test_utils") diff --git a/node/src/test_utils/test_input_data/smart_contract_for_on_blockchain_test b/node/src/test_utils/test_input_data/verify_bill_payments_smart_contract similarity index 100% rename from node/src/test_utils/test_input_data/smart_contract_for_on_blockchain_test rename to node/src/test_utils/test_input_data/verify_bill_payments_smart_contract