diff --git a/automap/.gitignore b/automap/.gitignore index b41078a47..0f6e0549c 100644 --- a/automap/.gitignore +++ b/automap/.gitignore @@ -1 +1,6 @@ automap.log + +## File-based project format: +*.iws +*.iml +*.ipr \ No newline at end of file diff --git a/dns_utility/.gitignore b/dns_utility/.gitignore index 9ab870da8..67cedea71 100644 --- a/dns_utility/.gitignore +++ b/dns_utility/.gitignore @@ -1 +1,6 @@ generated/ + +## File-based project format: +*.iws +*.iml +*.ipr diff --git a/masq/.gitignore b/masq/.gitignore new file mode 100644 index 000000000..e0264b089 --- /dev/null +++ b/masq/.gitignore @@ -0,0 +1,5 @@ + +## File-based project format: +*.iws +*.iml +*.ipr \ No newline at end of file diff --git a/masq_lib/src/percentage.rs b/masq_lib/src/percentage.rs index 20c03f14a..1e89bb045 100644 --- a/masq_lib/src/percentage.rs +++ b/masq_lib/src/percentage.rs @@ -1,77 +1,226 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use num::integer::mod_floor; use num::CheckedAdd; use num::CheckedSub; use num::{CheckedDiv, CheckedMul, Integer}; use std::any::type_name; use std::fmt::Debug; -use std::ops::Div; -use std::ops::Mul; +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. -// It's designed for a storage of values from 0 to 100, after which it can be used to compute -// the corresponding portion of many integer types. It should also take care of the least significant -// digit in order to diminish the effect of a precision loss genuinly implied by this kind of math -// operations done on integers. +#[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 Percentage { - per_cent: u8, +pub struct LoosePercentage { + multiples_of_100_percent: u32, + degrees_from_remainder: PurePercentage, +} + +pub trait PercentageInteger: + TryFrom + + CheckedMul + + CheckedAdd + + CheckedSub + + CheckedDiv + + PartialOrd + + Rem + + Integer + + Debug + + Copy +{ +} + +macro_rules! impl_percentage_integer { + ($($num_type: ty),+) => { + $(impl PercentageInteger for $num_type {})+ + } +} + +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); + + match by_wholes.checked_add(&by_remainder) { + Some(res) => Ok(res), + None => Err(BaseTypeOverflow {}), + } + } } -impl Percentage { - pub fn new(num: u8) -> Self { - match num { - 0..=100 => Self { per_cent: num }, - x => panic!("Accepts only range from 0 to 100 but {} was supplied", x), +#[derive(Debug, PartialEq, Eq)] +pub struct BaseTypeOverflow {} + +impl TryFrom for PurePercentage { + type Error = String; + + fn try_from(degree: u8) -> Result { + match degree { + 0..=100 => Ok(Self { degree }), + x => Err(format!( + "Accepts only range from 0 to 100 but {} was supplied", + x + )), } } +} +impl PurePercentage { pub fn of(&self, num: N) -> N where - N: From + CheckedMul + CheckedAdd + CheckedDiv + PartialOrd + Integer + Debug + Copy, + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, { - let zero = N::from(0); - if num == zero || N::from(self.per_cent) == zero { + if let Some(zero) = self.return_zero(num) { return zero; } - let a = match N::from(self.per_cent).checked_mul(&num) { + let product_before_final_div = match N::try_from(self.degree as i8) + .expect("Each type has 100") + .checked_mul(&num) + { Some(num) => num, None => return self.handle_upper_overflow(num), }; - if a < N::from(10) { - return N::from(0); - } + Self::div_by_100_and_round(product_before_final_div) + } - let rounding = if Percentage::should_be_rounded_down(a) { - N::from(0) + 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 { + Some(zero) } else { - N::from(1) - }; + 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, + } + } - let hundred = N::from(100); + 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, + } + } - if a < hundred { - rounding + fn abs_of_least_significant_digits(least_significant_digits: N, is_signed: bool) -> N + where + N: TryFrom + CheckedMul, + >::Error: Debug, + { + if is_signed { + 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") } else { - a.checked_div(&hundred) - .expect("div failed") - .checked_add(&rounding) - .expect("rounding failed") + least_significant_digits } } pub fn add_percent_to(&self, num: N) -> N where - N: From + CheckedMul + CheckedAdd + CheckedDiv + PartialOrd + Integer + Debug + Copy, + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, { - self.of(num).checked_add(&num).unwrap_or_else(|| { + let to_add = self.of(num); + num.checked_add(&to_add).unwrap_or_else(|| { panic!( - "Overflowed during addition of {} per cent, that is {:?}, to {:?} of type {}.", - self.per_cent, - self.of(num), + "Overflowed during addition of {} percent, that is {:?}, to {:?} of type {}.", + self.degree, + to_add, num, type_name::() ) @@ -80,178 +229,418 @@ impl Percentage { pub fn subtract_percent_from(&self, num: N) -> N where - N: From - + CheckedMul - + CheckedAdd - + CheckedSub - + CheckedDiv - + PartialOrd - + Integer - + Debug - + Copy, + N: PercentageInteger + CheckedSub, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, { - num.checked_sub(&self.of(num)) + let to_subtract = self.of(num); + num.checked_sub(&to_subtract) .expect("should never happen by its principle") } - fn should_be_rounded_down(num: N) -> bool + fn handle_upper_overflow(&self, num: N) -> N where - N: From + PartialEq + PartialOrd + Mul + CheckedMul + Integer + Copy, + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, { - let ten = N::from(10); - let upper_limit = ten * ten; - let enough_limit = ten; - if num == upper_limit { - true - } else if num >= enough_limit { - let modulo = mod_floor(num, upper_limit); - modulo - < N::from(5) - .checked_mul(&ten) - .expect("Couldn't create limit to compare with") - } else { - unreachable!("Check to prevent numbers with fewer than two digits failed") - } + 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 } - fn handle_upper_overflow(&self, num: N) -> N + fn treat_remainder(modulo: N, percent: N) -> N where - N: From + Div + Mul, + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, { - num / From::from(100) * N::from(self.per_cent) + 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") } } +#[derive(Debug, PartialEq, Eq)] +enum RoundingTo { + BiggerPositive, + BiggerNegative, + SmallerPositive, + SmallerNegative, +} + #[cfg(test)] mod tests { - use crate::percentage::Percentage; - use std::panic::catch_unwind; + use crate::percentage::{ + BaseTypeOverflow, LoosePercentage, PercentageInteger, PurePercentage, RoundingTo, + }; + use std::fmt::Debug; + + #[test] + 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); + } + + fn assert_integer_compatibility(subject: &PurePercentage, num: N, expected: N) + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + assert_eq!(subject.of(num), expected); + let half = num / N::try_from(2).unwrap(); + let one = N::try_from(1).unwrap(); + assert!((half - one) <= half && half <= (half + one)) + } #[test] - fn zero() { - assert_eq!(Percentage::new(45).of(0), 0); - assert_eq!(Percentage::new(0).of(33), 0) + fn zeros_for_pure_percentage() { + assert_eq!(PurePercentage::try_from(45).unwrap().of(0), 0); + assert_eq!(PurePercentage::try_from(0).unwrap().of(33), 0) + } + + #[test] + fn pure_percentage_end_to_end_test_for_unsigned() { + let expected_values = (0..=100).collect::>(); + + test_end_to_end(100, expected_values, |percent, base| { + PurePercentage::try_from(percent).unwrap().of(base) + }) } #[test] - fn end_to_end_test() { - let range = 0..=100; + fn pure_percentage_end_to_end_test_for_signed() { + let expected_values = (-100..=0).rev().collect::>(); + + test_end_to_end(-100, expected_values, |percent, base| { + PurePercentage::try_from(percent).unwrap().of(base) + }) + } + + fn test_end_to_end( + base: i8, + expected_values: Vec, + create_percentage_and_apply_it_on_number: F, + ) where + F: Fn(u8, i8) -> i8, + { + let range = 0_u8..=100; let round_returned_range = range - .clone() .into_iter() - .map(|per_cent| Percentage::new(per_cent).of(100_u64)) - .collect::>(); + .map(|percent| create_percentage_and_apply_it_on_number(percent, base)) + .collect::>(); - let expected = range - .into_iter() - .map(|num| num as u64) - .collect::>(); - assert_eq!(round_returned_range, expected) + assert_eq!(round_returned_range, expected_values) } #[test] fn only_numbers_up_to_100_are_accepted() { (101..=u8::MAX) - .map(|num| { - ( - catch_unwind(|| Percentage::new(num)).expect_err("expected panic"), - num, - ) - }) - .map(|(panic, num)| { - ( - panic - .downcast_ref::() - .expect("couldn't downcast to String") - .to_owned(), - num, - ) - }) - .for_each(|(panic_msg, num)| { + .map(|num| (PurePercentage::try_from(num), num)) + .for_each(|(res, num)| { assert_eq!( - panic_msg, - format!("Accepts only range from 0 to 100 but {} was supplied", num) + res, + Err(format!( + "Accepts only range from 0 to 100 but {} was supplied", + num + )) ) }); } + struct Case { + requested_percent: u32, + examined_base_number: i64, + expected_result: i64, + } + #[test] fn too_low_values() { - vec![((10, 1), 0), ((9, 1), 0), ((5, 14), 1), ((55, 40), 22)] - .into_iter() - .for_each(|((per_cent, examined_number), expected_result)| { - let result = Percentage::new(per_cent).of(examined_number); - assert_eq!( - result, expected_result, - "For {} per cent and number {} the expected result was {} but we got {}", - per_cent, examined_number, expected_result, result - ) - }) + vec![ + Case { + requested_percent: 49, + examined_base_number: 1, + expected_result: 0, + }, + Case { + requested_percent: 9, + examined_base_number: 1, + expected_result: 0, + }, + Case { + requested_percent: 5, + examined_base_number: 14, + expected_result: 1, + }, + Case { + requested_percent: 55, + examined_base_number: 41, + expected_result: 23, + }, + Case { + requested_percent: 55, + examined_base_number: 40, + expected_result: 22, + }, + ] + .into_iter() + .for_each(|case| { + let result = PurePercentage::try_from(u8::try_from(case.requested_percent).unwrap()) + .unwrap() + .of(case.examined_base_number); + assert_eq!( + result, case.expected_result, + "For {} percent and number {} the expected result was {} but we got {}", + case.requested_percent, case.examined_base_number, case.expected_result, result + ) + }) } #[test] - fn should_be_rounded_down_works_for_last_but_one_digit() { + fn should_be_rounded_to_works_for_last_but_one_digit() { [ - (787879, false), - (1114545, true), - (100, true), - (49, true), - (50, false), + (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, + ), ] .into_iter() - .for_each(|(num, expected_result)| { - assert_eq!(Percentage::should_be_rounded_down(num), expected_result) - }) + .for_each( + |(num, expected_result_for_unsigned_base, expected_result_for_signed_base)| { + let result = PurePercentage::should_be_rounded_to(num, 100); + 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 + ); + let signed = num as i64 * -1; + let result = PurePercentage::should_be_rounded_to(signed, 100); + assert_eq!( + result, + expected_result_for_signed_base, + "Signed number {} was identified for rounding as {:?} but it should've been {:?}", + signed, + result, + expected_result_for_signed_base + ) + }, + ) } #[test] fn add_percent_to_works() { - let percentage = Percentage::new(13); + let subject = PurePercentage::try_from(13).unwrap(); - let result = percentage.add_percent_to(100); + let unsigned = subject.add_percent_to(100); + let signed = subject.add_percent_to(-100); - assert_eq!(result, 113) + assert_eq!(unsigned, 113); + assert_eq!(signed, -113) } #[test] - #[should_panic(expected = "Overflowed during addition of 1 per cent, that is \ + #[should_panic(expected = "Overflowed during addition of 1 percent, that is \ 184467440737095516, to 18446744073709551615 of type u64.")] fn add_percent_to_hits_overflow() { - let _ = Percentage::new(1).add_percent_to(u64::MAX); + let _ = PurePercentage::try_from(1) + .unwrap() + .add_percent_to(u64::MAX); } #[test] fn subtract_percent_from_works() { - let percentage = Percentage::new(55); + let subject = PurePercentage::try_from(55).unwrap(); - let result = percentage.subtract_percent_from(100); + let unsigned = subject.subtract_percent_from(100); + let signed = subject.subtract_percent_from(-100); - assert_eq!(result, 45) + assert_eq!(unsigned, 45); + assert_eq!(signed, -45) } #[test] fn preventing_early_upper_overflow() { - let quite_large_value = u64::MAX / 60; - // The standard algorythm begins by a multiplication with this 61, which would cause + // The standard algorithm begins by a multiplication with this 61, which would cause // an overflow, so for such large numbers like this one we switch the order of operations. - // We're gonna devide it by 100 first and multiple after it. (However, we'd lose some + // We're going to divide it by 100 first and multiple after it. (However, we'd lose some // precision in smaller numbers that same way). Why that much effort? I don't want to see - // an overflow happen where most people would't anticipate it: when going for a percentage - // from their number, implaying a request to receive another number, but always smaller - // than that passed in. - let result = Percentage::new(61).of(quite_large_value); + // an overflow happen where most people wouldn't anticipate it: when going for + // a PurePercentage from their number, implying a request to receive another number, but + // always smaller than that passed in. + let case_one = PurePercentage::try_from(61).unwrap().of(u64::MAX / 60); + // There is more going on under the hood, which shows better on the following example: + // if we divide 255 by 100, we get 2. Then multiplied by 30, it amounts to 60. The right + // result, though, is 77 (with an extra 1 from rounding). Therefor there is another + // piece of code whose charge is to treat the remainder of modulo 100 that is pushed off + // the scoped, and if ignored, it would cause the result to be undervalued. This remainder + // is again treated the by the primary (reversed) methodology with num * percents done + // first, followed by the final division, keeping just one hundredth. + let case_two = PurePercentage::try_from(30).unwrap().of(u8::MAX); + // We apply the rounding even here. That's why we'll see the result drop by one compared to + // the previous case. As 254 * 30 is 7620, the two least significant digits come rounded + // by 100 as 0 which means 7620 divided by 100 makes 76. + let case_three = PurePercentage::try_from(30).unwrap().of(u8::MAX - 1); + + 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. + } + + #[test] + fn zeroes_for_loose_percentage() { + assert_eq!(LoosePercentage::new(45).of(0).unwrap(), 0); + assert_eq!(LoosePercentage::new(0).of(33).unwrap(), 0) + } + + #[test] + fn loose_percentage_end_to_end_test_for_standard_values_unsigned() { + let expected_values = (0..=100).collect::>(); - let expected_result = (quite_large_value / 100) * 61; - assert_eq!(result, expected_result); + test_end_to_end(100, expected_values, |percent, base| { + LoosePercentage::new(percent as u32).of(base).unwrap() + }) + } + + #[test] + fn loose_percentage_end_to_end_test_for_standard_values_signed() { + let expected_values = (-100..=0).rev().collect::>(); + + test_end_to_end(-100, expected_values, |percent, base| { + LoosePercentage::new(percent as u32).of(base).unwrap() + }) + } + + const TEST_SET: [Case; 5] = [ + Case { + requested_percent: 101, + examined_base_number: 10000, + expected_result: 10100, + }, + Case { + requested_percent: 150, + examined_base_number: 900, + expected_result: 1350, + }, + Case { + requested_percent: 999, + examined_base_number: 10, + expected_result: 100, + }, + Case { + requested_percent: 1234567, + examined_base_number: 20, + expected_result: 12345 * 20 + (67 * 20 / 100), + }, + Case { + requested_percent: u32::MAX, + examined_base_number: 1, + expected_result: (u32::MAX / 100) as i64 + 1, + }, + ]; + + #[test] + fn loose_percentage_for_large_values_unsigned() { + TEST_SET.into_iter().for_each(|case| { + let result = LoosePercentage::new(case.requested_percent) + .of(case.examined_base_number) + .unwrap(); + assert_eq!( + result, case.expected_result, + "Expected {} does not match actual {}. Percents {} of base {}.", + case.expected_result, result, case.requested_percent, case.examined_base_number + ) + }) + } + + #[test] + fn loose_percentage_end_to_end_test_for_large_values_signed() { + TEST_SET + .into_iter() + .map(|mut case| { + case.examined_base_number *= -1; + case.expected_result *= -1; + case + }) + .for_each(|case| { + let result = LoosePercentage::new(case.requested_percent) + .of(case.examined_base_number) + .unwrap(); + assert_eq!( + result, case.expected_result, + "Expected {} does not match actual {}. Percents {} of base {}.", + case.expected_result, result, case.requested_percent, case.examined_base_number + ) + }) + } + + #[test] + fn loose_percentage_multiple_of_percent_hits_limit() { + let percents = (u8::MAX as u32 + 1) * 100; + let subject = LoosePercentage::new(percents); + + let result: Result = subject.of(1); + + assert_eq!(result, Err(BaseTypeOverflow {})) + } + + #[test] + fn loose_percentage_multiplying_input_number_hits_limit() { + let percents = 200; + let subject = LoosePercentage::new(percents); + + let result: Result = subject.of(u8::MAX); + + assert_eq!(result, Err(BaseTypeOverflow {})) } #[test] - #[should_panic( - expected = "internal error: entered unreachable code: Check to prevent numbers with fewer \ - than two digits failed" - )] - fn broken_code_for_violation_of_already_checked_range() { - let _ = Percentage::should_be_rounded_down(2); + fn loose_percentage_adding_portion_from_remainder_hits_limit() { + let percents = 101; + let subject = LoosePercentage::new(percents); + + let result: Result = subject.of(u8::MAX); + + assert_eq!(result, Err(BaseTypeOverflow {})) } } diff --git a/masq_lib/src/test_utils/utils.rs b/masq_lib/src/test_utils/utils.rs index 2fed96981..07908280f 100644 --- a/masq_lib/src/test_utils/utils.rs +++ b/masq_lib/src/test_utils/utils.rs @@ -76,7 +76,6 @@ pub fn to_millis(dur: &Duration) -> u64 { (dur.as_secs() * 1000) + (u64::from(dur.subsec_nanos()) / 1_000_000) } -#[cfg(not(feature = "no_test_share"))] pub struct MutexIncrementInset(pub usize); #[cfg(test)] diff --git a/multinode_integration_tests/.gitignore b/multinode_integration_tests/.gitignore new file mode 100644 index 000000000..e0264b089 --- /dev/null +++ b/multinode_integration_tests/.gitignore @@ -0,0 +1,5 @@ + +## File-based project format: +*.iws +*.iml +*.ipr \ No newline at end of file diff --git a/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh b/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh index 013f9a68d..28d960392 100755 --- a/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh +++ b/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh @@ -1,4 +1,5 @@ #!/bin/sh +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. # All wallets begin with null balances. The only exception is the contract owner wallet whose means are to be # redistributed from there to every account that would need it. (Notice the argument --account ' { - pub name: &'a str, +pub struct BlockchainServer { + pub name: String, } -impl<'a> UrlHolder for BlockchainServer<'a> { +impl UrlHolder for BlockchainServer { fn url(&self) -> String { format!("http://{}:18545", self.ip().unwrap().trim()) } } -impl<'a> BlockchainServer<'a> { +impl BlockchainServer { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + } + } pub fn start(&self) { - MASQNodeUtils::clean_up_existing_container(self.name); + MASQNodeUtils::clean_up_existing_container(&self.name); let ip_addr = IpAddr::V4(Ipv4Addr::new(172, 18, 1, 250)); let ip_addr_string = ip_addr.to_string(); let args = vec![ "run", "--detach", "--name", - self.name, + &self.name, "--ip", ip_addr_string.as_str(), "-p", @@ -43,7 +48,7 @@ impl<'a> BlockchainServer<'a> { "inspect", "-f", "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", - self.name, + &self.name, ]; let mut command = Command::new("docker", Command::strings(args)); command.stdout_or_stderr() @@ -58,8 +63,8 @@ impl<'a> BlockchainServer<'a> { } } -impl<'a> Drop for BlockchainServer<'a> { +impl Drop for BlockchainServer { fn drop(&mut self) { - MASQNodeUtils::stop(self.name); + MASQNodeUtils::stop(&self.name); } } diff --git a/multinode_integration_tests/src/masq_node_cluster.rs b/multinode_integration_tests/src/masq_node_cluster.rs index 86a94af54..8bf6892e4 100644 --- a/multinode_integration_tests/src/masq_node_cluster.rs +++ b/multinode_integration_tests/src/masq_node_cluster.rs @@ -5,8 +5,9 @@ use crate::masq_mock_node::{ MutableMASQMockNodeStarter, }; use crate::masq_node::{MASQNode, MASQNodeUtils}; -use crate::masq_real_node::MASQRealNode; use crate::masq_real_node::NodeStartupConfig; +use crate::masq_real_node::{MASQRealNode, PreparedNodeInfo}; +use crate::utils::{node_chain_specific_data_directory, open_all_file_permissions}; use masq_lib::blockchains::chains::Chain; use masq_lib::test_utils::utils::TEST_DEFAULT_MULTINODE_CHAIN; use node_lib::sub_lib::cryptde::PublicKey; @@ -14,6 +15,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs}; +use std::path::PathBuf; pub struct MASQNodeCluster { startup_configs: HashMap<(String, usize), NodeStartupConfig>, @@ -21,7 +23,7 @@ pub struct MASQNodeCluster { mock_nodes: HashMap, host_node_parent_dir: Option, next_index: usize, - pub chain: Chain, + chain: Chain, } impl MASQNodeCluster { @@ -50,15 +52,21 @@ impl MASQNodeCluster { self.next_index } - pub fn prepare_real_node(&mut self, config: &NodeStartupConfig) -> (String, usize) { + pub fn prepare_real_node(&mut self, config: &NodeStartupConfig) -> PreparedNodeInfo { let index = self.startup_configs.len() + 1; let name = MASQRealNode::make_name(index); self.next_index = index + 1; self.startup_configs .insert((name.clone(), index), config.clone()); - MASQRealNode::prepare(&name); + MASQRealNode::prepare_node_directories_for_docker(&name); + let db_path: PathBuf = node_chain_specific_data_directory(&name).into(); + open_all_file_permissions(&db_path); - (name, index) + PreparedNodeInfo { + node_docker_name: name, + index, + db_path, + } } pub fn start_real_node(&mut self, config: NodeStartupConfig) -> MASQRealNode { @@ -191,6 +199,10 @@ impl MASQNodeCluster { ) } + pub fn chain(&self) -> Chain { + self.chain + } + pub fn is_in_jenkins() -> bool { match env::var("HOST_NODE_PARENT_DIR") { Ok(ref value) if value.is_empty() => false, diff --git a/multinode_integration_tests/src/masq_real_node.rs b/multinode_integration_tests/src/masq_real_node.rs index 5170bc763..767d50e1b 100644 --- a/multinode_integration_tests/src/masq_real_node.rs +++ b/multinode_integration_tests/src/masq_real_node.rs @@ -30,7 +30,7 @@ use std::fmt::Display; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::SocketAddr; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use std::str::FromStr; use std::string::ToString; @@ -630,8 +630,8 @@ impl NodeStartupConfigBuilder { self } - pub fn blockchain_service_url(mut self, blockchain_service_url: String) -> Self { - self.blockchain_service_url = Some(blockchain_service_url); + pub fn blockchain_service_url(mut self, blockchain_service_url: &str) -> Self { + self.blockchain_service_url = Some(blockchain_service_url.to_string()); self } @@ -778,7 +778,7 @@ impl MASQNode for MASQRealNode { } impl MASQRealNode { - pub fn prepare(name: &str) { + pub fn prepare_node_directories_for_docker(name: &str) { Self::do_prepare_for_docker_run(name).unwrap(); } @@ -1221,6 +1221,13 @@ impl MASQRealNode { } } +#[derive(Debug)] +pub struct PreparedNodeInfo { + pub node_docker_name: String, + pub index: usize, + pub db_path: PathBuf, +} + #[derive(Debug, Clone)] struct CryptDENullPair { main: CryptDENull, diff --git a/multinode_integration_tests/src/neighborhood_constructor.rs b/multinode_integration_tests/src/neighborhood_constructor.rs index 3ab6158f5..907314802 100644 --- a/multinode_integration_tests/src/neighborhood_constructor.rs +++ b/multinode_integration_tests/src/neighborhood_constructor.rs @@ -70,7 +70,7 @@ where model_db.root().public_key().to_string().as_str(), )) .rate_pack(model_db.root().inner.rate_pack) - .chain(cluster.chain); + .chain(cluster.chain()); let config = modify_config(config_builder); let real_node = cluster.start_real_node(config); let (mock_node_map, adjacent_mock_node_keys) = @@ -203,7 +203,7 @@ fn form_mock_node_skeleton( let standard_gossip = StandardBuilder::new() .add_masq_node(&node, 1) .half_neighbors(node.main_public_key(), real_node.main_public_key()) - .chain_id(cluster.chain) + .chain_id(cluster.chain()) .build(); node.transmit_multinode_gossip(real_node, &standard_gossip) .unwrap(); diff --git a/multinode_integration_tests/src/utils.rs b/multinode_integration_tests/src/utils.rs index 12c63f1ec..1d4fa92f7 100644 --- a/multinode_integration_tests/src/utils.rs +++ b/multinode_integration_tests/src/utils.rs @@ -18,7 +18,7 @@ use node_lib::sub_lib::cryptde::{CryptData, PlainData}; use std::collections::BTreeSet; use std::io::{ErrorKind, Read, Write}; use std::net::TcpStream; -use std::path::PathBuf; +use std::path::Path; use std::time::{Duration, Instant}; use std::{io, thread}; @@ -111,7 +111,7 @@ pub fn wait_for_shutdown(stream: &mut TcpStream, timeout: &Duration) -> Result<( } } -pub fn open_all_file_permissions(dir: PathBuf) { +pub fn open_all_file_permissions(dir: &Path) { match Command::new( "chmod", Command::strings(vec!["-R", "777", dir.to_str().unwrap()]), diff --git a/multinode_integration_tests/tests/blockchain_interaction_test.rs b/multinode_integration_tests/tests/blockchain_interaction_test.rs index 1eb6a3ca7..101617282 100644 --- a/multinode_integration_tests/tests/blockchain_interaction_test.rs +++ b/multinode_integration_tests/tests/blockchain_interaction_test.rs @@ -1,7 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use std::ops::Add; -use std::path::PathBuf; use std::time::{Duration, SystemTime}; use log::Level; @@ -18,10 +17,7 @@ use multinode_integration_tests_lib::masq_real_node::{ ConsumingWalletInfo, NodeStartupConfigBuilder, }; use multinode_integration_tests_lib::mock_blockchain_client_server::MBCSBuilder; -use multinode_integration_tests_lib::utils::{ - config_dao, node_chain_specific_data_directory, open_all_file_permissions, receivable_dao, - UrlHolder, -}; +use multinode_integration_tests_lib::utils::{config_dao, receivable_dao, UrlHolder}; use node_lib::accountant::db_access_objects::utils::CustomQuery; use node_lib::sub_lib::wallet::Wallet; @@ -68,20 +64,18 @@ fn debtors_are_credited_once_but_not_twice() { let node_config = NodeStartupConfigBuilder::standard() .log_level(Level::Debug) .scans(false) - .blockchain_service_url(blockchain_client_server.url()) + .blockchain_service_url(&blockchain_client_server.url()) .ui_port(ui_port) .build(); - let (node_name, node_index) = cluster.prepare_real_node(&node_config); - let chain_specific_dir = node_chain_specific_data_directory(&node_name); - open_all_file_permissions(PathBuf::from(chain_specific_dir)); + let (docker_id, _) = cluster.prepare_real_node(&node_config); { - let config_dao = config_dao(&node_name); + let config_dao = config_dao(&docker_id.node_docker_name); config_dao .set("start_block", Some("1000".to_string())) .unwrap(); } { - let receivable_dao = receivable_dao(&node_name); + let receivable_dao = receivable_dao(&docker_id.node_docker_name); receivable_dao .more_money_receivable( SystemTime::UNIX_EPOCH.add(Duration::from_secs(15_000_000)), @@ -92,7 +86,7 @@ fn debtors_are_credited_once_but_not_twice() { } // Use the receivable DAO to verify that the receivable's balance has been initialized { - let receivable_dao = receivable_dao(&node_name); + let receivable_dao = receivable_dao(&docker_id.node_docker_name); let receivable_accounts = receivable_dao .custom_query(CustomQuery::RangeQuery { min_age_s: 0, @@ -107,13 +101,14 @@ fn debtors_are_credited_once_but_not_twice() { } // Use the config DAO to verify that the start block has been set to 1000 { - let config_dao = config_dao(&node_name); + let config_dao = config_dao(&docker_id.node_docker_name); assert_eq!( config_dao.get("start_block").unwrap().value_opt.unwrap(), "1000" ); } - let node = cluster.start_named_real_node(&node_name, node_index, node_config); + let node = + cluster.start_named_real_node(&docker_id.node_docker_name, docker_id.index, node_config); let ui_client = node.make_ui(ui_port); // Command a scan log ui_client.send_request( @@ -129,7 +124,7 @@ fn debtors_are_credited_once_but_not_twice() { node.kill_node(); // Use the receivable DAO to verify that the receivable's balance has been adjusted { - let receivable_dao = receivable_dao(&node_name); + let receivable_dao = receivable_dao(&docker_id.node_docker_name); let receivable_accounts = receivable_dao .custom_query(CustomQuery::RangeQuery { min_age_s: 0, @@ -144,7 +139,7 @@ fn debtors_are_credited_once_but_not_twice() { } // Use the config DAO to verify that the start block has been advanced to 2001 { - let config_dao = config_dao(&node_name); + let config_dao = config_dao(&docker_id.node_docker_name); assert_eq!( config_dao.get("start_block").unwrap().value_opt.unwrap(), "2001" diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index fb19a283f..a250b8ed3 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -1,62 +1,38 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use bip39::{Language, Mnemonic, Seed}; -use futures::Future; -use itertools::Either; + +use crate::verify_bill_payment_utils::utils::{ + test_body, to_wei, AssertionsValues, Debt, DebtsSpecs, FinalServiceFeeBalancesByServingNodes, + NodeProfile, Ports, TestInputsBuilder, WholesomeConfig, +}; +use itertools::Itertools; use masq_lib::blockchains::chains::Chain; use masq_lib::messages::FromMessageBody; use masq_lib::messages::ToMessageBody; use masq_lib::messages::{ScanType, UiScanRequest, UiScanResponse}; -use masq_lib::percentage::Percentage; -use masq_lib::utils::{derivation_path, find_free_port, NeighborhoodModeLight}; -use multinode_integration_tests_lib::blockchain::BlockchainServer; +use masq_lib::percentage::PurePercentage; +use masq_lib::utils::find_free_port; use multinode_integration_tests_lib::masq_node::MASQNode; use multinode_integration_tests_lib::masq_node_cluster::MASQNodeCluster; -use multinode_integration_tests_lib::masq_real_node::{ - ConsumingWalletInfo, EarningWalletInfo, MASQRealNode, NodeStartupConfig, - NodeStartupConfigBuilder, -}; -use multinode_integration_tests_lib::utils::{ - node_chain_specific_data_directory, open_all_file_permissions, UrlHolder, -}; -use node_lib::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoReal}; -use node_lib::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoReal}; -use node_lib::accountant::db_access_objects::utils::to_time_t; -use node_lib::accountant::gwei_to_wei; -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::BlockchainInterface; -use node_lib::database::db_initializer::{ - DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, -}; +use multinode_integration_tests_lib::masq_real_node::{MASQRealNode, NodeStartupConfigBuilder}; use node_lib::sub_lib::accountant::PaymentThresholds; -use node_lib::sub_lib::blockchain_interface_web3::{ - compute_gas_limit, transaction_data_web3, web3_gas_limit_const_part, -}; -use node_lib::sub_lib::wallet::Wallet; -use node_lib::test_utils; -use rusqlite::ToSql; -use rustc_hex::{FromHex, ToHex}; +use node_lib::sub_lib::blockchain_interface_web3::{compute_gas_limit, web3_gas_limit_const_part}; use std::convert::TryFrom; -use std::path::{Path, PathBuf}; -use std::time::{Duration, Instant, SystemTime}; -use std::{thread, u128}; -use tiny_hderive::bip32::ExtendedPrivKey; -use web3::transports::Http; -use web3::types::{Address, Bytes, SignedTransaction, TransactionParameters, TransactionRequest}; -use web3::Web3; +use std::time::Duration; +use std::u128; + +mod verify_bill_payment_utils; #[test] -fn verify_bill_payment() { +fn full_payments_were_processed_for_sufficient_balances() { // Note: besides the main objectives of this test, it relies on (and so it proves) the premise - // that each Node, after it reaches its full connectivity and becomes able to make a route, - // activates its accountancy module whereas it also unleashes the first cycle of the scanners - // immediately. That's why some consideration has been made not to take out the passage with - // the intense startups of a bunch of Nodes, with that particular reason to fulfill the above - // depicted scenario, even though this test could be written more simply with the use of - // the `scans` command emitted from a UI, with a smaller PCU burden. (You may want to know that - // such an approach is implemented for another test in this file.) + // 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 + // an excuse hopefully good enough not to take out the passage in this test with the intense + // startup of a bunch of real Nodes, with the only purpose of fulfilling the conditions required + // for going through that above depicted sequence of events. That said, this test could've been + // written simpler with an emulated UI and its `scans` command, lowering the CPU burden. + // (You may be pleased to know that such an approach is implemented for another test in this + // file.) let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_500_000, debt_threshold_gwei: 1_000_000_000, @@ -65,98 +41,115 @@ fn verify_bill_payment() { permanent_debt_allowed_gwei: 10_000_000, unban_below_gwei: 10_000_000, }; - let owed_to_serving_node_1_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei) + 123_456; - let owed_to_serving_node_2_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei) + 456_789; - let owed_to_serving_node_3_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei) + 789_012; - let cons_node_initial_service_fee_balance_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei) * 4; - let exp_final_cons_node_service_fee_balance_minor = cons_node_initial_service_fee_balance_minor - - (owed_to_serving_node_1_minor - + owed_to_serving_node_2_minor - + owed_to_serving_node_3_minor); - let test_global_config = TestInputsOutputsConfig { - ui_ports_opt: None, - cons_node_initial_transaction_fee_balance_minor_opt: None, - cons_node_initial_service_fee_balance_minor, - debts_config: Either::Left(SimpleSimulatedDebts { + let debt_threshold_wei = to_wei(payment_thresholds.debt_threshold_gwei); + let owed_to_serving_node_1_minor = debt_threshold_wei + 123_456; + 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() + .consuming_node_initial_service_fee_balance_minor( + consuming_node_initial_service_fee_balance_minor, + ) + .debts_config(set_old_debts( + [ + owed_to_serving_node_1_minor, + owed_to_serving_node_2_minor, + owed_to_serving_node_3_minor, + ], + &payment_thresholds, + )) + .payment_thresholds_all_nodes(payment_thresholds) + .build(); + let debts_total = + owed_to_serving_node_1_minor + owed_to_serving_node_2_minor + owed_to_serving_node_3_minor; + let final_consuming_node_service_fee_balance_minor = + consuming_node_initial_service_fee_balance_minor - debts_total; + let assertions_values = AssertionsValues { + final_consuming_node_transaction_fee_balance_minor: to_wei(999_842_470), + final_consuming_node_service_fee_balance_minor, + final_service_fee_balances_by_serving_nodes: FinalServiceFeeBalancesByServingNodes::new( owed_to_serving_node_1_minor, owed_to_serving_node_2_minor, owed_to_serving_node_3_minor, - }), - payment_thresholds_all_nodes: payment_thresholds, - cons_node_transaction_fee_agreed_unit_price_opt: None, - exp_final_cons_node_transaction_fee_balance_minor: 999_842_470_000_000_000, - exp_final_cons_node_service_fee_balance_minor, - exp_final_service_fee_balance_serv_node_1_minor: owed_to_serving_node_1_minor, - exp_final_service_fee_balance_serv_node_2_minor: owed_to_serving_node_2_minor, - exp_final_service_fee_balance_serv_node_3_minor: owed_to_serving_node_3_minor, + ), }; - let stimulate_payments = - |cluster: &mut MASQNodeCluster, - real_consuming_node: &MASQRealNode, - _global_test_config: &TestInputsOutputsConfig| { - for _ in 0..6 { - cluster.start_real_node( - NodeStartupConfigBuilder::standard() - .chain(Chain::Dev) - .neighbor(real_consuming_node.node_reference()) - .build(), - ); - } - }; + test_body( + test_inputs, + assertions_values, + stimulate_consuming_node_to_pay_for_test_with_sufficient_funds, + activating_serving_nodes_for_test_with_sufficient_funds, + ); +} - let start_serving_nodes_and_run_check = - |cluster: &mut MASQNodeCluster, - serving_node_1_attributes: ServingNodeAttributes, - serving_node_2_attributes: ServingNodeAttributes, - serving_node_3_attributes: ServingNodeAttributes, - _test_global_config: &TestInputsOutputsConfig| { - let serving_node_1 = cluster.start_named_real_node( - &serving_node_1_attributes.name, - serving_node_1_attributes.index, - serving_node_1_attributes.config, - ); - let serving_node_2 = cluster.start_named_real_node( - &serving_node_2_attributes.name, - serving_node_2_attributes.index, - serving_node_2_attributes.config, - ); - let serving_node_3 = cluster.start_named_real_node( - &serving_node_3_attributes.name, - serving_node_3_attributes.index, - serving_node_3_attributes.config, - ); - for _ in 0..6 { - cluster.start_real_node( - NodeStartupConfigBuilder::standard() - .chain(Chain::Dev) - .neighbor(serving_node_1.node_reference()) - .neighbor(serving_node_2.node_reference()) - .neighbor(serving_node_3.node_reference()) - .build(), - ); - } +fn stimulate_consuming_node_to_pay_for_test_with_sufficient_funds( + cluster: &mut MASQNodeCluster, + real_consuming_node: &MASQRealNode, + _wholesome_config: &WholesomeConfig, +) { + for _ in 0..6 { + cluster.start_real_node( + NodeStartupConfigBuilder::standard() + .chain(Chain::Dev) + .neighbor(real_consuming_node.node_reference()) + .build(), + ); + } +} - (serving_node_1, serving_node_2, serving_node_3) - }; +fn activating_serving_nodes_for_test_with_sufficient_funds( + cluster: &mut MASQNodeCluster, + wholesome_values: &WholesomeConfig, +) -> [MASQRealNode; 3] { + let (node_references, serving_nodes): (Vec<_>, Vec<_>) = wholesome_values + .serving_nodes + .iter() + .map(|attributes| { + let node_id = &attributes.common.prepared_node; + cluster.start_named_real_node( + &node_id.node_docker_name, + node_id.index, + attributes + .common + .startup_config_opt + .borrow_mut() + .take() + .unwrap(), + ) + }) + .map(|node| (node.node_reference(), node)) + .unzip(); + let auxiliary_node_config = node_references + .into_iter() + .fold( + NodeStartupConfigBuilder::standard().chain(Chain::Dev), + |builder, serving_node_reference| builder.neighbor(serving_node_reference), + ) + .build(); - test_body( - test_global_config, - stimulate_payments, - start_serving_nodes_and_run_check, - ); + // Should be enough additional Nodes to provide the full connectivity + for _ in 0..3 { + let _ = cluster.start_real_node(auxiliary_node_config.clone()); + } + + serving_nodes.try_into().unwrap() +} + +fn set_old_debts( + owed_money_to_serving_nodes: [u128; 3], + payment_thresholds: &PaymentThresholds, +) -> DebtsSpecs { + let quite_long_ago = + payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec + 1; + let debts = owed_money_to_serving_nodes + .into_iter() + .map(|balance_minor| Debt::new(balance_minor, quite_long_ago)) + .collect_vec(); + DebtsSpecs::new(debts[0], debts[1], debts[2]) } #[test] fn payments_were_adjusted_due_to_insufficient_balances() { - let consuming_node_ui_port = find_free_port(); - let serving_node_1_ui_port = find_free_port(); - let serving_node_2_ui_port = find_free_port(); - let serving_node_3_ui_port = find_free_port(); let payment_thresholds = PaymentThresholds { threshold_interval_sec: 2_500_000, debt_threshold_gwei: 100_000_000, @@ -165,842 +158,159 @@ fn payments_were_adjusted_due_to_insufficient_balances() { permanent_debt_allowed_gwei: 10_000_000, unban_below_gwei: 1_000_000, }; - let owed_to_serv_node_1_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei + 5_000_000); - let owed_to_serv_node_2_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei + 20_000_000); - let owed_to_serv_node_3_minor = - gwei_to_wei::(payment_thresholds.debt_threshold_gwei + 60_000_000); // Assuming all Nodes rely on the same set of payment thresholds - let cons_node_initial_service_fee_balance_minor = (owed_to_serv_node_1_minor - + owed_to_serv_node_2_minor) - - gwei_to_wei::(2_345_678); - let agreed_transaction_fee_unit_price_major = 60; - let transaction_fee_needed_to_pay_for_one_payment_major = { - // We will need less but this should be accurate enough - let txn_data_with_maximized_cost = [0xff; 68]; + 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. + 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 consuming_node_initial_service_fee_balance_minor = + enough_balance_for_serving_node_1_and_2 - to_wei(2_345_678); + 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 + // all nonzero bytes in the data in both txs, which represents maximized costs + let txn_data_with_maximized_costs = [0xff; 68]; let gas_limit_dev_chain = { let const_part = web3_gas_limit_const_part(Chain::Dev); u64::try_from(compute_gas_limit( const_part, - txn_data_with_maximized_cost.as_slice(), + txn_data_with_maximized_costs.as_slice(), )) .unwrap() }; - let transaction_fee_margin = Percentage::new(15); - transaction_fee_margin - .add_percent_to(gas_limit_dev_chain * agreed_transaction_fee_unit_price_major) + let transaction_fee_margin = PurePercentage::try_from(15).unwrap(); + transaction_fee_margin.add_percent_to(gas_limit_dev_chain * gas_price_major) }; - eprintln!( - "Computed transaction fee: {}", - transaction_fee_needed_to_pay_for_one_payment_major - ); - let cons_node_transaction_fee_balance_minor = - 2 * gwei_to_wei::(transaction_fee_needed_to_pay_for_one_payment_major); - let test_global_config = TestInputsOutputsConfig { - ui_ports_opt: Some(Ports { - consuming_node: consuming_node_ui_port, - serving_node_1: serving_node_1_ui_port, - serving_node_2: serving_node_2_ui_port, - serving_node_3: serving_node_3_ui_port, - }), + const AFFORDABLE_PAYMENTS_COUNT: u128 = 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( + find_free_port(), + find_free_port(), + find_free_port(), + find_free_port(), + )) // Should be enough only for two payments, the least significant one will fall out - cons_node_initial_transaction_fee_balance_minor_opt: Some( - cons_node_transaction_fee_balance_minor + 1, - ), - cons_node_initial_service_fee_balance_minor, - debts_config: Either::Right(FullySpecifiedSimulatedDebts { + .consuming_node_initial_tx_fee_balance_minor(consuming_node_transaction_fee_balance_minor) + .consuming_node_initial_service_fee_balance_minor( + consuming_node_initial_service_fee_balance_minor, + ) + .debts_config(DebtsSpecs::new( // This account will be the most significant and will deserve the full balance - owed_to_serving_node_1: AccountedDebt { - balance_minor: owed_to_serv_node_1_minor, - age_s: payment_thresholds.maturity_threshold_sec + 1000, - }, + Debt::new( + owed_to_serv_node_1_minor, + payment_thresholds.maturity_threshold_sec + 1000, + ), // This balance is of a middle size it will be reduced as there won't be enough // after the first one is filled up. - owed_to_serving_node_2: AccountedDebt { - balance_minor: owed_to_serv_node_2_minor, - age_s: payment_thresholds.maturity_threshold_sec + 100_000, - }, - // This account will be the least significant and therefore eliminated - owed_to_serving_node_3: AccountedDebt { - balance_minor: owed_to_serv_node_3_minor, - age_s: payment_thresholds.maturity_threshold_sec + 30_000, - }, - }), - payment_thresholds_all_nodes: payment_thresholds, - cons_node_transaction_fee_agreed_unit_price_opt: Some( - agreed_transaction_fee_unit_price_major, - ), - // It seems like the ganache server sucked up quite less than those 55_000 units of gas?? - exp_final_cons_node_transaction_fee_balance_minor: 2_828_352_000_000_001, + Debt::new( + owed_to_serv_node_2_minor, + payment_thresholds.maturity_threshold_sec + 100_000, + ), + // This account will be the least significant, therefore eliminated due to tx fee + Debt::new( + 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(); + + let assertions_values = AssertionsValues { + // How much is left after the smart contract was successfully executed, those three payments + final_consuming_node_transaction_fee_balance_minor: to_wei(2_828_352), // Zero reached, because the algorithm is designed to exhaust the wallet completely - exp_final_cons_node_service_fee_balance_minor: 0, + final_consuming_node_service_fee_balance_minor: 0, // This account was granted with the full size as its lowest balance from the set makes // it weight the most - exp_final_service_fee_balance_serv_node_1_minor: owed_to_serv_node_1_minor, - exp_final_service_fee_balance_serv_node_2_minor: owed_to_serv_node_2_minor - - gwei_to_wei::(2_345_678), - // This account dropped out from the payment, so received no money - exp_final_service_fee_balance_serv_node_3_minor: 0, - }; - - let process_scan_request_to_node = - |real_node: &MASQRealNode, ui_port: u16, scan_type: ScanType, context_id: u64| { - let ui_client = real_node.make_ui(ui_port); - ui_client.send_request(UiScanRequest { scan_type }.tmb(context_id)); - let response = ui_client.wait_for_response(context_id, Duration::from_secs(10)); - UiScanResponse::fmb(response).unwrap(); - }; - - let stimulate_payments = - |_cluster: &mut MASQNodeCluster, - real_consuming_node: &MASQRealNode, - _global_test_config: &TestInputsOutputsConfig| { - process_scan_request_to_node( - &real_consuming_node, - consuming_node_ui_port, - ScanType::Payables, - 1111, - ) - }; - - let start_serving_nodes_and_run_check = |cluster: &mut MASQNodeCluster, - serving_node_1_attributes: ServingNodeAttributes, - serving_node_2_attributes: ServingNodeAttributes, - serving_node_3_attributes: ServingNodeAttributes, - test_global_config: &TestInputsOutputsConfig| - -> (MASQRealNode, MASQRealNode, MASQRealNode) { - let ports = test_global_config.ui_ports_opt.as_ref().unwrap(); - let mut vec: Vec = vec![ - (serving_node_1_attributes, ports.serving_node_1, 2222), - (serving_node_2_attributes, ports.serving_node_2, 3333), - (serving_node_3_attributes, ports.serving_node_3, 4444), - ] - .into_iter() - .map(|(serving_node_attributes, ui_port, context_id)| { - let serving_node = cluster.start_named_real_node( - &serving_node_attributes.name, - serving_node_attributes.index, - serving_node_attributes.config, - ); - - process_scan_request_to_node(&serving_node, ui_port, ScanType::Receivables, context_id); - - serving_node - }) - .collect(); - (vec.remove(0), vec.remove(0), vec.remove(0)) + final_service_fee_balances_by_serving_nodes: FinalServiceFeeBalancesByServingNodes::new( + owed_to_serv_node_1_minor, + owed_to_serv_node_2_minor - to_wei(2_345_678), + // This account dropped out from the payment, so received no money + 0, + ), }; test_body( - test_global_config, - stimulate_payments, - start_serving_nodes_and_run_check, + test_inputs, + assertions_values, + stimulate_consuming_node_to_pay_for_test_with_insufficient_funds, + activating_serving_nodes_for_test_with_insufficient_funds, ); } -fn make_init_config(chain: Chain) -> DbInitializationConfig { - DbInitializationConfig::create_or_migrate(ExternalData::new( - chain, - NeighborhoodModeLight::Standard, - None, - )) -} - -fn deploy_smart_contract(wallet: &Wallet, web3: &Web3, chain: Chain) -> Address { - let data = "608060405234801561001057600080fd5b5060038054600160a060020a031916331790819055604051600160a060020a0391909116906000907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a3610080336b01866de34549d620d8000000640100000000610b9461008582021704565b610156565b600160a060020a038216151561009a57600080fd5b6002546100b490826401000000006109a461013d82021704565b600255600160a060020a0382166000908152602081905260409020546100e790826401000000006109a461013d82021704565b600160a060020a0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b60008282018381101561014f57600080fd5b9392505050565b610c6a806101656000396000f3006080604052600436106100fb5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde038114610100578063095ea7b31461018a57806318160ddd146101c257806323b872dd146101e95780632ff2e9dc14610213578063313ce56714610228578063395093511461025357806342966c681461027757806370a0823114610291578063715018a6146102b257806379cc6790146102c75780638da5cb5b146102eb5780638f32d59b1461031c57806395d89b4114610331578063a457c2d714610346578063a9059cbb1461036a578063dd62ed3e1461038e578063f2fde38b146103b5575b600080fd5b34801561010c57600080fd5b506101156103d6565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561014f578181015183820152602001610137565b50505050905090810190601f16801561017c5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561019657600080fd5b506101ae600160a060020a0360043516602435610436565b604080519115158252519081900360200190f35b3480156101ce57600080fd5b506101d7610516565b60408051918252519081900360200190f35b3480156101f557600080fd5b506101ae600160a060020a036004358116906024351660443561051c565b34801561021f57600080fd5b506101d76105b9565b34801561023457600080fd5b5061023d6105c9565b6040805160ff9092168252519081900360200190f35b34801561025f57600080fd5b506101ae600160a060020a03600435166024356105ce565b34801561028357600080fd5b5061028f60043561067e565b005b34801561029d57600080fd5b506101d7600160a060020a036004351661068b565b3480156102be57600080fd5b5061028f6106a6565b3480156102d357600080fd5b5061028f600160a060020a0360043516602435610710565b3480156102f757600080fd5b5061030061071e565b60408051600160a060020a039092168252519081900360200190f35b34801561032857600080fd5b506101ae61072d565b34801561033d57600080fd5b5061011561073e565b34801561035257600080fd5b506101ae600160a060020a0360043516602435610775565b34801561037657600080fd5b506101ae600160a060020a03600435166024356107c0565b34801561039a57600080fd5b506101d7600160a060020a03600435811690602435166107d6565b3480156103c157600080fd5b5061028f600160a060020a0360043516610801565b606060405190810160405280602481526020017f486f7420746865206e657720746f6b656e20796f75277265206c6f6f6b696e6781526020017f20666f720000000000000000000000000000000000000000000000000000000081525081565b600081158061044c575061044a33846107d6565b155b151561050557604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f55736520696e637265617365417070726f76616c206f7220646563726561736560448201527f417070726f76616c20746f2070726576656e7420646f75626c652d7370656e6460648201527f2e00000000000000000000000000000000000000000000000000000000000000608482015290519081900360a40190fd5b61050f838361081d565b9392505050565b60025490565b600160a060020a038316600090815260016020908152604080832033845290915281205482111561054c57600080fd5b600160a060020a0384166000908152600160209081526040808320338452909152902054610580908363ffffffff61089b16565b600160a060020a03851660009081526001602090815260408083203384529091529020556105af8484846108b2565b5060019392505050565b6b01866de34549d620d800000081565b601281565b6000600160a060020a03831615156105e557600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff6109a416565b336000818152600160209081526040808320600160a060020a0389168085529083529281902085905580519485525191937f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929081900390910190a350600192915050565b61068833826109b6565b50565b600160a060020a031660009081526020819052604090205490565b6106ae61072d565b15156106b957600080fd5b600354604051600091600160a060020a0316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a36003805473ffffffffffffffffffffffffffffffffffffffff19169055565b61071a8282610a84565b5050565b600354600160a060020a031690565b600354600160a060020a0316331490565b60408051808201909152600381527f484f540000000000000000000000000000000000000000000000000000000000602082015281565b6000600160a060020a038316151561078c57600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff61089b16565b60006107cd3384846108b2565b50600192915050565b600160a060020a03918216600090815260016020908152604080832093909416825291909152205490565b61080961072d565b151561081457600080fd5b61068881610b16565b6000600160a060020a038316151561083457600080fd5b336000818152600160209081526040808320600160a060020a03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b600080838311156108ab57600080fd5b5050900390565b600160a060020a0383166000908152602081905260409020548111156108d757600080fd5b600160a060020a03821615156108ec57600080fd5b600160a060020a038316600090815260208190526040902054610915908263ffffffff61089b16565b600160a060020a03808516600090815260208190526040808220939093559084168152205461094a908263ffffffff6109a416565b600160a060020a038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008282018381101561050f57600080fd5b600160a060020a03821615156109cb57600080fd5b600160a060020a0382166000908152602081905260409020548111156109f057600080fd5b600254610a03908263ffffffff61089b16565b600255600160a060020a038216600090815260208190526040902054610a2f908263ffffffff61089b16565b600160a060020a038316600081815260208181526040808320949094558351858152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35050565b600160a060020a0382166000908152600160209081526040808320338452909152902054811115610ab457600080fd5b600160a060020a0382166000908152600160209081526040808320338452909152902054610ae8908263ffffffff61089b16565b600160a060020a038316600090815260016020908152604080832033845290915290205561071a82826109b6565b600160a060020a0381161515610b2b57600080fd5b600354604051600160a060020a038084169216907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a36003805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a0392909216919091179055565b600160a060020a0382161515610ba957600080fd5b600254610bbc908263ffffffff6109a416565b600255600160a060020a038216600090815260208190526040902054610be8908263ffffffff6109a416565b600160a060020a0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a350505600a165627a7a72305820d4ad56dfe541fec48c3ecb02cebad565a998dfca7774c0c4f4b1f4a8e2363a590029".from_hex::>().unwrap(); - let gas_price = 50_000_000_000_u64; - let gas_limit = 1_000_000_u64; - let tx = TransactionParameters { - nonce: Some(ethereum_types::U256::try_from(0).expect("Internal error")), - to: None, - gas: ethereum_types::U256::try_from(gas_limit).expect("Internal error"), - gas_price: Some(ethereum_types::U256::try_from(gas_price).expect("Internal error")), - value: ethereum_types::U256::zero(), - data: Bytes(data), - chain_id: Some(chain.rec().num_chain_id), - }; - let signed_tx = 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)) => Address { - 0: tx_receipt.contract_address.unwrap().0, - }, - 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, +fn stimulate_consuming_node_to_pay_for_test_with_insufficient_funds( + _cluster: &mut MASQNodeCluster, + real_consuming_node: &MASQRealNode, + wholesome_config: &WholesomeConfig, ) { - let data = transaction_data_web3(to_wallet, amount_minor); - let gas_price = 150_000_000_000_u64; - let gas_limit = 1_000_000_u64; - let tx = TransactionParameters { - nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), - to: Some(contract_addr), - gas: ethereum_types::U256::try_from(gas_limit).expect("Internal error"), - gas_price: Some(ethereum_types::U256::try_from(gas_price).expect("Internal error")), - value: ethereum_types::U256::zero(), - data: Bytes(data.to_vec()), - chain_id: Some(chain.rec().num_chain_id), - }; - - let signed_tx = 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 sign_transaction( - web3: &Web3, - tx: TransactionParameters, - signing_wallet: &Wallet, -) -> SignedTransaction { - web3.accounts() - .sign_transaction( - tx, - &signing_wallet - .prepare_secp256k1_secret() - .expect("wallet without 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 gas_price = 150_000_000_000_u64; - let gas_limit = 1_000_000_u64; - let tx = TransactionRequest { - from: from_wallet.address(), - to: Some(to_wallet.address()), - gas: Some(ethereum_types::U256::try_from(gas_limit).expect("Internal error")), - gas_price: Some(ethereum_types::U256::try_from(gas_price).expect("Internal error")), - 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: &BlockchainInterfaceWeb3, - expected_eth_balance: u128, - expected_token_balance: u128, -) { - let eth_balance = blockchain_interface - .lower_interface() - .get_transaction_fee_balance(&wallet) - .unwrap_or_else(|_| panic!("Failed to retrieve gas balance for {}", wallet)); - assert_eq!( - eth_balance, - web3::types::U256::from(expected_eth_balance), - "Actual EthBalance {} doesn't much with expected {} for {}", - eth_balance, - expected_eth_balance, - wallet - ); - let token_balance = blockchain_interface - .lower_interface() - .get_service_fee_balance(&wallet) - .unwrap_or_else(|_| panic!("Failed to retrieve masq balance for {}", wallet)); - assert_eq!( - token_balance, - web3::types::U256::from(expected_token_balance), - "Actual TokenBalance {} doesn't match with expected {} for {}", - token_balance, - expected_token_balance, - wallet - ); -} - -fn make_node_wallet(seed: &Seed, derivation_path: &str) -> (Wallet, String) { - let extended_priv_key = ExtendedPrivKey::derive(&seed.as_ref(), derivation_path).unwrap(); - let secret = extended_priv_key.secret().to_hex::(); - - ( - Wallet::from(Bip32EncryptionKeyProvider::from_key(extended_priv_key)), - secret, + process_scan_request_to_node( + &real_consuming_node, + wholesome_config + .consuming_node + .node_profile + .ui_port() + .expect("UI port missing"), + ScanType::Payables, + 1111, ) } -fn make_seed() -> Seed { - let phrase = "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle lamp absent write kind term toddler sphere ripple idle dragon curious hold"; - let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); - let seed = Seed::new(&mnemonic, ""); - seed -} - -fn build_config( - server_url_holder: &dyn UrlHolder, - seed: &Seed, - payment_thresholds: PaymentThresholds, - transaction_fee_agreed_price_per_unit_opt: Option, - wallet_derivation_path: String, - port_opt: Option, -) -> (NodeStartupConfig, Wallet) { - let (node_wallet, node_secret) = make_node_wallet(seed, wallet_derivation_path.as_str()); - let cfg_to_build = NodeStartupConfigBuilder::standard() - .blockchain_service_url(server_url_holder.url()) - .chain(Chain::Dev) - .payment_thresholds(payment_thresholds) - .consuming_wallet_info(ConsumingWalletInfo::PrivateKey(node_secret)) - .earning_wallet_info(EarningWalletInfo::Address(format!( - "{}", - node_wallet.clone() - ))); - let cfg_to_build = if let Some(port) = port_opt { - cfg_to_build.ui_port(port) - } else { - cfg_to_build - }; - let cfg_to_build = if let Some(price) = transaction_fee_agreed_price_per_unit_opt { - cfg_to_build.gas_price(price) - } else { - cfg_to_build - }; - let config = cfg_to_build.build(); - (config, node_wallet) -} - -fn expire_payables( - path: PathBuf, - debts_config: &Either, - now: SystemTime, - serving_node_1_wallet: &Wallet, - serving_node_2_wallet: &Wallet, - serving_node_3_wallet: &Wallet, -) { - let conn = DbInitializerReal::default() - .initialize(&path, DbInitializationConfig::panic_on_migration()) - .unwrap(); - match debts_config { - Either::Left(_) => { - let _ = conn - .prepare( - "update payable set last_paid_timestamp = 0 where pending_payable_rowid is null", - ) - .unwrap() - .execute([]) - .unwrap(); - } - Either::Right(fully_specified_config) => vec![ - ( - serving_node_1_wallet, - fully_specified_config.owed_to_serving_node_1.age_s, - ), - ( - serving_node_2_wallet, - fully_specified_config.owed_to_serving_node_2.age_s, - ), - ( - serving_node_3_wallet, - fully_specified_config.owed_to_serving_node_3.age_s, - ), - ] +fn activating_serving_nodes_for_test_with_insufficient_funds( + cluster: &mut MASQNodeCluster, + wholesome_config: &WholesomeConfig, +) -> [MASQRealNode; 3] { + let real_nodes: Vec<_> = wholesome_config + .serving_nodes .iter() - .for_each(|(wallet, age_s)| { - let time_t = to_time_t(now.checked_sub(Duration::from_secs(*age_s)).unwrap()); - conn.prepare("update payable set last_paid_timestamp = ? where wallet_address = ?") - .unwrap() - .execute(&[&time_t as &dyn ToSql, &(wallet.to_string())]) + .enumerate() + .map(|(idx, serving_node_attributes)| { + let node_config = serving_node_attributes + .common + .startup_config_opt + .borrow_mut() + .take() .unwrap(); - }), - } - - let mut config_stmt = conn - .prepare("update config set value = '0' where name = 'start_block'") - .unwrap(); - config_stmt.execute([]).unwrap(); -} - -fn expire_receivables(path: PathBuf) { - let conn = DbInitializerReal::default() - .initialize(&path, DbInitializationConfig::panic_on_migration()) - .unwrap(); - let mut statement = conn - .prepare("update receivable set last_received_timestamp = 0") - .unwrap(); - statement.execute([]).unwrap(); - - let mut config_stmt = conn - .prepare("update config set value = '0' where name = 'start_block'") - .unwrap(); - config_stmt.execute([]).unwrap(); -} - -struct TestInputsOutputsConfig { - ui_ports_opt: Option, - // The contract owner wallet is populated with 100 ETH as defined in the set of commands - // with which we start up the Ganache server. - // - // Specify number of wei this account should possess at its initialisation. - // The consuming node gets the full balance of the contract owner if left as None. - // Cannot ever get more than what the "owner" has. - cons_node_initial_transaction_fee_balance_minor_opt: Option, - cons_node_initial_service_fee_balance_minor: u128, - debts_config: Either, - payment_thresholds_all_nodes: PaymentThresholds, - cons_node_transaction_fee_agreed_unit_price_opt: Option, - - exp_final_cons_node_transaction_fee_balance_minor: u128, - exp_final_cons_node_service_fee_balance_minor: u128, - exp_final_service_fee_balance_serv_node_1_minor: u128, - exp_final_service_fee_balance_serv_node_2_minor: u128, - exp_final_service_fee_balance_serv_node_3_minor: u128, -} - -enum NodeByRole { - ConsNode, - ServNode1, - ServNode2, - ServNode3, -} - -struct SimpleSimulatedDebts { - owed_to_serving_node_1_minor: u128, - owed_to_serving_node_2_minor: u128, - owed_to_serving_node_3_minor: u128, -} - -struct FullySpecifiedSimulatedDebts { - owed_to_serving_node_1: AccountedDebt, - owed_to_serving_node_2: AccountedDebt, - owed_to_serving_node_3: AccountedDebt, -} + let common = &serving_node_attributes.common; + let serving_node = cluster.start_named_real_node( + &common.prepared_node.node_docker_name, + common.prepared_node.index, + node_config, + ); + let ui_port = serving_node_attributes + .serving_node_profile + .ui_port() + .expect("ui port missing"); -struct AccountedDebt { - balance_minor: u128, - age_s: u64, -} + process_scan_request_to_node( + &serving_node, + ui_port, + ScanType::Receivables, + (idx * 111) as u64, + ); -impl TestInputsOutputsConfig { - fn port(&self, requested: NodeByRole) -> Option { - self.ui_ports_opt.as_ref().map(|ports| match requested { - NodeByRole::ConsNode => ports.consuming_node, - NodeByRole::ServNode1 => ports.serving_node_1, - NodeByRole::ServNode2 => ports.serving_node_2, - NodeByRole::ServNode3 => ports.serving_node_3, + serving_node }) - } - - fn debt_size(&self, requested: NodeByRole) -> u128 { - match self.debts_config.as_ref() { - Either::Left(simple_config) => match requested { - NodeByRole::ServNode1 => simple_config.owed_to_serving_node_1_minor, - NodeByRole::ServNode2 => simple_config.owed_to_serving_node_2_minor, - NodeByRole::ServNode3 => simple_config.owed_to_serving_node_3_minor, - NodeByRole::ConsNode => panic!( - "Version simple: These configs are \ - serve to set owed to the consuming node, while that one should not be here." - ), - }, - Either::Right(fully_specified) => match requested { - NodeByRole::ServNode1 => fully_specified.owed_to_serving_node_1.balance_minor, - NodeByRole::ServNode2 => fully_specified.owed_to_serving_node_2.balance_minor, - NodeByRole::ServNode3 => fully_specified.owed_to_serving_node_3.balance_minor, - NodeByRole::ConsNode => panic!( - "Version fully specified: These configs \ - are serve to set owed to the consuming node, while that one should not \ - be here." - ), - }, - } - } -} - -struct Ports { - consuming_node: u16, - serving_node_1: u16, - serving_node_2: u16, - serving_node_3: u16, -} - -struct ServingNodeAttributes { - name: String, - index: usize, - config: NodeStartupConfig, + .collect(); + real_nodes.try_into().unwrap() } -fn test_body( - global_config: TestInputsOutputsConfig, - stimulate_consuming_node_to_pay: StimulateConsumingNodePayments, - start_serving_nodes_and_run_check: StartServingNodesAndLetThemPerformReceivablesCheck, -) where - StimulateConsumingNodePayments: - FnOnce(&mut MASQNodeCluster, &MASQRealNode, &TestInputsOutputsConfig), - StartServingNodesAndLetThemPerformReceivablesCheck: - FnOnce( - &mut MASQNodeCluster, - ServingNodeAttributes, - ServingNodeAttributes, - ServingNodeAttributes, - &TestInputsOutputsConfig, - ) -> (MASQRealNode, MASQRealNode, MASQRealNode), -{ - let mut cluster = match MASQNodeCluster::start() { - Ok(cluster) => cluster, - Err(e) => panic!("{}", e), - }; - let blockchain_server = BlockchainServer { - name: "ganache-cli", - }; - blockchain_server.start(); - blockchain_server.wait_until_ready(); - let url = blockchain_server.url().to_string(); - let (event_loop_handle, http) = Http::with_max_parallel(&url, REQUESTS_IN_PARALLEL).unwrap(); - let web3 = Web3::new(http.clone()); - let seed = make_seed(); - let (contract_owner_wallet, _) = make_node_wallet(&seed, &derivation_path(0, 0)); - let contract_addr = deploy_smart_contract(&contract_owner_wallet, &web3, cluster.chain); - assert_eq!( - contract_addr, - cluster.chain.rec().contract, - "Ganache is not as predictable as we thought: Update blockchain_interface::MULTINODE_CONTRACT_ADDRESS with {:?}", - contract_addr - ); - let blockchain_interface = BlockchainInterfaceWeb3::new(http, event_loop_handle, cluster.chain); - let (consuming_config, consuming_node_wallet) = build_config( - &blockchain_server, - &seed, - global_config.payment_thresholds_all_nodes, - global_config.cons_node_transaction_fee_agreed_unit_price_opt, - derivation_path(0, 1), - global_config.port(NodeByRole::ConsNode), - ); - eprintln!( - "Consuming node wallet established: {}\n", - consuming_node_wallet - ); - let initial_transaction_fee_balance = global_config - .cons_node_initial_transaction_fee_balance_minor_opt - .unwrap_or(1_000_000_000_000_000_000); - transfer_transaction_fee_amount_to_address( - &contract_owner_wallet, - &consuming_node_wallet, - initial_transaction_fee_balance, - 1, - &web3, - ); - transfer_service_fee_amount_to_address( - contract_addr, - &contract_owner_wallet, - &consuming_node_wallet, - global_config.cons_node_initial_service_fee_balance_minor, - 2, - &web3, - cluster.chain, - ); - assert_balances( - &consuming_node_wallet, - &blockchain_interface, - global_config - .cons_node_initial_transaction_fee_balance_minor_opt - .unwrap_or(1_000_000_000_000_000_000), - global_config.cons_node_initial_service_fee_balance_minor, - ); - let (serving_node_1_config, serving_node_1_wallet) = build_config( - &blockchain_server, - &seed, - global_config.payment_thresholds_all_nodes, - None, - derivation_path(0, 2), - global_config.port(NodeByRole::ServNode1), - ); - eprintln!( - "First serving node wallet established: {}\n", - serving_node_1_wallet - ); - let (serving_node_2_config, serving_node_2_wallet) = build_config( - &blockchain_server, - &seed, - global_config.payment_thresholds_all_nodes, - None, - derivation_path(0, 3), - global_config.port(NodeByRole::ServNode2), - ); - eprintln!( - "Second serving node wallet established: {}\n", - serving_node_2_wallet - ); - let (serving_node_3_config, serving_node_3_wallet) = build_config( - &blockchain_server, - &seed, - global_config.payment_thresholds_all_nodes, - None, - derivation_path(0, 4), - global_config.port(NodeByRole::ServNode3), - ); - eprintln!( - "Third serving node wallet established: {}\n", - serving_node_3_wallet - ); - let (consuming_node_name, consuming_node_index) = cluster.prepare_real_node(&consuming_config); - let consuming_node_path = node_chain_specific_data_directory(&consuming_node_name); - let consuming_node_connection = DbInitializerReal::default() - .initialize( - Path::new(&consuming_node_path), - make_init_config(cluster.chain), - ) - .unwrap(); - let consuming_node_payable_dao = PayableDaoReal::new(consuming_node_connection); - open_all_file_permissions(consuming_node_path.clone().into()); - assert_eq!( - format!("{}", &consuming_node_wallet), - "0x7a3cf474962646b18666b5a5be597bb0af013d81" - ); - assert_eq!( - format!("{}", &serving_node_1_wallet), - "0x0bd8bc4b8aba5d8abf13ea78a6668ad0e9985ad6" - ); - assert_eq!( - format!("{}", &serving_node_2_wallet), - "0xb329c8b029a2d3d217e71bc4d188e8e1a4a8b924" - ); - assert_eq!( - format!("{}", &serving_node_3_wallet), - "0xb45a33ef3e3097f34c826369b74141ed268cdb5a" - ); - let now = SystemTime::now(); - consuming_node_payable_dao - .more_money_payable( - now, - &serving_node_1_wallet, - global_config.debt_size(NodeByRole::ServNode1), - ) - .unwrap(); - consuming_node_payable_dao - .more_money_payable( - now, - &serving_node_2_wallet, - global_config.debt_size(NodeByRole::ServNode2), - ) - .unwrap(); - consuming_node_payable_dao - .more_money_payable( - now, - &serving_node_3_wallet, - global_config.debt_size(NodeByRole::ServNode3), - ) - .unwrap(); - let (serving_node_1_name, serving_node_1_index) = - cluster.prepare_real_node(&serving_node_1_config); - let serving_node_1_path = node_chain_specific_data_directory(&serving_node_1_name); - let serving_node_1_connection = DbInitializerReal::default() - .initialize( - Path::new(&serving_node_1_path), - make_init_config(cluster.chain), - ) - .unwrap(); - let serving_node_1_receivable_dao = ReceivableDaoReal::new(serving_node_1_connection); - serving_node_1_receivable_dao - .more_money_receivable( - SystemTime::now(), - &consuming_node_wallet, - global_config.debt_size(NodeByRole::ServNode1), - ) - .unwrap(); - open_all_file_permissions(serving_node_1_path.clone().into()); - let (serving_node_2_name, serving_node_2_index) = - cluster.prepare_real_node(&serving_node_2_config); - let serving_node_2_path = node_chain_specific_data_directory(&serving_node_2_name); - let serving_node_2_connection = DbInitializerReal::default() - .initialize( - Path::new(&serving_node_2_path), - make_init_config(cluster.chain), - ) - .unwrap(); - let serving_node_2_receivable_dao = ReceivableDaoReal::new(serving_node_2_connection); - serving_node_2_receivable_dao - .more_money_receivable( - SystemTime::now(), - &consuming_node_wallet, - global_config.debt_size(NodeByRole::ServNode2), - ) - .unwrap(); - open_all_file_permissions(serving_node_2_path.clone().into()); - let (serving_node_3_name, serving_node_3_index) = - cluster.prepare_real_node(&serving_node_3_config); - let serving_node_3_path = node_chain_specific_data_directory(&serving_node_3_name); - let serving_node_3_connection = DbInitializerReal::default() - .initialize( - Path::new(&serving_node_3_path), - make_init_config(cluster.chain), - ) - .unwrap(); - let serving_node_3_receivable_dao = ReceivableDaoReal::new(serving_node_3_connection); - serving_node_3_receivable_dao - .more_money_receivable( - SystemTime::now(), - &consuming_node_wallet, - global_config.debt_size(NodeByRole::ServNode3), - ) - .unwrap(); - open_all_file_permissions(serving_node_3_path.clone().into()); - expire_payables( - consuming_node_path.into(), - &global_config.debts_config, - now, - &serving_node_1_wallet, - &serving_node_2_wallet, - &serving_node_3_wallet, - ); - expire_receivables(serving_node_1_path.into()); - expire_receivables(serving_node_2_path.into()); - expire_receivables(serving_node_3_path.into()); - assert_balances(&serving_node_1_wallet, &blockchain_interface, 0, 0); - assert_balances(&serving_node_2_wallet, &blockchain_interface, 0, 0); - assert_balances(&serving_node_3_wallet, &blockchain_interface, 0, 0); - let real_consuming_node = - cluster.start_named_real_node(&consuming_node_name, consuming_node_index, consuming_config); - - stimulate_consuming_node_to_pay(&mut cluster, &real_consuming_node, &global_config); - - let now = Instant::now(); - while !consuming_node_payable_dao.non_pending_payables().is_empty() - && now.elapsed() < Duration::from_secs(10) - { - thread::sleep(Duration::from_millis(400)); - } - assert_balances( - &consuming_node_wallet, - &blockchain_interface, - global_config.exp_final_cons_node_transaction_fee_balance_minor, - global_config.exp_final_cons_node_service_fee_balance_minor, - ); - assert_balances( - &serving_node_1_wallet, - &blockchain_interface, - 0, - global_config.exp_final_service_fee_balance_serv_node_1_minor, - ); - assert_balances( - &serving_node_2_wallet, - &blockchain_interface, - 0, - global_config.exp_final_service_fee_balance_serv_node_2_minor, - ); - assert_balances( - &serving_node_3_wallet, - &blockchain_interface, - 0, - global_config.exp_final_service_fee_balance_serv_node_3_minor, - ); - let serving_node_1_attributes = ServingNodeAttributes { - name: serving_node_1_name.to_string(), - index: serving_node_1_index, - config: serving_node_1_config, - }; - let serving_node_2_attributes = ServingNodeAttributes { - name: serving_node_2_name.to_string(), - index: serving_node_2_index, - config: serving_node_2_config, - }; - let serving_node_3_attributes = ServingNodeAttributes { - name: serving_node_3_name.to_string(), - index: serving_node_3_index, - config: serving_node_3_config, - }; - start_serving_nodes_and_run_check( - &mut cluster, - serving_node_1_attributes, - serving_node_2_attributes, - serving_node_3_attributes, - &global_config, - ); - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_1_receivable_dao.account_status(&consuming_node_wallet) { - status.balance_wei - == i128::try_from( - global_config.debt_size(NodeByRole::ServNode1) - - global_config.exp_final_service_fee_balance_serv_node_1_minor, - ) - .unwrap() - } else { - false - } - }); - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_2_receivable_dao.account_status(&consuming_node_wallet) { - status.balance_wei - == i128::try_from( - global_config.debt_size(NodeByRole::ServNode2) - - global_config.exp_final_service_fee_balance_serv_node_2_minor, - ) - .unwrap() - } else { - false - } - }); - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_3_receivable_dao.account_status(&consuming_node_wallet) { - status.balance_wei - == i128::try_from( - global_config.debt_size(NodeByRole::ServNode3) - - global_config.exp_final_service_fee_balance_serv_node_3_minor, - ) - .unwrap() - } else { - false - } - }); +fn process_scan_request_to_node( + real_node: &MASQRealNode, + ui_port: u16, + scan_type: ScanType, + context_id: u64, +) { + let ui_client = real_node.make_ui(ui_port); + ui_client.send_request(UiScanRequest { scan_type }.tmb(context_id)); + let response = ui_client.wait_for_response(context_id, Duration::from_secs(10)); + UiScanResponse::fmb(response).expect("Scan request went wrong"); } diff --git a/multinode_integration_tests/tests/verify_bill_payment_utils/mod.rs b/multinode_integration_tests/tests/verify_bill_payment_utils/mod.rs new file mode 100644 index 000000000..583a5129a --- /dev/null +++ b/multinode_integration_tests/tests/verify_bill_payment_utils/mod.rs @@ -0,0 +1,3 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod utils; diff --git a/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs b/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs new file mode 100644 index 000000000..18eee8ae7 --- /dev/null +++ b/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs @@ -0,0 +1,1003 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use bip39::{Language, Mnemonic, Seed}; +use futures::Future; +use itertools::Itertools; +use lazy_static::lazy_static; +use masq_lib::blockchains::chains::Chain; +use masq_lib::utils::{derivation_path, NeighborhoodModeLight}; +use multinode_integration_tests_lib::blockchain::BlockchainServer; +use multinode_integration_tests_lib::masq_node_cluster::MASQNodeCluster; +use multinode_integration_tests_lib::masq_real_node::{ + ConsumingWalletInfo, EarningWalletInfo, MASQRealNode, NodeStartupConfig, + NodeStartupConfigBuilder, PreparedNodeInfo, +}; +use multinode_integration_tests_lib::utils::{open_all_file_permissions, UrlHolder}; +use node_lib::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoReal}; +use node_lib::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoReal}; +use node_lib::accountant::gwei_to_wei; +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::BlockchainInterface; +use node_lib::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, +}; +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 rustc_hex::{FromHex, ToHex}; +use std::cell::RefCell; +use std::fs::File; +use std::io::Read; +use std::path::Path; +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::Web3; + +pub type StimulateConsumingNodePayments = fn(&mut MASQNodeCluster, &MASQRealNode, &WholesomeConfig); + +pub type StartServingNodesAndLetThemPerformReceivablesCheck = + fn(&mut MASQNodeCluster, &WholesomeConfig) -> [MASQRealNode; 3]; + +pub fn test_body( + test_inputs: TestInputs, + assertions_values: AssertionsValues, + stimulate_consuming_node_to_pay: StimulateConsumingNodePayments, + start_serving_nodes_and_activate_their_accountancy: StartServingNodesAndLetThemPerformReceivablesCheck, +) { + // 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 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 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(), + ); + + stimulate_consuming_node_to_pay(&mut cluster, &real_consuming_node, &wholesome_config); + + let timeout_start = Instant::now(); + while !wholesome_config + .consuming_node + .payable_dao + .non_pending_payables() + .is_empty() + && timeout_start.elapsed() < Duration::from_secs(10) + { + thread::sleep(Duration::from_millis(400)); + } + wholesome_config.assert_payments_via_direct_blockchain_scanning(&assertions_values); + + let _ = start_serving_nodes_and_activate_their_accountancy( + &mut cluster, + // So that individual Configs can be pulled out and used + &wholesome_config, + ); + + 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 { + // The contract owner wallet is populated with 100 ETH as defined in the set of commands with + // which we start up the Ganache server. + // + // This specifies number of wei this account should possess at its initialisation. + // The consuming node gets the full balance of the contract owner if left as None. Cannot ever + // get more than what the "owner" has. + payment_thresholds_all_nodes: PaymentThresholds, + node_profiles: NodeProfiles, +} + +#[derive(Default)] +pub struct TestInputsBuilder { + 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, +} + +impl TestInputsBuilder { + pub fn ui_ports(mut self, ports: Ports) -> Self { + self.ui_ports_opt = Some(ports); + self + } + + pub fn consuming_node_initial_tx_fee_balance_minor(mut self, balance: u128) -> Self { + self.consuming_node_initial_tx_fee_balance_minor_opt = Some(balance); + self + } + + pub fn consuming_node_initial_service_fee_balance_minor(mut self, balance: u128) -> Self { + self.consuming_node_initial_service_fee_balance_minor_opt = Some(balance); + self + } + + pub fn debts_config(mut self, debts: DebtsSpecs) -> Self { + self.debts_config_opt = Some(debts); + self + } + + pub fn payment_thresholds_all_nodes(mut self, thresholds: PaymentThresholds) -> Self { + self.payment_thresholds_all_nodes_opt = Some(thresholds); + self + } + + pub fn consuming_node_gas_price_major(mut self, gas_price: u64) -> Self { + self.consuming_node_gas_price_opt = Some(gas_price); + self + } + + pub fn build(self) -> TestInputs { + let mut debts = self + .debts_config_opt + .expect("You forgot providing a mandatory input: debts config") + .debts + .to_vec(); + let (consuming_node_ui_port_opt, serving_nodes_ui_ports_opt) = + Self::resolve_ports(self.ui_ports_opt); + 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, + 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 + .expect("Mandatory input not provided: consuming node initial service fee balance"), + }; + let mut serving_nodes = [ + ServingNodeByName::ServingNode1, + ServingNodeByName::ServingNode2, + ServingNodeByName::ServingNode3, + ] + .into_iter() + .map(|serving_node_by_name| { + let debt = debts.remove(0); + let ui_port_opt = serving_nodes_ui_ports_opt.remove(0); + ServingNodeProfile { + serving_node_by_name, + debt, + ui_port_opt, + } + }) + .collect::>(); + let node_profiles = NodeProfiles { + consuming_node, + serving_nodes: core::array::from_fn(|_| serving_nodes.remove(0)), + }; + + TestInputs { + payment_thresholds_all_nodes: self + .payment_thresholds_all_nodes_opt + .expect("Mandatory input not provided: payment thresholds"), + node_profiles, + } + } + + 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)); + (Some(ui_ports.consuming_node), serving_nodes_array) + } + None => Default::default(), + } + } +} + +struct NodeProfiles { + consuming_node: ConsumingNodeProfile, + serving_nodes: [ServingNodeProfile; 3], +} + +#[derive(Debug, Clone)] +pub struct ConsumingNodeProfile { + ui_port_opt: Option, + gas_price_opt: Option, + initial_tx_fee_balance_minor_opt: Option, + initial_service_fee_balance_minor: u128, +} + +#[derive(Debug, Clone)] +pub struct ServingNodeProfile { + serving_node_by_name: ServingNodeByName, + debt: Debt, + ui_port_opt: Option, +} + +pub struct AssertionsValues { + pub final_consuming_node_transaction_fee_balance_minor: u128, + pub final_consuming_node_service_fee_balance_minor: u128, + pub final_service_fee_balances_by_serving_nodes: FinalServiceFeeBalancesByServingNodes, +} + +pub struct FinalServiceFeeBalancesByServingNodes { + balances: [u128; 3], +} + +impl FinalServiceFeeBalancesByServingNodes { + pub fn new(node_1: u128, node_2: u128, node_3: u128) -> Self { + let balances = [node_1, node_2, node_3]; + Self { balances } + } +} + +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, +} + +pub struct GlobalValues { + pub test_inputs: TestInputs, + pub blockchain_params: BlockchainParams, + pub now_in_common: SystemTime, + blockchain_interfaces: BlockchainInterfaces, +} + +pub struct WholesomeConfig { + pub global_values: GlobalValues, + pub consuming_node: ConsumingNode, + pub serving_nodes: [ServingNode; 3], +} + +pub struct DebtsSpecs { + debts: [Debt; 3], +} + +impl DebtsSpecs { + pub fn new(node_1: Debt, node_2: Debt, node_3: Debt) -> Self { + let debts = [node_1, node_2, node_3]; + Self { debts } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Debt { + pub balance_minor: u128, + pub age_s: u64, +} + +impl Debt { + pub fn new(balance_minor: u128, age_s: u64) -> Self { + Self { + balance_minor, + age_s, + } + } + + fn proper_timestamp(&self, now: SystemTime) -> SystemTime { + now.checked_sub(Duration::from_secs(self.age_s)).unwrap() + } +} + +pub trait NodeProfile { + fn ui_port(&self) -> Option; + + fn debt_specs(&self) -> Debt; + + fn derivation_path(&self) -> String; + + fn name(&self) -> String; + + fn gas_price_opt(&self) -> Option; +} + +impl NodeProfile for ConsumingNodeProfile { + fn ui_port(&self) -> Option { + self.ui_port_opt + } + + fn debt_specs(&self) -> Debt { + panic!("This method should be called only by the serving Nodes.") + } + + fn derivation_path(&self) -> String { + derivation_path(0, 1) + } + + fn name(&self) -> String { + "ConsumingNode".to_string() + } + + fn gas_price_opt(&self) -> Option { + self.gas_price_opt + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ServingNodeByName { + ServingNode1 = 1, + ServingNode2 = 2, + ServingNode3 = 3, +} + +impl NodeProfile for ServingNodeProfile { + fn ui_port(&self) -> Option { + self.ui_port_opt + } + + fn debt_specs(&self) -> Debt { + self.debt + } + + fn derivation_path(&self) -> String { + derivation_path(0, (self.serving_node_by_name as usize + 1) as u8) + } + + fn name(&self) -> String { + format!("{:?}", self.serving_node_by_name) + } + + fn gas_price_opt(&self) -> Option { + None + } +} + +pub fn establish_test_frame( + test_inputs: TestInputs, +) -> (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 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 global_values = GlobalValues { + test_inputs, + blockchain_params, + blockchain_interfaces, + 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 make_seed() -> Seed { + let mnemonic = Mnemonic::from_phrase(MNEMONIC_PHRASE, Language::English).unwrap(); + Seed::new(&mnemonic, "") +} + +pub fn to_wei(gwei: u64) -> u128 { + gwei_to_wei(gwei) +} + +fn make_db_init_config(chain: Chain) -> DbInitializationConfig { + DbInitializationConfig::create_or_migrate(ExternalData::new( + chain, + NeighborhoodModeLight::Standard, + None, + )) +} + +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 mut data = String::new(); + file.read_to_string(&mut data).unwrap(); + let data = data + .chars() + .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 + ); +} + +fn make_node_wallet_and_private_key(seed: &Seed, derivation_path: &str) -> (Wallet, String) { + let extended_private_key = ExtendedPrivKey::derive(&seed.as_ref(), derivation_path).unwrap(); + let str_private_key: String = extended_private_key.secret().to_hex(); + let wallet = Wallet::from(Bip32EncryptionKeyProvider::from_key(extended_private_key)); + (wallet, str_private_key) +} + +impl GlobalValues { + fn get_node_config_and_wallet(&self, node: &dyn NodeProfile) -> (NodeStartupConfig, Wallet) { + let wallet_derivation_path = node.derivation_path(); + let payment_thresholds = self.test_inputs.payment_thresholds_all_nodes; + let (node_wallet, node_secret) = make_node_wallet_and_private_key( + &self.blockchain_params.seed, + wallet_derivation_path.as_str(), + ); + let mut config_builder = NodeStartupConfigBuilder::standard() + .blockchain_service_url(&self.blockchain_params.server_url) + .chain(Chain::Dev) + .payment_thresholds(payment_thresholds) + .consuming_wallet_info(ConsumingWalletInfo::PrivateKey(node_secret)) + .earning_wallet_info(EarningWalletInfo::Address(format!( + "{}", + node_wallet.clone() + ))); + if let Some(port) = node.ui_port() { + config_builder = config_builder.ui_port(port) + } + if let Some(gas_price) = node.gas_price_opt() { + config_builder = config_builder.gas_price(gas_price) + } + eprintln!("{} wallet established: {}\n", node.name(), node_wallet,); + (config_builder.build(), node_wallet) + } + + fn prepare_consuming_node( + &self, + cluster: &mut MASQNodeCluster, + blockchain_interfaces: &BlockchainInterfaces, + ) -> 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; + let initial_tx_fee_balance_opt = consuming_node_profile.initial_tx_fee_balance_minor_opt; + + 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, + ); + + assert_balances( + &consuming_node_wallet, + blockchain_interfaces.standard_blockchain_interface.as_ref(), + initial_transaction_fee_balance, + initial_service_fee_balance_minor, + ); + + let prepared_node = cluster.prepare_real_node(&consuming_node_config); + let consuming_node_connection = DbInitializerReal::default() + .initialize(&prepared_node.db_path, make_db_init_config(cluster.chain())) + .unwrap(); + let consuming_node_payable_dao = PayableDaoReal::new(consuming_node_connection); + open_all_file_permissions(&prepared_node.db_path); + ConsumingNode::new( + consuming_node_profile, + prepared_node, + consuming_node_config, + consuming_node_wallet, + consuming_node_payable_dao, + ) + } + + fn prepare_serving_nodes(&self, cluster: &mut MASQNodeCluster) -> [ServingNode; 3] { + self.test_inputs + .node_profiles + .serving_nodes + .clone() + .into_iter() + .map(|serving_node_profile: ServingNodeProfile| { + let (serving_node_config, serving_node_earning_wallet) = + self.get_node_config_and_wallet(&serving_node_profile); + let prepared_node_info = cluster.prepare_real_node(&serving_node_config); + let serving_node_connection = DbInitializerReal::default() + .initialize( + &prepared_node_info.db_path, + make_db_init_config(cluster.chain()), + ) + .unwrap(); + let serving_node_receivable_dao = ReceivableDaoReal::new(serving_node_connection); + open_all_file_permissions(&prepared_node_info.db_path); + ServingNode::new( + serving_node_profile, + prepared_node_info, + serving_node_config, + serving_node_earning_wallet, + serving_node_receivable_dao, + ) + }) + .collect::>() + .try_into() + .expect("failed to make [T;3] of provided Vec") + } + + fn set_start_block_to_zero(path: &Path) { + DbInitializerReal::default() + .initialize(path, DbInitializationConfig::panic_on_migration()) + .unwrap() + .prepare("update config set value = '0' where name = 'start_block'") + .unwrap() + .execute([]) + .unwrap(); + } + + fn set_up_serving_nodes_databases( + &self, + serving_nodes_array: &[ServingNode; 3], + consuming_node: &ConsumingNode, + ) { + let now = self.now_in_common; + serving_nodes_array.iter().for_each(|serving_node| { + let (balance, timestamp) = serving_node.debt_balance_and_timestamp(now); + serving_node + .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::set_start_block_to_zero(&serving_node.common.prepared_node.db_path) + }) + } + + fn set_up_consuming_node_db( + &self, + serving_nodes_array: &[ServingNode; 3], + consuming_node: &ConsumingNode, + ) { + let now = self.now_in_common; + serving_nodes_array.iter().for_each(|serving_node| { + let (balance, timestamp) = serving_node.debt_balance_and_timestamp(now); + consuming_node + .payable_dao + .more_money_payable(timestamp, &serving_node.earning_wallet, balance) + .unwrap(); + }); + Self::set_start_block_to_zero(&consuming_node.common.prepared_node.db_path) + } +} + +impl WholesomeConfig { + fn new( + global_values: GlobalValues, + consuming_node: ConsumingNode, + serving_nodes: [ServingNode; 3], + ) -> Self { + WholesomeConfig { + global_values, + consuming_node, + serving_nodes, + } + } + + fn assert_expected_wallet_addresses(&self) { + let consuming_node_actual = self.consuming_node.consuming_wallet.to_string(); + let consuming_node_expected = "0x7a3cf474962646b18666b5a5be597bb0af013d81"; + assert_eq!( + &consuming_node_actual, consuming_node_expected, + "Consuming Node's wallet {} mismatched with expected {}", + consuming_node_actual, consuming_node_expected + ); + vec![ + "0x0bd8bc4b8aba5d8abf13ea78a6668ad0e9985ad6", + "0xb329c8b029a2d3d217e71bc4d188e8e1a4a8b924", + "0xb45a33ef3e3097f34c826369b74141ed268cdb5a", + ] + .iter() + .zip(self.serving_nodes.iter()) + .for_each(|(expected_wallet_addr, serving_node)| { + let serving_node_actual = serving_node.earning_wallet.to_string(); + assert_eq!( + &serving_node_actual, + expected_wallet_addr, + "{:?} wallet {} mismatched with expected {}", + serving_node.serving_node_profile.serving_node_by_name, + serving_node_actual, + expected_wallet_addr + ); + }) + } + + 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( + &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, + ); + assertions_values + .final_service_fee_balances_by_serving_nodes + .balances + .into_iter() + .zip(self.serving_nodes.iter()) + .for_each(|(expected_remaining_owed_value, serving_node)| { + assert_balances( + &serving_node.earning_wallet, + blockchain_interface, + 0, + expected_remaining_owed_value, + ); + }) + } + + fn assert_serving_nodes_addressed_received_payments( + &self, + assertions_values: &AssertionsValues, + ) { + let actually_received_payments = assertions_values + .final_service_fee_balances_by_serving_nodes + .balances; + let consuming_node_wallet = &self.consuming_node.consuming_wallet; + self.serving_nodes + .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 expected_final_balance = original_debt - received_payment; + Self::wait_for_exact_balance_in_receivables( + &serving_node.receivable_dao, + expected_final_balance, + consuming_node_wallet, + ) + }) + } + + fn wait_for_exact_balance_in_receivables( + node_receivable_dao: &ReceivableDaoReal, + expected_value: u128, + consuming_node_wallet: &Wallet, + ) { + test_utils::wait_for(Some(1000), Some(15000), || { + if let Some(status) = node_receivable_dao.account_status(&consuming_node_wallet) { + status.balance_wei == i128::try_from(expected_value).unwrap() + } else { + false + } + }); + } +} + +pub const ONE_ETH_IN_WEI: u128 = 10_u128.pow(18); + +pub struct Ports { + consuming_node: u16, + serving_nodes: [u16; 3], +} + +impl Ports { + pub fn new( + consuming_node: u16, + serving_node_1: u16, + serving_node_2: u16, + serving_node_3: u16, + ) -> Self { + Self { + consuming_node, + serving_nodes: [serving_node_1, serving_node_2, serving_node_3], + } + } +} + +#[derive(Debug)] +pub struct NodeAttributesCommon { + pub prepared_node: PreparedNodeInfo, + pub startup_config_opt: RefCell>, +} + +impl NodeAttributesCommon { + fn new(prepared_node: PreparedNodeInfo, config: NodeStartupConfig) -> Self { + NodeAttributesCommon { + prepared_node, + startup_config_opt: RefCell::new(Some(config)), + } + } +} + +#[derive(Debug)] +pub struct ConsumingNode { + pub node_profile: ConsumingNodeProfile, + pub common: NodeAttributesCommon, + pub consuming_wallet: Wallet, + pub payable_dao: PayableDaoReal, +} + +#[derive(Debug)] +pub struct ServingNode { + pub serving_node_profile: ServingNodeProfile, + pub common: NodeAttributesCommon, + pub earning_wallet: Wallet, + pub receivable_dao: ReceivableDaoReal, +} + +impl ServingNode { + fn debt_balance_and_timestamp(&self, now: SystemTime) -> (u128, SystemTime) { + let debt_specs = self.serving_node_profile.debt_specs(); + (debt_specs.balance_minor, debt_specs.proper_timestamp(now)) + } +} + +impl ConsumingNode { + fn new( + node_profile: ConsumingNodeProfile, + prepared_node: PreparedNodeInfo, + config: NodeStartupConfig, + consuming_wallet: Wallet, + payable_dao: PayableDaoReal, + ) -> Self { + let common = NodeAttributesCommon::new(prepared_node, config); + Self { + node_profile, + common, + consuming_wallet, + payable_dao, + } + } +} + +impl ServingNode { + fn new( + serving_node_profile: ServingNodeProfile, + prepared_node: PreparedNodeInfo, + config: NodeStartupConfig, + earning_wallet: Wallet, + receivable_dao: ReceivableDaoReal, + ) -> Self { + let common = NodeAttributesCommon::new(prepared_node, config); + Self { + serving_node_profile, + common, + earning_wallet, + receivable_dao, + } + } +} diff --git a/node/Cargo.lock b/node/Cargo.lock index 3ed00beaf..16cddec4f 100644 --- a/node/Cargo.lock +++ b/node/Cargo.lock @@ -430,8 +430,8 @@ version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "unicode-xid 0.2.1", ] @@ -689,8 +689,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" dependencies = [ "convert_case", - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "rustc_version 0.3.3", "syn 1.0.85", ] @@ -880,8 +880,8 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", "synstructure", ] @@ -2540,8 +2540,8 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95af56fee93df76d721d356ac1ca41fccf168bc448eb14049234df764ba3e76" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", ] @@ -2630,9 +2630,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2654,11 +2654,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro2 1.0.59", + "proc-macro2 1.0.86", ] [[package]] @@ -3270,8 +3270,8 @@ version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", ] @@ -3315,8 +3315,8 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", ] @@ -3533,8 +3533,8 @@ version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "unicode-xid 0.2.1", ] @@ -3544,8 +3544,8 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", "unicode-xid 0.2.1", ] @@ -3637,8 +3637,8 @@ version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", ] @@ -4335,7 +4335,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae2faf80ac463422992abf4de234731279c058aaf33171ca70277c98406b124" dependencies = [ - "quote 1.0.28", + "quote 1.0.37", "syn 1.0.85", ] @@ -4415,8 +4415,8 @@ dependencies = [ "bumpalo", "lazy_static", "log 0.4.18", - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", "wasm-bindgen-shared", ] @@ -4439,7 +4439,7 @@ version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ - "quote 1.0.28", + "quote 1.0.37", "wasm-bindgen-macro-support", ] @@ -4449,8 +4449,8 @@ version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -4730,8 +4730,8 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65f1a51723ec88c66d5d1fe80c841f17f63587d6691901d66be9bec6c3b51f73" dependencies = [ - "proc-macro2 1.0.59", - "quote 1.0.28", + "proc-macro2 1.0.86", + "quote 1.0.37", "syn 1.0.85", "synstructure", ] diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index fcc48becf..03ff00504 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -384,12 +384,13 @@ impl ThresholdUtils { and the denominator must be less than or equal to 10^9. These restrictions do not seem overly strict for having .permanent_debt_allowed greater - than or equal to .debt_threshold_gwei would not make any sense and setting - .threshold_interval_sec over 10^9 would mean stretching out for debts across more than - 31 years. + than or equal to .debt_threshold_gwei would be silly (this is because the former one defines + the absolutely lowest point of the threshold curves) and setting .threshold_interval_sec + to more than 10^9 seconds would mean the user would allow for debts stretching out into 31 + years of age. - If payment_thresholds are ever configurable by the user, these validations should be done - on the values before they are accepted. + As long as the thresholds are configurable in a set, a validation should always be done on + some of these values before they are loaded in. */ (gwei_to_wei::(payment_thresholds.permanent_debt_allowed_gwei) diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 205bd8680..c9551598a 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -139,12 +139,12 @@ pub struct QualifiedPayableAccount { impl QualifiedPayableAccount { pub fn new( - qualified_as: PayableAccount, + bare_account: PayableAccount, payment_threshold_intercept_minor: u128, creditor_thresholds: CreditorThresholds, ) -> QualifiedPayableAccount { Self { - bare_account: qualified_as, + bare_account, payment_threshold_intercept_minor, creditor_thresholds, } @@ -172,9 +172,9 @@ pub struct CreditorThresholds { } impl CreditorThresholds { - pub fn new(permanent_debt_allowed_wei: u128) -> Self { + pub fn new(permanent_debt_allowed_minor: u128) -> Self { Self { - permanent_debt_allowed_minor: permanent_debt_allowed_wei, + permanent_debt_allowed_minor, } } } @@ -265,7 +265,7 @@ impl Handler for Accountant { msg: BlockchainAgentWithContextMessage, _ctx: &mut Self::Context, ) -> Self::Result { - self.handle_payable_payment_setup(msg) + self.send_outbound_payments_instructions(msg) } } @@ -703,48 +703,59 @@ impl Accountant { }) } - fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { + fn send_outbound_payments_instructions(&mut self, msg: BlockchainAgentWithContextMessage) { let response_skeleton_opt = msg.response_skeleton_opt; - let blockchain_bridge_instructions_opt = match self + if let Some(blockchain_bridge_instructions) = self.try_composing_instructions(msg) { + self.outbound_payments_instructions_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(blockchain_bridge_instructions) + .expect("BlockchainBridge is dead") + } else { + self.handle_obstruction(response_skeleton_opt) + } + } + + fn try_composing_instructions( + &mut self, + msg: BlockchainAgentWithContextMessage, + ) -> Option { + let successfully_processed = match self .scanners .payable .try_skipping_payment_adjustment(msg, &self.logger) { - Some(Either::Left(complete_msg)) => Some(complete_msg), - Some(Either::Right(unaccepted_msg)) => { - //TODO we will eventually query info from Neighborhood before the adjustment, according to GH-699 + Some(analysed) => analysed, + None => return None, + }; + + match successfully_processed { + Either::Left(prepared_msg_with_unadjusted_payables) => { + Some(prepared_msg_with_unadjusted_payables) + } + Either::Right(adjustment_order) => { + //TODO we will eventually query info from Neighborhood before the adjustment, + // according to GH-699, but probably with asynchronous messages that will be + // more in favour after GH-676 self.scanners .payable - .perform_payment_adjustment(unaccepted_msg, &self.logger) + .perform_payment_adjustment(adjustment_order, &self.logger) } - None => None, - }; + } + } + + fn handle_obstruction(&mut self, response_skeleton_opt: Option) { + self.scanners.payable.cancel_scan(&self.logger); - match blockchain_bridge_instructions_opt { - Some(blockchain_bridge_instructions) => self - .outbound_payments_instructions_sub_opt + if let Some(response_skeleton) = response_skeleton_opt { + self.ui_message_sub_opt .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(blockchain_bridge_instructions) - .expect("BlockchainBridge is dead"), - None => { - error!( - self.logger, - "Payable scanner could not finish. If matured payables stay untreated long, your \ - creditors may impose a ban on you" - ); - self.scanners.payable.mark_as_ended(&self.logger); - if let Some(response_skeleton) = response_skeleton_opt { - self.ui_message_sub_opt - .as_ref() - .expect("UI gateway unbound") - .try_send(NodeToUiMessage { - target: MessageTarget::ClientId(response_skeleton.client_id), - body: UiScanResponse {}.tmb(response_skeleton.context_id), - }) - .expect("UI gateway is dead") - } - } + .expect("UI gateway unbound") + .try_send(NodeToUiMessage { + target: MessageTarget::ClientId(response_skeleton.client_id), + body: UiScanResponse {}.tmb(response_skeleton.context_id), + }) + .expect("UI gateway is dead") } } @@ -1067,19 +1078,24 @@ 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::{ - Adjustment, AdjustmentAnalysis, PaymentAdjusterError, TransactionFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, 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::test_utils::protect_qualified_payables_in_test; use crate::accountant::scanners::BeginScanError; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_analyzed_account, - make_guaranteed_qualified_payables, make_non_guaranteed_qualified_payable, - make_payable_account, make_unqualified_and_qualified_payables, BannedDaoFactoryMock, + bc_from_earning_wallet, bc_from_wallets, make_meaningless_analyzed_account, + make_meaningless_qualified_payable, make_payable_account, + make_qualified_and_unqualified_payables, make_qualified_payables, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, NullScanner, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock, ScannerMock, @@ -1131,7 +1147,6 @@ mod tests { use masq_lib::ui_gateway::{ MessageBody, MessagePath, MessageTarget, NodeFromUiMessage, NodeToUiMessage, }; - use masq_lib::utils::convert_collection; use std::any::TypeId; use std::ops::{Add, Sub}; use std::sync::Arc; @@ -1166,23 +1181,23 @@ mod tests { let receivable_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let banned_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_factory_params_arc = Arc::new(Mutex::new(vec![])); - let payable_dao_factory = PayableDaoFactoryMock::new() + let payable_dao_factory = PayableDaoFactoryMock::default() .make_params(&payable_dao_factory_params_arc) .make_result(PayableDaoMock::new()) // For Accountant .make_result(PayableDaoMock::new()) // For Payable Scanner .make_result(PayableDaoMock::new()); // For PendingPayable Scanner - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::default() .make_params(&pending_payable_dao_factory_params_arc) .make_result(PendingPayableDaoMock::new()) // For Accountant .make_result(PendingPayableDaoMock::new()) // For Payable Scanner .make_result(PendingPayableDaoMock::new()); // For PendingPayable Scanner - let receivable_dao_factory = ReceivableDaoFactoryMock::new() + let receivable_dao_factory = ReceivableDaoFactoryMock::default() .make_params(&receivable_dao_factory_params_arc) .make_result(ReceivableDaoMock::new()) // For Accountant .make_result(ReceivableDaoMock::new()); // For Receivable Scanner let banned_dao_factory = BannedDaoFactoryMock::new() .make_params(&banned_dao_factory_params_arc) - .make_result(BannedDaoMock::new()); // For Receivable Scanner + .make_result(BannedDaoMock::default()); // For Receivable Scanner let config_dao_factory = ConfigDaoFactoryMock::new() .make_params(&config_dao_factory_params_arc) .make_result(ConfigDaoMock::new()); // For receivable scanner @@ -1415,7 +1430,7 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let expected_qualified_payables = - make_guaranteed_qualified_payables(vec![payable], &DEFAULT_PAYMENT_THRESHOLDS, now); + make_qualified_payables(vec![payable], &DEFAULT_PAYMENT_THRESHOLDS, now); assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { @@ -1477,12 +1492,9 @@ mod tests { #[test] fn received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge( ) { - // the numbers for balances don't do real math, they need not match either the condition for - // the payment adjustment or the actual values that come from the payable size reducing - // algorithm: all that is mocked in this test init_test_logging(); let test_name = "received_balances_and_qualified_payables_under_our_money_limit_thus_all_forwarded_to_blockchain_bridge"; - let search_for_indispensable_adjustment_params_arc = Arc::new(Mutex::new(vec![])); + let consider_adjustment_params_arc = Arc::new(Mutex::new(vec![])); let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let instructions_recipient = blockchain_bridge .system_stop_conditions(match_every_type_id!(OutboundPaymentsInstructions)) @@ -1492,16 +1504,20 @@ mod tests { let account_1 = make_payable_account(44_444); let account_2 = make_payable_account(333_333); let qualified_payables = vec![ - QualifiedPayableAccount::new(account_1.clone(), 2345, CreditorThresholds::new(1111)), - QualifiedPayableAccount::new(account_2.clone(), 6789, CreditorThresholds::new(2222)), + { + let mut qp = make_meaningless_qualified_payable(1234); + qp.bare_account = account_1.clone(); + qp + }, + { + let mut qp = make_meaningless_qualified_payable(6789); + qp.bare_account = account_2.clone(); + qp + }, ]; let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_params( - &search_for_indispensable_adjustment_params_arc, - ) - .search_for_indispensable_adjustment_result(Ok(Either::Left( - qualified_payables.clone(), - ))); + .consider_adjustment_params(&consider_adjustment_params_arc) + .consider_adjustment_result(Ok(Either::Left(qualified_payables.clone()))); let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); @@ -1512,11 +1528,10 @@ mod tests { let system = System::new("test"); let expected_agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(expected_agent_id_stamp); - + let protected_qualified_payables = + protect_qualified_payables_in_test(qualified_payables.clone()); let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_qualified_payables_in_test( - qualified_payables.clone(), - ), + protected_qualified_payables, agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1527,15 +1542,12 @@ mod tests { subject_addr.try_send(msg).unwrap(); system.run(); - let mut search_for_indispensable_adjustment_params = - search_for_indispensable_adjustment_params_arc - .lock() - .unwrap(); + let mut consider_adjustment_params = consider_adjustment_params_arc.lock().unwrap(); let (actual_qualified_payables, actual_agent_id_stamp) = - search_for_indispensable_adjustment_params.remove(0); + consider_adjustment_params.remove(0); assert_eq!(actual_qualified_payables, qualified_payables); assert_eq!(actual_agent_id_stamp, expected_agent_id_stamp); - assert!(search_for_indispensable_adjustment_params.is_empty()); + assert!(consider_adjustment_params.is_empty()); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let payments_instructions = blockchain_bridge_recording.get_record::(0); @@ -1560,9 +1572,7 @@ mod tests { #[test] fn received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge( ) { - // the numbers for balances don't do real math, they need not match either the condition for - // the payment adjustment or the actual values that come from the payable size reducing algorithm; - // all that is mocked in this test + // The numbers in balances, etc. don't do real math, the payment adjuster is mocked init_test_logging(); let test_name = "received_qualified_payables_exceeding_our_masq_balance_are_adjusted_before_forwarded_to_blockchain_bridge"; let adjust_payments_params_arc = Arc::new(Mutex::new(vec![])); @@ -1572,24 +1582,18 @@ mod tests { .start() .recipient(); let mut subject = AccountantBuilder::default().build(); - let unadjusted_account_1 = QualifiedPayableAccount::new( - make_payable_account(111_111), - 1234567, - CreditorThresholds::new(1111111), - ); - let unadjusted_account_2 = QualifiedPayableAccount::new( - make_payable_account(999_999), - 444555666, - CreditorThresholds::new(111111111), - ); - let adjusted_account_1 = PayableAccount { - balance_wei: gwei_to_wei(55_550_u64), - ..unadjusted_account_1.bare_account.clone() - }; - let adjusted_account_2 = PayableAccount { - balance_wei: gwei_to_wei(100_000_u64), - ..unadjusted_account_2.bare_account.clone() + 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), + ..unadjusted_account.bare_account.clone() + }; + (unadjusted_account, adjusted_account) }; + let (unadjusted_account_1, adjusted_account_1) = + prepare_unadjusted_and_adjusted_payable(12345678); + let (unadjusted_account_2, adjusted_account_2) = + prepare_unadjusted_and_adjusted_payable(33445566); let response_skeleton = ResponseSkeleton { client_id: 12, context_id: 55, @@ -1616,11 +1620,12 @@ mod tests { agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; - let analyzed_accounts = convert_collection(unadjusted_qualified_accounts.clone()); + let analyzed_accounts = + convert_qualified_into_analyzed_payables_in_test(unadjusted_qualified_accounts.clone()); let adjustment_analysis = - AdjustmentAnalysis::new(Adjustment::ByServiceFee, analyzed_accounts.clone()); + AdjustmentAnalysisReport::new(Adjustment::ByServiceFee, analyzed_accounts.clone()); let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_result(Ok(Either::Right(adjustment_analysis))) + .consider_adjustment_result(Ok(Either::Right(adjustment_analysis))) .adjust_payments_params(&adjust_payments_params_arc) .adjust_payments_result(Ok(payments_instructions)); let payable_scanner = PayableScannerBuilder::new() @@ -1634,11 +1639,9 @@ mod tests { subject_addr.try_send(payable_payments_setup_msg).unwrap(); - let before = SystemTime::now(); assert_eq!(system.run(), 0); - let after = SystemTime::now(); let mut adjust_payments_params = adjust_payments_params_arc.lock().unwrap(); - let (actual_prepared_adjustment, captured_now) = adjust_payments_params.remove(0); + let actual_prepared_adjustment = adjust_payments_params.remove(0); assert_eq!( actual_prepared_adjustment.adjustment_analysis.adjustment, Adjustment::ByServiceFee @@ -1651,13 +1654,6 @@ mod tests { actual_prepared_adjustment.agent.arbitrary_id_stamp(), agent_id_stamp_first_phase ); - assert!( - before <= captured_now && captured_now <= after, - "captured timestamp should have been between {:?} and {:?} but was {:?}", - before, - after, - captured_now - ); assert!(adjust_payments_params.is_empty()); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let actual_payments_instructions = @@ -1677,7 +1673,7 @@ mod tests { assert_eq!(blockchain_bridge_recording.len(), 1); } - fn test_handling_payment_adjuster_error( + fn test_payment_adjuster_error_during_different_stages( test_name: &str, payment_adjuster: PaymentAdjusterMock, ) { @@ -1700,8 +1696,7 @@ mod tests { subject.ui_message_sub_opt = Some(ui_gateway_recipient); subject.logger = Logger::new(test_name); subject.scanners.payable = Box::new(payable_scanner); - let scan_started_at = SystemTime::now(); - subject.scanners.payable.mark_as_started(scan_started_at); + subject.scanners.payable.mark_as_started(SystemTime::now()); let subject_addr = subject.start(); let account = make_payable_account(111_111); let qualified_payable = @@ -1739,68 +1734,66 @@ mod tests { } #[test] - fn payment_adjuster_throws_out_an_error_from_the_insolvency_check() { + fn payment_adjuster_throws_out_an_error_during_stage_one_the_insolvency_check() { init_test_logging(); - let test_name = "payment_adjuster_throws_out_an_error_from_the_insolvency_check"; - let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_result(Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - number_of_accounts: 1, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor: 60 * 55_000, - cw_transaction_fee_balance_minor: gwei_to_wei(123_u64), - }), - service_fee_opt: None, - }, - )); + 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 { + 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, + }, + )); - test_handling_payment_adjuster_error(test_name, payment_adjuster); + 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}: Insolvency detected led to an analysis of feasibility for making \ - payments adjustment, however, giving no satisfactory solution. Please be advised that \ - your balances can cover neither reasonable portion of any of those payables recently \ - qualified for an imminent payment. You must add more funds into your consuming wallet \ - in order to stay off delinquency bans that your creditors may apply against you \ - 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 wei, \ + "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." )); 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 could not finish. If matured payables stay \ - untreated long, your creditors may impose a ban on you" + "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for payments. \ + The cause appears to be in competence of the user." )); } #[test] - fn payment_adjuster_throws_out_an_error_meaning_entry_check_passed_but_adjustment_went_wrong() { + fn payment_adjuster_throws_out_an_error_during_stage_two_adjustment_went_wrong() { init_test_logging(); let test_name = - "payment_adjuster_throws_out_an_error_meaning_entry_check_passed_but_adjustment_went_wrong"; + "payment_adjuster_throws_out_an_error_during_stage_two_adjustment_went_wrong"; let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_result(Ok(Either::Right(AdjustmentAnalysis::new( + .consider_adjustment_result(Ok(Either::Right(AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, - vec![make_analyzed_account(123)], + vec![make_meaningless_analyzed_account(123)], )))) - .adjust_payments_result(Err(PaymentAdjusterError::AllAccountsEliminated)); + .adjust_payments_result(Err(PaymentAdjusterError::RecursionDrainedAllAccounts)); - test_handling_payment_adjuster_error(test_name, payment_adjuster); + 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. Please \ - add funds into your consuming wallet in order to avoid bans from your creditors. Details: \ - The adjustment algorithm had to eliminate each payable from the recently urged payment due \ - to lack of resources" + "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 \ + 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 could not finish. If matured payables stay untreated \ - long, your creditors may impose a ban on you" + "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for \ + payments. The cause appears to be in competence of the user" )); } @@ -1810,23 +1803,22 @@ mod tests { let test_name = "payment_adjuster_error_is_not_reported_to_ui_if_scan_not_manually_requested"; let mut subject = AccountantBuilder::default().build(); - let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_result(Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - 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, - }, - )); + let payment_adjuster = PaymentAdjusterMock::default().consider_adjustment_result(Err( + PaymentAdjusterError::AbsolutelyInsufficientBalance { + 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, + }, + )); let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); subject.logger = Logger::new(test_name); subject.scanners.payable = Box::new(payable_scanner); - let qualified_payable = make_non_guaranteed_qualified_payable(111_111); + let qualified_payable = make_meaningless_qualified_payable(111_111); let protected_payables = protect_qualified_payables_in_test(vec![qualified_payable]); let blockchain_agent = BlockchainAgentMock::default(); let msg = BlockchainAgentWithContextMessage::new( @@ -1835,13 +1827,13 @@ mod tests { None, ); - subject.handle_payable_payment_setup(msg); + subject.send_outbound_payments_instructions(msg); - // Test didn't blow up while the subject was unbound to other actors - // therefore we didn't attempt to send the NodeUiMessage + // 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 could not finish. If matured payables stay untreated \ - long, your creditors may impose a ban on you" + "ERROR: {test_name}: Payable scanner is blocked from preparing instructions for payments. \ + The cause appears to be in competence of the user" )); } @@ -2044,7 +2036,7 @@ mod tests { let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let (qualified_payables, _, all_non_pending_payables) = - make_unqualified_and_qualified_payables(now, &payment_thresholds); + make_qualified_and_unqualified_payables(now, &payment_thresholds); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); let system = System::new( @@ -2409,7 +2401,7 @@ mod tests { .begin_scan_result(Err(BeginScanError::NothingToProcess)) .begin_scan_result(Ok(QualifiedPayablesMessage { protected_qualified_payables: protect_qualified_payables_in_test(vec![ - make_non_guaranteed_qualified_payable(123), + make_meaningless_qualified_payable(123), ]), response_skeleton_opt: None, })) @@ -2618,7 +2610,7 @@ mod tests { }, ]; let qualified_payables = - make_guaranteed_qualified_payables(payables.clone(), &DEFAULT_PAYMENT_THRESHOLDS, now); + make_qualified_payables(payables.clone(), &DEFAULT_PAYMENT_THRESHOLDS, now); let payable_dao = PayableDaoMock::default().non_pending_payables_result(payables); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); let blockchain_bridge = blockchain_bridge @@ -3430,15 +3422,15 @@ mod tests { #[test] fn pending_transaction_is_registered_and_monitored_until_it_gets_confirmed_or_canceled() { init_test_logging(); + let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); let build_blockchain_agent_params = Arc::new(Mutex::new(vec![])); let mark_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); - let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); - let get_transaction_receipt_params_arc = Arc::new(Mutex::new(vec![])); let return_all_errorless_fingerprints_params_arc = Arc::new(Mutex::new(vec![])); - let non_pending_payables_params_arc = Arc::new(Mutex::new(vec![])); + let get_transaction_receipt_params_arc = Arc::new(Mutex::new(vec![])); let update_fingerprint_params_arc = Arc::new(Mutex::new(vec![])); let mark_failure_params_arc = Arc::new(Mutex::new(vec![])); let delete_record_params_arc = Arc::new(Mutex::new(vec![])); + let transactions_confirmed_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_scan_for_pending_payable_params_arc = Arc::new(Mutex::new(vec![])); let notify_later_scan_for_pending_payable_arc_cloned = notify_later_scan_for_pending_payable_params_arc.clone(); // because it moves into a closure @@ -3517,7 +3509,7 @@ mod tests { last_paid_timestamp: past_payable_timestamp_2, pending_payable_opt: None, }; - let qualified_payables = make_guaranteed_qualified_payables( + let qualified_payables = make_qualified_payables( vec![account_1.clone(), account_2.clone()], &DEFAULT_PAYMENT_THRESHOLDS, now, @@ -3630,12 +3622,13 @@ mod tests { .build(); subject.scanners.receivable = Box::new(NullScanner::new()); let payment_adjuster = PaymentAdjusterMock::default() - .search_for_indispensable_adjustment_result(Ok(Either::Left( - qualified_payables, - ))); + .consider_adjustment_result(Ok(Either::Left(qualified_payables))); 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(), + ))) .payment_adjuster(payment_adjuster) .build(); subject.scanners.payable = Box::new(payable_scanner); diff --git a/node/src/accountant/payment_adjuster/adjustment_runners.rs b/node/src/accountant/payment_adjuster/adjustment_runners.rs deleted file mode 100644 index a02a0cee2..000000000 --- a/node/src/accountant/payment_adjuster/adjustment_runners.rs +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustedAccountBeforeFinalization, WeightedPayable, -}; -use crate::accountant::payment_adjuster::miscellaneous::helper_functions::sum_as; -use crate::accountant::payment_adjuster::{PaymentAdjusterError, PaymentAdjusterReal}; -use itertools::Either; -use masq_lib::utils::convert_collection; - -pub trait AdjustmentRunner { - type ReturnType; - - fn adjust_accounts( - &self, - payment_adjuster: &mut PaymentAdjusterReal, - weighted_accounts: Vec, - ) -> Self::ReturnType; -} - -pub struct TransactionAndServiceFeeAdjustmentRunner {} - -impl AdjustmentRunner for TransactionAndServiceFeeAdjustmentRunner { - type ReturnType = Result< - Either, Vec>, - PaymentAdjusterError, - >; - - fn adjust_accounts( - &self, - payment_adjuster: &mut PaymentAdjusterReal, - weighted_accounts: Vec, - ) -> Self::ReturnType { - match payment_adjuster.inner.transaction_fee_count_limit_opt() { - Some(limit) => { - return payment_adjuster - .begin_with_adjustment_by_transaction_fee(weighted_accounts, limit) - } - None => (), - }; - - Ok(Either::Left( - payment_adjuster.propose_possible_adjustment_recursively(weighted_accounts), - )) - } -} - -pub struct ServiceFeeOnlyAdjustmentRunner {} - -impl AdjustmentRunner for ServiceFeeOnlyAdjustmentRunner { - type ReturnType = Vec; - - fn adjust_accounts( - &self, - payment_adjuster: &mut PaymentAdjusterReal, - weighted_accounts: Vec, - ) -> Self::ReturnType { - let check_sum: u128 = sum_as(&weighted_accounts, |weighted_account| { - weighted_account.balance_minor() - }); - - let unallocated_cw_balance = payment_adjuster - .inner - .unallocated_cw_service_fee_balance_minor(); - - if check_sum <= unallocated_cw_balance { - // Fast return after a direct conversion into the expected type - return convert_collection(weighted_accounts); - } - - payment_adjuster.propose_possible_adjustment_recursively(weighted_accounts) - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::payment_adjuster::adjustment_runners::{ - AdjustmentRunner, ServiceFeeOnlyAdjustmentRunner, - }; - use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustedAccountBeforeFinalization, WeightedPayable, - }; - use crate::accountant::payment_adjuster::test_utils::make_initialized_subject; - use crate::accountant::payment_adjuster::{Adjustment, PaymentAdjusterReal}; - use crate::accountant::test_utils::{ - make_analyzed_account, make_non_guaranteed_qualified_payable, - }; - use crate::accountant::{AnalyzedPayableAccount, CreditorThresholds, QualifiedPayableAccount}; - use crate::sub_lib::wallet::Wallet; - use crate::test_utils::make_wallet; - use std::time::SystemTime; - - fn initialize_payment_adjuster( - now: SystemTime, - service_fee_balance: u128, - largest_exceeding_balance_recently_qualified: u128, - ) -> PaymentAdjusterReal { - make_initialized_subject( - Some(now), - Some(service_fee_balance), - None, - Some(largest_exceeding_balance_recently_qualified), - None, - ) - } - - fn make_weighed_payable(n: u64, initial_balance_minor: u128) -> WeightedPayable { - let mut payable = WeightedPayable::new(make_analyzed_account(111), n as u128 * 1234); - payable - .analyzed_account - .qualified_as - .bare_account - .balance_wei = initial_balance_minor; - payable - } - - fn test_surplus_incurred_after_disqualification_in_previous_iteration( - subject: ServiceFeeOnlyAdjustmentRunner, - payable_1: WeightedPayable, - payable_2: WeightedPayable, - cw_service_fee_balance_minor: u128, - expected_proposed_balance_1: u128, - expected_proposed_balance_2: u128, - ) { - // Explanation: The hypothesis is that the previous iteration disqualified an account after - // which the remaining means are enough for the other accounts. - // We could assign the accounts all they initially requested but a fairer way to do that - // is to give out only that much up to the disqualification limit of these accounts. Later - // on, the accounts that deserves it more, according to their ordering based on their former - // weights, will split the rest of the means among them (Their weights were higher). - let now = SystemTime::now(); - let mut payment_adjuster = - initialize_payment_adjuster(now, cw_service_fee_balance_minor, 12345678); - - let result = subject.adjust_accounts( - &mut payment_adjuster, - vec![payable_1.clone(), payable_2.clone()], - ); - - assert_eq!( - result, - vec![ - AdjustedAccountBeforeFinalization::new( - payable_1.analyzed_account.qualified_as.bare_account, - payable_1.weight, - expected_proposed_balance_1 - ), - AdjustedAccountBeforeFinalization::new( - payable_2.analyzed_account.qualified_as.bare_account, - payable_2.weight, - expected_proposed_balance_2 - ) - ] - ) - } - - fn weighted_payable_setup_for_surplus_test( - n: u64, - initial_balance_minor: u128, - ) -> WeightedPayable { - let mut account = make_weighed_payable(n, initial_balance_minor); - account - .analyzed_account - .qualified_as - .payment_threshold_intercept_minor = 3_000_000_000; - account.analyzed_account.qualified_as.creditor_thresholds = - CreditorThresholds::new(1_000_000_000); - account - } - - #[test] - fn means_equal_requested_money_after_dsq_in_previous_iteration_to_return_capped_accounts() { - let subject = ServiceFeeOnlyAdjustmentRunner {}; - let cw_service_fee_balance_minor = 10_000_000_000; - let mut payable_1 = weighted_payable_setup_for_surplus_test(111, 5_000_000_000); - payable_1.analyzed_account.disqualification_limit_minor = 3_444_333_444; - let mut payable_2 = weighted_payable_setup_for_surplus_test(222, 5_000_000_000); - payable_2.analyzed_account.disqualification_limit_minor = 3_555_333_555; - let expected_proposed_balance_1 = 3_444_333_444; - let expected_proposed_balance_2 = 3_555_333_555; - - test_surplus_incurred_after_disqualification_in_previous_iteration( - subject, - payable_1, - payable_2, - cw_service_fee_balance_minor, - expected_proposed_balance_1, - expected_proposed_balance_2, - ) - } - - #[test] - fn means_become_bigger_than_requested_after_dsq_in_previous_iteration_to_return_capped_accounts( - ) { - let subject = ServiceFeeOnlyAdjustmentRunner {}; - let cw_service_fee_balance_minor = 10_000_000_000; - let mut payable_1 = weighted_payable_setup_for_surplus_test(111, 5_000_000_000); - payable_1.analyzed_account.disqualification_limit_minor = 3_444_333_444; - let mut payable_2 = weighted_payable_setup_for_surplus_test(222, 4_999_999_999); - payable_2.analyzed_account.disqualification_limit_minor = 3_555_333_555; - let expected_proposed_balance_1 = 3_444_333_444; - let expected_proposed_balance_2 = 3_555_333_555; - - test_surplus_incurred_after_disqualification_in_previous_iteration( - subject, - payable_1, - payable_2, - cw_service_fee_balance_minor, - expected_proposed_balance_1, - expected_proposed_balance_2, - ) - } - - #[test] - fn adjust_accounts_for_service_fee_only_runner_is_not_supposed_to_care_about_transaction_fee() { - let balance = 5_000_000_000; - let mut account = make_non_guaranteed_qualified_payable(111); - account.bare_account.balance_wei = balance; - let wallet_1 = make_wallet("abc"); - let wallet_2 = make_wallet("def"); - let mut account_1 = account.clone(); - account_1.bare_account.wallet = wallet_1.clone(); - let mut account_2 = account; - account_2.bare_account.wallet = wallet_2.clone(); - let adjustment = Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 1, - }; - let service_fee_balance_minor = (10 * balance) / 8; - let mut payment_adjuster = PaymentAdjusterReal::new(); - payment_adjuster.initialize_inner( - service_fee_balance_minor, - adjustment, - 123456789, - SystemTime::now(), - ); - let subject = ServiceFeeOnlyAdjustmentRunner {}; - let weighted_account = |account: QualifiedPayableAccount| WeightedPayable { - analyzed_account: AnalyzedPayableAccount::new(account, 3_000_000_000), - weight: 4_000_000_000, - }; - let weighted_accounts = vec![weighted_account(account_1), weighted_account(account_2)]; - - let result = subject.adjust_accounts(&mut payment_adjuster, weighted_accounts); - - let returned_accounts = result - .into_iter() - .map(|account| account.original_account.wallet) - .collect::>(); - assert_eq!(returned_accounts, vec![wallet_1, wallet_2]) - // If the transaction fee adjustment had been available to be performed, only one account - // would've been returned. This test passes - } -} 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 cc37d0d61..77f086bd4 100644 --- a/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs +++ b/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs @@ -8,12 +8,8 @@ use crate::accountant::QualifiedPayableAccount; pub struct BalanceCriterionCalculator {} impl CriterionCalculator for BalanceCriterionCalculator { - fn calculate( - &self, - account: &QualifiedPayableAccount, - context: &dyn PaymentAdjusterInner, - ) -> u128 { - let largest = context.largest_exceeding_balance_recently_qualified(); + fn calculate(&self, account: &QualifiedPayableAccount, context: &PaymentAdjusterInner) -> u128 { + let largest = context.max_debt_above_threshold_in_qualified_payables_minor(); let this_account = account.bare_account.balance_wei - account.payment_threshold_intercept_minor; @@ -32,11 +28,10 @@ impl CriterionCalculator for BalanceCriterionCalculator { mod tests { use crate::accountant::payment_adjuster::criterion_calculators::balance_calculator::BalanceCriterionCalculator; use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; - use crate::accountant::payment_adjuster::inner::PaymentAdjusterInnerReal; + use crate::accountant::payment_adjuster::inner::PaymentAdjusterInner; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::find_largest_exceeding_balance; - use crate::accountant::payment_adjuster::test_utils::multiple_by_billion; - use crate::accountant::test_utils::make_analyzed_account; - use std::time::SystemTime; + use crate::accountant::payment_adjuster::test_utils::local_utils::multiply_by_billion; + use crate::accountant::test_utils::make_meaningless_analyzed_account; #[test] fn calculator_knows_its_name() { @@ -49,23 +44,22 @@ mod tests { #[test] fn balance_criterion_calculator_works() { - let now = SystemTime::now(); let analyzed_accounts = [50, 100, 2_222] .into_iter() .enumerate() .map(|(idx, n)| { - let mut basic_analyzed_payable = make_analyzed_account(idx as u64); + let mut basic_analyzed_payable = make_meaningless_analyzed_account(idx as u64); basic_analyzed_payable.qualified_as.bare_account.balance_wei = - multiple_by_billion(n); + multiply_by_billion(n); basic_analyzed_payable .qualified_as - .payment_threshold_intercept_minor = (multiple_by_billion(2) / 5) * 3; + .payment_threshold_intercept_minor = multiply_by_billion(2) * (idx as u128 + 1); basic_analyzed_payable }) .collect::>(); let largest_exceeding_balance = find_largest_exceeding_balance(&analyzed_accounts); - let payment_adjuster_inner = - PaymentAdjusterInnerReal::new(now, None, 123456789, largest_exceeding_balance); + let payment_adjuster_inner = PaymentAdjusterInner::default(); + payment_adjuster_inner.initialize_guts(None, 123456789, largest_exceeding_balance); let subject = BalanceCriterionCalculator::default(); let computed_criteria = analyzed_accounts @@ -75,18 +69,12 @@ mod tests { }) .collect::>(); - let zipped = analyzed_accounts + let expected_values = vec![4_384_000_000_000, 4_336_000_000_000, 2_216_000_000_000]; + computed_criteria .into_iter() - .zip(computed_criteria.into_iter()); - zipped.into_iter().for_each(|(account, actual_criterion)| { - let expected_criterion = { - let exceeding_balance_on_this_account = - account.qualified_as.bare_account.balance_wei - - account.qualified_as.payment_threshold_intercept_minor; - let diff = largest_exceeding_balance - exceeding_balance_on_this_account; - largest_exceeding_balance + diff - }; - assert_eq!(actual_criterion, expected_criterion) - }) + .zip(expected_values.into_iter()) + .for_each(|(actual_criterion, expected_criterion)| { + assert_eq!(actual_criterion, expected_criterion) + }) } } diff --git a/node/src/accountant/payment_adjuster/criterion_calculators/mod.rs b/node/src/accountant/payment_adjuster/criterion_calculators/mod.rs index 69853cde2..b28e7c1a5 100644 --- a/node/src/accountant/payment_adjuster/criterion_calculators/mod.rs +++ b/node/src/accountant/payment_adjuster/criterion_calculators/mod.rs @@ -7,11 +7,7 @@ use crate::accountant::QualifiedPayableAccount; // Caution: always remember to use checked math operations in the criteria formulas! pub trait CriterionCalculator { - fn calculate( - &self, - account: &QualifiedPayableAccount, - context: &dyn PaymentAdjusterInner, - ) -> u128; + fn calculate(&self, account: &QualifiedPayableAccount, context: &PaymentAdjusterInner) -> u128; fn parameter_name(&self) -> &'static str; } diff --git a/node/src/accountant/payment_adjuster/disqualification_arbiter.rs b/node/src/accountant/payment_adjuster/disqualification_arbiter.rs index d26ba4368..119b9c0ba 100644 --- a/node/src/accountant/payment_adjuster/disqualification_arbiter.rs +++ b/node/src/accountant/payment_adjuster/disqualification_arbiter.rs @@ -1,5 +1,6 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use web3::types::Address; use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::{ account_nominated_for_disqualification_diagnostics, try_finding_an_account_to_disqualify_diagnostics, @@ -7,9 +8,7 @@ use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::o use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::info_log_for_disqualified_account; use crate::accountant::payment_adjuster::miscellaneous::data_structures::UnconfirmedAdjustment; use crate::accountant::QualifiedPayableAccount; -use crate::sub_lib::wallet::Wallet; use masq_lib::logger::Logger; -use std::cmp::Ordering; pub struct DisqualificationArbiter { disqualification_gauge: Box, @@ -46,18 +45,18 @@ impl DisqualificationArbiter { &self, unconfirmed_adjustments: &[UnconfirmedAdjustment], logger: &Logger, - ) -> Wallet { + ) -> Address { let disqualification_suspected_accounts = Self::list_accounts_nominated_for_disqualification(unconfirmed_adjustments); let account_to_disqualify = Self::find_account_with_smallest_weight(&disqualification_suspected_accounts); - let wallet = account_to_disqualify.wallet.clone(); + let wallet = account_to_disqualify.wallet; try_finding_an_account_to_disqualify_diagnostics( &disqualification_suspected_accounts, - &wallet, + wallet, ); debug!( @@ -98,42 +97,36 @@ impl DisqualificationArbiter { .collect() } - fn find_account_with_smallest_weight<'accounts>( - accounts: &'accounts [DisqualificationSuspectedAccount], - ) -> &'accounts DisqualificationSuspectedAccount<'accounts> { - let first_account = accounts.first().expect("collection was empty"); - accounts.iter().fold( - first_account, - |with_smallest_weight_so_far, current| match Ord::cmp( - ¤t.weight, - &with_smallest_weight_so_far.weight, - ) { - Ordering::Less => current, - Ordering::Greater => with_smallest_weight_so_far, - Ordering::Equal => with_smallest_weight_so_far, - }, - ) + fn find_account_with_smallest_weight( + accounts: &[DisqualificationSuspectedAccount], + ) -> &DisqualificationSuspectedAccount { + accounts + .iter() + .min_by_key(|account| account.weight) + .expect("an empty collection of accounts") } } #[derive(Debug, PartialEq, Eq)] -pub struct DisqualificationSuspectedAccount<'account> { - pub wallet: &'account Wallet, +pub struct DisqualificationSuspectedAccount { + pub wallet: Address, pub weight: u128, - // The rest is for an INFO log + // The rest serves diagnostics and logging pub proposed_adjusted_balance_minor: u128, pub disqualification_limit_minor: u128, + pub initial_account_balance_minor: u128, } impl<'unconfirmed_accounts> From<&'unconfirmed_accounts UnconfirmedAdjustment> - for DisqualificationSuspectedAccount<'unconfirmed_accounts> + for DisqualificationSuspectedAccount { fn from(unconfirmed_account: &'unconfirmed_accounts UnconfirmedAdjustment) -> Self { DisqualificationSuspectedAccount { wallet: unconfirmed_account.wallet(), - weight: unconfirmed_account.weighted_account.weight, + weight: unconfirmed_account.weighed_account.weight, proposed_adjusted_balance_minor: unconfirmed_account.proposed_adjusted_balance_minor, disqualification_limit_minor: unconfirmed_account.disqualification_limit_minor(), + initial_account_balance_minor: unconfirmed_account.initial_balance_minor(), } } } @@ -157,78 +150,162 @@ impl DisqualificationGauge for DisqualificationGaugeReal { threshold_intercept_minor: u128, permanent_debt_allowed_minor: u128, ) -> u128 { + // This signs that the debt lies in the horizontal area of the payment thresholds, and thus + // should be paid in the whole size. if threshold_intercept_minor == permanent_debt_allowed_minor { return account_balance_minor; } - let exceeding_debt_part = account_balance_minor - threshold_intercept_minor; - if DisqualificationGaugeReal::qualifies_for_double_margin( + Self::determine_adequate_minimal_payment( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, - ) { - exceeding_debt_part + 2 * permanent_debt_allowed_minor - } else { - exceeding_debt_part + permanent_debt_allowed_minor - } + ) } } impl DisqualificationGaugeReal { - const FIRST_CONDITION_COEFFICIENT: u128 = 2; - const SECOND_CONDITION_COEFFICIENT: u128 = 2; - fn qualifies_for_double_margin( + const FIRST_QUALIFICATION_CONDITION_COEFFICIENT: u128 = 2; + const SECOND_QUALIFICATION_CONDITION_COEFFICIENT: u128 = 2; + const MULTIPLIER_FOR_THICKER_MARGIN: u128 = 2; + + fn qualifies_for_thicker_margin( account_balance_minor: u128, threshold_intercept_minor: u128, permanent_debt_allowed_minor: u128, ) -> bool { let exceeding_threshold = account_balance_minor - threshold_intercept_minor; let considered_forgiven = threshold_intercept_minor - permanent_debt_allowed_minor; - let minimal_payment_accepted = exceeding_threshold + permanent_debt_allowed_minor; + let minimal_acceptable_payment = exceeding_threshold + permanent_debt_allowed_minor; + + let condition_of_debt_fast_growth = minimal_acceptable_payment + >= Self::FIRST_QUALIFICATION_CONDITION_COEFFICIENT * considered_forgiven; - let first_condition = - minimal_payment_accepted >= Self::FIRST_CONDITION_COEFFICIENT * considered_forgiven; + let condition_of_position_on_rather_the_left_half_of_the_slope = considered_forgiven + >= Self::SECOND_QUALIFICATION_CONDITION_COEFFICIENT * permanent_debt_allowed_minor; - let second_condition = considered_forgiven - >= Self::SECOND_CONDITION_COEFFICIENT * permanent_debt_allowed_minor; + condition_of_debt_fast_growth && condition_of_position_on_rather_the_left_half_of_the_slope + } - first_condition && second_condition + fn determine_adequate_minimal_payment( + account_balance_minor: u128, + threshold_intercept_minor: u128, + permanent_debt_allowed_minor: u128, + ) -> u128 { + let debt_part_over_the_threshold = account_balance_minor - threshold_intercept_minor; + if DisqualificationGaugeReal::qualifies_for_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ) { + debt_part_over_the_threshold + + Self::MULTIPLIER_FOR_THICKER_MARGIN * permanent_debt_allowed_minor + } else { + debt_part_over_the_threshold + permanent_debt_allowed_minor + } } + + // This schema shows the conditions used to determine the disqualification limit + // (or minimal acceptable payment) + // + // | A + + // | | P -----------+ + // | | P | + // | | P | + // | | P | + // | B | P P -----+ | + // | + P P | | + // | |\ P P X Y + // | | \P P | | + // | | P P -----+ | + // | B'+ P\ P | + // | |\ P \P | + // | | \P P -----+-----+ + // | | U P\ + // | B"+ U\ P \ + // | \ U \P +C + // | \U P |\ + // | U P\ | \ + // | U\ P \| \ P P + // | U \P +C' \ P P + // | U U |\ \P P + // | U U\ | \ P P + // | U U \| \ P\ P + // | U U +C" \ P \ P + // | U U \ \P \ P + // | U U \ U \ D P E + // | U U \ U\ +------------P--------+ + // | U U \ U \ | P + // | U U \U \ | P + // | U U U \|D' P E' + // +---------------------------+---+---------------------+ + // 3 4 2 1 + // + // This diagram presents computation of the disqualification limit which differs by four cases. + // The debt portion illustrated with the use of the letter 'P' stands for the actual limit. + // That is the minimum amount we consider effective to keep us away from a ban for delinquent + // debtors. Beyond that mark, if the debt is bigger, it completes the column with 'U's. This + // part can be forgiven for the time being, until more funds is supplied for the consuming + // wallet. + // + // Points A, B, D, E make up a simple outline of possible payment thresholds. These are + // fundamental statements: The x-axis distance between B and D is "threshold_interval_sec". + // From B vertically down to the x-axis, it amounts to "debt_threshold_gwei". D is as far + // from D' as the size of the "permanent_debt_allowed_gwei" parameter. A few other line + // segments in the diagram are also derived from this last mentioned measurement, like B - B' + // and B' - B". + // + // 1. This debt is ordered entire strictly as well as any other one situated between D and E. + // (Note that the E isn't a real point, the axis goes endless this direction). + // 2. Since we are earlier in the time with debt, a different rule is applied. The limit is + // formed as the part above the threshold, plus an equivalent of the D - D' distance. + // It's notable that we are evaluating a debt older than the timestamp which would appear + // on the x-axis if we prolonged the C - C" line towards it. + // 3. Now we are before that timestamp, however the surplussing debt portion X isn't + // significant enough yet. Therefore the same rule as at No. 2 is applied also here. + // 4. This time we hold the condition for the age not reaching the decisive timestamp and + // the debt becomes sizable, measured as Y, which indicates that it might be linked to + // a Node that we've used extensively (or even that we're using right now). We then prefer + // to increase the margin added to the above-threshold amount, and so we double it. + // If true to the reality, the diagram would have to run much further upwards. That's + // because the condition to consider a debt's size significant says that the part under + // the threshold must be twice (or more) smaller than that above it (Y). + // } #[cfg(test)] mod tests { - use crate::accountant::db_access_objects::payable_dao::PayableAccount; - use crate::accountant::db_access_objects::utils::from_time_t; use crate::accountant::payment_adjuster::disqualification_arbiter::{ DisqualificationArbiter, DisqualificationGauge, DisqualificationGaugeReal, DisqualificationSuspectedAccount, }; use crate::accountant::payment_adjuster::miscellaneous::data_structures::UnconfirmedAdjustment; - use crate::accountant::payment_adjuster::miscellaneous::helper_functions::find_largest_exceeding_balance; - use crate::accountant::payment_adjuster::service_fee_adjuster::AdjustmentComputer; - use crate::accountant::payment_adjuster::test_utils::{ - make_initialized_subject, make_non_guaranteed_unconfirmed_adjustment, + use crate::accountant::payment_adjuster::test_utils::local_utils::{ + make_meaningless_weighed_account, make_non_guaranteed_unconfirmed_adjustment, }; - use crate::accountant::test_utils::make_guaranteed_qualified_payables; - use crate::sub_lib::accountant::PaymentThresholds; - use crate::test_utils::make_wallet; + use itertools::Itertools; use masq_lib::logger::Logger; use masq_lib::utils::convert_collection; - use std::time::SystemTime; #[test] fn constants_are_correct() { - assert_eq!(DisqualificationGaugeReal::FIRST_CONDITION_COEFFICIENT, 2); - assert_eq!(DisqualificationGaugeReal::SECOND_CONDITION_COEFFICIENT, 2) + assert_eq!( + DisqualificationGaugeReal::FIRST_QUALIFICATION_CONDITION_COEFFICIENT, + 2 + ); + assert_eq!( + DisqualificationGaugeReal::SECOND_QUALIFICATION_CONDITION_COEFFICIENT, + 2 + ); + assert_eq!(DisqualificationGaugeReal::MULTIPLIER_FOR_THICKER_MARGIN, 2) } #[test] - fn qualifies_for_double_margin_granted_on_both_conditions_returning_equals() { + fn qualifies_for_thicker_margin_granted_on_both_conditions_returning_equals() { let account_balance_minor = 6_000_000_000; let threshold_intercept_minor = 3_000_000_000; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -238,12 +315,12 @@ mod tests { } #[test] - fn qualifies_for_double_margin_granted_on_first_condition_bigger_second_equal() { + fn qualifies_for_thicker_margin_granted_on_first_condition_bigger_second_equal() { let account_balance_minor = 6_000_000_001; let threshold_intercept_minor = 3_000_000_000; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -253,12 +330,12 @@ mod tests { } #[test] - fn qualifies_for_double_margin_granted_on_first_condition_equal_second_bigger() { + fn qualifies_for_thicker_margin_granted_on_first_condition_equal_second_bigger() { let account_balance_minor = 6_000_000_003; let threshold_intercept_minor = 3_000_000_001; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -268,12 +345,12 @@ mod tests { } #[test] - fn qualifies_for_double_margin_granted_on_both_conditions_returning_bigger() { + fn qualifies_for_thicker_margin_granted_on_both_conditions_returning_bigger() { let account_balance_minor = 6_000_000_004; let threshold_intercept_minor = 3_000_000_001; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -283,12 +360,12 @@ mod tests { } #[test] - fn qualifies_for_double_margin_declined_on_first_condition() { + fn qualifies_for_thicker_margin_declined_on_first_condition() { let account_balance_minor = 5_999_999_999; let threshold_intercept_minor = 3_000_000_000; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -298,12 +375,12 @@ mod tests { } #[test] - fn qualifies_for_double_margin_declined_on_second_condition() { + fn qualifies_for_thicker_margin_declined_on_second_condition() { let account_balance_minor = 6_000_000_000; let threshold_intercept_minor = 2_999_999_999; let permanent_debt_allowed_minor = 1_000_000_000; - let result = DisqualificationGaugeReal::qualifies_for_double_margin( + let result = DisqualificationGaugeReal::qualifies_for_thicker_margin( account_balance_minor, threshold_intercept_minor, permanent_debt_allowed_minor, @@ -368,7 +445,7 @@ mod tests { let mut account = make_non_guaranteed_unconfirmed_adjustment(444); account.proposed_adjusted_balance_minor = 1_000_000_000; account - .weighted_account + .weighed_account .analyzed_account .disqualification_limit_minor = 1_000_000_000; let accounts = vec![account]; @@ -405,74 +482,67 @@ mod tests { #[test] fn only_account_with_the_smallest_weight_will_be_disqualified_in_single_iteration() { - let test_name = - "only_account_with_the_smallest_weight_will_be_disqualified_in_single_iteration"; - let now = SystemTime::now(); - let cw_service_fee_balance_minor = 200_000_000_000; - let mut payment_thresholds = PaymentThresholds::default(); - payment_thresholds.permanent_debt_allowed_gwei = 10; - payment_thresholds.maturity_threshold_sec = 1_000; - payment_thresholds.threshold_interval_sec = 10_000; - let logger = Logger::new(test_name); - let wallet_1 = make_wallet("abc"); - let common_timestamp = from_time_t( - (payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec - + 1) as i64, - ); - let account_1 = PayableAccount { - wallet: wallet_1.clone(), - balance_wei: 120_000_000_000 + 1, - last_paid_timestamp: common_timestamp, - pending_payable_opt: None, - }; - let wallet_2 = make_wallet("def"); - let account_2 = PayableAccount { - wallet: wallet_2.clone(), - balance_wei: 120_000_000_000, - last_paid_timestamp: common_timestamp, - pending_payable_opt: None, - }; - let wallet_3 = make_wallet("ghi"); - // This account has the largest exceeding balance and therefore has the smallest weight - let account_3 = PayableAccount { - wallet: wallet_3.clone(), - balance_wei: 120_000_000_000 + 2, - last_paid_timestamp: common_timestamp, - pending_payable_opt: None, - }; - let wallet_4 = make_wallet("jkl"); - let account_4 = PayableAccount { - wallet: wallet_4.clone(), - balance_wei: 120_000_000_000 - 1, - last_paid_timestamp: common_timestamp, - pending_payable_opt: None, - }; - let accounts = vec![account_1, account_2, account_3, account_4]; - let qualified_payables = - make_guaranteed_qualified_payables(accounts, &payment_thresholds, now); - let analyzed_accounts = convert_collection(qualified_payables); - let largest_exceeding_balance = find_largest_exceeding_balance(&analyzed_accounts); - let payment_adjuster = make_initialized_subject( - Some(now), - Some(cw_service_fee_balance_minor), - None, - Some(largest_exceeding_balance), - None, - ); - let weights_and_accounts = payment_adjuster.calculate_weights(analyzed_accounts); + let mut account_1 = make_meaningless_weighed_account(123); + account_1.analyzed_account.disqualification_limit_minor = 1_000_000; + account_1.weight = 1000; + let mut account_2 = make_meaningless_weighed_account(456); + account_2.analyzed_account.disqualification_limit_minor = 1_000_000; + account_2.weight = 1002; + let mut account_3 = make_meaningless_weighed_account(789); + account_3.analyzed_account.disqualification_limit_minor = 1_000_000; + account_3.weight = 999; + let wallet_3 = account_3 + .analyzed_account + .qualified_as + .bare_account + .wallet + .address(); + let mut account_4 = make_meaningless_weighed_account(012); + account_4.analyzed_account.disqualification_limit_minor = 1_000_000; + account_4.weight = 1001; + // Notice that each proposed adjustment is below 1_000_000 which makes it clear all these + // accounts are nominated for disqualification, only one can be picked though + let seeds = vec![ + (account_1, 900_000), + (account_2, 920_000), + (account_3, 910_000), + (account_4, 930_000), + ]; + let unconfirmed_adjustments = seeds + .into_iter() + .map( + |(weighed_account, proposed_adjusted_balance_minor)| UnconfirmedAdjustment { + weighed_account, + proposed_adjusted_balance_minor, + }, + ) + .collect_vec(); let subject = DisqualificationArbiter::default(); - let unconfirmed_adjustments = AdjustmentComputer::default() - .compute_unconfirmed_adjustments(weights_and_accounts, cw_service_fee_balance_minor); - let result = subject - .find_an_account_to_disqualify_in_this_iteration(&unconfirmed_adjustments, &logger); + let result = subject.find_an_account_to_disqualify_in_this_iteration( + &unconfirmed_adjustments, + &Logger::new("test"), + ); - unconfirmed_adjustments.iter().for_each(|payable| { - // Condition of disqualification at the horizontal threshold - assert!(payable.proposed_adjusted_balance_minor < 120_000_000_000) - }); 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 { @@ -481,17 +551,16 @@ mod tests { .enumerate() .map(|(idx, weight)| { let mut account = make_non_guaranteed_unconfirmed_adjustment(idx as u64); - account.weighted_account.weight = weight; + account.weighed_account.weight = weight; account }) .collect() } fn make_dsq_suspected_accounts( - accounts_and_dsq_edges: &[UnconfirmedAdjustment], + accounts: &[UnconfirmedAdjustment], ) -> Vec { - let with_referred_accounts: Vec<&UnconfirmedAdjustment> = - accounts_and_dsq_edges.iter().collect(); + let with_referred_accounts: Vec<&UnconfirmedAdjustment> = accounts.iter().collect(); convert_collection(with_referred_accounts) } } diff --git a/node/src/accountant/payment_adjuster/inner.rs b/node/src/accountant/payment_adjuster/inner.rs index 82ff80c7e..99bafd080 100644 --- a/node/src/accountant/payment_adjuster/inner.rs +++ b/node/src/accountant/payment_adjuster/inner.rs @@ -1,103 +1,132 @@ // Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use std::time::SystemTime; - -pub trait PaymentAdjusterInner { - fn now(&self) -> SystemTime; - fn largest_exceeding_balance_recently_qualified(&self) -> u128; - fn transaction_fee_count_limit_opt(&self) -> Option; - fn original_cw_service_fee_balance_minor(&self) -> u128; - fn unallocated_cw_service_fee_balance_minor(&self) -> u128; - fn subtract_from_unallocated_cw_service_fee_balance_minor(&mut self, subtrahend: u128); +use std::cell::RefCell; + +pub struct PaymentAdjusterInner { + initialized_guts_opt: RefCell>, } -pub struct PaymentAdjusterInnerReal { - now: SystemTime, - transaction_fee_count_limit_opt: Option, - largest_exceeding_balance_recently_qualified: u128, +impl Default for PaymentAdjusterInner { + fn default() -> Self { + PaymentAdjusterInner { + initialized_guts_opt: RefCell::new(None), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct GutsOfPaymentAdjusterInner { + transaction_count_limit_opt: Option, + max_debt_above_threshold_in_qualified_payables_minor: u128, original_cw_service_fee_balance_minor: u128, - unallocated_cw_service_fee_balance_minor: u128, + remaining_cw_service_fee_balance_minor: u128, } -impl PaymentAdjusterInnerReal { +impl GutsOfPaymentAdjusterInner { pub fn new( - now: SystemTime, - transaction_fee_count_limit_opt: Option, + transaction_count_limit_opt: Option, cw_service_fee_balance_minor: u128, - largest_exceeding_balance_recently_qualified: u128, + max_debt_above_threshold_in_qualified_payables_minor: u128, ) -> Self { Self { - now, - transaction_fee_count_limit_opt, - largest_exceeding_balance_recently_qualified, + transaction_count_limit_opt, + max_debt_above_threshold_in_qualified_payables_minor, original_cw_service_fee_balance_minor: cw_service_fee_balance_minor, - unallocated_cw_service_fee_balance_minor: cw_service_fee_balance_minor, + remaining_cw_service_fee_balance_minor: cw_service_fee_balance_minor, } } } -impl PaymentAdjusterInner for PaymentAdjusterInnerReal { - fn now(&self) -> SystemTime { - self.now - } +impl PaymentAdjusterInner { + pub fn initialize_guts( + &self, + tx_count_limit_opt: Option, + cw_service_fee_balance: u128, + max_debt_above_threshold_in_qualified_payables_minor: u128, + ) { + let initialized_guts = GutsOfPaymentAdjusterInner::new( + tx_count_limit_opt, + cw_service_fee_balance, + max_debt_above_threshold_in_qualified_payables_minor, + ); - fn largest_exceeding_balance_recently_qualified(&self) -> u128 { - self.largest_exceeding_balance_recently_qualified + self.initialized_guts_opt + .borrow_mut() + .replace(initialized_guts); } - fn transaction_fee_count_limit_opt(&self) -> Option { - self.transaction_fee_count_limit_opt + pub fn max_debt_above_threshold_in_qualified_payables_minor(&self) -> u128 { + self.get_value( + "max_debt_above_threshold_in_qualified_payables_minor", + |guts_ref| guts_ref.max_debt_above_threshold_in_qualified_payables_minor, + ) } - fn original_cw_service_fee_balance_minor(&self) -> u128 { - self.original_cw_service_fee_balance_minor + + pub fn transaction_count_limit_opt(&self) -> Option { + self.get_value("transaction_count_limit_opt", |guts_ref| { + guts_ref.transaction_count_limit_opt + }) } - fn unallocated_cw_service_fee_balance_minor(&self) -> u128 { - self.unallocated_cw_service_fee_balance_minor + pub fn original_cw_service_fee_balance_minor(&self) -> u128 { + self.get_value("original_cw_service_fee_balance_minor", |guts_ref| { + guts_ref.original_cw_service_fee_balance_minor + }) } - fn subtract_from_unallocated_cw_service_fee_balance_minor(&mut self, subtrahend: u128) { - let updated_thought_cw_balance = self - .unallocated_cw_service_fee_balance_minor - .checked_sub(subtrahend) - .expect("should never go beyond zero"); - self.unallocated_cw_service_fee_balance_minor = updated_thought_cw_balance + pub fn remaining_cw_service_fee_balance_minor(&self) -> u128 { + self.get_value("remaining_cw_service_fee_balance_minor", |guts_ref| { + guts_ref.remaining_cw_service_fee_balance_minor + }) } -} - -#[derive(Default)] -pub struct PaymentAdjusterInnerNull {} - -impl PaymentAdjusterInnerNull { - fn panicking_operation(operation: &str) -> ! { - panic!( - "Broken code: Broken code: Called the null implementation of the {} method in PaymentAdjusterInner", - operation + pub fn subtract_from_remaining_cw_service_fee_balance_minor(&self, subtrahend: u128) { + let updated_thought_cw_balance = self.get_value( + "subtract_from_remaining_cw_service_fee_balance_minor", + |guts_ref| { + guts_ref + .remaining_cw_service_fee_balance_minor + .checked_sub(subtrahend) + .expect("should never go beyond zero") + }, + ); + 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, ) } -} -impl PaymentAdjusterInner for PaymentAdjusterInnerNull { - fn now(&self) -> SystemTime { - PaymentAdjusterInnerNull::panicking_operation("now()") + pub fn invalidate_guts(&self) { + self.initialized_guts_opt.replace(None); } - fn largest_exceeding_balance_recently_qualified(&self) -> u128 { - PaymentAdjusterInnerNull::panicking_operation( - "largest_exceeding_balance_recently_qualified()", - ) - } + fn get_value(&self, method: &str, getter: F) -> T + where + F: FnOnce(&GutsOfPaymentAdjusterInner) -> T, + { + let guts_borrowed_opt = self.initialized_guts_opt.borrow(); - fn transaction_fee_count_limit_opt(&self) -> Option { - PaymentAdjusterInnerNull::panicking_operation("transaction_fee_count_limit_opt()") - } - fn original_cw_service_fee_balance_minor(&self) -> u128 { - PaymentAdjusterInnerNull::panicking_operation("original_cw_service_fee_balance_minor()") + let guts_ref = guts_borrowed_opt + .as_ref() + .unwrap_or_else(|| Self::uninitialized_panic(method)); + + getter(guts_ref) } - fn unallocated_cw_service_fee_balance_minor(&self) -> u128 { - PaymentAdjusterInnerNull::panicking_operation("unallocated_cw_service_fee_balance_minor()") + + fn set_value(&self, method: &str, mut setter: F) + where + F: FnMut(&mut GutsOfPaymentAdjusterInner), + { + let mut guts_borrowed_mut_opt = self.initialized_guts_opt.borrow_mut(); + + let guts_mut = guts_borrowed_mut_opt + .as_mut() + .unwrap_or_else(|| Self::uninitialized_panic(method)); + + setter(guts_mut) } - fn subtract_from_unallocated_cw_service_fee_balance_minor(&mut self, _subtrahend: u128) { - PaymentAdjusterInnerNull::panicking_operation( - "subtract_from_unallocated_cw_service_fee_balance_minor()", + + fn uninitialized_panic(method: &str) -> ! { + panic!( + "PaymentAdjusterInner is uninitialized. It was identified during the execution of \ + '{method}()'" ) } } @@ -105,100 +134,144 @@ impl PaymentAdjusterInner for PaymentAdjusterInnerNull { #[cfg(test)] mod tests { use crate::accountant::payment_adjuster::inner::{ - PaymentAdjusterInner, PaymentAdjusterInnerNull, PaymentAdjusterInnerReal, + GutsOfPaymentAdjusterInner, PaymentAdjusterInner, }; - use std::time::SystemTime; + use std::panic::{catch_unwind, AssertUnwindSafe}; #[test] - fn inner_real_is_constructed_correctly() { - let now = SystemTime::now(); - let transaction_fee_count_limit_opt = Some(3); + fn defaulted_payment_adjuster_inner() { + let subject = PaymentAdjusterInner::default(); + + let guts_is_none = subject.initialized_guts_opt.borrow().is_none(); + assert_eq!(guts_is_none, true) + } + + #[test] + fn initialization_and_getters_of_payment_adjuster_inner_work() { + let subject = PaymentAdjusterInner::default(); + let tx_count_limit_opt = Some(3); let cw_service_fee_balance = 123_456_789; - let largest_exceeding_balance_recently_qualified = 44_555_666; - let result = PaymentAdjusterInnerReal::new( - now, - transaction_fee_count_limit_opt, + let max_debt_above_threshold_in_qualified_payables_minor = 44_555_666; + + subject.initialize_guts( + tx_count_limit_opt, cw_service_fee_balance, - largest_exceeding_balance_recently_qualified, + max_debt_above_threshold_in_qualified_payables_minor, ); + let read_max_debt_above_threshold_in_qualified_payables_minor = + subject.max_debt_above_threshold_in_qualified_payables_minor(); + let read_tx_count_limit_opt = subject.transaction_count_limit_opt(); + let read_original_cw_service_fee_balance_minor = + subject.original_cw_service_fee_balance_minor(); + let read_remaining_cw_service_fee_balance_minor = + subject.remaining_cw_service_fee_balance_minor(); - assert_eq!(result.now, now); assert_eq!( - result.transaction_fee_count_limit_opt, - transaction_fee_count_limit_opt + read_max_debt_above_threshold_in_qualified_payables_minor, + max_debt_above_threshold_in_qualified_payables_minor ); + assert_eq!(read_tx_count_limit_opt, tx_count_limit_opt); assert_eq!( - result.original_cw_service_fee_balance_minor, + read_original_cw_service_fee_balance_minor, cw_service_fee_balance ); assert_eq!( - result.unallocated_cw_service_fee_balance_minor, + read_remaining_cw_service_fee_balance_minor, cw_service_fee_balance ); - assert_eq!( - result.largest_exceeding_balance_recently_qualified, - largest_exceeding_balance_recently_qualified - ) } #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the now() method in PaymentAdjusterInner" - )] - fn inner_null_calling_now() { - let subject = PaymentAdjusterInnerNull::default(); + fn reducing_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); + let amount_to_subtract = 555_666_777; - let _ = subject.now(); - } + subject.subtract_from_remaining_cw_service_fee_balance_minor(amount_to_subtract); - #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the largest_exceeding_balance_recently_qualified() \ - method in PaymentAdjusterInner" - )] - fn inner_null_calling_largest_exceeding_balance_recently_qualified() { - let subject = PaymentAdjusterInnerNull::default(); - - let _ = subject.largest_exceeding_balance_recently_qualified(); + let remaining_cw_service_fee_balance_minor = + subject.remaining_cw_service_fee_balance_minor(); + assert_eq!( + remaining_cw_service_fee_balance_minor, + initial_cw_service_fee_balance_minor - amount_to_subtract + ) } #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the transaction_fee_count_limit_opt() method in PaymentAdjusterInner" - )] - fn inner_null_calling_transaction_fee_count_limit_opt() { - let subject = PaymentAdjusterInnerNull::default(); + fn inner_can_be_invalidated_by_removing_its_guts() { + let subject = PaymentAdjusterInner::default(); + subject + .initialized_guts_opt + .replace(Some(GutsOfPaymentAdjusterInner { + transaction_count_limit_opt: None, + max_debt_above_threshold_in_qualified_payables_minor: 0, + original_cw_service_fee_balance_minor: 0, + remaining_cw_service_fee_balance_minor: 0, + })); - let _ = subject.transaction_fee_count_limit_opt(); - } + subject.invalidate_guts(); - #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the original_cw_service_fee_balance_minor() method in PaymentAdjusterInner" - )] - fn inner_null_calling_original_cw_service_fee_balance_minor() { - let subject = PaymentAdjusterInnerNull::default(); - - let _ = subject.original_cw_service_fee_balance_minor(); + let guts_removed = subject.initialized_guts_opt.borrow().is_none(); + assert_eq!(guts_removed, true) } #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the unallocated_cw_service_fee_balance_minor() method in PaymentAdjusterInner" - )] - fn inner_null_calling_unallocated_cw_balance() { - let subject = PaymentAdjusterInnerNull::default(); - - let _ = subject.unallocated_cw_service_fee_balance_minor(); + fn reasonable_panics_about_lacking_initialization_for_respective_methods() { + let uninitialized_subject = PaymentAdjusterInner::default(); + test_properly_implemented_panic( + &uninitialized_subject, + "max_debt_above_threshold_in_qualified_payables_minor", + |subject| { + subject.max_debt_above_threshold_in_qualified_payables_minor(); + }, + ); + test_properly_implemented_panic( + &uninitialized_subject, + "transaction_count_limit_opt", + |subject| { + subject.transaction_count_limit_opt(); + }, + ); + test_properly_implemented_panic( + &uninitialized_subject, + "original_cw_service_fee_balance_minor", + |subject| { + subject.original_cw_service_fee_balance_minor(); + }, + ); + test_properly_implemented_panic( + &uninitialized_subject, + "remaining_cw_service_fee_balance_minor", + |subject| { + subject.remaining_cw_service_fee_balance_minor(); + }, + ); + test_properly_implemented_panic( + &uninitialized_subject, + "subtract_from_remaining_cw_service_fee_balance_minor", + |subject| { + subject.subtract_from_remaining_cw_service_fee_balance_minor(123456); + }, + ) } - #[test] - #[should_panic( - expected = "Broken code: Called the null implementation of the subtract_from_unallocated_cw_service_fee_balance_minor() method in PaymentAdjusterInner" - )] - fn inner_null_calling_subtract_from_unallocated_cw_service_fee_balance_minor() { - let mut subject = PaymentAdjusterInnerNull::default(); - - let _ = subject.subtract_from_unallocated_cw_service_fee_balance_minor(123); + fn test_properly_implemented_panic( + subject: &PaymentAdjusterInner, + method_name: &str, + call_panicking_method: fn(&PaymentAdjusterInner), + ) { + let caught_panic = + catch_unwind(AssertUnwindSafe(|| call_panicking_method(subject))).unwrap_err(); + let actual_panic_msg = caught_panic.downcast_ref::().unwrap().to_owned(); + let expected_msg = format!( + "PaymentAdjusterInner is uninitialized. It was \ + identified during the execution of '{method_name}()'" + ); + assert_eq!( + actual_panic_msg, expected_msg, + "We expected this panic message: {}, but the panic looked different: {}", + expected_msg, actual_panic_msg + ) } } 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 c1a6b272c..45d7d1015 100644 --- a/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs +++ b/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs @@ -3,7 +3,7 @@ use masq_lib::constants::WALLET_ADDRESS_LENGTH; use std::fmt::Debug; -const PRINT_RESULTS_OF_PARTIAL_COMPUTATIONS: bool = false; +const RUN_DIAGNOSTICS_FOR_DEVS: bool = false; pub const DIAGNOSTICS_MIDDLE_COLUMN_WIDTH: usize = 58; @@ -30,9 +30,9 @@ macro_rules! diagnostics { ) }; // Displays an account by wallet address, brief description and formatted literal with arguments - ($wallet_ref: expr, $description: expr, $($formatted_values: tt)*) => { + ($wallet: expr, $description: expr, $($formatted_values: tt)*) => { diagnostics( - Some(||$wallet_ref.to_string()), + Some(||format!("{:?}", $wallet)), $description, Some(|| format!($($formatted_values)*)) ) @@ -49,7 +49,7 @@ pub fn diagnostics( F1: FnOnce() -> String, F2: FnOnce() -> String, { - if PRINT_RESULTS_OF_PARTIAL_COMPUTATIONS { + if RUN_DIAGNOSTICS_FOR_DEVS { let subject_column_length = if subject_renderer_opt.is_some() { WALLET_ADDRESS_LENGTH + 2 } else { @@ -82,7 +82,7 @@ pub fn collection_diagnostics( label: &str, accounts: &[DebuggableAccount], ) { - if PRINT_RESULTS_OF_PARTIAL_COMPUTATIONS { + if RUN_DIAGNOSTICS_FOR_DEVS { eprintln!("{}", label); accounts .iter() @@ -95,10 +95,10 @@ pub mod ordinary_diagnostic_functions { use crate::accountant::payment_adjuster::diagnostics; use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationSuspectedAccount; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeightedPayable, + AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeighedPayable, }; - use crate::sub_lib::wallet::Wallet; use thousands::Separable; + use web3::types::Address; pub fn thriving_competitor_found_diagnostics( account_info: &UnconfirmedAdjustment, @@ -130,11 +130,11 @@ pub mod ordinary_diagnostic_functions { } pub fn minimal_acceptable_balance_assigned_diagnostics( - weighted_account: &WeightedPayable, + weighed_account: &WeighedPayable, disqualification_limit: u128, ) { diagnostics!( - weighted_account.wallet(), + weighed_account.wallet(), "MINIMAL ACCEPTABLE BALANCE ASSIGNED", "Used disqualification limit for given account {}", disqualification_limit.separate_with_commas() @@ -167,7 +167,7 @@ pub mod ordinary_diagnostic_functions { } pub fn proposed_adjusted_balance_diagnostics( - account: &WeightedPayable, + account: &WeighedPayable, proposed_adjusted_balance: u128, ) { diagnostics!( @@ -180,7 +180,7 @@ pub mod ordinary_diagnostic_functions { pub fn try_finding_an_account_to_disqualify_diagnostics( disqualification_suspected_accounts: &[DisqualificationSuspectedAccount], - wallet: &Wallet, + wallet: Address, ) { diagnostics!( "PICKED DISQUALIFIED ACCOUNT", @@ -191,7 +191,7 @@ pub mod ordinary_diagnostic_functions { } pub fn calculated_criterion_and_weight_diagnostics( - wallet_ref: &Wallet, + wallet: Address, calculator: &dyn CriterionCalculator, criterion: u128, added_in_the_sum: u128, @@ -199,7 +199,7 @@ pub mod ordinary_diagnostic_functions { const FIRST_COLUMN_WIDTH: usize = 30; diagnostics!( - wallet_ref, + wallet, "PARTIAL CRITERION CALCULATED", "For {:, +pub fn accounts_before_and_after_debug( + original_account_balances_mapped: HashMap, adjusted_accounts: &[PayableAccount], ) -> String { - fn format_summary_for_included_accounts( - original_account_balances_mapped: &HashMap, - adjusted_accounts: &[PayableAccount], - ) -> String { - adjusted_accounts - .iter() - .sorted_by(|account_a, account_b| { - Ord::cmp(&account_b.balance_wei, &account_a.balance_wei) - }) - .map(|account| { - format!( - "{} {}\n{:^length$} {}", - account.wallet, - original_account_balances_mapped - .get(&account.wallet) - .expectv("initial balance") - .separate_with_commas(), - BLANK_SPACE, - account.balance_wei.separate_with_commas(), - length = WALLET_ADDRESS_LENGTH - ) - }) - .join("\n") - } - fn format_summary_for_excluded_accounts(excluded: &[(&Wallet, u128)]) -> String { - let title = once(format!( - "\n{: String { + format!( + "{: = adjusted_accounts +fn format_summary_for_included_accounts( + original_account_balances_mapped: &HashMap, + adjusted_accounts: &[PayableAccount], +) -> String { + adjusted_accounts .iter() - .map(|account| &account.wallet) + .sorted_by(|account_a, account_b| { + // 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) + .join("\n") +} + +fn format_single_included_account( + (processed_account, original_balance): (&PayableAccount, u128), +) -> String { + format!( + "{} {}\n{:^length$} {}", + processed_account.wallet, + original_balance.separate_with_commas(), + EMPTY_STR, + processed_account.balance_wei.separate_with_commas(), + length = WALLET_ADDRESS_LENGTH + ) +} + +fn excluded_accounts_title() -> String { + format!( + "{:, + adjusted_accounts: &[PayableAccount], +) -> Vec<(Address, u128)> { + let adjusted_accounts_wallets: Vec
= adjusted_accounts + .iter() + .map(|account| account.wallet.address()) .collect(); - let excluded: Vec<(&Wallet, u128)> = original_account_balances_mapped.iter().fold( - vec![], - |mut acc, (wallet, original_balance)| { - if !adjusted_accounts_wallets.contains(&wallet) { - acc.push((wallet, *original_balance)); + original_account_balances_mapped + .iter() + .fold(vec![], |mut acc, (wallet, original_balance)| { + if !adjusted_accounts_wallets.contains(wallet) { + acc.push((*wallet, *original_balance)); } acc - }, - ); - let adjusted_accounts_summary = - format_summary_for_included_accounts(&original_account_balances_mapped, adjusted_accounts); - let excluded_accounts_summary_opt = excluded - .is_empty() - .not() - .then(|| format_summary_for_excluded_accounts(&excluded)); + }) +} + +fn format_summary_for_excluded_accounts(excluded: &[(Address, u128)]) -> String { + excluded + .iter() + .sorted_by(|(_, balance_account_a), (_, balance_account_b)| { + Ord::cmp(&balance_account_b, &balance_account_a) + }) + .map(|(wallet, original_balance)| { + format!("{:?} {}", wallet, original_balance.separate_with_commas()) + }) + .join("\n") +} + +fn write_title_and_summary(title: &str, summary: &str) -> String { + format!("\n{}\n\n{}", title, summary) +} + +fn concatenate_summaries( + adjusted_accounts_summary: String, + excluded_accounts_summary_opt: Option, +) -> String { vec![ Some(adjusted_accounts_summary), excluded_accounts_summary_opt, @@ -96,43 +144,16 @@ pub fn format_brief_adjustment_summary( .join("\n") } -pub fn accounts_before_and_after_debug( - original_account_balances_mapped: HashMap, - adjusted_accounts: &[PayableAccount], -) -> String { - format!( - "\n\ - {: for PayableAccount { fn from(qualified_payable: QualifiedPayableAccount) -> Self { qualified_payable.bare_account } } -// After transaction fee adjustment while no need to go off with the other fee, and so we keep -// the original balance, drop the weights etc. -impl From for PayableAccount { - fn from(weighted_account: WeightedPayable) -> Self { - weighted_account.analyzed_account.qualified_as.bare_account +// Transaction fee adjustment just done, but no need to go off with the other fee, so we only +// extract the original payable accounts of those retained after the adjustment. PA is done and can +// begin to return. +impl From for PayableAccount { + fn from(weighed_account: WeighedPayable) -> Self { + weighed_account.analyzed_account.qualified_as.bare_account } } +// When the consuming balance is being exhausted to zero. This represents the PA's resulted values. impl From for PayableAccount { fn from(non_finalized_adjustment: AdjustedAccountBeforeFinalization) -> Self { let mut account = non_finalized_adjustment.original_account; @@ -31,22 +33,22 @@ impl From for PayableAccount { } } -// Preparing "remaining, unresolved accounts" for another iteration that always begins with -// WeightedPayable types -impl From for WeightedPayable { +// Makes "remaining unresolved accounts" ready for another recursion that always begins with +// structures in the type of WeighedPayable +impl From for WeighedPayable { fn from(unconfirmed_adjustment: UnconfirmedAdjustment) -> Self { - unconfirmed_adjustment.weighted_account + unconfirmed_adjustment.weighed_account } } -// Used after the unconfirmed adjustment pass through all confirmations +// Used if an unconfirmed adjustment passes the confirmation impl From for AdjustedAccountBeforeFinalization { fn from(unconfirmed_adjustment: UnconfirmedAdjustment) -> Self { let proposed_adjusted_balance_minor = unconfirmed_adjustment.proposed_adjusted_balance_minor; - let weight = unconfirmed_adjustment.weighted_account.weight; + let weight = unconfirmed_adjustment.weighed_account.weight; let original_account = unconfirmed_adjustment - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account; @@ -59,18 +61,15 @@ impl From for AdjustedAccountBeforeFinalization { } } -// This is used when we detect that the upcoming iterations begins with a surplus in the remaining -// unallocated CW service fee, and therefore we grant the remaining accounts with the full balance -// they requested -impl From for AdjustedAccountBeforeFinalization { - fn from(weighted_account: WeightedPayable) -> Self { - let limited_adjusted_balance = weighted_account.disqualification_limit(); - minimal_acceptable_balance_assigned_diagnostics( - &weighted_account, - limited_adjusted_balance, - ); - let weight = weighted_account.weight; - let original_account = weighted_account.analyzed_account.qualified_as.bare_account; +// When we detect that the upcoming iterations will begin with a surplus in the remaining +// unallocated CW service fee, therefore the remaining accounts' balances are automatically granted +// 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 weight = weighed_account.weight; + let original_account = weighed_account.analyzed_account.qualified_as.bare_account; AdjustedAccountBeforeFinalization::new(original_account, weight, limited_adjusted_balance) } } @@ -79,15 +78,13 @@ impl From for AdjustedAccountBeforeFinalization { mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeightedPayable, - }; - use crate::accountant::test_utils::{ - make_non_guaranteed_qualified_payable, make_payable_account, + AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeighedPayable, }; + use crate::accountant::test_utils::{make_meaningless_qualified_payable, make_payable_account}; use crate::accountant::AnalyzedPayableAccount; #[test] - fn conversion_between_non_finalized_account_and_payable_account_is_implemented() { + fn conversion_between_non_finalized_account_and_payable_account() { let mut original_payable_account = make_payable_account(123); original_payable_account.balance_wei = 200_000_000; let non_finalized_account = AdjustedAccountBeforeFinalization::new( @@ -102,37 +99,37 @@ mod tests { assert_eq!(result, original_payable_account) } - fn prepare_weighted_account(payable_account: PayableAccount) -> WeightedPayable { + fn prepare_weighed_account(payable_account: PayableAccount) -> WeighedPayable { let garbage_disqualification_limit = 333_333_333; let garbage_weight = 777_777_777; let mut analyzed_account = AnalyzedPayableAccount::new( - make_non_guaranteed_qualified_payable(111), + make_meaningless_qualified_payable(111), garbage_disqualification_limit, ); analyzed_account.qualified_as.bare_account = payable_account; - WeightedPayable::new(analyzed_account, garbage_weight) + WeighedPayable::new(analyzed_account, garbage_weight) } #[test] - fn conversation_between_weighted_payable_and_standard_payable_account() { + fn conversation_between_weighed_payable_and_standard_payable_account() { let original_payable_account = make_payable_account(345); - let weighted_account = prepare_weighted_account(original_payable_account.clone()); + let weighed_account = prepare_weighed_account(original_payable_account.clone()); - let result = PayableAccount::from(weighted_account); + let result = PayableAccount::from(weighed_account); assert_eq!(result, original_payable_account) } #[test] - fn conversion_between_weighted_payable_and_non_finalized_account() { + fn conversion_between_weighed_payable_and_non_finalized_account() { let original_payable_account = make_payable_account(123); - let mut weighted_account = prepare_weighted_account(original_payable_account.clone()); - weighted_account + let mut weighed_account = prepare_weighed_account(original_payable_account.clone()); + weighed_account .analyzed_account .disqualification_limit_minor = 200_000_000; - weighted_account.weight = 78910; + weighed_account.weight = 78910; - let result = AdjustedAccountBeforeFinalization::from(weighted_account); + let result = AdjustedAccountBeforeFinalization::from(weighed_account); let expected_result = AdjustedAccountBeforeFinalization::new(original_payable_account, 78910, 200_000_000); @@ -143,14 +140,20 @@ mod tests { fn conversion_between_unconfirmed_adjustment_and_non_finalized_account() { let mut original_payable_account = make_payable_account(123); original_payable_account.balance_wei = 200_000_000; - let mut weighted_account = prepare_weighted_account(original_payable_account.clone()); - weighted_account.weight = 321654; - let unconfirmed_adjustment = UnconfirmedAdjustment::new(weighted_account, 111_222_333); + let mut weighed_account = prepare_weighed_account(original_payable_account.clone()); + let weight = 321654; + weighed_account.weight = weight; + let proposed_adjusted_balance_minor = 111_222_333; + let unconfirmed_adjustment = + UnconfirmedAdjustment::new(weighed_account, proposed_adjusted_balance_minor); let result = AdjustedAccountBeforeFinalization::from(unconfirmed_adjustment); - let expected_result = - AdjustedAccountBeforeFinalization::new(original_payable_account, 321654, 111_222_333); + let expected_result = AdjustedAccountBeforeFinalization::new( + original_payable_account, + weight, + proposed_adjusted_balance_minor, + ); assert_eq!(result, expected_result) } } diff --git a/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs b/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs index 0f3fd705f..597aaaf61 100644 --- a/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs +++ b/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs @@ -2,16 +2,15 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::AnalyzedPayableAccount; -use crate::sub_lib::wallet::Wallet; -use web3::types::U256; +use web3::types::{Address, U256}; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct WeightedPayable { +pub struct WeighedPayable { pub analyzed_account: AnalyzedPayableAccount, pub weight: u128, } -impl WeightedPayable { +impl WeighedPayable { pub fn new(analyzed_account: AnalyzedPayableAccount, weight: u128) -> Self { Self { analyzed_account, @@ -19,49 +18,23 @@ impl WeightedPayable { } } - pub fn wallet(&self) -> &Wallet { - &self.analyzed_account.qualified_as.bare_account.wallet + pub fn wallet(&self) -> Address { + self.analyzed_account + .qualified_as + .bare_account + .wallet + .address() } - pub fn balance_minor(&self) -> u128 { + pub fn initial_balance_minor(&self) -> u128 { self.analyzed_account.qualified_as.bare_account.balance_wei } } #[derive(Debug, PartialEq, Eq)] pub struct AdjustmentIterationResult { - pub decided_accounts: DecidedAccounts, - pub remaining_undecided_accounts: Vec, -} - -pub struct RecursionResults { - pub here_decided_accounts: Vec, - pub downstream_decided_accounts: Vec, -} - -impl RecursionResults { - pub fn new( - here_decided_accounts: Vec, - downstream_decided_accounts: Vec, - ) -> Self { - Self { - here_decided_accounts, - downstream_decided_accounts, - } - } - - pub fn merge_results_from_recursion(self) -> Vec { - self.here_decided_accounts - .into_iter() - .chain(self.downstream_decided_accounts.into_iter()) - .collect() - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum DecidedAccounts { - LowGainingAccountEliminated, - SomeAccountsProcessed(Vec), + pub decided_accounts: Vec, + pub remaining_undecided_accounts: Vec, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -87,41 +60,41 @@ impl AdjustedAccountBeforeFinalization { #[derive(Debug, PartialEq, Eq, Clone)] pub struct UnconfirmedAdjustment { - pub weighted_account: WeightedPayable, + pub weighed_account: WeighedPayable, pub proposed_adjusted_balance_minor: u128, } impl UnconfirmedAdjustment { - pub fn new(weighted_account: WeightedPayable, proposed_adjusted_balance_minor: u128) -> Self { + pub fn new(weighed_account: WeighedPayable, proposed_adjusted_balance_minor: u128) -> Self { Self { - weighted_account, + weighed_account, proposed_adjusted_balance_minor, } } - pub fn wallet(&self) -> &Wallet { - self.weighted_account.wallet() + pub fn wallet(&self) -> Address { + self.weighed_account.wallet() } - pub fn balance_minor(&self) -> u128 { - self.weighted_account.balance_minor() + pub fn initial_balance_minor(&self) -> u128 { + self.weighed_account.initial_balance_minor() } pub fn disqualification_limit_minor(&self) -> u128 { - self.weighted_account + self.weighed_account .analyzed_account .disqualification_limit_minor } } -pub struct TransactionCountsBy16bits { +pub struct AffordableAndRequiredTxCounts { pub affordable: u16, pub required: u16, } -impl TransactionCountsBy16bits { +impl AffordableAndRequiredTxCounts { pub fn new(max_possible_tx_count: U256, number_of_accounts: usize) -> Self { - TransactionCountsBy16bits { + AffordableAndRequiredTxCounts { affordable: u16::try_from(max_possible_tx_count).unwrap_or(u16::MAX), required: u16::try_from(number_of_accounts).unwrap_or(u16::MAX), } @@ -130,49 +103,20 @@ impl TransactionCountsBy16bits { #[cfg(test)] mod tests { - use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustedAccountBeforeFinalization, RecursionResults, TransactionCountsBy16bits, - }; - use crate::accountant::test_utils::make_payable_account; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::AffordableAndRequiredTxCounts; use ethereum_types::U256; - #[test] - fn merging_results_from_recursion_works() { - let non_finalized_account_1 = - AdjustedAccountBeforeFinalization::new(make_payable_account(111), 12345, 1234); - let non_finalized_account_2 = - AdjustedAccountBeforeFinalization::new(make_payable_account(222), 543, 5555); - let non_finalized_account_3 = - AdjustedAccountBeforeFinalization::new(make_payable_account(333), 789987, 6789); - let subject = RecursionResults { - here_decided_accounts: vec![non_finalized_account_1.clone()], - downstream_decided_accounts: vec![ - non_finalized_account_2.clone(), - non_finalized_account_3.clone(), - ], - }; - - let result = subject.merge_results_from_recursion(); - - assert_eq!( - result, - vec![ - non_finalized_account_1, - non_finalized_account_2, - non_finalized_account_3 - ] - ) - } - #[test] fn there_is_u16_ceiling_for_possible_tx_count() { let corrections_from_u16_max = [-3_i8, -1, 0, 1, 10]; - let result = corrections_from_u16_max + let prepared_input_numbers = corrections_from_u16_max .into_iter() .map(plus_minus_correction_for_u16_max) - .map(U256::from) + .map(U256::from); + let result = prepared_input_numbers .map(|max_possible_tx_count| { - let detected_tx_counts = TransactionCountsBy16bits::new(max_possible_tx_count, 123); + let detected_tx_counts = + AffordableAndRequiredTxCounts::new(max_possible_tx_count, 123); detected_tx_counts.affordable }) .collect::>(); @@ -186,12 +130,13 @@ mod tests { #[test] fn there_is_u16_ceiling_for_required_number_of_accounts() { let corrections_from_u16_max = [-9_i8, -1, 0, 1, 5]; - let result = corrections_from_u16_max + let right_input_numbers = corrections_from_u16_max .into_iter() - .map(plus_minus_correction_for_u16_max) + .map(plus_minus_correction_for_u16_max); + let result = right_input_numbers .map(|required_tx_count_usize| { let detected_tx_counts = - TransactionCountsBy16bits::new(U256::from(123), required_tx_count_usize); + AffordableAndRequiredTxCounts::new(U256::from(123), required_tx_count_usize); detected_tx_counts.required }) .collect::>(); @@ -204,7 +149,7 @@ mod tests { fn plus_minus_correction_for_u16_max(correction: i8) -> usize { if correction < 0 { - (u16::MAX - correction.abs() as u16) as usize + u16::MAX as usize - (correction.abs() as usize) } else { u16::MAX as usize + correction as usize } diff --git a/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs b/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs index b8a3d4c27..c372d5108 100644 --- a/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs +++ b/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs @@ -1,15 +1,16 @@ // Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +use std::iter::Sum; use crate::accountant::db_access_objects::payable_dao::PayableAccount; 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, WeightedPayable}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AdjustedAccountBeforeFinalization, WeighedPayable}; use crate::accountant::{AnalyzedPayableAccount}; use itertools::{Either, Itertools}; -pub fn zero_affordable_accounts_found( +pub fn no_affordable_accounts_found( accounts: &Either, Vec>, ) -> bool { match accounts { @@ -20,23 +21,17 @@ pub fn zero_affordable_accounts_found( pub fn sum_as(collection: &[T], arranger: F) -> N where - N: From, - F: Fn(&T) -> u128, + N: Sum, + F: Fn(&T) -> N, { - collection.iter().map(arranger).sum::().into() + collection.iter().map(arranger).sum::() } -pub fn weights_total(weights_and_accounts: &[WeightedPayable]) -> u128 { - sum_as(weights_and_accounts, |weighted_account| { - weighted_account.weight - }) -} - -pub fn dump_unaffordable_accounts_by_transaction_fee( - weighted_accounts: Vec, +pub fn eliminate_accounts_by_tx_fee_limit( + weighed_accounts: Vec, affordable_transaction_count: u16, -) -> Vec { - let sorted_accounts = sort_in_descendant_order_by_weights(weighted_accounts); +) -> Vec { + let sorted_accounts = sort_in_descending_order_by_weights(weighed_accounts); diagnostics!( "ACCOUNTS CUTBACK FOR TRANSACTION FEE", @@ -54,7 +49,7 @@ pub fn dump_unaffordable_accounts_by_transaction_fee( .collect() } -fn sort_in_descendant_order_by_weights(unsorted: Vec) -> Vec { +fn sort_in_descending_order_by_weights(unsorted: Vec) -> Vec { unsorted .into_iter() .sorted_by(|account_a, account_b| Ord::cmp(&account_b.weight, &account_a.weight)) @@ -79,19 +74,7 @@ pub fn find_largest_exceeding_balance(qualified_accounts: &[AnalyzedPayableAccou .expect("should be: balance > intercept!") }) .collect::>(); - find_largest_u128(&diffs) -} - -fn find_largest_u128(slice: &[u128]) -> u128 { - slice - .iter() - .fold(0, |largest_so_far, num| largest_so_far.max(*num)) -} - -pub fn find_smallest_u128(slice: &[u128]) -> u128 { - slice - .iter() - .fold(u128::MAX, |smallest_so_far, num| smallest_so_far.min(*num)) + *diffs.iter().max().expect("No account found") } pub fn exhaust_cw_balance_entirely( @@ -102,7 +85,7 @@ pub fn exhaust_cw_balance_entirely( account_info.proposed_adjusted_balance_minor }); - let cw_reminder = original_cw_service_fee_balance_minor + let cw_remaining = original_cw_service_fee_balance_minor .checked_sub(adjusted_balances_total) .unwrap_or_else(|| { panic!( @@ -111,7 +94,7 @@ pub fn exhaust_cw_balance_entirely( ) }); - let init = ConsumingWalletExhaustingStatus::new(cw_reminder); + let init = ConsumingWalletExhaustingStatus::new(cw_remaining); approved_accounts .into_iter() .sorted_by(|info_a, info_b| Ord::cmp(&info_b.weight, &info_a.weight)) @@ -126,7 +109,7 @@ fn run_cw_exhausting_on_possibly_sub_optimal_adjusted_balances( status: ConsumingWalletExhaustingStatus, non_finalized_account: AdjustedAccountBeforeFinalization, ) -> ConsumingWalletExhaustingStatus { - if status.remainder != 0 { + if !status.is_cw_exhausted_to_0() { let balance_gap_minor = non_finalized_account .original_account .balance_wei @@ -139,15 +122,20 @@ fn run_cw_exhausting_on_possibly_sub_optimal_adjusted_balances( non_finalized_account.original_account.balance_wei ) }); - let possible_extra_addition = if balance_gap_minor < status.remainder { + let possible_extra_addition = if balance_gap_minor < status.remaining_cw_balance { balance_gap_minor } else { - status.remainder + status.remaining_cw_balance }; exhausting_cw_balance_diagnostics(&non_finalized_account, possible_extra_addition); - status.handle_balance_update_and_add(non_finalized_account, possible_extra_addition) + let updated_non_finalized_account = ConsumingWalletExhaustingStatus::update_account_balance( + non_finalized_account, + possible_extra_addition, + ); + let updated_status = status.reduce_cw_balance_remaining(possible_extra_addition); + updated_status.add(updated_non_finalized_account) } else { not_exhausting_cw_balance_diagnostics(&non_finalized_account); @@ -156,32 +144,36 @@ fn run_cw_exhausting_on_possibly_sub_optimal_adjusted_balances( } struct ConsumingWalletExhaustingStatus { - remainder: u128, + remaining_cw_balance: u128, accounts_finalized_so_far: Vec, } impl ConsumingWalletExhaustingStatus { - fn new(remainder: u128) -> Self { + fn new(remaining_cw_balance: u128) -> Self { Self { - remainder, + remaining_cw_balance, accounts_finalized_so_far: vec![], } } - fn handle_balance_update_and_add( - mut self, - mut non_finalized_account_info: AdjustedAccountBeforeFinalization, - possible_extra_addition: u128, - ) -> Self { - let corrected_adjusted_account_before_finalization = { - non_finalized_account_info.proposed_adjusted_balance_minor += possible_extra_addition; - non_finalized_account_info - }; - self.remainder = self - .remainder - .checked_sub(possible_extra_addition) + fn is_cw_exhausted_to_0(&self) -> bool { + self.remaining_cw_balance == 0 + } + + fn reduce_cw_balance_remaining(mut self, subtrahend: u128) -> Self { + self.remaining_cw_balance = self + .remaining_cw_balance + .checked_sub(subtrahend) .expect("we hit zero"); - self.add(corrected_adjusted_account_before_finalization) + self + } + + fn update_account_balance( + mut non_finalized_account: AdjustedAccountBeforeFinalization, + addition: u128, + ) -> AdjustedAccountBeforeFinalization { + non_finalized_account.proposed_adjusted_balance_minor += addition; + non_finalized_account } fn add(mut self, non_finalized_account_info: AdjustedAccountBeforeFinalization) -> Self { @@ -195,28 +187,27 @@ 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::helper_functions::{ - compute_mul_coefficient_preventing_fractional_numbers, - dump_unaffordable_accounts_by_transaction_fee, exhaust_cw_balance_entirely, - find_largest_exceeding_balance, find_largest_u128, find_smallest_u128, - zero_affordable_accounts_found, ConsumingWalletExhaustingStatus, + compute_mul_coefficient_preventing_fractional_numbers, eliminate_accounts_by_tx_fee_limit, + exhaust_cw_balance_entirely, find_largest_exceeding_balance, no_affordable_accounts_found, + ConsumingWalletExhaustingStatus, }; - use crate::accountant::payment_adjuster::test_utils::make_weighed_account; - use crate::accountant::test_utils::{make_analyzed_account, make_payable_account}; + use crate::accountant::payment_adjuster::test_utils::local_utils::make_meaningless_weighed_account; + 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 std::time::SystemTime; #[test] - fn found_zero_affordable_accounts_found_returns_true_for_non_finalized_accounts() { - let result = zero_affordable_accounts_found(&Either::Left(vec![])); + fn no_affordable_accounts_found_found_returns_true_for_non_finalized_accounts() { + let result = no_affordable_accounts_found(&Either::Left(vec![])); assert_eq!(result, true) } #[test] - fn zero_affordable_accounts_found_returns_false_for_non_finalized_accounts() { - let result = zero_affordable_accounts_found(&Either::Left(vec![ + fn no_affordable_accounts_found_returns_false_for_non_finalized_accounts() { + let result = no_affordable_accounts_found(&Either::Left(vec![ AdjustedAccountBeforeFinalization::new(make_payable_account(456), 5678, 1234), ])); @@ -224,76 +215,53 @@ mod tests { } #[test] - fn found_zero_affordable_accounts_returns_true_for_finalized_accounts() { - let result = zero_affordable_accounts_found(&Either::Right(vec![])); + fn no_affordable_accounts_found_returns_true_for_finalized_accounts() { + let result = no_affordable_accounts_found(&Either::Right(vec![])); assert_eq!(result, true) } #[test] - fn found_zero_affordable_accounts_returns_false_for_finalized_accounts() { - let result = - zero_affordable_accounts_found(&Either::Right(vec![make_payable_account(123)])); + fn no_affordable_accounts_found_returns_false_for_finalized_accounts() { + let result = no_affordable_accounts_found(&Either::Right(vec![make_payable_account(123)])); assert_eq!(result, false) } - #[test] - fn find_largest_u128_begins_with_zero() { - let result = find_largest_u128(&[]); - - assert_eq!(result, 0) - } - - #[test] - fn find_largest_u128_works() { - let result = find_largest_u128(&[45, 2, 456565, 0, 2, 456565, 456564]); - - assert_eq!(result, 456565) - } - - #[test] - fn find_smallest_u128_begins_with_u128_max() { - let result = find_smallest_u128(&[]); - - assert_eq!(result, u128::MAX) - } - - #[test] - fn find_smallest_u128_works() { - let result = find_smallest_u128(&[45, 1112, 456565, 3, 7, 456565, 456564]); - - assert_eq!(result, 3) - } - #[test] fn find_largest_exceeding_balance_works() { - let mut account_1 = make_analyzed_account(111); + let mut account_1 = make_meaningless_analyzed_account(111); account_1.qualified_as.bare_account.balance_wei = 5_000_000_000; - account_1.qualified_as.payment_threshold_intercept_minor = 2_000_000_000; - let mut account_2 = make_analyzed_account(222); - account_2.qualified_as.bare_account.balance_wei = 4_000_000_000; - account_2.qualified_as.payment_threshold_intercept_minor = 800_000_000; - let qualified_accounts = &[account_1, account_2]; + account_1.qualified_as.payment_threshold_intercept_minor = 2_000_000_001; + let mut account_2 = make_meaningless_analyzed_account(222); + account_2.qualified_as.bare_account.balance_wei = 5_000_000_000; + account_2.qualified_as.payment_threshold_intercept_minor = 2_000_000_001; + let mut account_3 = make_meaningless_analyzed_account(333); + account_3.qualified_as.bare_account.balance_wei = 5_000_000_000; + account_3.qualified_as.payment_threshold_intercept_minor = 1_999_999_999; + let mut account_4 = make_meaningless_analyzed_account(444); + account_4.qualified_as.bare_account.balance_wei = 5_000_000_000; + account_4.qualified_as.payment_threshold_intercept_minor = 2_000_000_000; + let qualified_accounts = &[account_1, account_2, account_3, account_4]; let result = find_largest_exceeding_balance(qualified_accounts); - assert_eq!(result, 4_000_000_000 - 800_000_000) + assert_eq!(result, 5_000_000_000 - 1_999_999_999) } #[test] - fn dump_unaffordable_accounts_by_transaction_fee_works() { - let mut account_1 = make_weighed_account(123); + fn eliminate_accounts_by_tx_fee_limit_works() { + let mut account_1 = make_meaningless_weighed_account(123); account_1.weight = 1_000_000_000; - let mut account_2 = make_weighed_account(456); + let mut account_2 = make_meaningless_weighed_account(456); account_2.weight = 999_999_999; - let mut account_3 = make_weighed_account(789); + let mut account_3 = make_meaningless_weighed_account(789); account_3.weight = 999_999_999; - let mut account_4 = make_weighed_account(1011); + let mut account_4 = make_meaningless_weighed_account(1011); account_4.weight = 1_000_000_001; let affordable_transaction_count = 2; - let result = dump_unaffordable_accounts_by_transaction_fee( + let result = eliminate_accounts_by_tx_fee_limit( vec![account_1.clone(), account_2, account_3, account_4.clone()], affordable_transaction_count, ); @@ -309,8 +277,10 @@ mod tests { let result = compute_mul_coefficient_preventing_fractional_numbers(cw_service_fee_balance_minor); - let expected_result = u128::MAX / cw_service_fee_balance_minor; - assert_eq!(result, expected_result) + let expected_result_conceptually = u128::MAX / cw_service_fee_balance_minor; + let expected_result_exact = 27562873980751681962171264100016; + assert_eq!(result, expected_result_exact); + assert_eq!(expected_result_exact, expected_result_conceptually) } fn make_non_finalized_adjusted_account( @@ -353,23 +323,24 @@ mod tests { #[test] fn exhaustive_status_is_constructed_properly() { - let cw_balance_remainder = 45678; + let cw_remaining_balance = 45678; - let result = ConsumingWalletExhaustingStatus::new(cw_balance_remainder); + let result = ConsumingWalletExhaustingStatus::new(cw_remaining_balance); - assert_eq!(result.remainder, cw_balance_remainder); + assert_eq!(result.remaining_cw_balance, cw_remaining_balance); assert_eq!(result.accounts_finalized_so_far, vec![]) } #[test] - fn three_non_exhaustive_accounts_all_refilled() { - // A seemingly irrational situation, this can happen when some of those originally qualified - // payables could get disqualified. Those would free some means that could be used for - // the other accounts. In the end, we have a final set with suboptimal balances, despite - // the unallocated cw balance is larger than the entire sum of the original balances for - // this few resulting accounts. We can pay every account fully, so, why did we need to call - // the PaymentAdjuster in first place? The detail is in the loss of some accounts, allowing - // to pay more for the others. + fn proposed_balance_refills_up_to_original_balance_for_all_three_non_exhaustive_accounts() { + // Despite looking irrational, this can happen if some of those originally qualified + // payables were eliminated. That would free some assets to be eventually used for + // the accounts left. Going forward, we've got a confirmed final accounts but with + // suboptimal balances caused by, so far, declaring them by their disqualification limits + // and no more. Therefore, we can live on a situation where the consuming wallet balance is + // more than the final, already reduced, set of accounts. This tested operation should + // ensure that the available assets will be given out maximally, resulting in a total + // pay-off on those selected accounts. let wallet_1 = make_wallet("abc"); let original_requested_balance_1 = 45_000_000_000; let proposed_adjusted_balance_1 = 44_999_897_000; diff --git a/node/src/accountant/payment_adjuster/mod.rs b/node/src/accountant/payment_adjuster/mod.rs index 878bd9721..77307b8aa 100644 --- a/node/src/accountant/payment_adjuster/mod.rs +++ b/node/src/accountant/payment_adjuster/mod.rs @@ -1,7 +1,6 @@ // Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -// If possible, let these modules be private -mod adjustment_runners; +// If possible, keep these modules private mod criterion_calculators; mod disqualification_arbiter; mod inner; @@ -11,13 +10,11 @@ mod miscellaneous; mod non_unit_tests; mod preparatory_analyser; mod service_fee_adjuster; +// Intentionally public #[cfg(test)] -mod test_utils; +pub mod test_utils; use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::payment_adjuster::adjustment_runners::{ - AdjustmentRunner, ServiceFeeOnlyAdjustmentRunner, TransactionAndServiceFeeAdjustmentRunner, -}; use crate::accountant::payment_adjuster::criterion_calculators::balance_calculator::BalanceCriterionCalculator; use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::calculated_criterion_and_weight_diagnostics; @@ -26,19 +23,16 @@ use crate::accountant::payment_adjuster::disqualification_arbiter::{ DisqualificationArbiter, }; use crate::accountant::payment_adjuster::inner::{ - PaymentAdjusterInner, PaymentAdjusterInnerNull, PaymentAdjusterInnerReal, + PaymentAdjusterInner, }; use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::{ accounts_before_and_after_debug, }; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::DecidedAccounts::{ - LowGainingAccountEliminated, SomeAccountsProcessed, -}; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AdjustedAccountBeforeFinalization, AdjustmentIterationResult, RecursionResults, WeightedPayable}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AdjustedAccountBeforeFinalization, WeighedPayable}; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ - dump_unaffordable_accounts_by_transaction_fee, + eliminate_accounts_by_tx_fee_limit, exhaust_cw_balance_entirely, find_largest_exceeding_balance, - sum_as, zero_affordable_accounts_found, + sum_as, no_affordable_accounts_found, }; use crate::accountant::payment_adjuster::preparatory_analyser::{LateServiceFeeSingleTxErrorFactory, PreparatoryAnalyzer}; use crate::accountant::payment_adjuster::service_fee_adjuster::{ @@ -49,39 +43,43 @@ use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::Prepare use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; use crate::diagnostics; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use crate::sub_lib::wallet::Wallet; use itertools::Either; use masq_lib::logger::Logger; use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::time::SystemTime; use thousands::Separable; use variant_count::VariantCount; -use web3::types::U256; +use web3::types::{Address, U256}; use masq_lib::utils::convert_collection; - -// PaymentAdjuster is a very efficient recursive and scalable algorithm that inspects payments under -// the condition of an acute insolvency. You can expand the scope of the evaluation by writing your -// own CriterionCalculator, that should be specialized on a distinct parameter of a payable account, -// and sticking it inside the vector that stores them. +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. +// +// 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. +// +// Once the new calculator exists, its place belongs in the vector of calculators which is the heart +// of this module. pub type AdjustmentAnalysisResult = - Result, AdjustmentAnalysis>, PaymentAdjusterError>; + Result, PaymentAdjusterError>; + +pub type IntactOriginalAccounts = Vec; pub trait PaymentAdjuster { - fn search_for_indispensable_adjustment( + fn consider_adjustment( &self, qualified_payables: Vec, agent: &dyn BlockchainAgent, ) -> AdjustmentAnalysisResult; fn adjust_payments( - &mut self, + &self, setup: PreparedAdjustment, - now: SystemTime, ) -> Result; - - as_any_ref_in_trait!(); } pub struct PaymentAdjusterReal { @@ -89,12 +87,12 @@ pub struct PaymentAdjusterReal { disqualification_arbiter: DisqualificationArbiter, service_fee_adjuster: Box, calculators: Vec>, - inner: Box, + inner: PaymentAdjusterInner, logger: Logger, } impl PaymentAdjuster for PaymentAdjusterReal { - fn search_for_indispensable_adjustment( + fn consider_adjustment( &self, qualified_payables: Vec, agent: &dyn BlockchainAgent, @@ -107,32 +105,30 @@ impl PaymentAdjuster for PaymentAdjusterReal { } fn adjust_payments( - &mut self, + &self, setup: PreparedAdjustment, - now: SystemTime, ) -> Result { let analyzed_payables = setup.adjustment_analysis.accounts; let response_skeleton_opt = setup.response_skeleton_opt; let agent = setup.agent; let initial_service_fee_balance_minor = agent.service_fee_balance_minor(); let required_adjustment = setup.adjustment_analysis.adjustment; - let largest_exceeding_balance_recently_qualified = + let max_debt_above_threshold_in_qualified_payables_minor = find_largest_exceeding_balance(&analyzed_payables); self.initialize_inner( - initial_service_fee_balance_minor, required_adjustment, - largest_exceeding_balance_recently_qualified, - now, + initial_service_fee_balance_minor, + max_debt_above_threshold_in_qualified_payables_minor, ); - let sketched_debug_info_opt = self.sketch_debug_info_opt(&analyzed_payables); + let sketched_debug_log_opt = self.sketch_debug_log_opt(&analyzed_payables); let affordable_accounts = self.run_adjustment(analyzed_payables)?; - self.complete_debug_info_if_enabled(sketched_debug_info_opt, &affordable_accounts); + self.complete_debug_log_if_enabled(sketched_debug_log_opt, &affordable_accounts); - self.reset_inner(); + self.inner.invalidate_guts(); Ok(OutboundPaymentsInstructions::new( Either::Right(affordable_accounts), @@ -140,8 +136,6 @@ impl PaymentAdjuster for PaymentAdjusterReal { response_skeleton_opt, )) } - - as_any_ref_in_trait_impl!(); } impl Default for PaymentAdjusterReal { @@ -157,51 +151,40 @@ impl PaymentAdjusterReal { disqualification_arbiter: DisqualificationArbiter::default(), service_fee_adjuster: Box::new(ServiceFeeAdjusterReal::default()), calculators: vec![Box::new(BalanceCriterionCalculator::default())], - inner: Box::new(PaymentAdjusterInnerNull::default()), + inner: PaymentAdjusterInner::default(), logger: Logger::new("PaymentAdjuster"), } } fn initialize_inner( - &mut self, - cw_service_fee_balance: u128, + &self, required_adjustment: Adjustment, - largest_exceeding_balance_recently_qualified: u128, - now: SystemTime, + initial_service_fee_balance_minor: u128, + max_debt_above_threshold_in_qualified_payables_minor: u128, ) { let transaction_fee_limitation_opt = match required_adjustment { - Adjustment::TransactionFeeInPriority { - affordable_transaction_count, - } => Some(affordable_transaction_count), + Adjustment::BeginByTransactionFee { + transaction_count_limit, + } => Some(transaction_count_limit), Adjustment::ByServiceFee => None, }; - let inner = PaymentAdjusterInnerReal::new( - now, + self.inner.initialize_guts( transaction_fee_limitation_opt, - cw_service_fee_balance, - largest_exceeding_balance_recently_qualified, - ); - - self.inner = Box::new(inner); - } - - fn reset_inner(&mut self) { - self.inner = Box::new(PaymentAdjusterInnerNull::default()) + initial_service_fee_balance_minor, + max_debt_above_threshold_in_qualified_payables_minor, + ) } fn run_adjustment( - &mut self, + &self, analyzed_accounts: Vec, ) -> Result, PaymentAdjusterError> { - let weighted_accounts = self.calculate_weights(analyzed_accounts); - let processed_accounts = self.propose_adjustments_recursively( - weighted_accounts, - TransactionAndServiceFeeAdjustmentRunner {}, - )?; - - if zero_affordable_accounts_found(&processed_accounts) { - return Err(PaymentAdjusterError::AllAccountsEliminated); + let weighed_accounts = self.calculate_weights(analyzed_accounts); + let processed_accounts = self.resolve_initial_adjustment_dispatch(weighed_accounts)?; + + if no_affordable_accounts_found(&processed_accounts) { + return Err(PaymentAdjusterError::RecursionDrainedAllAccounts); } match processed_accounts { @@ -218,81 +201,105 @@ impl PaymentAdjusterReal { } } - fn propose_adjustments_recursively( - &mut self, - unresolved_accounts: Vec, - adjustment_runner: AR, - ) -> RT - where - AR: AdjustmentRunner, - { - diagnostics!( - "\nUNRESOLVED QUALIFIED ACCOUNTS IN CURRENT ITERATION:", - &unresolved_accounts - ); + fn resolve_initial_adjustment_dispatch( + &self, + weighed_payables: Vec, + ) -> Result< + Either, Vec>, + PaymentAdjusterError, + > { + if let Some(limit) = self.inner.transaction_count_limit_opt() { + return self.begin_with_adjustment_by_transaction_fee(weighed_payables, limit); + } - adjustment_runner.adjust_accounts(self, unresolved_accounts) + Ok(Either::Left( + self.propose_possible_adjustment_recursively(weighed_payables), + )) } fn begin_with_adjustment_by_transaction_fee( - &mut self, - weighted_accounts: Vec, - already_known_affordable_transaction_count: u16, + &self, + weighed_accounts: Vec, + transaction_count_limit: u16, ) -> Result< Either, Vec>, PaymentAdjusterError, > { - let error_factory = LateServiceFeeSingleTxErrorFactory::new(&weighted_accounts); + diagnostics!( + "\nBEGINNING WITH ADJUSTMENT BY TRANSACTION FEE FOR ACCOUNTS:", + &weighed_accounts + ); - let weighted_accounts_affordable_by_transaction_fee = - dump_unaffordable_accounts_by_transaction_fee( - weighted_accounts, - already_known_affordable_transaction_count, - ); + let error_factory = LateServiceFeeSingleTxErrorFactory::new(&weighed_accounts); + + let weighed_accounts_affordable_by_transaction_fee = + eliminate_accounts_by_tx_fee_limit(weighed_accounts, transaction_count_limit); let cw_service_fee_balance_minor = self.inner.original_cw_service_fee_balance_minor(); if self.analyzer.recheck_if_service_fee_adjustment_is_needed( - &weighted_accounts_affordable_by_transaction_fee, + &weighed_accounts_affordable_by_transaction_fee, cw_service_fee_balance_minor, error_factory, &self.logger, )? { - diagnostics!("STILL NECESSARY TO CONTINUE BY ADJUSTMENT IN BALANCES"); - - let adjustment_result_before_verification = self + let final_set_before_exhausting_cw_balance = self .propose_possible_adjustment_recursively( - weighted_accounts_affordable_by_transaction_fee, + weighed_accounts_affordable_by_transaction_fee, ); - Ok(Either::Left(adjustment_result_before_verification)) + Ok(Either::Left(final_set_before_exhausting_cw_balance)) } else { let accounts_not_needing_adjustment = - convert_collection(weighted_accounts_affordable_by_transaction_fee); + convert_collection(weighed_accounts_affordable_by_transaction_fee); Ok(Either::Right(accounts_not_needing_adjustment)) } } fn propose_possible_adjustment_recursively( - &mut self, - weighed_accounts: Vec, + &self, + weighed_accounts: Vec, ) -> Vec { + diagnostics!( + "\nUNRESOLVED ACCOUNTS IN CURRENT ITERATION:", + &weighed_accounts + ); + let disqualification_arbiter = &self.disqualification_arbiter; - let unallocated_cw_service_fee_balance = - self.inner.unallocated_cw_service_fee_balance_minor(); + let remaining_cw_service_fee_balance = self.inner.remaining_cw_service_fee_balance_minor(); let logger = &self.logger; let current_iteration_result = self.service_fee_adjuster.perform_adjustment_by_service_fee( weighed_accounts, disqualification_arbiter, - unallocated_cw_service_fee_balance, + remaining_cw_service_fee_balance, logger, ); - let recursion_results = self.resolve_current_iteration_result(current_iteration_result); + let decided_accounts = current_iteration_result.decided_accounts; + let remaining_undecided_accounts = current_iteration_result.remaining_undecided_accounts; - let merged = recursion_results.merge_results_from_recursion(); + if remaining_undecided_accounts.is_empty() { + return decided_accounts; + } + + if !decided_accounts.is_empty() { + self.adjust_remaining_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), + ) + }; diagnostics!( "\nFINAL SET OF ADJUSTED ACCOUNTS IN CURRENT ITERATION:", @@ -302,38 +309,26 @@ impl PaymentAdjusterReal { merged } - fn resolve_current_iteration_result( - &mut self, - adjustment_iteration_result: AdjustmentIterationResult, - ) -> RecursionResults { - let remaining_undecided_accounts = adjustment_iteration_result.remaining_undecided_accounts; - let here_decided_accounts = match adjustment_iteration_result.decided_accounts { - LowGainingAccountEliminated => { - if remaining_undecided_accounts.is_empty() { - return RecursionResults::new(vec![], vec![]); - } - - vec![] - } - SomeAccountsProcessed(decided_accounts) => { - if remaining_undecided_accounts.is_empty() { - return RecursionResults::new(decided_accounts, vec![]); - } - - self.adjust_remaining_unallocated_cw_balance_down(&decided_accounts); - decided_accounts - } - }; - - let down_stream_decided_accounts = self.propose_adjustments_recursively( - remaining_undecided_accounts, - ServiceFeeOnlyAdjustmentRunner {}, - ); + fn is_cw_balance_enough_to_remaining_accounts( + &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() + }); + minimum_sum_required <= remaining_cw_service_fee_balance + } - RecursionResults::new(here_decided_accounts, down_stream_decided_accounts) + fn merge_accounts( + mut previously_decided_accounts: Vec, + newly_decided_accounts: Vec, + ) -> Vec { + previously_decided_accounts.extend(newly_decided_accounts); + previously_decided_accounts } - fn calculate_weights(&self, accounts: Vec) -> Vec { + fn calculate_weights(&self, accounts: Vec) -> Vec { self.apply_criteria(self.calculators.as_slice(), accounts) } @@ -341,7 +336,7 @@ impl PaymentAdjusterReal { &self, criteria_calculators: &[Box], qualified_accounts: Vec, - ) -> Vec { + ) -> Vec { qualified_accounts .into_iter() .map(|payable| { @@ -349,13 +344,13 @@ impl PaymentAdjusterReal { criteria_calculators .iter() .fold(0_u128, |weight, criterion_calculator| { - let new_criterion = criterion_calculator - .calculate(&payable.qualified_as, self.inner.as_ref()); + let new_criterion = + criterion_calculator.calculate(&payable.qualified_as, &self.inner); let summed_up = weight + new_criterion; calculated_criterion_and_weight_diagnostics( - &payable.qualified_as.bare_account.wallet, + payable.qualified_as.bare_account.wallet.address(), criterion_calculator.as_ref(), new_criterion, summed_up, @@ -364,57 +359,57 @@ impl PaymentAdjusterReal { summed_up }); - WeightedPayable::new(payable, weight) + WeighedPayable::new(payable, weight) }) .collect() } - fn adjust_remaining_unallocated_cw_balance_down( - &mut self, - processed_outweighed: &[AdjustedAccountBeforeFinalization], + fn adjust_remaining_remaining_cw_balance_down( + &self, + decided_accounts: &[AdjustedAccountBeforeFinalization], ) { - let subtrahend_total: u128 = sum_as(processed_outweighed, |account| { + let subtrahend_total: u128 = sum_as(decided_accounts, |account| { account.proposed_adjusted_balance_minor }); self.inner - .subtract_from_unallocated_cw_service_fee_balance_minor(subtrahend_total); + .subtract_from_remaining_cw_service_fee_balance_minor(subtrahend_total); diagnostics!( "LOWERED CW BALANCE", "Unallocated balance lowered by {} to {}", subtrahend_total.separate_with_commas(), self.inner - .unallocated_cw_service_fee_balance_minor() + .remaining_cw_service_fee_balance_minor() .separate_with_commas() ) } - fn sketch_debug_info_opt( + fn sketch_debug_log_opt( &self, qualified_payables: &[AnalyzedPayableAccount], - ) -> Option> { + ) -> Option> { self.logger.debug_enabled().then(|| { qualified_payables .iter() .map(|payable| { ( - payable.qualified_as.bare_account.wallet.clone(), + payable.qualified_as.bare_account.wallet.address(), payable.qualified_as.bare_account.balance_wei, ) }) - .collect::>() + .collect() }) } - fn complete_debug_info_if_enabled( + fn complete_debug_log_if_enabled( &self, - sketched_debug_info_opt: Option>, - affordable_accounts: &[PayableAccount], + sketched_debug_info_opt: Option>, + fully_processed_accounts: &[PayableAccount], ) { self.logger.debug(|| { let sketched_debug_info = sketched_debug_info_opt.expect("debug is enabled, so info should exist"); - accounts_before_and_after_debug(sketched_debug_info, affordable_accounts) + accounts_before_and_after_debug(sketched_debug_info, fully_processed_accounts) }) } } @@ -422,18 +417,18 @@ impl PaymentAdjusterReal { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Adjustment { ByServiceFee, - TransactionFeeInPriority { affordable_transaction_count: u16 }, + BeginByTransactionFee { transaction_count_limit: u16 }, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AdjustmentAnalysis { +pub struct AdjustmentAnalysisReport { pub adjustment: Adjustment, pub accounts: Vec, } -impl AdjustmentAnalysis { +impl AdjustmentAnalysisReport { pub fn new(adjustment: Adjustment, accounts: Vec) -> Self { - AdjustmentAnalysis { + AdjustmentAnalysisReport { adjustment, accounts, } @@ -442,18 +437,18 @@ impl AdjustmentAnalysis { #[derive(Debug, PartialEq, Eq, VariantCount)] pub enum PaymentAdjusterError { - EarlyNotEnoughFeeForSingleTransaction { + AbsolutelyInsufficientBalance { number_of_accounts: usize, transaction_fee_opt: Option, service_fee_opt: Option, }, - LateNotEnoughFeeForSingleTransaction { + AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts: usize, number_of_accounts: usize, - original_service_fee_required_total_minor: u128, + original_total_service_fee_required_minor: u128, cw_service_fee_balance_minor: u128, }, - AllAccountsEliminated, + RecursionDrainedAllAccounts, } #[derive(Debug, PartialEq, Eq)] @@ -471,14 +466,17 @@ pub struct ServiceFeeImmoderateInsufficiency { impl PaymentAdjusterError { pub fn insolvency_detected(&self) -> bool { match self { - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { .. } => true, - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { .. } => true, - PaymentAdjusterError::AllAccountsEliminated => true, - // We haven't needed to worry so yet, but adding an error not implying that - // an insolvency was found out, might become relevant in the future. Then, it'll - // be important to check for those consequences (Hint: It is anticipated to affect - // the wording of error announcements that take place back nearer to the Accountant's - // general area) + 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 + // 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 + // the wording of the error message whose forming takes place back out, nearer to the + // Accountant's general area) } } } @@ -486,7 +484,7 @@ impl PaymentAdjusterError { impl Display for PaymentAdjusterError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts, transaction_fee_opt, service_fee_opt, @@ -514,7 +512,7 @@ impl Display for PaymentAdjusterError { (Some(transaction_fee_check_summary), Some(service_fee_check_summary)) => write!( f, - "Neither transaction fee or service fee balance is enough to pay a single payment. \ + "Neither transaction fee nor service fee balance is enough to pay a single payment. \ Number of payments considered: {}. Transaction fee per payment: {} wei, while in \ wallet: {} wei. Total service fee required: {} wei, while in wallet: {} wei", number_of_accounts, @@ -526,10 +524,10 @@ impl Display for PaymentAdjusterError { (None, None) => unreachable!("This error contains no specifications") } }, - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts, number_of_accounts, - original_service_fee_required_total_minor, + 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 \ @@ -537,13 +535,13 @@ impl Display for PaymentAdjusterError { contains {} wei.", original_number_of_accounts, number_of_accounts, - original_service_fee_required_total_minor.separate_with_commas(), + original_total_service_fee_required_minor.separate_with_commas(), cw_service_fee_balance_minor.separate_with_commas() ), - PaymentAdjusterError::AllAccountsEliminated => write!( + PaymentAdjusterError::RecursionDrainedAllAccounts => write!( f, - "The adjustment algorithm had to eliminate each payable from the recently urged \ - payment due to lack of resources." + "The payments adjusting process failed to find any combination of payables that \ + can be paid immediately with the finances provided." ), } } @@ -552,156 +550,114 @@ impl Display for PaymentAdjusterError { #[cfg(test)] mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; - use crate::accountant::payment_adjuster::adjustment_runners::TransactionAndServiceFeeAdjustmentRunner; - use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; - use crate::accountant::payment_adjuster::inner::PaymentAdjusterInnerReal; + 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::DecidedAccounts::SomeAccountsProcessed; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustmentIterationResult, WeightedPayable, + AdjustmentIterationResult, WeighedPayable, }; - use crate::accountant::payment_adjuster::miscellaneous::helper_functions::find_largest_exceeding_balance; - use crate::accountant::payment_adjuster::service_fee_adjuster::AdjustmentComputer; - use crate::accountant::payment_adjuster::test_utils::{ - make_analyzed_account_by_wallet, make_extreme_payables, make_initialized_subject, - multiple_by_billion, CriterionCalculatorMock, DisqualificationGaugeMock, - ServiceFeeAdjusterMock, MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, - PRESERVED_TEST_PAYMENT_THRESHOLDS, + 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::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, + CriterionCalculatorMock, PaymentAdjusterBuilder, ServiceFeeAdjusterMock, + MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, PRESERVED_TEST_PAYMENT_THRESHOLDS, }; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysis, PaymentAdjuster, PaymentAdjusterError, PaymentAdjusterReal, - ServiceFeeImmoderateInsufficiency, TransactionFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, PaymentAdjuster, PaymentAdjusterError, + PaymentAdjusterReal, ServiceFeeImmoderateInsufficiency, + TransactionFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; use crate::accountant::test_utils::{ - make_guaranteed_analyzed_payables, make_guaranteed_qualified_payables, make_payable_account, + make_analyzed_payables, make_meaningless_analyzed_account, make_payable_account, + make_qualified_payables, }; use crate::accountant::{ - gwei_to_wei, CreditorThresholds, QualifiedPayableAccount, ResponseSkeleton, + AnalyzedPayableAccount, CreditorThresholds, QualifiedPayableAccount, ResponseSkeleton, }; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_FEE_MARGIN; - use crate::sub_lib::wallet::Wallet; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; use crate::test_utils::make_wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; - use itertools::Either; + use itertools::{Either, Itertools}; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use masq_lib::utils::convert_collection; use std::collections::HashMap; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; use std::{usize, vec}; use thousands::Separable; - use web3::types::U256; + use web3::types::{Address, U256}; #[test] - #[should_panic(expected = "Broken code: Called the null implementation of \ - the unallocated_cw_service_fee_balance_minor() method in PaymentAdjusterInner")] + #[should_panic( + expected = "PaymentAdjusterInner is uninitialized. It was identified during \ + the execution of 'remaining_cw_service_fee_balance_minor()'" + )] fn payment_adjuster_new_is_created_with_inner_null() { - let result = PaymentAdjusterReal::new(); - - let _ = result.inner.unallocated_cw_service_fee_balance_minor(); - } - - fn test_initialize_inner_works( - required_adjustment: Adjustment, - expected_tx_fee_limit_opt_result: Option, - ) { - let mut subject = PaymentAdjusterReal::default(); - let cw_service_fee_balance = 111_222_333_444; - let largest_exceeding_balance_recently_qualified = 3_555_666; - let now = SystemTime::now(); - - subject.initialize_inner( - cw_service_fee_balance, - required_adjustment, - largest_exceeding_balance_recently_qualified, - now, - ); - - assert_eq!(subject.inner.now(), now); - assert_eq!( - subject.inner.transaction_fee_count_limit_opt(), - expected_tx_fee_limit_opt_result - ); - assert_eq!( - subject.inner.original_cw_service_fee_balance_minor(), - cw_service_fee_balance - ); - assert_eq!( - subject.inner.unallocated_cw_service_fee_balance_minor(), - cw_service_fee_balance - ); - assert_eq!( - subject.inner.largest_exceeding_balance_recently_qualified(), - largest_exceeding_balance_recently_qualified - ) - } + let subject = PaymentAdjusterReal::new(); - #[test] - fn initialize_inner_processes_works() { - test_initialize_inner_works(Adjustment::ByServiceFee, None); - test_initialize_inner_works( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 5, - }, - Some(5), - ); + let _ = subject.inner.remaining_cw_service_fee_balance_minor(); } #[test] - fn search_for_indispensable_adjustment_happy_path() { + fn consider_adjustment_happy_path() { init_test_logging(); - let test_name = "search_for_indispensable_adjustment_gives_negative_answer"; + let test_name = "consider_adjustment_happy_path"; let mut subject = PaymentAdjusterReal::new(); subject.logger = Logger::new(test_name); // Service fee balance > payments let input_1 = make_input_for_initial_check_tests( Some(TestConfigForServiceFeeBalances { - account_balances: Either::Right(vec![ - gwei_to_wei::(85), - gwei_to_wei::(15) - 1, - ]), - cw_balance_minor: gwei_to_wei(100_u64), + payable_account_balances_minor: vec![ + multiply_by_billion(85), + multiply_by_billion(15) - 1, + ], + cw_balance_minor: multiply_by_billion(100), }), None, ); // Service fee balance == payments let input_2 = make_input_for_initial_check_tests( Some(TestConfigForServiceFeeBalances { - account_balances: Either::Left(vec![85, 15]), - cw_balance_minor: gwei_to_wei(100_u64), + payable_account_balances_minor: vec![ + multiply_by_billion(85), + multiply_by_billion(15), + ], + cw_balance_minor: multiply_by_billion(100), }), None, ); + 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); + multiply_by_billion(with_margin) + }; // Transaction fee balance > payments let input_3 = make_input_for_initial_check_tests( None, Some(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 100, + gas_price_major: 100, number_of_accounts: 6, - estimated_transaction_fee_units_per_transaction: 53_000, - cw_transaction_fee_balance_major: { - let base_value = 100 * 6 * 53_000; - let exact_equality = TRANSACTION_FEE_MARGIN.add_percent_to(base_value); - exact_equality + 1 - }, + tx_computation_units: 53_000, + cw_transaction_fee_balance_minor: transaction_fee_balance_exactly_required_minor + + 1, }), ); // Transaction fee balance == payments let input_4 = make_input_for_initial_check_tests( None, Some(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 100, + gas_price_major: 100, number_of_accounts: 6, - estimated_transaction_fee_units_per_transaction: 53_000, - cw_transaction_fee_balance_major: { - let base_value = 100 * 6 * 53_000; - TRANSACTION_FEE_MARGIN.add_percent_to(base_value) - }, + tx_computation_units: 53_000, + cw_transaction_fee_balance_minor: transaction_fee_balance_exactly_required_minor, }), ); @@ -710,8 +666,7 @@ mod tests { .enumerate() .for_each(|(idx, (qualified_payables, agent))| { assert_eq!( - subject - .search_for_indispensable_adjustment(qualified_payables.clone(), &*agent), + subject.consider_adjustment(qualified_payables.clone(), &*agent), Ok(Either::Left(qualified_payables)), "failed for tested input number {:?}", idx + 1 @@ -722,40 +677,42 @@ mod tests { } #[test] - fn search_for_indispensable_adjustment_sad_path_for_transaction_fee() { + fn consider_adjustment_sad_path_for_transaction_fee() { init_test_logging(); - let test_name = "search_for_indispensable_adjustment_sad_path_positive_for_transaction_fee"; + let test_name = "consider_adjustment_sad_path_for_transaction_fee"; let mut subject = PaymentAdjusterReal::new(); subject.logger = Logger::new(test_name); let number_of_accounts = 3; - let service_fee_balances_config_opt = None; let (qualified_payables, agent) = make_input_for_initial_check_tests( - service_fee_balances_config_opt, + None, Some(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 100, + gas_price_major: 100, number_of_accounts, - estimated_transaction_fee_units_per_transaction: 55_000, - cw_transaction_fee_balance_major: 100 * 3 * 55_000 - 1, + 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)) + - 1, }), ); - let analyzed_payables = convert_collection(qualified_payables.clone()); - let result = subject.search_for_indispensable_adjustment(qualified_payables, &*agent); + let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); + let analyzed_payables = + convert_qualified_into_analyzed_payables_in_test(qualified_payables); assert_eq!( result, - Ok(Either::Right(AdjustmentAnalysis::new( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 2 + Ok(Either::Right(AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit: 2 }, analyzed_payables ))) ); let log_handler = TestLogHandler::new(); log_handler.exists_log_containing(&format!( - "WARN: {test_name}: Transaction fee balance of 16,499,999,000,000,000 wei is not \ - going to cover the anticipated fee to send 3 transactions, with 6,325,000,000,000,000 \ - wei required per one. Maximum count is set to 2. Adjustment must be performed." + "WARN: {test_name}: Transaction fee balance of 18,974,999,999,999,999 wei cannot cover \ + the anticipated 18,975,000,000,000,000 wei for 3 transactions. Maximal count is set to 2. \ + Adjustment must be performed." )); log_handler.exists_log_containing(&format!( "INFO: {test_name}: Please be aware that abandoning your debts is going to result in \ @@ -765,37 +722,40 @@ mod tests { } #[test] - fn search_for_indispensable_adjustment_sad_path_for_service_fee_balance() { + fn consider_adjustment_sad_path_for_service_fee_balance() { init_test_logging(); - let test_name = "search_for_indispensable_adjustment_positive_for_service_fee_balance"; + let test_name = "consider_adjustment_positive_for_service_fee_balance"; let logger = Logger::new(test_name); let mut subject = PaymentAdjusterReal::new(); subject.logger = logger; let (qualified_payables, agent) = make_input_for_initial_check_tests( Some(TestConfigForServiceFeeBalances { - account_balances: Either::Right(vec![ - gwei_to_wei::(85), - gwei_to_wei::(15) + 1, - ]), - cw_balance_minor: gwei_to_wei(100_u64), + payable_account_balances_minor: vec![ + multiply_by_billion(85), + multiply_by_billion(15) + 1, + ], + cw_balance_minor: multiply_by_billion(100), }), None, ); - let analyzed_payables = convert_collection(qualified_payables.clone()); - let result = subject.search_for_indispensable_adjustment(qualified_payables, &*agent); + let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); + let analyzed_payables = + convert_qualified_into_analyzed_payables_in_test(qualified_payables); assert_eq!( result, - Ok(Either::Right(AdjustmentAnalysis::new( + Ok(Either::Right(AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, analyzed_payables ))) ); let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing(&format!("WARN: {test_name}: Total of 100,000,\ - 000,001 wei in MASQ was ordered while the consuming wallet held only 100,000,000,000 wei of \ - MASQ token. Adjustment of their count or balances is required.")); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Mature payables \ + amount to 100,000,000,001 MASQ wei while the consuming wallet holds only 100,000,000,000 \ + wei. Adjustment in their count or balances is necessary." + )); 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 \ @@ -804,109 +764,129 @@ mod tests { } #[test] - fn transaction_fee_balance_is_unbearably_low_but_service_fee_balance_is_fine() { + fn service_fee_balance_is_fine_but_transaction_fee_balance_throws_error() { let subject = PaymentAdjusterReal::new(); 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) + }; + let cw_transaction_fee_balance_minor = tx_fee_exactly_required_for_single_tx - 1; let (qualified_payables, agent) = make_input_for_initial_check_tests( Some(TestConfigForServiceFeeBalances { - account_balances: Either::Left(vec![123]), - cw_balance_minor: gwei_to_wei::(444), + payable_account_balances_minor: vec![multiply_by_billion(123)], + cw_balance_minor: multiply_by_billion(444), }), Some(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 100, + gas_price_major: 100, number_of_accounts, - estimated_transaction_fee_units_per_transaction: 55_000, - cw_transaction_fee_balance_major: 54_000 * 100, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor, }), ); - let result = subject.search_for_indispensable_adjustment(qualified_payables, &*agent); + let result = subject.consider_adjustment(qualified_payables, &*agent); - let per_transaction_requirement_minor = - TRANSACTION_FEE_MARGIN.add_percent_to(55_000 * gwei_to_wei::(100)); - let cw_transaction_fee_balance_minor = U256::from(54_000 * gwei_to_wei::(100)); + let per_transaction_requirement_minor = { + let base_minor = multiply_by_billion(55_000 * 100); + TX_FEE_MARGIN_IN_PERCENT.add_percent_to(base_minor) + }; assert_eq!( result, - Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - number_of_accounts, - transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { - per_transaction_requirement_minor, - cw_transaction_fee_balance_minor, - }), - service_fee_opt: None - } - ) + Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + 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 + }) ); } #[test] - fn checking_three_accounts_happy_for_transaction_fee_but_service_fee_balance_is_unbearably_low() - { - let test_name = "checking_three_accounts_happy_for_transaction_fee_but_service_fee_balance_is_unbearably_low"; - let cw_service_fee_balance_minor = gwei_to_wei::(120_u64) / 2 - 1; // this would normally kick a serious error + fn checking_three_accounts_happy_for_transaction_fee_but_service_fee_balance_throws_error() { + let test_name = "checking_three_accounts_happy_for_transaction_fee_but_service_fee_balance_throws_error"; + let garbage_cw_service_fee_balance = u128::MAX; let service_fee_balances_config_opt = Some(TestConfigForServiceFeeBalances { - account_balances: Either::Left(vec![120, 300, 500]), - cw_balance_minor: cw_service_fee_balance_minor, + payable_account_balances_minor: vec![ + multiply_by_billion(120), + multiply_by_billion(300), + multiply_by_billion(500), + ], + cw_balance_minor: garbage_cw_service_fee_balance, }); - let (qualified_payables, agent) = + 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 minimal_disqualification_limit = analyzed_accounts + .iter() + .map(|account| account.disqualification_limit_minor) + .min() + .unwrap(); + // Condition for the error to be thrown + let actual_insufficient_cw_service_fee_balance = minimal_disqualification_limit - 1; + let agent_accessible = reconstruct_mock_agent(boxed_agent); + // Dropping the garbage value on the floor + let _ = agent_accessible.service_fee_balance_minor(); + let agent = agent_accessible + .service_fee_balance_minor_result(actual_insufficient_cw_service_fee_balance); let mut subject = PaymentAdjusterReal::new(); subject.logger = Logger::new(test_name); - let result = subject.search_for_indispensable_adjustment(qualified_payables, &*agent); + let result = subject.consider_adjustment(qualified_payables, &agent); assert_eq!( result, - Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - number_of_accounts: 3, - transaction_fee_opt: None, - service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { - total_service_fee_required_minor: 920_000_000_000, - cw_service_fee_balance_minor - }) - } - ) + Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + 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 + }) + }) ); } #[test] - fn both_balances_are_unbearably_low() { + fn both_balances_are_not_enough_even_for_single_transaction() { let subject = PaymentAdjusterReal::new(); let number_of_accounts = 2; let (qualified_payables, agent) = make_input_for_initial_check_tests( Some(TestConfigForServiceFeeBalances { - account_balances: Either::Left(vec![200, 300]), + payable_account_balances_minor: vec![ + multiply_by_billion(200), + multiply_by_billion(300), + ], cw_balance_minor: 0, }), Some(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 123, + gas_price_major: 123, number_of_accounts, - estimated_transaction_fee_units_per_transaction: 55_000, - cw_transaction_fee_balance_major: 0, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor: 0, }), ); - let result = subject.search_for_indispensable_adjustment(qualified_payables, &*agent); + let result = subject.consider_adjustment(qualified_payables, &*agent); let per_transaction_requirement_minor = - TRANSACTION_FEE_MARGIN.add_percent_to(55_000 * gwei_to_wei::(123)); + TX_FEE_MARGIN_IN_PERCENT.add_percent_to(55_000 * multiply_by_billion(123)); assert_eq!( result, - Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - 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: multiple_by_billion(500), - cw_service_fee_balance_minor: 0 - }) - } - ) + Err(PaymentAdjusterError::AbsolutelyInsufficientBalance { + 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 + }) + }) ); } @@ -914,10 +894,10 @@ mod tests { fn payment_adjuster_error_implements_display() { let inputs = vec![ ( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 4, transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency{ - per_transaction_requirement_minor: 70_000_000_000_000, + per_transaction_requirement_minor: multiply_by_billion(70_000), cw_transaction_fee_balance_minor: U256::from(90_000), }), service_fee_opt: None @@ -927,7 +907,7 @@ mod tests { the wallet contains: 90,000 wei", ), ( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 5, transaction_fee_opt: None, service_fee_opt: Some(ServiceFeeImmoderateInsufficiency{ @@ -940,7 +920,7 @@ mod tests { contains: 333,000,000 wei", ), ( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 5, transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency{ per_transaction_requirement_minor: 5_000_000_000, @@ -951,16 +931,16 @@ mod tests { cw_service_fee_balance_minor: 100_000_000 }) }, - "Neither transaction fee or service fee balance is enough to pay a single payment. \ + "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, \ while in wallet: 3,000,000,000 wei. Total service fee required: 7,000,000,000 wei, \ while in wallet: 100,000,000 wei", ), ( - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts: 6, number_of_accounts: 3, - original_service_fee_required_total_minor: 1234567891011, + 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. \ @@ -968,9 +948,9 @@ mod tests { required amount of service fee: 1,234,567,891,011 wei, while the wallet contains \ 333,333 wei."), ( - PaymentAdjusterError::AllAccountsEliminated, - "The adjustment algorithm had to eliminate each payable from the recently urged \ - payment due to lack of resources.", + PaymentAdjusterError::RecursionDrainedAllAccounts, + "The payments adjusting process failed to find any combination of payables that \ + can be paid immediately with the finances provided.", ), ]; let inputs_count = inputs.len(); @@ -986,7 +966,7 @@ mod tests { specifications" )] fn error_message_for_input_referring_to_no_issues_cannot_be_made() { - let _ = PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + let _ = PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 0, transaction_fee_opt: None, service_fee_opt: None, @@ -997,8 +977,8 @@ mod tests { #[test] fn we_can_say_if_error_occurred_after_insolvency_was_detected() { let inputs = vec![ - PaymentAdjusterError::AllAccountsEliminated, - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::RecursionDrainedAllAccounts, + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 0, transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { per_transaction_requirement_minor: 0, @@ -1006,7 +986,7 @@ mod tests { }), service_fee_opt: None, }, - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 0, transaction_fee_opt: None, service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { @@ -1014,7 +994,7 @@ mod tests { cw_service_fee_balance_minor: 0, }), }, - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts: 0, transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { per_transaction_requirement_minor: 0, @@ -1025,10 +1005,10 @@ mod tests { cw_service_fee_balance_minor: 0, }), }, - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts: 0, number_of_accounts: 0, - original_service_fee_required_total_minor: 0, + original_total_service_fee_required_minor: 0, cw_service_fee_balance_minor: 0, }, ]; @@ -1042,170 +1022,120 @@ mod tests { } #[test] - fn tinier_but_larger_in_weight_account_is_prioritized_and_gains_up_to_its_disqualification_limit( + fn adjusted_balance_threats_to_outgrow_the_original_account_but_is_capped_by_disqualification_limit( ) { - let cw_service_fee_balance_minor = multiple_by_billion(4_200_000); - let determine_limit_params_arc = Arc::new(Mutex::new(vec![])); - let mut account_1 = make_analyzed_account_by_wallet("abc"); - let balance_1 = multiple_by_billion(3_000_000); - let disqualification_limit_1 = multiple_by_billion(2_300_000); + let cw_service_fee_balance_minor = multiply_by_billion(4_200_000); + let mut account_1 = make_meaningless_analyzed_account_by_wallet("abc"); + let balance_1 = multiply_by_billion(3_000_000); + let disqualification_limit_1 = multiply_by_billion(2_300_000); account_1.qualified_as.bare_account.balance_wei = balance_1; account_1.disqualification_limit_minor = disqualification_limit_1; - let mut account_2 = make_analyzed_account_by_wallet("def"); + let weight_account_1 = multiply_by_billion(2_000_100); + let mut account_2 = make_meaningless_analyzed_account_by_wallet("def"); let wallet_2 = account_2.qualified_as.bare_account.wallet.clone(); - let balance_2 = multiple_by_billion(2_500_000); - let disqualification_limit_2 = multiple_by_billion(1_800_000); + let balance_2 = multiply_by_billion(2_500_000); + let disqualification_limit_2 = multiply_by_billion(1_800_000); account_2.qualified_as.bare_account.balance_wei = balance_2; account_2.disqualification_limit_minor = disqualification_limit_2; + let weighed_account_2 = multiply_by_billion(3_999_900); let largest_exceeding_balance = (balance_1 - account_1.qualified_as.payment_threshold_intercept_minor) .max(balance_2 - account_2.qualified_as.payment_threshold_intercept_minor); - let mut subject = make_initialized_subject( - None, - Some(cw_service_fee_balance_minor), - None, - Some(largest_exceeding_balance), - None, - ); - let disqualification_gauge = DisqualificationGaugeMock::default() - .determine_limit_result(disqualification_limit_2) - .determine_limit_result(disqualification_limit_1) - .determine_limit_result(disqualification_limit_1) - .determine_limit_params(&determine_limit_params_arc); - subject.disqualification_arbiter = - DisqualificationArbiter::new(Box::new(disqualification_gauge)); - let weighted_payables = vec![ - WeightedPayable::new(account_1, multiple_by_billion(2_000_100)), - WeightedPayable::new(account_2, multiple_by_billion(3_999_900)), + let subject = PaymentAdjusterBuilder::default() + .cw_service_fee_balance_minor(cw_service_fee_balance_minor) + .max_debt_above_threshold_in_qualified_payables_minor(largest_exceeding_balance) + .build(); + let weighed_payables = vec![ + WeighedPayable::new(account_1, weight_account_1), + WeighedPayable::new(account_2, weighed_account_2), ]; let mut result = subject - .propose_adjustments_recursively( - weighted_payables.clone(), - TransactionAndServiceFeeAdjustmentRunner {}, - ) + .resolve_initial_adjustment_dispatch(weighed_payables.clone()) .unwrap() .left() .unwrap(); - // Let's have an example to explain why this test is important. - prove_that_proposed_adjusted_balance_could_have_exceeded_the_original_value( - subject, + // 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 + // above the original debt size, which is clearly unfair. + illustrate_why_we_need_to_prevent_exceeding_the_original_value( cw_service_fee_balance_minor, - weighted_payables.clone(), - wallet_2, + weighed_payables.clone(), + wallet_2.address(), balance_2, ); - // So the assertion above showed the concern true. + let payable_account_1 = &weighed_payables[0] + .analyzed_account + .qualified_as + .bare_account; + let payable_account_2 = &weighed_payables[1] + .analyzed_account + .qualified_as + .bare_account; let first_returned_account = result.remove(0); - // Outweighed accounts always take the first places - assert_eq!( - &first_returned_account.original_account, - &weighted_payables[1] - .analyzed_account - .qualified_as - .bare_account - ); + 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); - assert_eq!( - &second_returned_account.original_account, - &weighted_payables[0] - .analyzed_account - .qualified_as - .bare_account - ); + assert_eq!(&second_returned_account.original_account, payable_account_1); assert_eq!( second_returned_account.proposed_adjusted_balance_minor, - 2_300_000_000_000_000 + disqualification_limit_1 ); assert!(result.is_empty()); } - fn prove_that_proposed_adjusted_balance_could_have_exceeded_the_original_value( - mut subject: PaymentAdjusterReal, - cw_service_fee_balance_minor: u128, - weighted_accounts: Vec, - wallet_of_expected_outweighed: Wallet, - original_balance_of_outweighed_account: u128, - ) { - let garbage_largest_exceeding_balance_recently_qualified = 123456789; - subject.inner = Box::new(PaymentAdjusterInnerReal::new( - SystemTime::now(), - None, - cw_service_fee_balance_minor, - garbage_largest_exceeding_balance_recently_qualified, - )); - let unconfirmed_adjustments = AdjustmentComputer::default() - .compute_unconfirmed_adjustments(weighted_accounts, cw_service_fee_balance_minor); - // The results are sorted from the biggest weights down - assert_eq!( - unconfirmed_adjustments[1].wallet(), - &wallet_of_expected_outweighed - ); - // The weight of this account grew progressively due to the additional criterion added - // in to the sum. Consequences would've been that redistribution of the adjusted balances - // would've attributed this account with a larger amount to pay than it would've - // contained before the test started. To prevent that, we used to secure a rule that - // an account could never demand more than 100% of itself. - // - // Later it was changed to other policy. so called "outweighed" account gains automatically - // a balance equal to its disqualification limit, also a prominent front position in - // the resulting set of the accounts to pay out. Additionally, due to its favorable position, - // it can be given a bit more from the remains still languishing in the consuming wallet. - 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 {}", - original_balance_of_outweighed_account.separate_with_commas(), - proposed_adjusted_balance.separate_with_commas() - ); - } - #[test] fn adjustment_started_but_all_accounts_were_eliminated_anyway() { let test_name = "adjustment_started_but_all_accounts_were_eliminated_anyway"; let now = SystemTime::now(); - let balance_1 = multiple_by_billion(3_000_000); + // This simplifies the overall picture, the debt age doesn't mean anything to our calculator, + // still, it influences the height of the intercept point read out from the payment thresholds + // which can induce an impact on the value of the disqualification limit which is derived + // from the intercept + let common_unimportant_age_for_accounts = + now.checked_sub(Duration::from_secs(200_000)).unwrap(); + let balance_1 = multiply_by_quintillion_concise(0.003); let account_1 = PayableAccount { wallet: make_wallet("abc"), balance_wei: balance_1, - last_paid_timestamp: now.checked_sub(Duration::from_secs(50_000)).unwrap(), + last_paid_timestamp: common_unimportant_age_for_accounts, pending_payable_opt: None, }; - let balance_2 = multiple_by_billion(2_000_000); + let balance_2 = multiply_by_quintillion_concise(0.002); let account_2 = PayableAccount { wallet: make_wallet("def"), balance_wei: balance_2, - last_paid_timestamp: now.checked_sub(Duration::from_secs(50_000)).unwrap(), + last_paid_timestamp: common_unimportant_age_for_accounts, pending_payable_opt: None, }; - let balance_3 = multiple_by_billion(5_000_000); + let balance_3 = multiply_by_quintillion_concise(0.005); let account_3 = PayableAccount { wallet: make_wallet("ghi"), balance_wei: balance_3, - last_paid_timestamp: now.checked_sub(Duration::from_secs(70_000)).unwrap(), + last_paid_timestamp: common_unimportant_age_for_accounts, pending_payable_opt: None, }; let payables = vec![account_1, account_2, account_3]; let qualified_payables = - make_guaranteed_qualified_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); + make_qualified_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(multiple_by_billion(2_000_000_000)) + .calculate_result(multiply_by_quintillion(2)) .calculate_result(0) .calculate_result(0); - let mut subject = PaymentAdjusterReal::new(); + let mut subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .logger(Logger::new(test_name)) + .build(); subject.calculators.push(Box::new(calculator_mock)); - subject.logger = Logger::new(test_name); - let agent_id_stamp = ArbitraryIdStamp::new(); let cw_service_fee_balance_minor = balance_2; let disqualification_arbiter = &subject.disqualification_arbiter; let agent_for_analysis = BlockchainAgentMock::default() - .agreed_transaction_fee_margin_result(*TRANSACTION_FEE_MARGIN) + .gas_price_margin_result(*TX_FEE_MARGIN_IN_PERCENT) .service_fee_balance_minor_result(cw_service_fee_balance_minor) .transaction_fee_balance_minor_result(U256::MAX) .estimated_transaction_fee_per_transaction_minor_result(12356); @@ -1215,14 +1145,25 @@ mod tests { qualified_payables, &subject.logger, ); - // If the initial analysis at the entry into the PaymentAdjuster concludes there is no point - // going off because even the least demanding account could not be satisfied, and we would - // get an error here. - // However, it can only assess the lowest disqualification limit of an account in that set. - // Probably not as usual, but this particular account can be later outplayed by another one - // that is equipped with some extra significance while its disqualification limit does not - // fit inder the consuming wallet balance anymore. A late error, possibly two different, is - // born. + // The initial intelligent check that PA runs can feel out if the hypothetical adjustment + // would have some minimal chance to complete successfully. Still, this aspect of it is + // rather a weak spot, as the only guarantee it sets on works for an assurance that at + // least the smallest account, with its specific disqualification limit, can be fulfilled + // by the available funds. + // In this test it would be a yes there. There's even a surplus in case of the second + // account. + // Then the adjustment itself spins off. The accounts get their weights. The second one as + // to its lowest size should be granted a big one, wait until the other two are eliminated + // by the recursion and win for the scarce money as paid in the full scale. + // Normally, what was said would hold true. The big difference is caused by an extra, + // actually made up, parameter which comes in with the mock calculator stuck in to join + // the others. It changes the distribution of weights among those three accounts and makes + // the first account be the most important one. Because of that two other accounts are + // eliminated, the account three first, and then the account two. + // When we look back to the preceding entry check, the minimal condition was exercised on + // the account two, because at that time the weights hadn't been known yet. As the result, + // the recursion will continue to even eliminate the last account, the account one, for + // which there isn't enough money to get over its disqualification limit. let adjustment_analysis = match analysis_result { Ok(Either::Right(analysis)) => analysis, x => panic!( @@ -1230,108 +1171,127 @@ mod tests { x ), }; - let agent = { - let mock = BlockchainAgentMock::default() - .set_arbitrary_id_stamp(agent_id_stamp) - .service_fee_balance_minor_result(cw_service_fee_balance_minor); - Box::new(mock) - }; + let agent = Box::new( + BlockchainAgentMock::default() + .service_fee_balance_minor_result(cw_service_fee_balance_minor), + ); let adjustment_setup = PreparedAdjustment { agent, response_skeleton_opt: None, adjustment_analysis, }; - let result = subject.adjust_payments(adjustment_setup, now); + let result = subject.adjust_payments(adjustment_setup); let err = match result { Err(e) => e, Ok(ok) => panic!( - "we expected to get an error but it was ok: {:?}", + "we expected to get an error, but it was ok: {:?}", ok.affordable_accounts ), }; - assert_eq!(err, PaymentAdjusterError::AllAccountsEliminated) + assert_eq!(err, PaymentAdjusterError::RecursionDrainedAllAccounts) } #[test] - fn account_disqualification_makes_the_rest_outweighed_as_cw_balance_becomes_excessive_for_them() - { - // We test that a condition to short-circuit through is integrated in for a situation where - // a performed disqualification frees means that will become available for other accounts, - // and it happens that the remaining accounts require together less than what is left to - // give out. + fn account_disqualification_makes_the_rest_flooded_with_enough_money_suddenly() { + // We test a condition to short-circuit that is built in for the case of an account + // disqualification has just been processed which has freed means, until then tied with this + // account that is gone now, and which will become an extra portion newly available for + // the other accounts from which they can gain, however, at the same time the remaining + // accounts require together less than how much can be given out. init_test_logging(); - let test_name = "account_disqualification_makes_the_rest_outweighed_as_cw_balance_becomes_excessive_for_them"; + let test_name = + "account_disqualification_makes_the_rest_flooded_with_enough_money_suddenly"; let now = SystemTime::now(); - let balance_1 = multiple_by_billion(80_000_000_000); + // This common value simplifies the settings for visualisation, the debt age doesn't mean + // anything, especially with all calculators mocked out, it only influences the height of + // the intercept with the payment thresholds which can in turn take role in evaluating + // the disqualification limit in each account + let common_age_for_accounts_as_unimportant = + now.checked_sub(Duration::from_secs(200_000)).unwrap(); + let balance_1 = multiply_by_quintillion(80); let account_1 = PayableAccount { wallet: make_wallet("abc"), balance_wei: balance_1, - last_paid_timestamp: now.checked_sub(Duration::from_secs(24_000)).unwrap(), + last_paid_timestamp: common_age_for_accounts_as_unimportant, pending_payable_opt: None, }; - let balance_2 = multiple_by_billion(60_000_000_000); + let balance_2 = multiply_by_quintillion(60); let account_2 = PayableAccount { wallet: make_wallet("def"), balance_wei: balance_2, - last_paid_timestamp: now.checked_sub(Duration::from_secs(200_000)).unwrap(), + last_paid_timestamp: common_age_for_accounts_as_unimportant, pending_payable_opt: None, }; - let balance_3 = multiple_by_billion(40_000_000_000); + let balance_3 = multiply_by_quintillion(40); let account_3 = PayableAccount { wallet: make_wallet("ghi"), balance_wei: balance_3, - last_paid_timestamp: now.checked_sub(Duration::from_secs(160_000)).unwrap(), + last_paid_timestamp: common_age_for_accounts_as_unimportant, pending_payable_opt: None, }; let payables = vec![account_1, account_2.clone(), account_3.clone()]; let analyzed_accounts = - make_guaranteed_analyzed_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); + make_analyzed_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(0) - .calculate_result(multiple_by_billion(50_000_000_000)) - .calculate_result(multiple_by_billion(50_000_000_000)); - let mut subject = PaymentAdjusterReal::new(); - subject.calculators.push(Box::new(calculator_mock)); - subject.logger = Logger::new(test_name); + // If we consider that the consuming wallet holds less than the sum of + // the disqualification limits of all these 3 accounts (as also formally checked by one + // of the attached assertions below), this must mean that disqualification has to be + // ruled in the first round, where the first account is eventually eliminated for its + // lowest weight. + .calculate_result(multiply_by_quintillion(10)) + .calculate_result(multiply_by_quintillion(30)) + .calculate_result(multiply_by_quintillion(50)); + let sum_of_disqualification_limits = sum_as(&analyzed_accounts, |account| { + account.disqualification_limit_minor + }); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .logger(Logger::new(test_name)) + .build(); let agent_id_stamp = ArbitraryIdStamp::new(); - let accounts_sum: u128 = balance_1 + balance_2 + balance_3; - let service_fee_balance_in_minor_units = accounts_sum - ((balance_1 * 90) / 100); + let service_fee_balance_minor = balance_2 + balance_3 + ((balance_1 * 10) / 100); let agent = { let mock = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) - .service_fee_balance_minor_result(service_fee_balance_in_minor_units); + .service_fee_balance_minor_result(service_fee_balance_minor); Box::new(mock) }; let adjustment_setup = PreparedAdjustment { agent, - adjustment_analysis: AdjustmentAnalysis::new( + adjustment_analysis: AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, analyzed_accounts, ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); let expected_affordable_accounts = { vec![account_3, account_2] }; assert_eq!(result.affordable_accounts, expected_affordable_accounts); assert_eq!(result.response_skeleton_opt, None); - assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp) + assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); + // This isn't any kind of universal requirement, but this condition is enough to be + // certain that at least one account must be offered a smaller amount than what says its + // disqualification limit, and therefore a disqualification needs to take place. + assert!(sum_of_disqualification_limits > service_fee_balance_minor); } #[test] - fn overloading_with_exaggerated_debt_conditions_to_see_if_we_can_pass_through_safely() { + fn overloaded_by_mammoth_debts_to_see_if_we_can_pass_through_without_blowing_up() { init_test_logging(); let test_name = - "overloading_with_exaggerated_debt_conditions_to_see_if_we_can_pass_through_safely"; + "overloaded_by_mammoth_debts_to_see_if_we_can_pass_through_without_blowing_up"; let now = SystemTime::now(); // Each of the 3 accounts refers to a debt sized as the entire MASQ token supply and being - // 10 years old which generates enormously large numbers in the criteria + // 10 years old which generates enormously large numbers in the algorithm, especially for + // the calculated criteria of over accounts let extreme_payables = { let debt_age_in_months = vec![120, 120, 120]; - make_extreme_payables( + make_mammoth_payables( Either::Left(( debt_age_in_months, *MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, @@ -1339,151 +1299,214 @@ mod tests { now, ) }; - let analyzed_payables = make_guaranteed_analyzed_payables( - extreme_payables, - &PRESERVED_TEST_PAYMENT_THRESHOLDS, - now, - ); + let analyzed_payables = + make_analyzed_payables(extreme_payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); let mut subject = PaymentAdjusterReal::new(); subject.logger = Logger::new(test_name); // In turn, tiny cw balance - let cw_service_fee_balance = 1_000; + let cw_service_fee_balance_minor = 1_000; let agent = { let mock = BlockchainAgentMock::default() - .service_fee_balance_minor_result(cw_service_fee_balance); + .service_fee_balance_minor_result(cw_service_fee_balance_minor); Box::new(mock) }; let adjustment_setup = PreparedAdjustment { agent, - adjustment_analysis: AdjustmentAnalysis::new( + adjustment_analysis: AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, analyzed_payables, ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now); + let result = subject.adjust_payments(adjustment_setup); // The error isn't important. Received just because we set an almost empty wallet let err = match result { Ok(_) => panic!("we expected err but got ok"), Err(e) => e, }; - assert_eq!(err, PaymentAdjusterError::AllAccountsEliminated); - let expected_log = |wallet: &str, proposed_adjusted_balance_in_this_iteration: u64| { + assert_eq!(err, PaymentAdjusterError::RecursionDrainedAllAccounts); + let expected_log = |wallet: &str| { format!( - "INFO: {test_name}: Shortage of MASQ in your consuming wallet will impact payable \ - {wallet}, ruled out from this round of payments. The proposed adjustment {} wei was \ - below the disqualification limit {} wei", - proposed_adjusted_balance_in_this_iteration.separate_with_commas(), + "INFO: {test_name}: Ready payment to {wallet} was eliminated to spare MASQ for \ + those higher prioritized. {} wei owed at the moment.", (*MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR).separate_with_commas() ) }; let log_handler = TestLogHandler::new(); - // Notice that the proposals grow as one disqualified account drops out in each iteration - log_handler.exists_log_containing(&expected_log( + [ "0x000000000000000000000000000000626c616830", - 333, - )); - log_handler.exists_log_containing(&expected_log( "0x000000000000000000000000000000626c616831", - 499, - )); - log_handler.exists_log_containing(&expected_log( "0x000000000000000000000000000000626c616832", - 999, - )); + ] + .into_iter() + .for_each(|address| { + let _ = log_handler.exists_log_containing(&expected_log(address)); + }); + + // Nothing blew up from the giant inputs, the test was a success } - fn meaningless_timestamp() -> SystemTime { - SystemTime::now() + fn make_weighed_payable(n: u64, initial_balance_minor: u128) -> WeighedPayable { + let mut payable = + WeighedPayable::new(make_meaningless_analyzed_account(111), n as u128 * 1234); + payable + .analyzed_account + .qualified_as + .bare_account + .balance_wei = initial_balance_minor; + payable } - // This function should take just such essential args as balances and those that play rather - // a secondary role, yet an important one in the verification processes for proposed adjusted - // balances. Refrain from employing more of the weights-affecting parameters as they would - // only burden us with their consideration in these tests. - fn make_plucked_qualified_account( - wallet_addr_fragment: &str, - balance_minor: u128, - threshold_intercept_major: u128, - permanent_debt_allowed_major: u128, - ) -> QualifiedPayableAccount { - QualifiedPayableAccount::new( - PayableAccount { - wallet: make_wallet(wallet_addr_fragment), - balance_wei: balance_minor, - last_paid_timestamp: meaningless_timestamp(), - pending_payable_opt: None, - }, - multiple_by_billion(threshold_intercept_major), - CreditorThresholds::new(multiple_by_billion(permanent_debt_allowed_major)), + fn test_is_cw_balance_enough_to_remaining_accounts( + initial_disqualification_limit_for_each_account: u128, + remaining_cw_service_fee_balance_minor: u128, + expected_result: bool, + ) { + let subject = PaymentAdjusterReal::new(); + subject.initialize_inner( + Adjustment::ByServiceFee, + remaining_cw_service_fee_balance_minor, + 1234567, + ); + let mut payable_1 = + make_weighed_payable(111, 2 * initial_disqualification_limit_for_each_account); + payable_1.analyzed_account.disqualification_limit_minor = + initial_disqualification_limit_for_each_account; + let mut payable_2 = + make_weighed_payable(222, 3 * initial_disqualification_limit_for_each_account); + payable_2.analyzed_account.disqualification_limit_minor = + 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); + + assert_eq!(result, expected_result) + } + + #[test] + fn remaining_balance_is_equal_to_sum_of_disqualification_limits_in_remaining_accounts() { + let disqualification_limit_for_each_account = multiply_by_billion(5); + let remaining_cw_service_fee_balance_minor = + disqualification_limit_for_each_account + disqualification_limit_for_each_account; + + test_is_cw_balance_enough_to_remaining_accounts( + disqualification_limit_for_each_account, + remaining_cw_service_fee_balance_minor, + true, ) } #[test] - fn count_of_qualified_accounts_before_equals_the_one_of_payments_after() { - // In other words, adjustment by service fee with no account eliminated + fn remaining_balance_is_more_than_sum_of_disqualification_limits_in_remaining_accounts() { + let disqualification_limit_for_each_account = multiply_by_billion(5); + let remaining_cw_service_fee_balance_minor = + disqualification_limit_for_each_account + disqualification_limit_for_each_account + 1; + + test_is_cw_balance_enough_to_remaining_accounts( + disqualification_limit_for_each_account, + remaining_cw_service_fee_balance_minor, + true, + ) + } + + #[test] + fn remaining_balance_is_less_than_sum_of_disqualification_limits_in_remaining_accounts() { + let disqualification_limit_for_each_account = multiply_by_billion(5); + let remaining_cw_service_fee_balance_minor = + disqualification_limit_for_each_account + disqualification_limit_for_each_account - 1; + + test_is_cw_balance_enough_to_remaining_accounts( + disqualification_limit_for_each_account, + remaining_cw_service_fee_balance_minor, + false, + ) + } + + //---------------------------------------------------------------------------------------------- + // The following overall tests demonstrate showcases for PA through different situations that + // can come about during an adjustment + + #[test] + fn accounts_count_does_not_change_during_adjustment() { init_test_logging(); let calculate_params_arc = Arc::new(Mutex::new(vec![])); - let test_name = "count_of_qualified_accounts_before_equals_the_one_of_payments_after"; - let now = SystemTime::now(); - let balance_1 = multiple_by_billion(5_444_444_444); - let qualified_account_1 = - make_plucked_qualified_account("abc", balance_1, 2_000_000_000, 1_000_000_000); - let balance_2 = multiple_by_billion(6_000_000_000); - let qualified_account_2 = - make_plucked_qualified_account("def", balance_2, 2_500_000_000, 2_000_000_000); - let balance_3 = multiple_by_billion(6_666_666_666); - let qualified_account_3 = - make_plucked_qualified_account("ghi", balance_3, 2_000_000_000, 1_111_111_111); - let qualified_payables = vec![ - qualified_account_1.clone(), - qualified_account_2.clone(), - qualified_account_3.clone(), + let test_name = "accounts_count_does_not_change_during_adjustment"; + let balance_account_1 = 5_100_100_100_200_200_200; + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: balance_account_1, + threshold_intercept_major: 2_000_000_000, + permanent_debt_allowed_major: 1_000_000_000, + }; + + let balance_account_2 = 6_000_000_000_123_456_789; + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: balance_account_2, + threshold_intercept_major: 2_500_000_000, + permanent_debt_allowed_major: 2_000_000_000, + }; + let balance_account_3 = 6_666_666_666_666_666_666; + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: balance_account_3, + threshold_intercept_major: 2_000_000_000, + permanent_debt_allowed_major: 1_111_111_111, + }; + let total_weight_account_1 = multiply_by_quintillion_concise(0.4); + let total_weight_account_2 = multiply_by_quintillion_concise(0.3); + let total_weight_account_3 = multiply_by_quintillion_concise(0.2); + let account_seeds = [ + sketched_account_1.clone(), + sketched_account_2.clone(), + sketched_account_3.clone(), ]; - let analyzed_payables = convert_collection(qualified_payables); - let mut subject = PaymentAdjusterReal::new(); + let (analyzed_payables, actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(account_seeds); let calculator_mock = CriterionCalculatorMock::default() .calculate_params(&calculate_params_arc) - .calculate_result(multiple_by_billion(4_500_000_000)) - .calculate_result(multiple_by_billion(4_200_000_000)) - .calculate_result(multiple_by_billion(3_800_000_000)); - subject.calculators = vec![Box::new(calculator_mock)]; - subject.logger = Logger::new(test_name); + .calculate_result(total_weight_account_1) + .calculate_result(total_weight_account_2) + .calculate_result(total_weight_account_3); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .logger(Logger::new(test_name)) + .build(); let agent_id_stamp = ArbitraryIdStamp::new(); - let accounts_sum_minor = balance_1 + balance_2 + balance_3; - let cw_service_fee_balance_minor = accounts_sum_minor - multiple_by_billion(2_000_000_000); + let accounts_sum_minor = balance_account_1 + balance_account_2 + balance_account_3; + let cw_service_fee_balance_minor = accounts_sum_minor - multiply_by_billion(2_000_000_000); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) .service_fee_balance_minor_result(cw_service_fee_balance_minor); let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( + adjustment_analysis: AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, - analyzed_payables, + analyzed_payables.clone().into(), ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); - let expected_adjusted_balance_1 = 4_833_333_333_000_000_000; - let expected_adjusted_balance_2 = 5_500_000_000_000_000_000; - let expected_adjusted_balance_3 = 5_777_777_777_000_000_000; + actual_disqualification_limits.validate_against_expected( + 4_100_100_100_200_200_200, + 5_500_000_000_123_456_789, + 5_777_777_777_666_666_666, + ); + let expected_adjusted_balance_1 = 4_488_988_989_200_200_200; + let expected_adjusted_balance_2 = 5_500_000_000_123_456_789; + let expected_adjusted_balance_3 = 5_777_777_777_666_666_666; let expected_criteria_computation_output = { - let account_1_adjusted = PayableAccount { - balance_wei: expected_adjusted_balance_1, - ..qualified_account_1.bare_account.clone() - }; - let account_2_adjusted = PayableAccount { - balance_wei: expected_adjusted_balance_2, - ..qualified_account_2.bare_account.clone() - }; - let account_3_adjusted = PayableAccount { - balance_wei: expected_adjusted_balance_3, - ..qualified_account_3.bare_account.clone() - }; + let account_1_adjusted = + account_with_new_balance(&analyzed_payables[0], expected_adjusted_balance_1); + let account_2_adjusted = + account_with_new_balance(&analyzed_payables[1], expected_adjusted_balance_2); + let account_3_adjusted = + account_with_new_balance(&analyzed_payables[2], expected_adjusted_balance_3); vec![account_1_adjusted, account_2_adjusted, account_3_adjusted] }; assert_eq!( @@ -1493,14 +1516,11 @@ mod tests { assert_eq!(result.response_skeleton_opt, None); assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); let calculate_params = calculate_params_arc.lock().unwrap(); - assert_eq!( - *calculate_params, - vec![ - qualified_account_1, - qualified_account_2, - qualified_account_3 - ] - ); + let expected_calculate_params = analyzed_payables + .into_iter() + .map(|account| account.qualified_as) + .collect_vec(); + assert_eq!(*calculate_params, expected_calculate_params); let log_msg = format!( "DEBUG: {test_name}: \n\ |Payable Account Balance Wei @@ -1514,11 +1534,11 @@ mod tests { | {} |0x0000000000000000000000000000000000616263 {} | {}", - balance_3.separate_with_commas(), + balance_account_3.separate_with_commas(), expected_adjusted_balance_3.separate_with_commas(), - balance_2.separate_with_commas(), + balance_account_2.separate_with_commas(), expected_adjusted_balance_2.separate_with_commas(), - balance_1.separate_with_commas(), + balance_account_1.separate_with_commas(), expected_adjusted_balance_1.separate_with_commas() ); TestLogHandler::new().exists_log_containing(&log_msg.replace("|", "")); @@ -1530,46 +1550,69 @@ mod tests { init_test_logging(); let test_name = "only_transaction_fee_causes_limitations_and_the_service_fee_balance_suffices"; - let now = SystemTime::now(); - let balance_1 = multiple_by_billion(111_000_000); - let account_1 = make_plucked_qualified_account("abc", balance_1, 100_000_000, 20_000_000); - let balance_2 = multiple_by_billion(300_000_000); - let account_2 = make_plucked_qualified_account("def", balance_2, 120_000_000, 50_000_000); - let balance_3 = multiple_by_billion(222_222_222); - let account_3 = make_plucked_qualified_account("ghi", balance_3, 100_000_000, 40_000_000); - let qualified_payables = vec![account_1.clone(), account_2, account_3.clone()]; - let analyzed_payables = convert_collection(qualified_payables); + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: multiply_by_quintillion_concise(0.111), + threshold_intercept_major: multiply_by_billion_concise(0.1), + permanent_debt_allowed_major: multiply_by_billion_concise(0.02), + }; + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: multiply_by_quintillion_concise(0.3), + threshold_intercept_major: multiply_by_billion_concise(0.12), + permanent_debt_allowed_major: multiply_by_billion_concise(0.05), + }; + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: multiply_by_billion(222_222_222), + threshold_intercept_major: multiply_by_billion_concise(0.1), + permanent_debt_allowed_major: multiply_by_billion_concise(0.04), + }; + let total_weight_account_1 = multiply_by_quintillion_concise(0.4); + // This account will have to fall off because of its lowest weight and that only two + // accounts can be kept according to the limitations detected in the transaction fee + // balance + let total_weight_account_2 = multiply_by_quintillion_concise(0.2); + let total_weight_account_3 = multiply_by_quintillion_concise(0.3); + let sketched_accounts = [sketched_account_1, sketched_account_2, sketched_account_3]; + let (analyzed_payables, _actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(sketched_accounts); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(multiple_by_billion(400_000_000)) - // This account will be cut off because it has the lowest weight and only two accounts - // can be kept according to the limitations detected in the transaction fee balance - .calculate_result(multiple_by_billion(120_000_000)) - .calculate_result(multiple_by_billion(250_000_000)); - let mut subject = PaymentAdjusterReal::new(); - subject.calculators = vec![Box::new(calculator_mock)]; - subject.logger = Logger::new(test_name); + .calculate_result(total_weight_account_1) + .calculate_result(total_weight_account_2) + .calculate_result(total_weight_account_3); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .logger(Logger::new(test_name)) + .build(); let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) .service_fee_balance_minor_result(u128::MAX); + let transaction_count_limit = 2; let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 2, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, }, - analyzed_payables, + analyzed_payables.clone().into(), ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); // The account 1 takes the first place for its weight being the biggest - assert_eq!( - result.affordable_accounts, - vec![account_1.bare_account, account_3.bare_account] - ); + let expected_affordable_accounts = { + let mut analyzed_payables = analyzed_payables.to_vec(); + let account_1_unchanged = analyzed_payables.remove(0).qualified_as.bare_account; + let _ = analyzed_payables.remove(0); + let account_3_unchanged = analyzed_payables.remove(0).qualified_as.bare_account; + vec![account_1_unchanged, account_3_unchanged] + }; + assert_eq!(result.affordable_accounts, expected_affordable_accounts); assert_eq!(result.response_skeleton_opt, None); assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); let log_msg = format!( @@ -1593,34 +1636,50 @@ mod tests { } #[test] - fn both_balances_insufficient_but_adjustment_by_service_fee_will_not_affect_the_payments_count() + fn both_balances_insufficient_but_adjustment_by_service_fee_will_not_affect_the_payment_count() { // The course of events: // 1) adjustment by transaction fee (always means accounts elimination), // 2) adjustment by service fee (can but not have to cause an account drop-off) init_test_logging(); - let now = SystemTime::now(); - let balance_1 = multiple_by_billion(111_000_000); - let account_1 = make_plucked_qualified_account("abc", balance_1, 50_000_000, 10_000_000); - let balance_2 = multiple_by_billion(333_000_000); - let account_2 = make_plucked_qualified_account("def", balance_2, 200_000_000, 50_000_000); - let balance_3 = multiple_by_billion(222_000_000); - let account_3 = make_plucked_qualified_account("ghi", balance_3, 100_000_000, 35_000_000); - let disqualification_arbiter = DisqualificationArbiter::default(); - let disqualification_limit_1 = - disqualification_arbiter.calculate_disqualification_edge(&account_1); - let disqualification_limit_3 = - disqualification_arbiter.calculate_disqualification_edge(&account_3); - let qualified_payables = vec![account_1.clone(), account_2, account_3.clone()]; - let analyzed_payables = convert_collection(qualified_payables); + let balance_account_1 = multiply_by_quintillion_concise(0.111); + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: balance_account_1, + threshold_intercept_major: multiply_by_billion_concise(0.05), + permanent_debt_allowed_major: multiply_by_billion_concise(0.010), + }; + let balance_account_2 = multiply_by_quintillion_concise(0.333); + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: balance_account_2, + threshold_intercept_major: multiply_by_billion_concise(0.2), + permanent_debt_allowed_major: multiply_by_billion_concise(0.05), + }; + let balance_account_3 = multiply_by_quintillion_concise(0.222); + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: balance_account_3, + threshold_intercept_major: multiply_by_billion_concise(0.1), + permanent_debt_allowed_major: multiply_by_billion_concise(0.035), + }; + let total_weight_account_1 = multiply_by_quintillion_concise(0.4); + let total_weight_account_2 = multiply_by_quintillion_concise(0.2); + let total_weight_account_3 = multiply_by_quintillion_concise(0.3); + let sketched_accounts = [sketched_account_1, sketched_account_2, sketched_account_3]; + let (analyzed_payables, actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(sketched_accounts); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(multiple_by_billion(400_000_000)) - .calculate_result(multiple_by_billion(200_000_000)) - .calculate_result(multiple_by_billion(300_000_000)); - let mut subject = PaymentAdjusterReal::new(); - subject.calculators = vec![Box::new(calculator_mock)]; - let cw_service_fee_balance_minor = - disqualification_limit_1 + disqualification_limit_3 + multiple_by_billion(10_000_000); + .calculate_result(total_weight_account_1) + .calculate_result(total_weight_account_2) + .calculate_result(total_weight_account_3); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .build(); + let cw_service_fee_balance_minor = actual_disqualification_limits.account_1 + + actual_disqualification_limits.account_3 + + multiply_by_quintillion_concise(0.01); let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) @@ -1629,29 +1688,33 @@ mod tests { client_id: 123, context_id: 321, }); // Just hardening, not so important + let transaction_count_limit = 2; let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 2, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, }, - analyzed_payables, + analyzed_payables.clone().into(), ), response_skeleton_opt, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); + actual_disqualification_limits.validate_against_expected( + multiply_by_quintillion_concise(0.071), + multiply_by_quintillion_concise(0.183), + multiply_by_quintillion_concise(0.157), + ); // Account 2, the least important one, was eliminated for a lack of transaction fee in the cw + let expected_adjusted_balance_1 = multiply_by_quintillion_concise(0.081); + let expected_adjusted_balance_3 = multiply_by_quintillion_concise(0.157); let expected_accounts = { - let account_1_adjusted = PayableAccount { - balance_wei: 81_000_000_000_000_000, - ..account_1.bare_account - }; - let account_3_adjusted = PayableAccount { - balance_wei: 157_000_000_000_000_000, - ..account_3.bare_account - }; + let account_1_adjusted = + account_with_new_balance(&analyzed_payables[0], expected_adjusted_balance_1); + let account_3_adjusted = + account_with_new_balance(&analyzed_payables[2], expected_adjusted_balance_3); vec![account_1_adjusted, account_3_adjusted] }; assert_eq!(result.affordable_accounts, expected_accounts); @@ -1664,26 +1727,48 @@ mod tests { fn only_service_fee_balance_limits_the_payments_count() { init_test_logging(); let test_name = "only_service_fee_balance_limits_the_payments_count"; - let now = SystemTime::now(); // Account to be adjusted to keep as much as it is left in the cw balance - let balance_1 = multiple_by_billion(333_000_000); - let account_1 = make_plucked_qualified_account("abc", balance_1, 200_000_000, 50_000_000); + let balance_account_1 = multiply_by_billion(333_000_000); + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: balance_account_1, + threshold_intercept_major: 200_000_000, + permanent_debt_allowed_major: 50_000_000, + }; // Account to be outweighed and fully preserved - let balance_2 = multiple_by_billion(111_000_000); - let account_2 = make_plucked_qualified_account("def", balance_2, 50_000_000, 10_000_000); + let balance_account_2 = multiply_by_billion(111_000_000); + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: balance_account_2, + threshold_intercept_major: 50_000_000, + permanent_debt_allowed_major: 10_000_000, + }; // Account to be disqualified - let balance_3 = multiple_by_billion(600_000_000); - let account_3 = make_plucked_qualified_account("ghi", balance_3, 400_000_000, 100_000_000); - let qualified_payables = vec![account_1.clone(), account_2.clone(), account_3]; - let analyzed_payables = convert_collection(qualified_payables); + let balance_account_3 = multiply_by_billion(600_000_000); + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: balance_account_3, + threshold_intercept_major: 400_000_000, + permanent_debt_allowed_major: 100_000_000, + }; + let total_weight_account_1 = multiply_by_billion(900_000_000); + let total_weight_account_2 = multiply_by_billion(1_100_000_000); + let total_weight_account_3 = multiply_by_billion(600_000_000); + let sketched_accounts = [sketched_account_1, sketched_account_2, sketched_account_3]; + let (analyzed_payables, actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(sketched_accounts); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(multiple_by_billion(900_000_000)) - .calculate_result(multiple_by_billion(1_100_000_000)) - .calculate_result(multiple_by_billion(600_000_000)); - let mut subject = PaymentAdjusterReal::new(); - subject.calculators = vec![Box::new(calculator_mock)]; - subject.logger = Logger::new(test_name); - let service_fee_balance_in_minor_units = balance_1 + balance_2 - 55; + .calculate_result(total_weight_account_1) + .calculate_result(total_weight_account_2) + .calculate_result(total_weight_account_3); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .logger(Logger::new(test_name)) + .build(); + let service_fee_balance_in_minor_units = actual_disqualification_limits.account_1 + + actual_disqualification_limits.account_2 + + 123_456_789; let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) @@ -1694,19 +1779,30 @@ mod tests { }); let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( + adjustment_analysis: AdjustmentAnalysisReport::new( Adjustment::ByServiceFee, - analyzed_payables, + analyzed_payables.clone().into(), ), response_skeleton_opt, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); + actual_disqualification_limits.validate_against_expected( + multiply_by_billion(183_000_000), + multiply_by_billion(71_000_000), + multiply_by_billion(300_000_000), + ); let expected_accounts = { - let mut account_1_adjusted = account_1; - account_1_adjusted.bare_account.balance_wei -= 55; - vec![account_2.bare_account, account_1_adjusted.bare_account] + let adjusted_account_2 = account_with_new_balance( + &analyzed_payables[1], + actual_disqualification_limits.account_2 + 123_456_789, + ); + let adjusted_account_1 = account_with_new_balance( + &analyzed_payables[0], + actual_disqualification_limits.account_1, + ); + vec![adjusted_account_2, adjusted_account_1] }; assert_eq!(result.affordable_accounts, expected_accounts); assert_eq!(result.response_skeleton_opt, response_skeleton_opt); @@ -1719,10 +1815,9 @@ mod tests { ); assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); TestLogHandler::new().exists_log_containing(&format!( - "INFO: {test_name}: Shortage of MASQ in your consuming wallet will impact payable \ - 0x0000000000000000000000000000000000676869, ruled out from this round of payments. \ - The proposed adjustment 189,999,999,999,999,944 wei was below the disqualification \ - limit 300,000,000,000,000,000 wei" + "INFO: {test_name}: Ready payment to 0x0000000000000000000000000000000000676869 was \ + eliminated to spare MASQ for those higher prioritized. 600,000,000,000,000,000 wei owed \ + at the moment." )); test_inner_was_reset_to_null(subject) } @@ -1731,53 +1826,75 @@ mod tests { fn service_fee_as_well_as_transaction_fee_limits_the_payments_count() { init_test_logging(); let test_name = "service_fee_as_well_as_transaction_fee_limits_the_payments_count"; - let now = SystemTime::now(); - let balance_1 = multiple_by_billion(100_000_000_000); - let account_1 = - make_plucked_qualified_account("abc", balance_1, 60_000_000_000, 10_000_000_000); + let balance_account_1 = multiply_by_quintillion(100); + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: balance_account_1, + threshold_intercept_major: multiply_by_billion(60), + permanent_debt_allowed_major: multiply_by_billion(10), + }; // The second is thrown away first in a response to the shortage of transaction fee, // as its weight is the least significant - let balance_2 = multiple_by_billion(500_000_000_000); - let account_2 = - make_plucked_qualified_account("def", balance_2, 100_000_000_000, 30_000_000_000); + let balance_account_2 = multiply_by_quintillion(500); + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: balance_account_2, + threshold_intercept_major: multiply_by_billion(100), + permanent_debt_allowed_major: multiply_by_billion(30), + }; // Thrown away as the second one due to a shortage in the service fee, // listed among accounts to disqualify and picked eventually for its // lowest weight - let balance_3 = multiple_by_billion(250_000_000_000); - let account_3 = - make_plucked_qualified_account("ghi", balance_3, 90_000_000_000, 20_000_000_000); - let qualified_payables = vec![account_1.clone(), account_2, account_3]; - let analyzed_payables = convert_collection(qualified_payables); + let balance_account_3 = multiply_by_quintillion(250); + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: balance_account_3, + threshold_intercept_major: multiply_by_billion(90), + permanent_debt_allowed_major: multiply_by_billion(20), + }; + let total_weight_account_1 = multiply_by_quintillion(900); + let total_weight_account_2 = multiply_by_quintillion(500); + let total_weight_account_3 = multiply_by_quintillion(750); + let sketched_accounts = [sketched_account_1, sketched_account_2, sketched_account_3]; + let (analyzed_payables, actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(sketched_accounts); let calculator_mock = CriterionCalculatorMock::default() - .calculate_result(multiple_by_billion(900_000_000_000)) - .calculate_result(multiple_by_billion(500_000_000_000)) - .calculate_result(multiple_by_billion(750_000_000_000)); - let mut subject = PaymentAdjusterReal::new(); - subject.calculators = vec![Box::new(calculator_mock)]; - subject.logger = Logger::new(test_name); - let service_fee_balance_in_minor = balance_1 - multiple_by_billion(10_000_000_000); + .calculate_result(total_weight_account_1) + .calculate_result(total_weight_account_2) + .calculate_result(total_weight_account_3); + let subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .replace_calculators_with_mock(calculator_mock) + .logger(Logger::new(test_name)) + .build(); + let service_fee_balance_in_minor = balance_account_1 - multiply_by_quintillion(10); let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) .service_fee_balance_minor_result(service_fee_balance_in_minor); + let transaction_count_limit = 2; let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 2, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, }, - analyzed_payables, + analyzed_payables.clone().into(), ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now).unwrap(); + let result = subject.adjust_payments(adjustment_setup).unwrap(); - let expected_accounts = { - let mut account = account_1; - account.bare_account.balance_wei = service_fee_balance_in_minor; - vec![account.bare_account] - }; + actual_disqualification_limits.validate_against_expected( + multiply_by_quintillion(50), + multiply_by_quintillion(460), + multiply_by_quintillion(200), + ); + let expected_accounts = vec![account_with_new_balance( + &analyzed_payables[0], + service_fee_balance_in_minor, + )]; assert_eq!(result.affordable_accounts, expected_accounts); assert_eq!(result.response_skeleton_opt, None); assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); @@ -1800,64 +1917,176 @@ mod tests { test_inner_was_reset_to_null(subject) } - #[test] - fn late_error_after_transaction_fee_adjustment_but_rechecked_transaction_fee_found_fatally_insufficient( + #[derive(Debug, PartialEq, Clone)] + struct SketchedPayableAccount { + wallet_addr_seed: &'static str, + balance_minor: u128, + threshold_intercept_major: u128, + permanent_debt_allowed_major: u128, + } + + #[derive(Debug, PartialEq)] + struct QuantifiedDisqualificationLimits { + account_1: u128, + account_2: u128, + account_3: u128, + } + + impl QuantifiedDisqualificationLimits { + fn validate_against_expected( + &self, + expected_limit_account_1: u128, + expected_limit_account_2: u128, + expected_limit_account_3: u128, + ) { + let actual = [self.account_1, self.account_2, self.account_3]; + let expected = [ + expected_limit_account_1, + expected_limit_account_2, + expected_limit_account_3, + ]; + assert_eq!( + actual, expected, + "Test manifests disqualification limits as {:?} to help with visualising \ + the conditions but such limits are ot true, because the accounts in the input \ + actually evaluates to these limits {:?}", + expected, actual + ); + } + } + + impl From<&[AnalyzedPayableAccount; 3]> for QuantifiedDisqualificationLimits { + fn from(accounts: &[AnalyzedPayableAccount; 3]) -> Self { + Self { + account_1: accounts[0].disqualification_limit_minor, + account_2: accounts[1].disqualification_limit_minor, + account_3: accounts[2].disqualification_limit_minor, + } + } + } + + fn make_analyzed_accounts_and_show_their_actual_disqualification_limits( + accounts_seeds: [SketchedPayableAccount; 3], + ) -> ( + [AnalyzedPayableAccount; 3], + QuantifiedDisqualificationLimits, ) { + let qualified_payables: Vec<_> = accounts_seeds + .into_iter() + .map(|account_seed| { + QualifiedPayableAccount::new( + PayableAccount { + wallet: make_wallet(account_seed.wallet_addr_seed), + balance_wei: account_seed.balance_minor, + last_paid_timestamp: meaningless_timestamp(), + pending_payable_opt: None, + }, + multiply_by_billion(account_seed.threshold_intercept_major), + CreditorThresholds::new(multiply_by_billion( + account_seed.permanent_debt_allowed_major, + )), + ) + }) + .collect(); + let analyzed_accounts = + convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let analyzed_accounts: [AnalyzedPayableAccount; 3] = analyzed_accounts.try_into().unwrap(); + let disqualification_limits: QuantifiedDisqualificationLimits = (&analyzed_accounts).into(); + (analyzed_accounts, disqualification_limits) + } + + fn meaningless_timestamp() -> SystemTime { + SystemTime::now() + } + + fn account_with_new_balance( + analyzed_payable: &AnalyzedPayableAccount, + adjusted_balance: u128, + ) -> PayableAccount { + PayableAccount { + balance_wei: adjusted_balance, + ..analyzed_payable.qualified_as.bare_account.clone() + } + } + + //---------------------------------------------------------------------------------------------- + // End of happy path section + + #[test] + fn late_error_after_tx_fee_adjusted_but_rechecked_service_fee_found_fatally_insufficient() { init_test_logging(); - let test_name = "late_error_after_transaction_fee_adjustment_but_rechecked_transaction_fee_found_fatally_insufficient"; - let now = SystemTime::now(); - let balance_1 = multiple_by_billion(500_000_000_000); - let account_1 = - make_plucked_qualified_account("abc", balance_1, 300_000_000_000, 100_000_000_000); + let test_name = + "late_error_after_tx_fee_adjusted_but_rechecked_service_fee_found_fatally_insufficient"; + let balance_account_1 = multiply_by_quintillion(500); + let sketched_account_1 = SketchedPayableAccount { + wallet_addr_seed: "abc", + balance_minor: balance_account_1, + threshold_intercept_major: multiply_by_billion(300), + permanent_debt_allowed_major: multiply_by_billion(100), + }; // This account is eliminated in the transaction fee cut - let balance_2 = multiple_by_billion(111_000_000_000); - let account_2 = - make_plucked_qualified_account("def", balance_2, 50_000_000_000, 10_000_000_000); - let balance_3 = multiple_by_billion(300_000_000_000); - let account_3 = - make_plucked_qualified_account("ghi", balance_3, 150_000_000_000, 50_000_000_000); + let balance_account_2 = multiply_by_quintillion(111); + let sketched_account_2 = SketchedPayableAccount { + wallet_addr_seed: "def", + balance_minor: balance_account_2, + threshold_intercept_major: multiply_by_billion(50), + permanent_debt_allowed_major: multiply_by_billion(10), + }; + let balance_account_3 = multiply_by_quintillion(300); + let sketched_account_3 = SketchedPayableAccount { + wallet_addr_seed: "ghi", + balance_minor: balance_account_3, + threshold_intercept_major: multiply_by_billion(150), + permanent_debt_allowed_major: multiply_by_billion(50), + }; + let sketched_accounts = [sketched_account_1, sketched_account_2, sketched_account_3]; + let (analyzed_payables, actual_disqualification_limits) = + make_analyzed_accounts_and_show_their_actual_disqualification_limits(sketched_accounts); let mut subject = PaymentAdjusterReal::new(); subject.logger = Logger::new(test_name); - let disqualification_arbiter = DisqualificationArbiter::default(); - let disqualification_limit_2 = - disqualification_arbiter.calculate_disqualification_edge(&account_2); - // This is exactly the amount which will provoke an error - let cw_service_fee_balance_minor = disqualification_limit_2 - 1; - let qualified_payables = vec![account_1, account_2, account_3]; - let analyzed_payables = convert_collection(qualified_payables); + // This is exactly the amount which provokes an error + let cw_service_fee_balance_minor = actual_disqualification_limits.account_2 - 1; let agent = BlockchainAgentMock::default() .service_fee_balance_minor_result(cw_service_fee_balance_minor); + let transaction_count_limit = 2; let adjustment_setup = PreparedAdjustment { agent: Box::new(agent), - adjustment_analysis: AdjustmentAnalysis::new( - Adjustment::TransactionFeeInPriority { - affordable_transaction_count: 2, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, }, - analyzed_payables, + analyzed_payables.into(), ), response_skeleton_opt: None, }; - let result = subject.adjust_payments(adjustment_setup, now); + let result = subject.adjust_payments(adjustment_setup); + actual_disqualification_limits.validate_against_expected( + multiply_by_quintillion(300), + multiply_by_quintillion(71), + multiply_by_quintillion(250), + ); let err = match result { Ok(_) => panic!("expected an error but got Ok()"), Err(e) => e, }; assert_eq!( err, - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts: 3, number_of_accounts: 2, - original_service_fee_required_total_minor: balance_1 + balance_2 + balance_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![ &format!( - "WARN: {test_name}: Total of 411,000,000,000,000,000,000 wei in MASQ was \ - ordered while the consuming wallet held only 70,999,999,999,999,999,999 wei of MASQ \ - token. Adjustment of their count or balances is required." + "WARN: {test_name}: Mature payables amount to 411,000,000,000,000,000,000 MASQ \ + wei while the consuming wallet holds only 70,999,999,999,999,999,999 wei. \ + Adjustment in their count or balances is necessary." ), &format!( "INFO: {test_name}: Please be aware that abandoning your debts is going to \ @@ -1872,89 +2101,74 @@ mod tests { } struct TestConfigForServiceFeeBalances { - // Either gwei or wei - account_balances: Either, Vec>, + payable_account_balances_minor: Vec, cw_balance_minor: u128, } + impl Default for TestConfigForServiceFeeBalances { + fn default() -> Self { + TestConfigForServiceFeeBalances { + payable_account_balances_minor: vec![1, 2], + cw_balance_minor: u64::MAX as u128, + } + } + } + struct TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: u64, + gas_price_major: u64, number_of_accounts: usize, - estimated_transaction_fee_units_per_transaction: u64, - cw_transaction_fee_balance_major: u64, + tx_computation_units: u64, + cw_transaction_fee_balance_minor: u128, } fn make_input_for_initial_check_tests( - service_fee_balances_config_opt: Option, - transaction_fee_config_opt: Option, + service_fee_config_opt: Option, + tx_fee_config_opt: Option, ) -> (Vec, Box) { - let service_fee_balances_config = - get_service_fee_balances_config(service_fee_balances_config_opt); - let balances_of_accounts_minor = - get_service_fee_balances(service_fee_balances_config.account_balances); + let service_fee_balances_config = service_fee_config_opt.unwrap_or_default(); + let balances_of_accounts_minor = service_fee_balances_config.payable_account_balances_minor; let accounts_count_from_sf_config = balances_of_accounts_minor.len(); - let transaction_fee_config = - get_transaction_fee_config(transaction_fee_config_opt, accounts_count_from_sf_config); - - let payable_accounts = prepare_payable_accounts( - transaction_fee_config.number_of_accounts, - accounts_count_from_sf_config, - balances_of_accounts_minor, - ); + let transaction_fee_config = tx_fee_config_opt + .unwrap_or_else(|| default_transaction_fee_config(accounts_count_from_sf_config)); + let payable_accounts = if transaction_fee_config.number_of_accounts + != accounts_count_from_sf_config + { + prepare_payable_accounts_from(Either::Left(transaction_fee_config.number_of_accounts)) + } else { + prepare_payable_accounts_from(Either::Right(balances_of_accounts_minor)) + }; let qualified_payables = prepare_qualified_payables(payable_accounts); - let blockchain_agent = make_agent( - transaction_fee_config.cw_transaction_fee_balance_major, - transaction_fee_config.estimated_transaction_fee_units_per_transaction, - transaction_fee_config.agreed_transaction_fee_per_computed_unit_major, + let blockchain_agent = prepare_agent( + transaction_fee_config.cw_transaction_fee_balance_minor, + transaction_fee_config.tx_computation_units, + transaction_fee_config.gas_price_major, service_fee_balances_config.cw_balance_minor, ); (qualified_payables, blockchain_agent) } - fn get_service_fee_balances_config( - service_fee_balances_config_opt: Option, - ) -> TestConfigForServiceFeeBalances { - service_fee_balances_config_opt.unwrap_or_else(|| TestConfigForServiceFeeBalances { - account_balances: Either::Left(vec![1, 1]), - cw_balance_minor: u64::MAX as u128, - }) - } - fn get_service_fee_balances(account_balances: Either, Vec>) -> Vec { - match account_balances { - Either::Left(in_major) => in_major - .into_iter() - .map(|major| gwei_to_wei(major)) - .collect(), - Either::Right(in_minor) => in_minor, - } - } - - fn get_transaction_fee_config( - transaction_fee_config_opt: Option, + fn default_transaction_fee_config( accounts_count_from_sf_config: usize, ) -> TestConfigForTransactionFees { - transaction_fee_config_opt.unwrap_or(TestConfigForTransactionFees { - agreed_transaction_fee_per_computed_unit_major: 120, + TestConfigForTransactionFees { + gas_price_major: 120, number_of_accounts: accounts_count_from_sf_config, - estimated_transaction_fee_units_per_transaction: 55_000, - cw_transaction_fee_balance_major: u64::MAX, - }) + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor: u128::MAX, + } } - fn prepare_payable_accounts( - accounts_count_from_tf_config: usize, - accounts_count_from_sf_config: usize, - balances_of_accounts_minor: Vec, + fn prepare_payable_accounts_from( + balances_or_desired_accounts_count: Either>, ) -> Vec { - if accounts_count_from_tf_config != accounts_count_from_sf_config { - (0..accounts_count_from_tf_config) + match balances_or_desired_accounts_count { + Either::Left(desired_accounts_count) => (0..desired_accounts_count) .map(|idx| make_payable_account(idx as u64)) - .collect() - } else { - balances_of_accounts_minor + .collect(), + Either::Right(balances_of_accounts_minor) => balances_of_accounts_minor .into_iter() .enumerate() .map(|(idx, balance)| { @@ -1962,7 +2176,7 @@ mod tests { account.balance_wei = balance; account }) - .collect() + .collect(), } } @@ -1984,20 +2198,18 @@ mod tests { .collect() } - fn make_agent( - cw_balance_transaction_fee_major: u64, - estimated_transaction_fee_units_per_transaction: u64, - agreed_transaction_fee_price: u64, + fn prepare_agent( + cw_transaction_fee_minor: u128, + tx_computation_units: u64, + gas_price: u64, cw_service_fee_balance_minor: u128, ) -> Box { - let cw_transaction_fee_minor = gwei_to_wei(cw_balance_transaction_fee_major); - let estimated_transaction_fee_per_transaction_minor = gwei_to_wei( - estimated_transaction_fee_units_per_transaction * agreed_transaction_fee_price, - ); + let estimated_transaction_fee_per_transaction_minor = + multiply_by_billion((tx_computation_units * gas_price) as u128); let blockchain_agent = BlockchainAgentMock::default() - .agreed_transaction_fee_margin_result(*TRANSACTION_FEE_MARGIN) - .transaction_fee_balance_minor_result(cw_transaction_fee_minor) + .gas_price_margin_result(*TX_FEE_MARGIN_IN_PERCENT) + .transaction_fee_balance_minor_result(cw_transaction_fee_minor.into()) .service_fee_balance_minor_result(cw_service_fee_balance_minor) .estimated_transaction_fee_per_transaction_minor_result( estimated_transaction_fee_per_transaction_minor, @@ -2006,6 +2218,16 @@ mod tests { Box::new(blockchain_agent) } + fn reconstruct_mock_agent(boxed: Box) -> BlockchainAgentMock { + BlockchainAgentMock::default() + .gas_price_margin_result(boxed.gas_price_margin()) + .transaction_fee_balance_minor_result(boxed.transaction_fee_balance_minor()) + .service_fee_balance_minor_result(boxed.service_fee_balance_minor()) + .estimated_transaction_fee_per_transaction_minor_result( + boxed.estimated_transaction_fee_per_transaction_minor(), + ) + } + fn test_inner_was_reset_to_null(subject: PaymentAdjusterReal) { let err = catch_unwind(AssertUnwindSafe(|| { subject.inner.original_cw_service_fee_balance_minor() @@ -2014,12 +2236,13 @@ mod tests { let panic_msg = err.downcast_ref::().unwrap(); assert_eq!( panic_msg, - "Broken code: Broken code: Called the null implementation of \ - the original_cw_service_fee_balance_minor() method in PaymentAdjusterInner" + "PaymentAdjusterInner is uninitialized. It was identified during the execution of \ + 'original_cw_service_fee_balance_minor()'" ) } - // The following tests together prove the use of correct calculators in the production code + // The following tests put together evidences pointing to the use of correct calculators in + // the production code #[test] fn each_of_defaulted_calculators_returns_different_value() { @@ -2028,64 +2251,80 @@ mod tests { let qualified_payable = QualifiedPayableAccount { bare_account: PayableAccount { wallet: make_wallet("abc"), - balance_wei: gwei_to_wei::(444_666_888), + balance_wei: multiply_by_billion(444_666_888), last_paid_timestamp: now.checked_sub(Duration::from_secs(123_000)).unwrap(), pending_payable_opt: None, }, - payment_threshold_intercept_minor: gwei_to_wei::(20_000), - creditor_thresholds: CreditorThresholds::new(gwei_to_wei::(10_000)), + payment_threshold_intercept_minor: multiply_by_billion(20_000), + creditor_thresholds: CreditorThresholds::new(multiply_by_billion(10_000)), }; - let cw_service_fee_balance_minor = gwei_to_wei::(3_000); + let cw_service_fee_balance_minor = multiply_by_billion(3_000); let exceeding_balance = qualified_payable.bare_account.balance_wei - qualified_payable.payment_threshold_intercept_minor; - let context = PaymentAdjusterInnerReal::new( - now, - None, - cw_service_fee_balance_minor, - exceeding_balance, - ); - let _ = payment_adjuster + let context = PaymentAdjusterInner::default(); + context.initialize_guts(None, cw_service_fee_balance_minor, exceeding_balance); + + payment_adjuster .calculators .into_iter() .map(|calculator| calculator.calculate(&qualified_payable, &context)) .fold(0, |previous_result, current_result| { - let min = (current_result * 97) / 100; - let max = (current_result * 97) / 100; + let slightly_less_than_current = (current_result * 97) / 100; + let slightly_more_than_current = (current_result * 103) / 100; assert_ne!(current_result, 0); - assert!(min <= previous_result || previous_result <= max); + assert!( + previous_result <= slightly_less_than_current + || slightly_more_than_current <= previous_result + ); current_result }); } + struct CalculatorTestScenario { + payable: QualifiedPayableAccount, + expected_weight: u128, + } + type InputMatrixConfigurator = fn( (QualifiedPayableAccount, QualifiedPayableAccount, SystemTime), - ) -> Vec<[(QualifiedPayableAccount, u128); 2]>; + ) -> Vec<[CalculatorTestScenario; 2]>; + + // This is the value that is computed if the account stays unmodified. Same for both nominal + // accounts. + const NOMINAL_ACCOUNT_WEIGHT: u128 = 8000000000000000; #[test] fn defaulted_calculators_react_on_correct_params() { - // When adding a test case for a new calculator, you need to make an array of inputs. Don't - // create brand-new accounts but clone the provided nominal accounts and modify them - // accordingly. Modify only those parameters that affect your calculator. + // When adding a test case for a new calculator, you need to make a two-dimensional array + // of inputs. Don't create brand-new accounts but clone the provided nominal accounts and + // modify them accordingly. Modify only those parameters that affect your calculator. // It's recommended to orientate the modifications rather positively (additions), because // there is a smaller chance you would run into some limit let input_matrix: InputMatrixConfigurator = |(nominal_account_1, nominal_account_2, _now)| { vec![ - // First test case: BalanceCalculator + // This puts only the first calculator on test, the BalanceCalculator... { let mut account_1 = nominal_account_1; - account_1.bare_account.balance_wei += 123_456_789; + account_1.bare_account.balance_wei += 123456789; let mut account_2 = nominal_account_2; - account_2.bare_account.balance_wei += 999_999_999; - [(account_1, 8000001876543209), (account_2, 8000000999999999)] + account_2.bare_account.balance_wei += 999999999; + [ + CalculatorTestScenario { + payable: account_1, + expected_weight: 8000001876543209, + }, + CalculatorTestScenario { + payable: account_2, + expected_weight: 8000000999999999, + }, + ] }, + // ...your newly added calculator should come here, and so on... ] }; - // This is the value that is computed if the account stays unmodified. Same for both nominal - // accounts. - let current_nominal_weight = 8000000000000000; - test_calculators_reactivity(input_matrix, current_nominal_weight) + test_calculators_reactivity(input_matrix) } #[derive(Clone, Copy)] @@ -2094,23 +2333,23 @@ mod tests { } struct ExpectedWeightWithWallet { - wallet: Wallet, + wallet: Address, weight: u128, } - fn test_calculators_reactivity( - input_matrix_configurator: InputMatrixConfigurator, - nominal_weight: u128, - ) { + fn test_calculators_reactivity(input_matrix_configurator: InputMatrixConfigurator) { let calculators_count = PaymentAdjusterReal::default().calculators.len(); let now = SystemTime::now(); - let cw_service_fee_balance_minor = gwei_to_wei::(1_000_000); + let cw_service_fee_balance_minor = multiply_by_billion(1_000_000); let (template_accounts, template_computed_weight) = prepare_nominal_data_before_loading_actual_test_input( now, cw_service_fee_balance_minor, ); - assert_eq!(template_computed_weight.common_weight, nominal_weight); + assert_eq!( + template_computed_weight.common_weight, + NOMINAL_ACCOUNT_WEIGHT + ); let mut template_accounts = template_accounts.to_vec(); let mut pop_account = || template_accounts.remove(0); let nominal_account_1 = pop_account(); @@ -2119,13 +2358,14 @@ mod tests { assert_eq!( input_matrix.len(), calculators_count, - "If you've recently added in a new calculator, you should add in its new test case to \ - this test. See the input matrix, it is the place where you should use the two accounts \ - you can clone. Make sure you modify only those parameters processed by your new calculator " + "Testing production code, the number of defaulted calculators should match the number \ + of test scenarios included in this test. If there are any missing, and you've recently \ + added in a new calculator, you should construct a new test case to it. See the input \ + matrix, it is the place where you should use the two accounts you can clone. Be careful \ + to modify only those parameters that are processed within your new calculator " ); test_accounts_from_input_matrix( input_matrix, - now, cw_service_fee_balance_minor, template_computed_weight, ) @@ -2138,7 +2378,6 @@ mod tests { let template_accounts = initialize_template_accounts(now); let template_weight = compute_common_weight_for_templates( template_accounts.clone(), - now, cw_service_fee_balance_minor, ); (template_accounts, template_weight) @@ -2148,12 +2387,12 @@ mod tests { let make_qualified_payable = |wallet| QualifiedPayableAccount { bare_account: PayableAccount { wallet, - balance_wei: gwei_to_wei::(20_000_000), + balance_wei: multiply_by_quintillion_concise(0.02), last_paid_timestamp: now.checked_sub(Duration::from_secs(10_000)).unwrap(), pending_payable_opt: None, }, - payment_threshold_intercept_minor: gwei_to_wei::(12_000_000), - creditor_thresholds: CreditorThresholds::new(gwei_to_wei::(1_000_000)), + payment_threshold_intercept_minor: multiply_by_quintillion_concise(0.012), + creditor_thresholds: CreditorThresholds::new(multiply_by_quintillion_concise(0.001)), }; [ @@ -2164,12 +2403,10 @@ mod tests { fn compute_common_weight_for_templates( template_accounts: [QualifiedPayableAccount; 2], - now: SystemTime, cw_service_fee_balance_minor: u128, ) -> TemplateComputedWeight { - let template_results = exercise_production_code_to_get_weighted_accounts( + let template_results = exercise_production_code_to_get_weighed_accounts( template_accounts.to_vec(), - now, cw_service_fee_balance_minor, ); let templates_common_weight = template_results @@ -2188,21 +2425,20 @@ mod tests { } } - fn exercise_production_code_to_get_weighted_accounts( + fn exercise_production_code_to_get_weighed_accounts( qualified_payables: Vec, - now: SystemTime, cw_service_fee_balance_minor: u128, - ) -> Vec { - let analyzed_payables = convert_collection(qualified_payables); - let largest_exceeding_balance_recently_qualified = + ) -> Vec { + let analyzed_payables = + convert_qualified_into_analyzed_payables_in_test(qualified_payables); + let max_debt_above_threshold_in_qualified_payables_minor = find_largest_exceeding_balance(&analyzed_payables); - let mut subject = make_initialized_subject( - Some(now), - Some(cw_service_fee_balance_minor), - None, - Some(largest_exceeding_balance_recently_qualified), - None, - ); + let mut subject = PaymentAdjusterBuilder::default() + .cw_service_fee_balance_minor(cw_service_fee_balance_minor) + .max_debt_above_threshold_in_qualified_payables_minor( + max_debt_above_threshold_in_qualified_payables_minor, + ) + .build(); let perform_adjustment_by_service_fee_params_arc = Arc::new(Mutex::new(Vec::new())); let service_fee_adjuster_mock = ServiceFeeAdjusterMock::default() // We use this container to intercept those values we are after @@ -2210,58 +2446,59 @@ mod tests { // This is just a sentinel that allows us to shorten the adjustment execution. // We care only for the params captured inside the container from above .perform_adjustment_by_service_fee_result(AdjustmentIterationResult { - decided_accounts: SomeAccountsProcessed(vec![]), + decided_accounts: vec![], remaining_undecided_accounts: vec![], }); subject.service_fee_adjuster = Box::new(service_fee_adjuster_mock); let result = subject.run_adjustment(analyzed_payables); - less_important_constant_assertions_and_weighted_accounts_extraction( + less_important_constant_assertions_and_weighed_accounts_extraction( result, perform_adjustment_by_service_fee_params_arc, cw_service_fee_balance_minor, ) } - fn less_important_constant_assertions_and_weighted_accounts_extraction( + fn less_important_constant_assertions_and_weighed_accounts_extraction( actual_result: Result, PaymentAdjusterError>, - perform_adjustment_by_service_fee_params_arc: Arc, u128)>>>, + perform_adjustment_by_service_fee_params_arc: Arc, u128)>>>, cw_service_fee_balance_minor: u128, - ) -> Vec { + ) -> Vec { // This error should be ignored, as it has no real meaning. // It allows to halt the code executions without a dive in the recursion assert_eq!( actual_result, - Err(PaymentAdjusterError::AllAccountsEliminated) + Err(PaymentAdjusterError::RecursionDrainedAllAccounts) ); let mut perform_adjustment_by_service_fee_params = perform_adjustment_by_service_fee_params_arc.lock().unwrap(); - let (weighted_accounts, captured_cw_service_fee_balance_minor) = + let (weighed_accounts, captured_cw_service_fee_balance_minor) = perform_adjustment_by_service_fee_params.remove(0); assert_eq!( captured_cw_service_fee_balance_minor, cw_service_fee_balance_minor ); assert!(perform_adjustment_by_service_fee_params.is_empty()); - weighted_accounts + weighed_accounts } fn test_accounts_from_input_matrix( - input_matrix: Vec<[(QualifiedPayableAccount, u128); 2]>, - now: SystemTime, + input_matrix: Vec<[CalculatorTestScenario; 2]>, cw_service_fee_balance_minor: u128, template_computed_weight: TemplateComputedWeight, ) { - fn prepare_args_expected_weights_for_comparison( - (qualified_payable, expected_computed_weight): (QualifiedPayableAccount, u128), + fn prepare_inputs_with_expected_weights( + particular_calculator_scenario: CalculatorTestScenario, ) -> (QualifiedPayableAccount, ExpectedWeightWithWallet) { - let wallet = qualified_payable.bare_account.wallet.clone(); - let expected_weight = ExpectedWeightWithWallet { - wallet, - weight: expected_computed_weight, - }; - (qualified_payable, expected_weight) + let wallet = particular_calculator_scenario + .payable + .bare_account + .wallet + .address(); + let weight = particular_calculator_scenario.expected_weight; + let expected_weight = ExpectedWeightWithWallet { wallet, weight }; + (particular_calculator_scenario.payable, expected_weight) } input_matrix @@ -2269,23 +2506,22 @@ mod tests { .map(|test_case| { test_case .into_iter() - .map(prepare_args_expected_weights_for_comparison) + .map(prepare_inputs_with_expected_weights) .collect::>() }) - .for_each(|qualified_payments_and_expected_computed_weights| { + .for_each(|qualified_payables_and_their_expected_weights| { let (qualified_payments, expected_computed_weights): (Vec<_>, Vec<_>) = - qualified_payments_and_expected_computed_weights + qualified_payables_and_their_expected_weights .into_iter() .unzip(); - let weighted_accounts = exercise_production_code_to_get_weighted_accounts( + let actual_weighed_accounts = exercise_production_code_to_get_weighed_accounts( qualified_payments, - now, cw_service_fee_balance_minor, ); assert_results( - weighted_accounts, + actual_weighed_accounts, expected_computed_weights, template_computed_weight, ) @@ -2293,25 +2529,25 @@ mod tests { } fn make_comparison_hashmap( - weighted_accounts: Vec, - ) -> HashMap { - let feeding_iterator = weighted_accounts + weighed_accounts: Vec, + ) -> HashMap { + let feeding_iterator = weighed_accounts .into_iter() - .map(|account| (account.wallet().clone(), account)); + .map(|account| (account.wallet(), account)); HashMap::from_iter(feeding_iterator) } fn assert_results( - weighted_accounts: Vec, + weighed_accounts: Vec, expected_computed_weights: Vec, template_computed_weight: TemplateComputedWeight, ) { - let weighted_accounts_as_hash_map = make_comparison_hashmap(weighted_accounts); + let weighed_accounts_as_hash_map = make_comparison_hashmap(weighed_accounts); expected_computed_weights.into_iter().fold( 0, |previous_account_actual_weight, expected_account_weight| { let wallet = expected_account_weight.wallet; - let actual_account = weighted_accounts_as_hash_map + let actual_account = weighed_accounts_as_hash_map .get(&wallet) .unwrap_or_else(|| panic!("Account for wallet {:?} disappeared", wallet)); assert_ne!( @@ -2329,9 +2565,9 @@ mod tests { ); assert_ne!( actual_account.weight, previous_account_actual_weight, - "You were expected to prepare two accounts with at least slightly \ - different parameters. Therefore, the evenness of their weights is \ - highly improbable and suspicious." + "You were expected to prepare two accounts with at least slightly different \ + parameters. Therefore, the evenness of their weights is highly improbable and \ + suspicious." ); actual_account.weight }, 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 118e085e4..32346cd7f 100644 --- a/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs +++ b/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs @@ -6,9 +6,11 @@ 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::PRESERVED_TEST_PAYMENT_THRESHOLDS; +use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_into_analyzed_payables_in_test; +use crate::accountant::payment_adjuster::test_utils::local_utils::PRESERVED_TEST_PAYMENT_THRESHOLDS; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysis, PaymentAdjuster, PaymentAdjusterError, PaymentAdjusterReal, + Adjustment, AdjustmentAnalysisReport, PaymentAdjuster, PaymentAdjusterError, + PaymentAdjusterReal, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; @@ -16,50 +18,64 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ PayableInspector, PayableThresholdsGaugeReal, }; use crate::accountant::test_utils::{ - make_single_qualified_payable_opt, try_making_guaranteed_qualified_payables, + make_single_qualified_payable_opt, try_to_make_guaranteed_qualified_payables, }; use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use itertools::{Either, Itertools}; -use masq_lib::percentage::Percentage; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; -use masq_lib::utils::convert_collection; use rand; use rand::distributions::uniform::SampleUniform; use rand::rngs::ThreadRng; use rand::{thread_rng, Rng}; use std::collections::HashMap; +use std::fmt::{Display, Formatter}; use std::fs::File; use std::io::Write; use std::time::SystemTime; use thousands::Separable; -use web3::types::U256; +use web3::types::{Address, U256}; #[test] // 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, a generator of possibly an overwhelming amount of scenarios that could - // get the PaymentAdjuster to be asked to sort them out even in real situations while there - // might be many and many combinations that a human is having a hard time just imagining; of - // them some might be corner cases whose threatening wasn't known when this was being designed. - // This test is to prove that even a huge number of runs, with hopefully highly variable inputs, - // will not shoot the PaymentAdjuster down and the Node with it; on the contrary, it should - // be able to give reasonable results and live up to its original purpose of adjustments. - - // Part of the requested count is rejected before the test begins as there are generated - // scenarios with such parameters that don't fit to a variety of conditions. It's easier to keep - // it this way than setting up an algorithm with enough "tamed" randomness. Other bunch of them - // will likely be marked as legitimate errors that the PaymentAdjuster can detect. - // When the test reaches its end, a text file is filled in with some key figures of the performed - // exercises and finally also an overall summary with useful statistics that can serve to - // evaluate the actual behavior against the desired. + // 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 + // 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 + // prove that despite highly variable input over a lot of attempts, the PaymentAdjuster can do + // its job reliably and won't endanger the Node. Also, it is important that it should give + // reasonable payment adjustments. + + // We can consider the test having an exo-parameter. It's the count of scenarios to be generated. + // This number must be thought of just as a rough parameter, because many of those attempted + // scenarios, loosely randomized, will be rejected in the setup stage. + + // The rejection happens before the actual test unwinds as there will always be scenarios with + // attributes that don't fit to a variety of conditions which needs to be insisted on. Those are + // that the accounts under each scenario can hold that they are legitimately qualified payables + // as those to be passed on to the payment adjuster in the real world. It goes much easier if + // 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. + + // 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 + + // This file begins with some key figures of those exercises just run, which is followed by + // a summary loaded with statistics that can serve well on inspection of the actual behavior + // against the desired. // If you are new to this algorithm, there might be results (maybe rare, but absolutely valid - // and wanted, and so deserving some interest) that can have one puzzled, though. + // and wanted) that can keep one puzzled. // The example further below presents a tricky-to-understand output belonging to one set of // payables. See those percentages. They may not excel at explaining themselves when it comes to @@ -79,7 +95,7 @@ fn loading_test_with_randomized_params() { // CW service fee balance: 32,041,461,894,055,482 wei // Portion of CW balance used: 100% - // Maximal txt count due to CW txt fee balance: UNLIMITED + // Maximal txn count due to CW txn fee balance: UNLIMITED // Used PaymentThresholds: DEFAULTED // 2000000|1000|1000|1000000|500000|1000000 // _____________________________________________________________________________________________ @@ -97,20 +113,19 @@ fn loading_test_with_randomized_params() { let now = SystemTime::now(); let mut gn = thread_rng(); - let mut subject = PaymentAdjusterReal::new(); + let subject = PaymentAdjusterReal::new(); let number_of_requested_scenarios = 2000; let scenarios = generate_scenarios(&mut gn, now, number_of_requested_scenarios); let invalidly_generated_scenarios = number_of_requested_scenarios - scenarios.len(); - let test_overall_output_collector = - TestOverallOutputCollector::new(invalidly_generated_scenarios); + let output_collector = TestOverallOutputCollector::new(invalidly_generated_scenarios); struct FirstStageOutput { - test_overall_output_collector: TestOverallOutputCollector, + output_collector: TestOverallOutputCollector, allowed_scenarios: Vec, } let init = FirstStageOutput { - test_overall_output_collector, + output_collector, allowed_scenarios: vec![], }; let first_stage_output = scenarios @@ -125,10 +140,8 @@ fn loading_test_with_randomized_params() { .iter() .map(|account| account.qualified_as.clone()) .collect(); - let initial_check_result = subject.search_for_indispensable_adjustment( - qualified_payables, - &*scenario.prepared_adjustment.agent, - ); + let initial_check_result = subject + .consider_adjustment(qualified_payables, &*scenario.prepared_adjustment.agent); let allowed_scenario_opt = match initial_check_result { Ok(check_factual_output) => { match check_factual_output { @@ -142,7 +155,7 @@ fn loading_test_with_randomized_params() { } Err(_) => { output_collector - .test_overall_output_collector + .output_collector .scenarios_denied_before_adjustment_started += 1; None } @@ -157,7 +170,7 @@ fn loading_test_with_randomized_params() { }); let second_stage_scenarios = first_stage_output.allowed_scenarios; - let test_overall_output_collector = first_stage_output.test_overall_output_collector; + let test_overall_output_collector = first_stage_output.output_collector; let scenario_adjustment_results = second_stage_scenarios .into_iter() .map(|scenario| { @@ -168,12 +181,12 @@ fn loading_test_with_randomized_params() { let cw_service_fee_balance_minor = prepared_adjustment.agent.service_fee_balance_minor(); - let payment_adjuster_result = subject.adjust_payments(prepared_adjustment, now); + let payment_adjuster_result = subject.adjust_payments(prepared_adjustment); administrate_single_scenario_result( payment_adjuster_result, account_infos, - scenario.used_thresholds, + scenario.applied_thresholds, required_adjustment, cw_service_fee_balance_minor, ) @@ -202,87 +215,33 @@ fn try_making_single_valid_scenario( now: SystemTime, ) -> Option { let accounts_count = generate_non_zero_usize(gn, 25) + 1; - let thresholds_to_be_used = choose_thresholds(gn, accounts_count); - let (cw_service_fee_balance, qualified_payables, wallet_and_thresholds_pairs) = - try_generating_qualified_payables_and_cw_balance( - gn, - &thresholds_to_be_used, - accounts_count, - now, - )?; - let used_thresholds = - thresholds_to_be_used.fix_individual_thresholds_if_needed(wallet_and_thresholds_pairs); - let analyzed_accounts: Vec = convert_collection(qualified_payables); + + 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 agent = make_agent(cw_service_fee_balance); let adjustment = make_adjustment(gn, analyzed_accounts.len()); let prepared_adjustment = PreparedAdjustment::new( Box::new(agent), None, - AdjustmentAnalysis::new(adjustment, analyzed_accounts), + AdjustmentAnalysisReport::new(adjustment, analyzed_accounts), ); Some(PreparedAdjustmentAndThresholds { prepared_adjustment, - used_thresholds, + applied_thresholds, }) } fn make_payable_account( - idx: usize, + wallet: Wallet, thresholds: &PaymentThresholds, now: SystemTime, gn: &mut ThreadRng, ) -> PayableAccount { - // Why is this construction so complicated? Well, I wanted to get the test showing partially - // fulfilling adjustments where the final accounts can be paid enough but still not all up to - // their formerly claimed balance. It turned out it is very difficult to achieve with the use of - // randomized ranges, I couldn't really come up with parameters that would promise this condition. - // I ended up experimenting and looking for an algorithm that would make the parameters as random - // as possible because the generator alone is not much good at it, using gradually, but - // individually generated parameters that I put together for better chances of randomness. Many - // produced accounts will not make it through into the actual test, filtered out when attempted - // to be converted into a proper QualifiedPayableAccount. This isn't optimal, sure, but it allows - // to observe some of those partial adjustments, however, with rather a low rate of occurrence - // among those all attempts of acceptable scenarios. - let wallet = make_wallet(&format!("wallet{}", idx)); - let mut generate_age_segment = || { - generate_non_zero_usize( - gn, - (thresholds.maturity_threshold_sec + thresholds.threshold_interval_sec) as usize, - ) / 2 - }; - let debt_age = generate_age_segment() + generate_age_segment(); - let service_fee_balance_minor = { - let mut generate_u128 = || generate_non_zero_usize(gn, 100) as u128; - let parameter_a = generate_u128(); - let parameter_b = generate_u128(); - let parameter_c = generate_u128(); - let parameter_d = generate_u128(); - let parameter_e = generate_u128(); - let parameter_f = generate_u128(); - let mut use_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 - } - } - }); - - thresholds.permanent_debt_allowed_gwei as u128 + addition - }; + let debt_age = generate_debt_age(gn, thresholds); + let service_fee_balance_minor = + generate_highly_randomized_payable_account_balance(gn, thresholds); let last_paid_timestamp = from_time_t(to_time_t(now) - debt_age as i64); PayableAccount { wallet, @@ -292,119 +251,191 @@ fn make_payable_account( } } +fn generate_debt_age(gn: &mut ThreadRng, thresholds: &PaymentThresholds) -> u64 { + generate_range( + 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; + + let parameter_a = generate_u128(); + let parameter_b = generate_u128(); + let parameter_c = generate_u128(); + let parameter_d = generate_u128(); + let parameter_e = generate_u128(); + let parameter_f = generate_u128(); + + let mut use_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 + } + } + }); + + thresholds.permanent_debt_allowed_gwei as u128 + addition +} + +fn try_make_qualified_payables_by_applied_thresholds( + payable_accounts: Vec, + applied_thresholds: &AppliedThresholds, + now: SystemTime, +) -> Vec { + let payment_inspector = PayableInspector::new(Box::new(PayableThresholdsGaugeReal::default())); + match applied_thresholds { + AppliedThresholds::Defaulted => try_to_make_guaranteed_qualified_payables( + payable_accounts, + &PRESERVED_TEST_PAYMENT_THRESHOLDS, + now, + false, + ), + AppliedThresholds::CommonButRandomized { common_thresholds } => { + try_to_make_guaranteed_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() + } + } +} + fn try_generating_qualified_payables_and_cw_balance( gn: &mut ThreadRng, - thresholds_to_be_used: &AppliedThresholds, accounts_count: usize, now: SystemTime, -) -> Option<( - u128, - Vec, - Vec<(Wallet, PaymentThresholds)>, -)> { - let payables = make_payables_according_to_thresholds_setup( - gn, - &thresholds_to_be_used, - accounts_count, - now, - ); +) -> Option<(u128, Vec, AppliedThresholds)> { + let (payables, applied_thresholds) = + make_payables_according_to_thresholds_setup(gn, accounts_count, now); - let (qualified_payables, wallet_and_thresholds_pairs) = - try_make_qualified_payables_by_applied_thresholds(payables, &thresholds_to_be_used, now); + let qualified_payables = + try_make_qualified_payables_by_applied_thresholds(payables, &applied_thresholds, now); - let balance_average = { - let sum: u128 = sum_as(&qualified_payables, |account| account.balance_minor()); - sum / accounts_count as u128 - }; - let cw_service_fee_balance_minor = { - let multiplier = 1000; - let max_pieces = accounts_count * multiplier; - let number_of_pieces = generate_usize(gn, max_pieces - 2) as u128 + 2; - balance_average / multiplier as u128 * number_of_pieces - }; - let required_service_fee_total: u128 = - sum_as(&qualified_payables, |account| account.balance_minor()); + let cw_service_fee_balance_minor = + pick_appropriate_cw_service_fee_balance(gn, &qualified_payables, accounts_count); + + let required_service_fee_total: u128 = sum_as(&qualified_payables, |account| { + account.initial_balance_minor() + }); if required_service_fee_total <= cw_service_fee_balance_minor { None } else { Some(( cw_service_fee_balance_minor, qualified_payables, - wallet_and_thresholds_pairs, + applied_thresholds, )) } } +fn pick_appropriate_cw_service_fee_balance( + gn: &mut ThreadRng, + qualified_payables: &[QualifiedPayableAccount], + accounts_count: usize, +) -> u128 { + // Value picked empirically + const COEFFICIENT: usize = 1000; + 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 +} + fn make_payables_according_to_thresholds_setup( gn: &mut ThreadRng, - thresholds_to_be_used: &AppliedThresholds, accounts_count: usize, now: SystemTime, -) -> Vec { - match thresholds_to_be_used { +) -> (Vec, AppliedThresholds) { + let wallets = prepare_account_wallets(accounts_count); + + 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, - accounts_count, now, ), - AppliedThresholds::SingleButRandomized { common_thresholds } => { - make_payables_with_common_thresholds(gn, common_thresholds, accounts_count, now) + AppliedThresholds::CommonButRandomized { common_thresholds } => { + make_payables_with_common_thresholds(gn, wallets, common_thresholds, now) } AppliedThresholds::RandomizedForEachAccount { individual_thresholds, - } => { - let vec_of_thresholds = individual_thresholds - .thresholds - .as_ref() - .left() - .expect("should be Vec at this stage"); - assert_eq!(vec_of_thresholds.len(), accounts_count); - make_payables_with_individual_thresholds(gn, vec_of_thresholds, now) - } - } -} + } => make_payables_with_individual_thresholds(gn, wallets, individual_thresholds, now), + }; -fn make_payables_with_common_thresholds( - gn: &mut ThreadRng, - common_thresholds: &PaymentThresholds, - accounts_count: usize, - now: SystemTime, -) -> Vec { - (0..accounts_count) - .map(|idx| make_payable_account(idx, common_thresholds, now, gn)) - .collect::>() + (payables, nominated_thresholds) } -fn make_payables_with_individual_thresholds( - gn: &mut ThreadRng, - individual_thresholds: &[PaymentThresholds], - now: SystemTime, -) -> Vec { - individual_thresholds - .iter() - .enumerate() - .map(|(idx, thresholds)| make_payable_account(idx, thresholds, now, gn)) +fn prepare_account_wallets(accounts_count: usize) -> Vec { + (0..accounts_count) + .map(|idx| make_wallet(&format!("wallet{}", idx))) .collect() } -fn choose_thresholds(gn: &mut ThreadRng, accounts_count: usize) -> AppliedThresholds { +fn choose_thresholds(gn: &mut ThreadRng, prepared_wallets: &[Wallet]) -> AppliedThresholds { let be_defaulted = generate_boolean(gn); if be_defaulted { AppliedThresholds::Defaulted } else { - let be_common_for_all = generate_boolean(gn); - if be_common_for_all { - AppliedThresholds::SingleButRandomized { + 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 thresholds_set = (0..accounts_count) - .map(|_| return_single_randomized_thresholds(gn)) - .collect(); - let individual_thresholds = IndividualThresholds { - thresholds: Either::Left(thresholds_set), - }; + let individual_thresholds = prepared_wallets + .iter() + .map(|wallet| (wallet.address(), return_single_randomized_thresholds(gn))) + .collect::>(); AppliedThresholds::RandomizedForEachAccount { individual_thresholds, } @@ -412,20 +443,48 @@ fn choose_thresholds(gn: &mut ThreadRng, accounts_count: usize) -> AppliedThresh } } +fn make_payables_with_common_thresholds( + gn: &mut ThreadRng, + prepared_wallets: Vec, + common_thresholds: &PaymentThresholds, + now: SystemTime, +) -> Vec { + prepared_wallets + .into_iter() + .map(|wallet| make_payable_account(wallet, common_thresholds, now, gn)) + .collect() +} + +fn make_payables_with_individual_thresholds( + gn: &mut ThreadRng, + wallets: Vec, + wallet_addresses_and_thresholds: &HashMap, + now: SystemTime, +) -> Vec { + let mut wallets_by_address = wallets + .into_iter() + .map(|wallet| (wallet.address(), wallet)) + .collect::>(); + wallet_addresses_and_thresholds + .iter() + .map(|(wallet, thresholds)| { + let wallet = wallets_by_address.remove(wallet).expect("missing wallet"); + make_payable_account(wallet, thresholds, now, gn) + }) + .collect() +} + fn return_single_randomized_thresholds(gn: &mut ThreadRng) -> PaymentThresholds { let permanent_debt_allowed_gwei = generate_range(gn, 100, 1_000_000_000); let debt_threshold_gwei = permanent_debt_allowed_gwei + generate_range(gn, 10_000, 10_000_000_000); - let maturity_threshold_sec = generate_range(gn, 100, 10_000); - let threshold_interval_sec = generate_range(gn, 1000, 100_000); - let unban_below_gwei = permanent_debt_allowed_gwei; PaymentThresholds { debt_threshold_gwei, - maturity_threshold_sec, + maturity_threshold_sec: generate_range(gn, 100, 10_000), payment_grace_period_sec: 0, permanent_debt_allowed_gwei, - threshold_interval_sec, - unban_below_gwei, + threshold_interval_sec: generate_range(gn, 1000, 100_000), + unban_below_gwei: permanent_debt_allowed_gwei, } } @@ -441,16 +500,16 @@ 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) - .agreed_transaction_fee_margin_result(Percentage::new(15)) + .gas_price_margin_result(TX_FEE_MARGIN_IN_PERCENT.clone()) } fn make_adjustment(gn: &mut ThreadRng, accounts_count: usize) -> Adjustment { let also_by_transaction_fee = generate_boolean(gn); if also_by_transaction_fee && accounts_count > 2 { - let affordable_transaction_count = + let transaction_count_limit = u16::try_from(generate_non_zero_usize(gn, accounts_count)).unwrap(); - Adjustment::TransactionFeeInPriority { - affordable_transaction_count, + Adjustment::BeginByTransactionFee { + transaction_count_limit, } } else { Adjustment::ByServiceFee @@ -467,24 +526,26 @@ fn administrate_single_scenario_result( let common = CommonScenarioInfo { cw_service_fee_balance_minor, required_adjustment, - used_thresholds, + payment_thresholds: used_thresholds, }; let reinterpreted_result = match payment_adjuster_result { Ok(outbound_payment_instructions) => { - let mut adjusted_accounts = outbound_payment_instructions.affordable_accounts; - let portion_of_cw_cumulatively_used_percents = { - let used_absolute: u128 = sum_as(&adjusted_accounts, |account| account.balance_wei); - ((100 * used_absolute) / common.cw_service_fee_balance_minor) as u8 - }; - let adjusted_accounts = - interpretable_adjustment_results(account_infos, &mut adjusted_accounts); + let adjusted_accounts = outbound_payment_instructions.affordable_accounts; + let portion_of_cw_cumulatively_used_percents = + 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) = - sort_interpretable_adjustments(adjusted_accounts); + sort_interpretable_adjustments(interpretable_adjustments); Ok(SuccessfulAdjustment { common, portion_of_cw_cumulatively_used_percents, partially_sorted_interpretable_adjustments, - were_no_accounts_eliminated, + no_accounts_eliminated: were_no_accounts_eliminated, }) } Err(adjuster_error) => Err(FailedAdjustment { @@ -497,18 +558,57 @@ fn administrate_single_scenario_result( ScenarioResult::new(reinterpreted_result) } -fn interpretable_adjustment_results( - account_infos: Vec, - adjusted_accounts: &mut Vec, -) -> Vec { - account_infos +fn merge_information_about_particular_account( + accounts_infos: Vec, + accounts_after_adjustment: Vec, +) -> Vec<(AccountInfo, Option)> { + let mut accounts_hashmap = accounts_after_adjustment + .into_iter() + .map(|account| (account.wallet.address(), account)) + .collect::>(); + + accounts_infos .into_iter() - .map(|account_info| { - prepare_interpretable_adjustment_result(account_info, adjusted_accounts) + .map(|info| { + let adjusted_account_opt = accounts_hashmap.remove(&info.wallet); + (info, adjusted_account_opt) }) .collect() } +enum PercentPortionOfCWUsed { + Percents(u8), + LessThanOnePercent, +} + +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 { + PercentPortionOfCWUsed::LessThanOnePercent + } + } + + fn as_plain_number(&self) -> u8 { + match self { + Self::Percents(percents) => *percents, + Self::LessThanOnePercent => 1, + } + } +} + +impl Display for PercentPortionOfCWUsed { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Percents(percents) => write!(f, "{percents}"), + Self::LessThanOnePercent => write!(f, "< 1"), + } + } +} + struct ScenarioResult { result: Result, } @@ -521,9 +621,9 @@ impl ScenarioResult { struct SuccessfulAdjustment { common: CommonScenarioInfo, - portion_of_cw_cumulatively_used_percents: u8, - partially_sorted_interpretable_adjustments: Vec, - were_no_accounts_eliminated: bool, + portion_of_cw_cumulatively_used_percents: PercentPortionOfCWUsed, + partially_sorted_interpretable_adjustments: Vec, + no_accounts_eliminated: bool, } struct FailedAdjustment { @@ -539,7 +639,7 @@ fn preserve_account_infos( accounts .iter() .map(|account| AccountInfo { - wallet: account.qualified_as.bare_account.wallet.clone(), + wallet: account.qualified_as.bare_account.wallet.address(), initially_requested_service_fee_minor: account.qualified_as.bare_account.balance_wei, debt_age_s: now .duration_since(account.qualified_as.bare_account.last_paid_timestamp) @@ -552,30 +652,27 @@ fn preserve_account_infos( fn render_results_to_file_and_attempt_basic_assertions( scenario_results: Vec, number_of_requested_scenarios: usize, - overall_output_collector: TestOverallOutputCollector, + output_collector: TestOverallOutputCollector, ) { let file_dir = ensure_node_home_directory_exists("payment_adjuster", "tests"); - let mut file = File::create(file_dir.join("loading_test_output.txt")).unwrap(); - introduction(&mut file); - let test_overall_output_collector = + let mut output_file = File::create(file_dir.join("loading_test_output.txt")).unwrap(); + introduction(&mut output_file); + let output_collector = scenario_results .into_iter() - .fold(overall_output_collector, |acc, scenario_result| { - do_final_processing_of_single_scenario(&mut file, acc, scenario_result) + .fold(output_collector, |acc, scenario_result| { + do_final_processing_of_single_scenario(&mut output_file, acc, scenario_result) }); - let total_scenarios_evaluated = test_overall_output_collector - .scenarios_denied_before_adjustment_started - + test_overall_output_collector.oks - + test_overall_output_collector.all_accounts_eliminated - + test_overall_output_collector.late_immoderately_insufficient_service_fee_balance; - write_brief_test_summary_into_file( - &mut file, - &test_overall_output_collector, + let total_scenarios_evaluated = + output_collector.total_evaluated_scenarios_except_those_discarded_early(); + write_brief_test_summary_at_file_s_tail( + &mut output_file, + &output_collector, number_of_requested_scenarios, total_scenarios_evaluated, ); let total_scenarios_handled_including_invalid_ones = - total_scenarios_evaluated + test_overall_output_collector.invalidly_generated_scenarios; + output_collector.total_evaluated_scenarios_including_invalid_ones(); assert_eq!( total_scenarios_handled_including_invalid_ones, number_of_requested_scenarios, "All handled scenarios including those invalid ones ({}) != requested scenarios count ({})", @@ -586,27 +683,27 @@ fn render_results_to_file_and_attempt_basic_assertions( // 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. - let entry_check_pass_rate = 100 - - ((test_overall_output_collector.scenarios_denied_before_adjustment_started * 100) + let actual_entry_check_pass_percentage = 100 + - ((output_collector.scenarios_denied_before_adjustment_started * 100) / total_scenarios_evaluated); - let required_pass_rate = 50; + const REQUIRED_ENTRY_CHECK_PASS_PERCENTAGE: usize = 50; assert!( - entry_check_pass_rate >= required_pass_rate, - "Not at least {}% from those {} scenarios \ - generated for this test allows PaymentAdjuster to continue doing its job and ends too early. \ - Instead only {}%. Setup of the test might be needed", - required_pass_rate, + actual_entry_check_pass_percentage >= REQUIRED_ENTRY_CHECK_PASS_PERCENTAGE, + "Not at least {}% from those {} scenarios generated for this test allows PaymentAdjuster to \ + continue doing its job and ends too early. Instead only {}%. Setup of the test might be \ + needed", + REQUIRED_ENTRY_CHECK_PASS_PERCENTAGE, total_scenarios_evaluated, - entry_check_pass_rate + actual_entry_check_pass_percentage ); - let ok_adjustment_percentage = (test_overall_output_collector.oks * 100) - / (total_scenarios_evaluated - - test_overall_output_collector.scenarios_denied_before_adjustment_started); - let required_success_rate = 70; + let ok_adjustment_percentage = (output_collector.oks * 100) + / (total_scenarios_evaluated - output_collector.scenarios_denied_before_adjustment_started); + const REQUIRED_SUCCESSFUL_ADJUSTMENT_PERCENTAGE: usize = 70; assert!( - ok_adjustment_percentage >= required_success_rate, - "Not at least {}% from {} adjustment procedures from PaymentAdjuster runs finished with success, only {}%", - required_success_rate, + ok_adjustment_percentage >= REQUIRED_SUCCESSFUL_ADJUSTMENT_PERCENTAGE, + "Not at least {}% from {} adjustment procedures from PaymentAdjuster runs finished with \ + success, only {}%", + REQUIRED_SUCCESSFUL_ADJUSTMENT_PERCENTAGE, total_scenarios_evaluated, ok_adjustment_percentage ); @@ -625,11 +722,11 @@ fn introduction(file: &mut File) { write_thick_dividing_line(file) } -fn write_brief_test_summary_into_file( +fn write_brief_test_summary_at_file_s_tail( file: &mut File, - overall_output_collector: &TestOverallOutputCollector, - number_of_requested_scenarios: usize, - total_of_scenarios_evaluated: usize, + output_collector: &TestOverallOutputCollector, + scenarios_requested: usize, + scenarios_evaluated: usize, ) { write_thick_dividing_line(file); file.write_fmt(format_args!( @@ -647,29 +744,30 @@ fn write_brief_test_summary_into_file( {}\n\n\ Unsuccessful\n\ Caught by the entry check:............. {}\n\ - With 'AllAccountsEliminated':.......... {}\n\ + With 'RecursionDrainedAllAccounts':.... {}\n\ With late insufficient balance errors:. {}\n\n\ Legend\n\ - Partially adjusted accounts mark:...... {}", - number_of_requested_scenarios, - total_of_scenarios_evaluated, - overall_output_collector.oks, - overall_output_collector.with_no_accounts_eliminated, - overall_output_collector + Adjusted balances are highlighted by \ + these marks by the side:............. . {}", + scenarios_requested, + scenarios_evaluated, + output_collector.oks, + output_collector.with_no_accounts_eliminated, + output_collector .fulfillment_distribution_for_transaction_fee_adjustments .total_scenarios(), - overall_output_collector + output_collector .fulfillment_distribution_for_transaction_fee_adjustments .render_in_two_lines(), - overall_output_collector + output_collector .fulfillment_distribution_for_service_fee_adjustments .total_scenarios(), - overall_output_collector + output_collector .fulfillment_distribution_for_service_fee_adjustments .render_in_two_lines(), - overall_output_collector.scenarios_denied_before_adjustment_started, - overall_output_collector.all_accounts_eliminated, - overall_output_collector.late_immoderately_insufficient_service_fee_balance, + output_collector.scenarios_denied_before_adjustment_started, + output_collector.all_accounts_eliminated, + output_collector.late_immoderately_insufficient_service_fee_balance, NON_EXHAUSTED_ACCOUNT_MARKER )) .unwrap() @@ -677,47 +775,58 @@ fn write_brief_test_summary_into_file( fn do_final_processing_of_single_scenario( file: &mut File, - mut test_overall_output: TestOverallOutputCollector, + mut output_collector: TestOverallOutputCollector, scenario: ScenarioResult, ) -> TestOverallOutputCollector { match scenario.result { Ok(positive) => { - if positive.were_no_accounts_eliminated { - test_overall_output.with_no_accounts_eliminated += 1 + if positive.no_accounts_eliminated { + output_collector.with_no_accounts_eliminated += 1 } if matches!( positive.common.required_adjustment, - Adjustment::TransactionFeeInPriority { .. } + Adjustment::BeginByTransactionFee { .. } ) { - test_overall_output + output_collector .fulfillment_distribution_for_transaction_fee_adjustments .collected_fulfillment_percentages - .push(positive.portion_of_cw_cumulatively_used_percents) + .push( + positive + .portion_of_cw_cumulatively_used_percents + .as_plain_number(), + ) } - if positive.common.required_adjustment == Adjustment::ByServiceFee { - test_overall_output + if matches!( + positive.common.required_adjustment, + Adjustment::ByServiceFee + ) { + output_collector .fulfillment_distribution_for_service_fee_adjustments .collected_fulfillment_percentages - .push(positive.portion_of_cw_cumulatively_used_percents) + .push( + positive + .portion_of_cw_cumulatively_used_percents + .as_plain_number(), + ) } render_positive_scenario(file, positive); - test_overall_output.oks += 1; - test_overall_output + output_collector.oks += 1; + output_collector } Err(negative) => { match negative.adjuster_error { - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { .. } => { + PaymentAdjusterError::AbsolutelyInsufficientBalance { .. } => { panic!("Such errors should be already filtered out") } - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { .. } => { - test_overall_output.late_immoderately_insufficient_service_fee_balance += 1 + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { .. } => { + output_collector.late_immoderately_insufficient_service_fee_balance += 1 } - PaymentAdjusterError::AllAccountsEliminated => { - test_overall_output.all_accounts_eliminated += 1 + PaymentAdjusterError::RecursionDrainedAllAccounts => { + output_collector.all_accounts_eliminated += 1 } } render_negative_scenario(file, negative); - test_overall_output + output_collector } } } @@ -725,138 +834,151 @@ fn do_final_processing_of_single_scenario( fn render_scenario_header( file: &mut File, scenario_common: &CommonScenarioInfo, - portion_of_cw_used_percents: u8, + portion_of_cw_used_percents: PercentPortionOfCWUsed, ) { write_thick_dividing_line(file); file.write_fmt(format_args!( - "CW service fee balance: {} wei\n\ - Portion of CW balance used: {}%\n\ - Maximal txt count due to CW txt fee balance: {}\n\ - Used PaymentThresholds: {}\n", + "CW service fee balance: {} wei\n\ + Portion of CW balance used: {} %\n\ + Maximal txn count due to CW txn fee balance: {}\n\ + Used PaymentThresholds: {}\n\n", scenario_common .cw_service_fee_balance_minor .separate_with_commas(), portion_of_cw_used_percents, - resolve_affordable_transaction_count(&scenario_common.required_adjustment), - resolve_comment_on_thresholds(&scenario_common.used_thresholds) + scenario_common.resolve_affordable_tx_count_by_tx_fee(), + scenario_common.resolve_thresholds_description() )) .unwrap(); } -fn resolve_comment_on_thresholds(applied_thresholds: &AppliedThresholds) -> String { - match applied_thresholds { - AppliedThresholds::Defaulted | AppliedThresholds::SingleButRandomized { .. } => { - if let AppliedThresholds::SingleButRandomized { common_thresholds } = applied_thresholds - { - format!("SHARED BUT CUSTOM\n{}", common_thresholds) - } else { - format!("DEFAULTED\n{}", PRESERVED_TEST_PAYMENT_THRESHOLDS) - } - } - AppliedThresholds::RandomizedForEachAccount { .. } => "INDIVIDUAL".to_string(), - } -} - fn render_positive_scenario(file: &mut File, result: SuccessfulAdjustment) { render_scenario_header( file, &result.common, result.portion_of_cw_cumulatively_used_percents, ); - write_thin_dividing_line(file); let adjusted_accounts = result.partially_sorted_interpretable_adjustments; render_accounts( file, &adjusted_accounts, - &result.common.used_thresholds, + &result.common.payment_thresholds, |file, account, individual_thresholds_opt| { single_account_output( file, - account.info.initially_requested_service_fee_minor, - account.info.debt_age_s, individual_thresholds_opt, - account.bills_coverage_in_percentage_opt, + &account.info, + account.bill_coverage_in_percentage_opt, ) }, ) } -fn render_accounts( +fn render_negative_scenario(file: &mut File, negative_result: FailedAdjustment) { + render_scenario_header( + file, + &negative_result.common, + PercentPortionOfCWUsed::Percents(0), + ); + render_accounts( + file, + &negative_result.account_infos, + &negative_result.common.payment_thresholds, + |file, account, individual_thresholds_opt| { + single_account_output(file, individual_thresholds_opt, account, None) + }, + ); + write_thin_dividing_line(file); + write_error(file, negative_result.adjuster_error) +} + +trait AccountWithWallet { + fn wallet(&self) -> Address; +} + +fn render_accounts( file: &mut File, - accounts: &[A], + accounts: &[Account], used_thresholds: &AppliedThresholds, mut render_account: F, ) where - A: AccountWithWallet, - F: FnMut(&mut File, &A, Option<&PaymentThresholds>), + Account: AccountWithWallet, + F: FnMut(&mut File, &Account, Option<&PaymentThresholds>), { - let set_of_individual_thresholds_opt = if let AppliedThresholds::RandomizedForEachAccount { + let individual_thresholds_opt = if let AppliedThresholds::RandomizedForEachAccount { individual_thresholds, } = used_thresholds { - Some(individual_thresholds.thresholds.as_ref().right().unwrap()) + Some(individual_thresholds) } else { None }; + accounts .iter() .map(|account| { ( account, - set_of_individual_thresholds_opt.map(|thresholds| { - thresholds - .get(&account.wallet()) - .expect("Original thresholds missing") - }), + fetch_individual_thresholds_for_account_opt(individual_thresholds_opt, account), ) }) .for_each(|(account, individual_thresholds_opt)| { render_account(file, account, individual_thresholds_opt) }); + file.write(b"\n").unwrap(); } -trait AccountWithWallet { - fn wallet(&self) -> &Wallet; +fn fetch_individual_thresholds_for_account_opt<'a, Account>( + individual_thresholds_opt: Option<&'a HashMap>, + account: &'a Account, +) -> Option<&'a PaymentThresholds> +where + Account: AccountWithWallet, +{ + individual_thresholds_opt.map(|wallets_and_thresholds| { + wallets_and_thresholds + .get(&account.wallet()) + .expect("Original thresholds missing") + }) } -const FIRST_COLUMN_WIDTH: usize = 50; +const FIRST_COLUMN_WIDTH: usize = 34; const AGE_COLUMN_WIDTH: usize = 8; - const STARTING_GAP: usize = 6; fn single_account_output( file: &mut File, - balance_minor: u128, - age_s: u64, individual_thresholds_opt: Option<&PaymentThresholds>, + account_info: &AccountInfo, bill_coverage_in_percentage_opt: Option, ) { let first_column_width = FIRST_COLUMN_WIDTH; let age_width = AGE_COLUMN_WIDTH; let starting_gap = STARTING_GAP; - let _ = file - .write_fmt(format_args!( - "{}{:first_column_width$} wei | {:>age_width$} s | {}\n", - individual_thresholds_opt - .map(|thresholds| format!( - "{:first_column_width$}\n", - "", thresholds - )) - .unwrap_or("".to_string()), - "", - balance_minor.separate_with_commas(), - age_s.separate_with_commas(), - resolve_account_ending_status_graphically(bill_coverage_in_percentage_opt), - )) - .unwrap(); + file.write_fmt(format_args!( + "{}{:first_column_width$} wei | {:>age_width$} s | {}\n", + individual_thresholds_opt + .map(|thresholds| format!( + "{:first_column_width$}\n", + "", thresholds + )) + .unwrap_or("".to_string()), + "", + account_info + .initially_requested_service_fee_minor + .separate_with_commas(), + account_info.debt_age_s.separate_with_commas(), + resolve_account_fulfilment_status_graphically(bill_coverage_in_percentage_opt), + )) + .unwrap(); } const NON_EXHAUSTED_ACCOUNT_MARKER: &str = "# # # # # # # #"; -fn resolve_account_ending_status_graphically( +fn resolve_account_fulfilment_status_graphically( bill_coverage_in_percentage_opt: Option, ) -> String { match bill_coverage_in_percentage_opt { @@ -872,27 +994,6 @@ fn resolve_account_ending_status_graphically( } } -fn render_negative_scenario(file: &mut File, negative_result: FailedAdjustment) { - render_scenario_header(file, &negative_result.common, 0); - write_thin_dividing_line(file); - render_accounts( - file, - &negative_result.account_infos, - &negative_result.common.used_thresholds, - |file, account, individual_thresholds_opt| { - single_account_output( - file, - account.initially_requested_service_fee_minor, - account.debt_age_s, - individual_thresholds_opt, - None, - ) - }, - ); - write_thin_dividing_line(file); - write_error(file, negative_result.adjuster_error) -} - fn write_error(file: &mut File, error: PaymentAdjusterError) { file.write_fmt(format_args!( "Scenario resulted in a failure: {:?}\n", @@ -901,15 +1002,6 @@ fn write_error(file: &mut File, error: PaymentAdjusterError) { .unwrap() } -fn resolve_affordable_transaction_count(adjustment: &Adjustment) -> String { - match adjustment { - Adjustment::ByServiceFee => "UNLIMITED".to_string(), - Adjustment::TransactionFeeInPriority { - affordable_transaction_count, - } => affordable_transaction_count.to_string(), - } -} - fn write_thick_dividing_line(file: &mut dyn Write) { write_ln_made_of(file, '=') } @@ -926,64 +1018,34 @@ fn write_ln_made_of(file: &mut dyn Write, char: char) { .unwrap(); } -fn prepare_interpretable_adjustment_result( - account_info: AccountInfo, - resulted_affordable_accounts: &mut Vec, -) -> InterpretableAdjustmentResult { - let adjusted_account_idx_opt = resulted_affordable_accounts - .iter() - .position(|account| account.wallet == account_info.wallet); - let bills_coverage_in_percentage_opt = match adjusted_account_idx_opt { - Some(idx) => { - let adjusted_account = resulted_affordable_accounts.remove(idx); - assert_eq!(adjusted_account.wallet, account_info.wallet); - let bill_coverage_in_percentage = { - let percentage = (adjusted_account.balance_wei * 100) - / account_info.initially_requested_service_fee_minor; - u8::try_from(percentage).unwrap() - }; - Some(bill_coverage_in_percentage) - } - None => None, - }; - InterpretableAdjustmentResult { - info: AccountInfo { - wallet: account_info.wallet, - debt_age_s: account_info.debt_age_s, - initially_requested_service_fee_minor: account_info - .initially_requested_service_fee_minor, - }, - - bills_coverage_in_percentage_opt, - } -} - fn sort_interpretable_adjustments( - interpretable_adjustments: Vec, -) -> (Vec, bool) { + interpretable_adjustments: Vec, +) -> (Vec, bool) { let (finished, eliminated): ( - Vec, - Vec, + Vec, + Vec, ) = interpretable_adjustments .into_iter() - .partition(|adjustment| adjustment.bills_coverage_in_percentage_opt.is_some()); + .partition(|adjustment| adjustment.bill_coverage_in_percentage_opt.is_some()); let were_no_accounts_eliminated = eliminated.is_empty(); + // Sorting in descending order by bills coverage in percentage and ascending by balances let finished_sorted = finished.into_iter().sorted_by(|result_a, result_b| { Ord::cmp( &( - result_b.bills_coverage_in_percentage_opt, + result_b.bill_coverage_in_percentage_opt, result_a.info.initially_requested_service_fee_minor, ), &( - result_a.bills_coverage_in_percentage_opt, + result_a.bill_coverage_in_percentage_opt, result_b.info.initially_requested_service_fee_minor, ), ) }); + // Sorting in descending order let eliminated_sorted = eliminated.into_iter().sorted_by(|result_a, result_b| { Ord::cmp( - &result_a.info.initially_requested_service_fee_minor, &result_b.info.initially_requested_service_fee_minor, + &result_a.info.initially_requested_service_fee_minor, ) }); let all_results = finished_sorted.chain(eliminated_sorted).collect(); @@ -1038,6 +1100,18 @@ impl TestOverallOutputCollector { late_immoderately_insufficient_service_fee_balance: 0, } } + + fn total_evaluated_scenarios_except_those_discarded_early(&self) -> usize { + self.scenarios_denied_before_adjustment_started + + self.oks + + self.all_accounts_eliminated + + self.late_immoderately_insufficient_service_fee_balance + } + + fn total_evaluated_scenarios_including_invalid_ones(&self) -> usize { + self.total_evaluated_scenarios_except_those_discarded_early() + + self.invalidly_generated_scenarios + } } #[derive(Default)] @@ -1118,141 +1192,93 @@ impl PercentageFulfillmentDistribution { struct PreparedAdjustmentAndThresholds { prepared_adjustment: PreparedAdjustment, - used_thresholds: AppliedThresholds, + applied_thresholds: AppliedThresholds, } struct CommonScenarioInfo { cw_service_fee_balance_minor: u128, required_adjustment: Adjustment, - used_thresholds: AppliedThresholds, + payment_thresholds: AppliedThresholds, +} + +impl CommonScenarioInfo { + fn resolve_affordable_tx_count_by_tx_fee(&self) -> String { + match self.required_adjustment { + Adjustment::ByServiceFee => "UNLIMITED".to_string(), + Adjustment::BeginByTransactionFee { + transaction_count_limit, + } => transaction_count_limit.to_string(), + } + } + + fn resolve_thresholds_description(&self) -> String { + match self.payment_thresholds { + AppliedThresholds::Defaulted => { + format!("DEFAULTED\n{}", PRESERVED_TEST_PAYMENT_THRESHOLDS) + } + AppliedThresholds::CommonButRandomized { common_thresholds } => { + format!("SHARED BUT CUSTOM\n{}", common_thresholds) + } + AppliedThresholds::RandomizedForEachAccount { .. } => "INDIVIDUAL".to_string(), + } + } } -struct InterpretableAdjustmentResult { + +struct InterpretableAccountAdjustmentResult { info: AccountInfo, // Account was eliminated from payment if None - bills_coverage_in_percentage_opt: Option, + bill_coverage_in_percentage_opt: Option, } -impl AccountWithWallet for InterpretableAdjustmentResult { - fn wallet(&self) -> &Wallet { - &self.info.wallet +impl AccountWithWallet for InterpretableAccountAdjustmentResult { + fn wallet(&self) -> Address { + self.info.wallet + } +} + +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, + }, + + bill_coverage_in_percentage_opt, + } } } struct AccountInfo { - wallet: Wallet, + wallet: Address, initially_requested_service_fee_minor: u128, debt_age_s: u64, } impl AccountWithWallet for AccountInfo { - fn wallet(&self) -> &Wallet { - &self.wallet + fn wallet(&self) -> Address { + self.wallet } } enum AppliedThresholds { Defaulted, - SingleButRandomized { + CommonButRandomized { common_thresholds: PaymentThresholds, }, RandomizedForEachAccount { - individual_thresholds: IndividualThresholds, + individual_thresholds: HashMap, }, } - -impl AppliedThresholds { - fn fix_individual_thresholds_if_needed( - self, - wallet_and_thresholds_pairs: Vec<(Wallet, PaymentThresholds)>, - ) -> Self { - match self { - AppliedThresholds::RandomizedForEachAccount { .. } => { - assert!( - !wallet_and_thresholds_pairs.is_empty(), - "Pairs should be missing by now" - ); - let hash_map = HashMap::from_iter(wallet_and_thresholds_pairs); - let individual_thresholds = IndividualThresholds { - thresholds: Either::Right(hash_map), - }; - AppliedThresholds::RandomizedForEachAccount { - individual_thresholds, - } - } - x => x, - } - } -} - -struct IndividualThresholds { - thresholds: Either, HashMap>, -} - -fn try_make_qualified_payables_by_applied_thresholds( - payable_accounts: Vec, - applied_thresholds: &AppliedThresholds, - now: SystemTime, -) -> ( - Vec, - Vec<(Wallet, PaymentThresholds)>, -) { - let payment_inspector = PayableInspector::new(Box::new(PayableThresholdsGaugeReal::default())); - match applied_thresholds { - AppliedThresholds::Defaulted => ( - try_making_guaranteed_qualified_payables( - payable_accounts, - &PRESERVED_TEST_PAYMENT_THRESHOLDS, - now, - false, - ), - vec![], - ), - AppliedThresholds::SingleButRandomized { common_thresholds } => ( - try_making_guaranteed_qualified_payables( - payable_accounts, - common_thresholds, - now, - false, - ), - vec![], - ), - AppliedThresholds::RandomizedForEachAccount { - individual_thresholds, - } => { - let vec_of_thresholds = individual_thresholds - .thresholds - .as_ref() - .left() - .expect("should be Vec at this stage"); - assert_eq!( - payable_accounts.len(), - vec_of_thresholds.len(), - "The number of generated \ - payables {} differs from their sets of thresholds {}, but one should've been derived \ - from the other", - payable_accounts.len(), - vec_of_thresholds.len() - ); - let zipped = payable_accounts.into_iter().zip(vec_of_thresholds.iter()); - zipped.fold( - (vec![], vec![]), - |(mut qualified_payables, mut wallet_thresholds_pairs), - (payable, its_thresholds)| match make_single_qualified_payable_opt( - payable, - &payment_inspector, - &its_thresholds, - false, - now, - ) { - Some(qualified_payable) => { - let wallet = qualified_payable.bare_account.wallet.clone(); - qualified_payables.push(qualified_payable); - wallet_thresholds_pairs.push((wallet, *its_thresholds)); - (qualified_payables, wallet_thresholds_pairs) - } - None => (qualified_payables, wallet_thresholds_pairs), - }, - ) - } - } -} diff --git a/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs b/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs index fde0c6a7a..4ac4fb25a 100644 --- a/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs +++ b/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs @@ -1,41 +1,35 @@ -use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::WeightedPayable; -use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub trait DisqualificationAnalysableAccount: BalanceProvidingAccount -where - Product: BalanceProvidingAccount + DisqualificationLimitProvidingAccount, -{ - fn prepare_analyzable_account( - self, - disqualification_arbiter: &DisqualificationArbiter, - ) -> Product; -} +use crate::accountant::payment_adjuster::miscellaneous::data_structures::WeighedPayable; +use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; pub trait BalanceProvidingAccount { - fn balance_minor(&self) -> u128; + fn initial_balance_minor(&self) -> u128; } -pub trait DisqualificationLimitProvidingAccount { - fn disqualification_limit(&self) -> u128; +impl BalanceProvidingAccount for WeighedPayable { + fn initial_balance_minor(&self) -> u128 { + self.analyzed_account.initial_balance_minor() + } } -impl DisqualificationAnalysableAccount for WeightedPayable { - fn prepare_analyzable_account( - self, - _disqualification_arbiter: &DisqualificationArbiter, - ) -> WeightedPayable { - self +impl BalanceProvidingAccount for AnalyzedPayableAccount { + fn initial_balance_minor(&self) -> u128 { + self.qualified_as.initial_balance_minor() } } -impl BalanceProvidingAccount for WeightedPayable { - fn balance_minor(&self) -> u128 { - self.analyzed_account.balance_minor() +impl BalanceProvidingAccount for QualifiedPayableAccount { + fn initial_balance_minor(&self) -> u128 { + self.bare_account.balance_wei } } -impl DisqualificationLimitProvidingAccount for WeightedPayable { +pub trait DisqualificationLimitProvidingAccount { + fn disqualification_limit(&self) -> u128; +} + +impl DisqualificationLimitProvidingAccount for WeighedPayable { fn disqualification_limit(&self) -> u128 { self.analyzed_account.disqualification_limit() } @@ -46,25 +40,3 @@ impl DisqualificationLimitProvidingAccount for AnalyzedPayableAccount { self.disqualification_limit_minor } } - -impl BalanceProvidingAccount for AnalyzedPayableAccount { - fn balance_minor(&self) -> u128 { - self.qualified_as.balance_minor() - } -} - -impl DisqualificationAnalysableAccount for QualifiedPayableAccount { - fn prepare_analyzable_account( - self, - disqualification_arbiter: &DisqualificationArbiter, - ) -> AnalyzedPayableAccount { - let dsq_limit = disqualification_arbiter.calculate_disqualification_edge(&self); - AnalyzedPayableAccount::new(self, dsq_limit) - } -} - -impl BalanceProvidingAccount for QualifiedPayableAccount { - fn balance_minor(&self) -> u128 { - self.bare_account.balance_wei - } -} diff --git a/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs index 1b139a1e1..62ddde7ac 100644 --- a/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs +++ b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs @@ -8,16 +8,14 @@ use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions: log_transaction_fee_adjustment_ok_but_by_service_fee_undoable, }; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - TransactionCountsBy16bits, WeightedPayable, -}; -use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ - find_smallest_u128, sum_as, + AffordableAndRequiredTxCounts, WeighedPayable, }; +use crate::accountant::payment_adjuster::miscellaneous::helper_functions::sum_as; use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::{ BalanceProvidingAccount, DisqualificationLimitProvidingAccount, }; use crate::accountant::payment_adjuster::{ - Adjustment, AdjustmentAnalysis, PaymentAdjusterError, ServiceFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, PaymentAdjusterError, ServiceFeeImmoderateInsufficiency, TransactionFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; @@ -25,7 +23,7 @@ use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; use ethereum_types::U256; use itertools::Either; use masq_lib::logger::Logger; -use masq_lib::percentage::Percentage; +use masq_lib::percentage::PurePercentage; pub struct PreparatoryAnalyzer {} @@ -40,19 +38,19 @@ impl PreparatoryAnalyzer { disqualification_arbiter: &DisqualificationArbiter, qualified_payables: Vec, logger: &Logger, - ) -> Result, AdjustmentAnalysis>, PaymentAdjusterError> + ) -> Result, AdjustmentAnalysisReport>, PaymentAdjusterError> { let number_of_accounts = qualified_payables.len(); let cw_transaction_fee_balance_minor = agent.transaction_fee_balance_minor(); - let per_transaction_requirement_minor = + let required_tx_fee_per_transaction_minor = agent.estimated_transaction_fee_per_transaction_minor(); - let agreed_transaction_fee_margin = agent.agreed_transaction_fee_margin(); + let gas_price_margin = agent.gas_price_margin(); let transaction_fee_check_result = self .determine_transaction_count_limit_by_transaction_fee( cw_transaction_fee_balance_minor, - agreed_transaction_fee_margin, - per_transaction_requirement_minor, + gas_price_margin, + required_tx_fee_per_transaction_minor, number_of_accounts, logger, ); @@ -93,12 +91,12 @@ impl PreparatoryAnalyzer { let adjustment = match transaction_fee_limitation_opt { None => Adjustment::ByServiceFee, - Some(affordable_transaction_count) => Adjustment::TransactionFeeInPriority { - affordable_transaction_count, + Some(transaction_count_limit) => Adjustment::BeginByTransactionFee { + transaction_count_limit, }, }; - Ok(Either::Right(AdjustmentAnalysis::new( + Ok(Either::Right(AdjustmentAnalysisReport::new( adjustment, prepared_accounts, ))) @@ -109,48 +107,47 @@ impl PreparatoryAnalyzer { transaction_fee_check_result: Result, TransactionFeeImmoderateInsufficiency>, service_fee_check_result: Result<(), ServiceFeeImmoderateInsufficiency>, ) -> Result, PaymentAdjusterError> { - match (transaction_fee_check_result, service_fee_check_result) { - (Err(transaction_fee_check_error), Ok(_)) => Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - number_of_accounts, - transaction_fee_opt: Some(transaction_fee_check_error), - service_fee_opt: None, - }, - ), - (Err(transaction_fee_check_error), Err(service_fee_check_error)) => Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { + let construct_error = + |tx_fee_check_err_opt: Option, + service_fee_check_err_opt: Option| { + PaymentAdjusterError::AbsolutelyInsufficientBalance { number_of_accounts, - transaction_fee_opt: Some(transaction_fee_check_error), - service_fee_opt: Some(service_fee_check_error), - }, - ), - (Ok(_), Err(service_fee_check_error)) => Err( - PaymentAdjusterError::EarlyNotEnoughFeeForSingleTransaction { - number_of_accounts, - transaction_fee_opt: None, - service_fee_opt: Some(service_fee_check_error), - }, - ), - (Ok(transaction_fee_limiting_count_opt), Ok(())) => { - Ok(transaction_fee_limiting_count_opt) + transaction_fee_opt: tx_fee_check_err_opt, + service_fee_opt: service_fee_check_err_opt, + } + }; + + match (transaction_fee_check_result, service_fee_check_result) { + (Err(transaction_fee_check_error), Ok(_)) => { + Err(construct_error(Some(transaction_fee_check_error), None)) + } + (Err(transaction_fee_check_error), Err(service_fee_check_error)) => { + Err(construct_error( + Some(transaction_fee_check_error), + Some(service_fee_check_error), + )) } + (Ok(_), Err(service_fee_check_error)) => { + Err(construct_error(None, Some(service_fee_check_error))) + } + (Ok(tx_count_limit_opt), Ok(())) => Ok(tx_count_limit_opt), } } pub fn recheck_if_service_fee_adjustment_is_needed( &self, - weighted_accounts: &[WeightedPayable], + weighed_accounts: &[WeighedPayable], cw_service_fee_balance_minor: u128, error_factory: LateServiceFeeSingleTxErrorFactory, logger: &Logger, ) -> Result { if Self::is_service_fee_adjustment_needed( - weighted_accounts, + weighed_accounts, cw_service_fee_balance_minor, logger, ) { if let Err(e) = Self::check_adjustment_possibility( - weighted_accounts, + weighed_accounts, cw_service_fee_balance_minor, error_factory, ) { @@ -167,13 +164,13 @@ impl PreparatoryAnalyzer { fn determine_transaction_count_limit_by_transaction_fee( &self, cw_transaction_fee_balance_minor: U256, - agreed_transaction_fee_margin: Percentage, + gas_price_margin: PurePercentage, per_transaction_requirement_minor: u128, number_of_qualified_accounts: usize, logger: &Logger, ) -> Result, TransactionFeeImmoderateInsufficiency> { let per_txn_requirement_minor_with_margin = - agreed_transaction_fee_margin.add_percent_to(per_transaction_requirement_minor); + gas_price_margin.add_percent_to(per_transaction_requirement_minor); let verified_tx_counts = Self::transaction_counts_verification( cw_transaction_fee_balance_minor, @@ -208,11 +205,11 @@ impl PreparatoryAnalyzer { cw_transaction_fee_balance_minor: U256, txn_fee_required_per_txn_minor: u128, number_of_qualified_accounts: usize, - ) -> TransactionCountsBy16bits { + ) -> AffordableAndRequiredTxCounts { let max_possible_tx_count_u256 = cw_transaction_fee_balance_minor / U256::from(txn_fee_required_per_txn_minor); - TransactionCountsBy16bits::new(max_possible_tx_count_u256, number_of_qualified_accounts) + AffordableAndRequiredTxCounts::new(max_possible_tx_count_u256, number_of_qualified_accounts) } fn check_adjustment_possibility( @@ -222,7 +219,7 @@ impl PreparatoryAnalyzer { ) -> Result<(), Error> where AnalyzableAccounts: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, - ErrorFactory: ServiceFeeSingleTXErrorFactory, + ErrorFactory: ServiceFeeSingleTXErrorFactory, { let lowest_disqualification_limit = Self::find_lowest_disqualification_limit(prepared_accounts); @@ -234,14 +231,8 @@ impl PreparatoryAnalyzer { if lowest_disqualification_limit <= cw_service_fee_balance_minor { Ok(()) } else { - let analyzed_accounts_count = prepared_accounts.len(); - let required_service_fee_total = - Self::compute_total_of_service_fee_required(prepared_accounts); - let err = service_fee_error_factory.make( - analyzed_accounts_count, - required_service_fee_total, - cw_service_fee_balance_minor, - ); + let err = + service_fee_error_factory.make(prepared_accounts, cw_service_fee_balance_minor); Err(err) } } @@ -260,11 +251,11 @@ impl PreparatoryAnalyzer { .collect() } - fn compute_total_of_service_fee_required(payables: &[Account]) -> u128 + fn compute_total_service_fee_required(payables: &[Account]) -> u128 where Account: BalanceProvidingAccount, { - sum_as(payables, |account| account.balance_minor()) + sum_as(payables, |account| account.initial_balance_minor()) } fn is_service_fee_adjustment_needed( @@ -276,7 +267,7 @@ impl PreparatoryAnalyzer { Account: BalanceProvidingAccount, { let service_fee_totally_required_minor = - Self::compute_total_of_service_fee_required(qualified_payables); + Self::compute_total_service_fee_required(qualified_payables); (service_fee_totally_required_minor > cw_service_fee_balance_minor) .then(|| { log_adjustment_by_service_fee_is_required( @@ -292,36 +283,34 @@ impl PreparatoryAnalyzer { where Account: DisqualificationLimitProvidingAccount, { - find_smallest_u128( - &accounts - .iter() - .map(|account| account.disqualification_limit()) - .collect::>(), - ) + accounts + .iter() + .map(|account| account.disqualification_limit()) + .min() + .expect("No account to consider") } } -pub trait ServiceFeeSingleTXErrorFactory { - fn make( - &self, - number_of_accounts: usize, - required_service_fee_total: u128, - cw_service_fee_balance_minor: u128, - ) -> E; +pub trait ServiceFeeSingleTXErrorFactory +where + AnalyzableAccount: BalanceProvidingAccount, +{ + fn make(&self, accounts: &[AnalyzableAccount], cw_service_fee_balance_minor: u128) -> Error; } #[derive(Default)] pub struct EarlyServiceFeeSingleTXErrorFactory {} -impl ServiceFeeSingleTXErrorFactory +impl ServiceFeeSingleTXErrorFactory for EarlyServiceFeeSingleTXErrorFactory { fn make( &self, - _number_of_accounts: usize, - total_service_fee_required_minor: u128, + accounts: &[AnalyzedPayableAccount], cw_service_fee_balance_minor: u128, ) -> ServiceFeeImmoderateInsufficiency { + let total_service_fee_required_minor = + PreparatoryAnalyzer::compute_total_service_fee_required(accounts); ServiceFeeImmoderateInsufficiency { total_service_fee_required_minor, cw_service_fee_balance_minor, @@ -332,33 +321,36 @@ impl ServiceFeeSingleTXErrorFactory #[derive(Debug, PartialEq, Eq, Clone)] pub struct LateServiceFeeSingleTxErrorFactory { original_number_of_accounts: usize, - original_service_fee_required_total_minor: u128, + original_total_service_fee_required_minor: u128, } impl LateServiceFeeSingleTxErrorFactory { - pub fn new(unadjusted_accounts: &[WeightedPayable]) -> Self { + pub fn new(unadjusted_accounts: &[WeighedPayable]) -> Self { let original_number_of_accounts = unadjusted_accounts.len(); - let original_service_fee_required_total_minor = - sum_as(unadjusted_accounts, |account| account.balance_minor()); + let original_total_service_fee_required_minor = sum_as(unadjusted_accounts, |account| { + account.initial_balance_minor() + }); Self { original_number_of_accounts, - original_service_fee_required_total_minor, + original_total_service_fee_required_minor, } } } -impl ServiceFeeSingleTXErrorFactory for LateServiceFeeSingleTxErrorFactory { +impl ServiceFeeSingleTXErrorFactory + for LateServiceFeeSingleTxErrorFactory +{ fn make( &self, - number_of_accounts: usize, - _required_service_fee_total: u128, + current_set_of_accounts: &[WeighedPayable], cw_service_fee_balance_minor: u128, ) -> PaymentAdjusterError { - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + let number_of_accounts = current_set_of_accounts.len(); + PaymentAdjusterError::AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { original_number_of_accounts: self.original_number_of_accounts, number_of_accounts, - original_service_fee_required_total_minor: self - .original_service_fee_required_total_minor, + original_total_service_fee_required_minor: self + .original_total_service_fee_required_minor, cw_service_fee_balance_minor, } } @@ -369,23 +361,27 @@ mod tests { use crate::accountant::payment_adjuster::disqualification_arbiter::{ DisqualificationArbiter, DisqualificationGauge, }; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::WeighedPayable; use crate::accountant::payment_adjuster::miscellaneous::helper_functions::sum_as; + use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::{ + BalanceProvidingAccount, DisqualificationLimitProvidingAccount, + }; use crate::accountant::payment_adjuster::preparatory_analyser::{ EarlyServiceFeeSingleTXErrorFactory, LateServiceFeeSingleTxErrorFactory, PreparatoryAnalyzer, ServiceFeeSingleTXErrorFactory, }; - use crate::accountant::payment_adjuster::test_utils::{ - make_weighed_account, multiple_by_billion, DisqualificationGaugeMock, + 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, AdjustmentAnalysis, PaymentAdjusterError, ServiceFeeImmoderateInsufficiency, + Adjustment, AdjustmentAnalysisReport, PaymentAdjusterError, + ServiceFeeImmoderateInsufficiency, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{ - make_analyzed_account, make_non_guaranteed_qualified_payable, - }; + use crate::accountant::test_utils::make_meaningless_qualified_payable; use crate::accountant::QualifiedPayableAccount; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_FEE_MARGIN; + use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; @@ -398,23 +394,20 @@ mod tests { test_name: &str, disqualification_gauge: DisqualificationGaugeMock, original_accounts: [QualifiedPayableAccount; 2], - cw_service_fee_balance: u128, + cw_service_fee_balance_minor: u128, ) { init_test_logging(); let determine_limit_params_arc = Arc::new(Mutex::new(vec![])); - let disqualification_gauge = double_mock_results_queue(disqualification_gauge) - .determine_limit_params(&determine_limit_params_arc); + let disqualification_gauge = + make_mock_with_two_results_doubled_into_four(disqualification_gauge) + .determine_limit_params(&determine_limit_params_arc); let total_amount_required: u128 = sum_as(original_accounts.as_slice(), |account| { account.bare_account.balance_wei }); let disqualification_arbiter = DisqualificationArbiter::new(Box::new(disqualification_gauge)); let subject = PreparatoryAnalyzer {}; - let blockchain_agent = BlockchainAgentMock::default() - .agreed_transaction_fee_margin_result(*TRANSACTION_FEE_MARGIN) - .transaction_fee_balance_minor_result(U256::MAX) - .estimated_transaction_fee_per_transaction_minor_result(123456) - .service_fee_balance_minor_result(cw_service_fee_balance); + let blockchain_agent = make_populated_blockchain_agent(cw_service_fee_balance_minor); let result = subject.analyze_accounts( &blockchain_agent, @@ -423,13 +416,12 @@ mod tests { &Logger::new(test_name), ); - let expected_adjustment_analysis = { - let analyzed_accounts = PreparatoryAnalyzer::pre_process_accounts_for_adjustments( - original_accounts.to_vec(), - &disqualification_arbiter, - ); - AdjustmentAnalysis::new(Adjustment::ByServiceFee, analyzed_accounts) - }; + let analyzed_accounts = PreparatoryAnalyzer::pre_process_accounts_for_adjustments( + original_accounts.to_vec(), + &disqualification_arbiter, + ); + let expected_adjustment_analysis = + AdjustmentAnalysisReport::new(Adjustment::ByServiceFee, analyzed_accounts); assert_eq!(result, Ok(Either::Right(expected_adjustment_analysis))); let determine_limit_params = determine_limit_params_arc.lock().unwrap(); let account_1 = &original_accounts[0]; @@ -448,23 +440,31 @@ mod tests { ]; assert_eq!(&determine_limit_params[0..2], expected_params); TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Total of {} wei in MASQ was ordered while the consuming wallet \ - held only {} wei of MASQ token. Adjustment of their count or balances is required.", + "WARN: {test_name}: Mature payables amount to {} MASQ wei while the consuming wallet \ + holds only {} wei. Adjustment in their count or balances is necessary.", total_amount_required.separate_with_commas(), - cw_service_fee_balance.separate_with_commas() + cw_service_fee_balance_minor.separate_with_commas() )); } + fn make_populated_blockchain_agent(cw_service_fee_balance_minor: u128) -> BlockchainAgentMock { + BlockchainAgentMock::default() + .gas_price_margin_result(*TX_FEE_MARGIN_IN_PERCENT) + .transaction_fee_balance_minor_result(U256::MAX) + .estimated_transaction_fee_per_transaction_minor_result(123456) + .service_fee_balance_minor_result(cw_service_fee_balance_minor) + } + #[test] fn adjustment_possibility_nearly_rejected_when_cw_balance_slightly_bigger() { - let mut account_1 = make_non_guaranteed_qualified_payable(111); - account_1.bare_account.balance_wei = 1_000_000_000; - let mut account_2 = make_non_guaranteed_qualified_payable(333); - account_2.bare_account.balance_wei = 2_000_000_000; - let cw_service_fee_balance = 750_000_001; + let mut account_1 = make_meaningless_qualified_payable(111); + account_1.bare_account.balance_wei = multiply_by_billion_concise(1.0); + let mut account_2 = make_meaningless_qualified_payable(333); + account_2.bare_account.balance_wei = multiply_by_billion_concise(2.0); + let cw_service_fee_balance = multiply_by_billion_concise(0.75) + 1; let disqualification_gauge = DisqualificationGaugeMock::default() - .determine_limit_result(750_000_000) - .determine_limit_result(1_500_000_000); + .determine_limit_result(multiply_by_billion_concise(0.75)) + .determine_limit_result(multiply_by_billion_concise(1.5)); let original_accounts = [account_1, account_2]; test_adjustment_possibility_nearly_rejected( @@ -477,14 +477,14 @@ mod tests { #[test] fn adjustment_possibility_nearly_rejected_when_cw_balance_equal() { - let mut account_1 = make_non_guaranteed_qualified_payable(111); - account_1.bare_account.balance_wei = 2_000_000_000; - let mut account_2 = make_non_guaranteed_qualified_payable(333); - account_2.bare_account.balance_wei = 1_000_000_000; - let cw_service_fee_balance = 750_000_000; + let mut account_1 = make_meaningless_qualified_payable(111); + account_1.bare_account.balance_wei = multiply_by_billion_concise(2.0); + let mut account_2 = make_meaningless_qualified_payable(333); + account_2.bare_account.balance_wei = multiply_by_billion_concise(1.0); + let cw_service_fee_balance = multiply_by_billion_concise(0.75); let disqualification_gauge = DisqualificationGaugeMock::default() - .determine_limit_result(1_500_000_000) - .determine_limit_result(750_000_000); + .determine_limit_result(multiply_by_billion_concise(1.5)) + .determine_limit_result(multiply_by_billion_concise(0.75)); let original_accounts = [account_1, account_2]; test_adjustment_possibility_nearly_rejected( @@ -495,47 +495,76 @@ mod tests { ) } - fn test_not_enough_for_even_the_least_demanding_account_causes_error( + fn test_not_enough_even_for_the_smallest_account_error< + ErrorFactory, + Error, + EnsureAccountsRightType, + PrepareExpectedError, + AnalyzableAccount, + >( error_factory: ErrorFactory, - expected_error_preparer: F, + ensure_account_right_type: EnsureAccountsRightType, + prepare_expected_error: PrepareExpectedError, ) where - F: FnOnce(usize, u128, u128) -> Error, - ErrorFactory: ServiceFeeSingleTXErrorFactory, + EnsureAccountsRightType: FnOnce(Vec) -> Vec, + PrepareExpectedError: FnOnce(usize, u128, u128) -> Error, + ErrorFactory: ServiceFeeSingleTXErrorFactory, Error: Debug + PartialEq, + AnalyzableAccount: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, { - let mut account_1 = make_analyzed_account(111); - account_1.qualified_as.bare_account.balance_wei = 2_000_000_000; - account_1.disqualification_limit_minor = 1_500_000_000; - let mut account_2 = make_analyzed_account(222); - account_2.qualified_as.bare_account.balance_wei = 1_000_050_000; - account_2.disqualification_limit_minor = 1_000_000_101; - let mut account_3 = make_analyzed_account(333); - account_3.qualified_as.bare_account.balance_wei = 1_000_111_111; - account_3.disqualification_limit_minor = 1_000_000_222; - let cw_service_fee_balance = 1_000_000_100; + let mut account_1 = make_meaningless_weighed_account(111); + account_1 + .analyzed_account + .qualified_as + .bare_account + .balance_wei = 2_000_000_000; + account_1.analyzed_account.disqualification_limit_minor = 1_500_000_000; + let mut account_2 = make_meaningless_weighed_account(222); + account_2 + .analyzed_account + .qualified_as + .bare_account + .balance_wei = 1_000_050_000; + account_2.analyzed_account.disqualification_limit_minor = 1_000_000_101; + let mut account_3 = make_meaningless_weighed_account(333); + account_3 + .analyzed_account + .qualified_as + .bare_account + .balance_wei = 1_000_111_111; + account_3.analyzed_account.disqualification_limit_minor = 1_000_000_222; + let cw_service_fee_balance_minor = 1_000_000_100; + let service_fee_total_of_the_known_set = account_1.initial_balance_minor() + + account_2.initial_balance_minor() + + account_3.initial_balance_minor(); let supplied_accounts = vec![account_1, account_2, account_3]; let supplied_accounts_count = supplied_accounts.len(); - let service_fee_total_of_the_known_set = 2_000_000_000 + 1_000_050_000 + 1_000_111_111; + let rightly_typed_accounts = ensure_account_right_type(supplied_accounts); let result = PreparatoryAnalyzer::check_adjustment_possibility( - &supplied_accounts, - cw_service_fee_balance, + &rightly_typed_accounts, + cw_service_fee_balance_minor, error_factory, ); - let expected_error = expected_error_preparer( + let expected_error = prepare_expected_error( supplied_accounts_count, service_fee_total_of_the_known_set, - cw_service_fee_balance, + cw_service_fee_balance_minor, ); assert_eq!(result, Err(expected_error)) } #[test] - fn not_enough_for_even_the_least_demanding_account_error_right_after_positive_tx_fee_check() { + fn not_enough_for_even_the_smallest_account_error_right_after_alarmed_tx_fee_check() { let error_factory = EarlyServiceFeeSingleTXErrorFactory::default(); - - let expected_error_preparer = + let ensure_accounts_right_type = |weighed_payables: Vec| { + weighed_payables + .into_iter() + .map(|weighed_account| weighed_account.analyzed_account) + .collect() + }; + let prepare_expected_error = |_, total_amount_demanded_in_accounts_in_place, cw_service_fee_balance_minor| { ServiceFeeImmoderateInsufficiency { total_service_fee_required_minor: total_amount_demanded_in_accounts_in_place, @@ -543,67 +572,75 @@ mod tests { } }; - test_not_enough_for_even_the_least_demanding_account_causes_error( + test_not_enough_even_for_the_smallest_account_error( error_factory, - expected_error_preparer, + ensure_accounts_right_type, + prepare_expected_error, ) } #[test] - fn not_enough_for_even_the_least_demanding_account_error_right_after_tx_fee_accounts_dump() { + fn not_enough_for_even_the_smallest_account_error_right_after_accounts_dumped_for_tx_fee() { let original_accounts = vec![ - make_weighed_account(123), - make_weighed_account(456), - make_weighed_account(789), - make_weighed_account(1011), + make_meaningless_weighed_account(123), + make_meaningless_weighed_account(456), + make_meaningless_weighed_account(789), + make_meaningless_weighed_account(1011), ]; let original_number_of_accounts = original_accounts.len(); - let initial_sum = sum_as(&original_accounts, |account| account.balance_minor()); + let initial_sum = sum_as(&original_accounts, |account| { + account.initial_balance_minor() + }); let error_factory = LateServiceFeeSingleTxErrorFactory::new(&original_accounts); - let expected_error_preparer = |number_of_accounts, _, cw_service_fee_balance_minor| { - PaymentAdjusterError::LateNotEnoughFeeForSingleTransaction { + 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, number_of_accounts, - original_service_fee_required_total_minor: initial_sum, + original_total_service_fee_required_minor: initial_sum, cw_service_fee_balance_minor, } }; - test_not_enough_for_even_the_least_demanding_account_causes_error( + test_not_enough_even_for_the_smallest_account_error( error_factory, - expected_error_preparer, + ensure_accounts_right_type, + prepare_expected_error, ) } #[test] - fn accounts_analyzing_works_even_for_weighted_payable() { + fn recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighted_payables() { init_test_logging(); - let test_name = "accounts_analyzing_works_even_for_weighted_payable"; - let balance_1 = multiple_by_billion(2_000_000); - let mut weighted_account_1 = make_weighed_account(123); - weighted_account_1 + let test_name = + "recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighted_payables"; + let balance_1 = multiply_by_billion(2_000_000); + let mut weighed_account_1 = make_meaningless_weighed_account(123); + weighed_account_1 .analyzed_account .qualified_as .bare_account .balance_wei = balance_1; - let balance_2 = multiple_by_billion(3_456_000); - let mut weighted_account_2 = make_weighed_account(456); - weighted_account_2 + let balance_2 = multiply_by_billion(3_456_000); + let mut weighed_account_2 = make_meaningless_weighed_account(456); + weighed_account_2 .analyzed_account .qualified_as .bare_account .balance_wei = balance_2; - let accounts = vec![weighted_account_1, weighted_account_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 logger = Logger::new(test_name); let subject = PreparatoryAnalyzer::new(); [(0, false), (1, false), (2, true)].iter().for_each( - |(subtrahend_from_cw_balance, expected_result)| { + |(subtrahend_from_cw_balance, adjustment_is_needed_expected)| { let service_fee_balance = cw_service_fee_balance_minor - subtrahend_from_cw_balance; - let result = subject + let adjustment_is_needed_actual = subject .recheck_if_service_fee_adjustment_is_needed( &accounts, service_fee_balance, @@ -611,12 +648,14 @@ mod tests { &logger, ) .unwrap(); - assert_eq!(result, *expected_result); + assert_eq!(adjustment_is_needed_actual, *adjustment_is_needed_expected); }, ); + TestLogHandler::new().exists_log_containing(&format!( - "WARN: {test_name}: Total of {} wei in MASQ was ordered while the consuming wallet held \ - only {}", service_fee_totally_required_minor.separate_with_commas(), + "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() )); } @@ -624,42 +663,44 @@ mod tests { #[test] fn construction_of_error_context_with_accounts_dumped_works() { let balance_1 = 1234567; - let mut account_1 = make_weighed_account(123); + let mut account_1 = make_meaningless_weighed_account(123); account_1 .analyzed_account .qualified_as .bare_account .balance_wei = balance_1; let balance_2 = 999888777; - let mut account_2 = make_weighed_account(345); + let mut account_2 = make_meaningless_weighed_account(345); account_2 .analyzed_account .qualified_as .bare_account .balance_wei = balance_2; - let weighted_accounts = vec![account_1, account_2]; + let weighed_accounts = vec![account_1, account_2]; - let result = LateServiceFeeSingleTxErrorFactory::new(&weighted_accounts); + let result = LateServiceFeeSingleTxErrorFactory::new(&weighed_accounts); assert_eq!( result, LateServiceFeeSingleTxErrorFactory { original_number_of_accounts: 2, - original_service_fee_required_total_minor: balance_1 + balance_2 + original_total_service_fee_required_minor: balance_1 + balance_2 } ) } - fn double_mock_results_queue(mock: DisqualificationGaugeMock) -> DisqualificationGaugeMock { - let originally_prepared_results = (0..2) + fn make_mock_with_two_results_doubled_into_four( + mock: DisqualificationGaugeMock, + ) -> DisqualificationGaugeMock { + let popped_results = (0..2) .map(|_| mock.determine_limit(0, 0, 0)) .collect::>(); - originally_prepared_results + popped_results .into_iter() .cycle() .take(4) - .fold(mock, |mock, result_to_be_added| { - mock.determine_limit_result(result_to_be_added) + .fold(mock, |mock, single_result| { + mock.determine_limit_result(single_result) }) } } diff --git a/node/src/accountant/payment_adjuster/service_fee_adjuster.rs b/node/src/accountant/payment_adjuster/service_fee_adjuster.rs index 5954a224d..f9e86a7b5 100644 --- a/node/src/accountant/payment_adjuster/service_fee_adjuster.rs +++ b/node/src/accountant/payment_adjuster/service_fee_adjuster.rs @@ -1,18 +1,13 @@ // Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::DecidedAccounts::{ - LowGainingAccountEliminated, SomeAccountsProcessed, -}; use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ AdjustedAccountBeforeFinalization, AdjustmentIterationResult, UnconfirmedAdjustment, - WeightedPayable, + WeighedPayable, }; use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics:: ordinary_diagnostic_functions::{proposed_adjusted_balance_diagnostics}; -use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ - compute_mul_coefficient_preventing_fractional_numbers, weights_total, -}; +use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{compute_mul_coefficient_preventing_fractional_numbers, sum_as}; use itertools::Either; use masq_lib::logger::Logger; use masq_lib::utils::convert_collection; @@ -22,89 +17,75 @@ use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::o pub trait ServiceFeeAdjuster { fn perform_adjustment_by_service_fee( &self, - weighted_accounts: Vec, + weighed_accounts: Vec, disqualification_arbiter: &DisqualificationArbiter, - unallocated_cw_service_fee_balance_minor: u128, + remaining_cw_service_fee_balance_minor: u128, logger: &Logger, ) -> AdjustmentIterationResult; } -pub struct ServiceFeeAdjusterReal { - adjustment_computer: AdjustmentComputer, -} +#[derive(Default)] +pub struct ServiceFeeAdjusterReal {} impl ServiceFeeAdjuster for ServiceFeeAdjusterReal { fn perform_adjustment_by_service_fee( &self, - weighted_accounts: Vec, + weighed_accounts: Vec, disqualification_arbiter: &DisqualificationArbiter, cw_service_fee_balance_minor: u128, logger: &Logger, ) -> AdjustmentIterationResult { - let unconfirmed_adjustments = self - .adjustment_computer - .compute_unconfirmed_adjustments(weighted_accounts, cw_service_fee_balance_minor); + let unconfirmed_adjustments = + compute_unconfirmed_adjustments(weighed_accounts, cw_service_fee_balance_minor); - let checked_accounts = Self::handle_winning_accounts(unconfirmed_adjustments); + let checked_accounts = Self::try_confirm_some_accounts(unconfirmed_adjustments); match checked_accounts { - Either::Left(no_thriving_competitors) => Self::disqualify_single_account( + Either::Left(no_accounts_above_disq_limit) => Self::disqualify_single_account( disqualification_arbiter, - no_thriving_competitors, + no_accounts_above_disq_limit, logger, ), - Either::Right(thriving_competitors) => thriving_competitors, + Either::Right(some_accounts_above_disq_limit) => some_accounts_above_disq_limit, } } } -impl Default for ServiceFeeAdjusterReal { - fn default() -> Self { - Self::new() - } -} - impl ServiceFeeAdjusterReal { - fn new() -> Self { - Self { - adjustment_computer: Default::default(), - } - } - - // The thin term "outweighed account" coma from a phenomenon with an account whose weight - // increases significantly based on a different parameter than the debt size. Untreated, we - // would wind up granting the account (much) more money than what it got recorded by + // The thin term "outweighed account" comes from a phenomenon related to an account whose weight + // increases significantly based on a different parameter than the debt size. Untreated, it + // could easily wind up with granting the account (much) more money than it was recorded by // the Accountant. // - // Each outweighed account, as well as any one with the proposed balance computed as a value - // between the disqualification limit of this account and the entire balance (originally - // requested), will gain instantly the same portion that equals the disqualification limit - // of this account. Anything below that is, in turn, considered unsatisfying, hence a reason - // for that account to go away simply disqualified. + // Each outweighed account, and even further, also any account with the proposed adjusted + // balance higher than its disqualification limit, will gain instantly equally to its + // disqualification limit. Anything below that is, in turn, considered unsatisfying, hence + // the reason to be disqualified. // - // The idea is that we want to spare as much as possible in the held means that could be - // continuously distributed among the rest of accounts until it is possible to adjust an account - // and unmeet the condition for a disqualification. + // The idea is that we try to spare as much as possible from the means that could be, if done + // wisely, better redistributed among the rest of accounts, as much as the wider group of them + // can be satisfied, even though just partially. // - // On the other hand, if it begins being clear that the remaining money can keep no other - // account up in the selection there is yet another operation to come where the already - // selected accounts are reviewed again in the order of their significance and some of - // the unused money is poured into them, which goes on until zero. - fn handle_winning_accounts( + // However, if it begins to be clear that the remaining money doesn't allow to keep 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 (thriving_competitors, losing_competitors) = - Self::filter_and_process_winners(unconfirmed_adjustments); + let (accounts_above_disq_limit, accounts_below_disq_limit) = + Self::filter_and_process_confirmable_accounts(unconfirmed_adjustments); - if thriving_competitors.is_empty() { - Either::Left(losing_competitors) + if accounts_above_disq_limit.is_empty() { + Either::Left(accounts_below_disq_limit) } else { - let remaining_undecided_accounts: Vec = - convert_collection(losing_competitors); + let remaining_undecided_accounts: Vec = + convert_collection(accounts_below_disq_limit); let pre_processed_decided_accounts: Vec = - convert_collection(thriving_competitors); + convert_collection(accounts_above_disq_limit); Either::Right(AdjustmentIterationResult { - decided_accounts: SomeAccountsProcessed(pre_processed_decided_accounts), + decided_accounts: pre_processed_decided_accounts, remaining_undecided_accounts, }) } @@ -120,179 +101,175 @@ impl ServiceFeeAdjusterReal { let remaining = unconfirmed_adjustments .into_iter() - .filter(|account_info| account_info.wallet() != &disqualified_account_wallet) + .filter(|account_info| account_info.wallet() != disqualified_account_wallet) .collect(); let remaining_reverted = convert_collection(remaining); AdjustmentIterationResult { - decided_accounts: LowGainingAccountEliminated, + decided_accounts: vec![], remaining_undecided_accounts: remaining_reverted, } } - fn filter_and_process_winners( + fn filter_and_process_confirmable_accounts( unconfirmed_adjustments: Vec, ) -> ( Vec, Vec, ) { let init: (Vec, Vec) = (vec![], vec![]); - let (thriving_competitors, losing_competitors) = unconfirmed_adjustments.into_iter().fold( - init, - |(mut thriving_competitors, mut losing_competitors), current| { - 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; - thriving_competitors.push(adjusted) - } else { - losing_competitors.push(current) - } - (thriving_competitors, losing_competitors) - }, - ); + 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 (accounts_above_disq_limit, accounts_below_disq_limit) = + unconfirmed_adjustments.into_iter().fold(init, fold_guts); - let decided_accounts = if thriving_competitors.is_empty() { + let decided_accounts = if accounts_above_disq_limit.is_empty() { vec![] } else { - convert_collection(thriving_competitors) + convert_collection(accounts_above_disq_limit) }; - (decided_accounts, losing_competitors) + (decided_accounts, accounts_below_disq_limit) } } -#[derive(Default)] -pub struct AdjustmentComputer {} +fn compute_unconfirmed_adjustments( + weighed_accounts: Vec, + remaining_cw_service_fee_balance_minor: u128, +) -> Vec { + let weights_total = sum_as(&weighed_accounts, |weighed_account| weighed_account.weight); -impl AdjustmentComputer { - pub fn compute_unconfirmed_adjustments( - &self, - weighted_accounts: Vec, - unallocated_cw_service_fee_balance_minor: u128, - ) -> Vec { - let weights_total = weights_total(&weighted_accounts); - let cw_service_fee_balance = unallocated_cw_service_fee_balance_minor; + let multiplication_coefficient = compute_mul_coefficient_preventing_fractional_numbers( + remaining_cw_service_fee_balance_minor, + ); - let multiplication_coefficient = - compute_mul_coefficient_preventing_fractional_numbers(cw_service_fee_balance); + let proportional_cw_fragment = compute_proportional_cw_fragment( + remaining_cw_service_fee_balance_minor, + weights_total, + multiplication_coefficient, + ); - let proportional_cw_balance_fragment = Self::compute_proportional_cw_fragment( - cw_service_fee_balance, - weights_total, - multiplication_coefficient, - ); + let compute_proposed_adjusted_balance = + |weight| weight * proportional_cw_fragment / multiplication_coefficient; - let compute_proposed_adjusted_balance = - |weight: u128| weight * proportional_cw_balance_fragment / multiplication_coefficient; + weighed_accounts + .into_iter() + .map(|weighed_account| { + let proposed_adjusted_balance = + compute_proposed_adjusted_balance(weighed_account.weight); - weighted_accounts - .into_iter() - .map(|weighted_account| { - let proposed_adjusted_balance = - compute_proposed_adjusted_balance(weighted_account.weight); + proposed_adjusted_balance_diagnostics(&weighed_account, proposed_adjusted_balance); - proposed_adjusted_balance_diagnostics(&weighted_account, proposed_adjusted_balance); - - UnconfirmedAdjustment::new(weighted_account, proposed_adjusted_balance) - }) - .collect() - } + UnconfirmedAdjustment::new(weighed_account, proposed_adjusted_balance) + }) + .collect() +} - fn compute_proportional_cw_fragment( - cw_service_fee_balance_minor: u128, - weights_total: u128, - multiplication_coefficient: u128, - ) -> u128 { - cw_service_fee_balance_minor - // Considered safe due to the process of getting this coefficient - .checked_mul(multiplication_coefficient) - .unwrap_or_else(|| { - panic!( - "mul overflow from {} * {}", - cw_service_fee_balance_minor, multiplication_coefficient - ) - }) - .checked_div(weights_total) - .expect("div overflow") - } +fn compute_proportional_cw_fragment( + cw_service_fee_balance_minor: u128, + weights_total: u128, + multiplication_coefficient: u128, +) -> u128 { + cw_service_fee_balance_minor + // Considered safe for the nature of the calculus producing this coefficient + .checked_mul(multiplication_coefficient) + .unwrap_or_else(|| { + panic!( + "mul overflow from {} * {}", + cw_service_fee_balance_minor, multiplication_coefficient + ) + }) + .checked_div(weights_total) + .expect("div overflow") } #[cfg(test)] 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::{ - make_non_guaranteed_unconfirmed_adjustment, multiple_by_billion, + use crate::accountant::payment_adjuster::test_utils::local_utils::{ + make_non_guaranteed_unconfirmed_adjustment, multiply_by_quintillion, + multiply_by_quintillion_concise, }; #[test] - fn filter_and_process_winners_limits_them_by_their_disqualification_edges() { + fn filter_and_process_confirmable_accounts_limits_them_by_their_disqualification_edges() { let mut account_1 = make_non_guaranteed_unconfirmed_adjustment(111); - let weight_1 = account_1.weighted_account.weight; + let weight_1 = account_1.weighed_account.weight; account_1 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account - .balance_wei = multiple_by_billion(2_000_000_000); + .balance_wei = multiply_by_quintillion(2); account_1 - .weighted_account + .weighed_account .analyzed_account - .disqualification_limit_minor = multiple_by_billion(1_800_000_000); - account_1.proposed_adjusted_balance_minor = multiple_by_billion(3_000_000_000); + .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 weight_2 = account_2.weighted_account.weight; + let weight_2 = account_2.weighed_account.weight; account_2 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account - .balance_wei = multiple_by_billion(5_000_000_000); + .balance_wei = multiply_by_quintillion(5); account_2 - .weighted_account + .weighed_account .analyzed_account - .disqualification_limit_minor = multiple_by_billion(4_200_000_000) - 1; - account_2.proposed_adjusted_balance_minor = multiple_by_billion(4_200_000_000); + .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); account_3 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account - .balance_wei = multiple_by_billion(3_000_000_000); + .balance_wei = multiply_by_quintillion(3); account_3 - .weighted_account + .weighed_account .analyzed_account - .disqualification_limit_minor = multiple_by_billion(2_000_000_000) + 1; - account_3.proposed_adjusted_balance_minor = multiple_by_billion(2_000_000_000); + .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 weight_4 = account_4.weighted_account.weight; + let weight_4 = account_4.weighed_account.weight; account_4 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account - .balance_wei = multiple_by_billion(1_500_000_000); + .balance_wei = multiply_by_quintillion_concise(1.5); account_4 - .weighted_account + .weighed_account .analyzed_account - .disqualification_limit_minor = multiple_by_billion(500_000_000); - account_4.proposed_adjusted_balance_minor = multiple_by_billion(500_000_000); + .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); account_5 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account - .balance_wei = multiple_by_billion(2_000_000_000); + .balance_wei = multiply_by_quintillion(2); account_5 - .weighted_account + .weighed_account .analyzed_account - .disqualification_limit_minor = multiple_by_billion(1_000_000_000) + 1; - account_5.proposed_adjusted_balance_minor = multiple_by_billion(1_000_000_000); + .disqualification_limit_minor = multiply_by_quintillion(1) + 1; + account_5.proposed_adjusted_balance_minor = multiply_by_quintillion(1); let unconfirmed_accounts = vec![ account_1.clone(), account_2.clone(), @@ -302,38 +279,76 @@ mod tests { ]; let (thriving_competitors, losing_competitors) = - ServiceFeeAdjusterReal::filter_and_process_winners(unconfirmed_accounts); + ServiceFeeAdjusterReal::filter_and_process_confirmable_accounts(unconfirmed_accounts); assert_eq!(losing_competitors, vec![account_3, account_5]); let expected_adjusted_outweighed_accounts = vec![ AdjustedAccountBeforeFinalization::new( account_1 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account, weight_1, - multiple_by_billion(1_800_000_000), + multiply_by_quintillion_concise(1.8), ), AdjustedAccountBeforeFinalization::new( account_2 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account, weight_2, - multiple_by_billion(4_200_000_000) - 1, + multiply_by_quintillion_concise(4.2) - 1, ), AdjustedAccountBeforeFinalization::new( account_4 - .weighted_account + .weighed_account .analyzed_account .qualified_as .bare_account, weight_4, - multiple_by_billion(500_000_000), + multiply_by_quintillion_concise(0.5), ), ]; assert_eq!(thriving_competitors, expected_adjusted_outweighed_accounts) } } + +#[cfg(test)] +pub mod illustrative_util { + use crate::accountant::payment_adjuster::miscellaneous::data_structures::WeighedPayable; + use crate::accountant::payment_adjuster::service_fee_adjuster::compute_unconfirmed_adjustments; + use thousands::Separable; + use web3::types::Address; + + pub fn illustrate_why_we_need_to_prevent_exceeding_the_original_value( + cw_service_fee_balance_minor: u128, + weighed_accounts: Vec, + wallet_of_expected_outweighed: Address, + original_balance_of_outweighed_account: u128, + ) { + let unconfirmed_adjustments = + compute_unconfirmed_adjustments(weighed_accounts, cw_service_fee_balance_minor); + // The results are sorted from the biggest weights down + assert_eq!( + 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. + + // 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. + 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 {}", + 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 cec292475..e6c1e3eab 100644 --- a/node/src/accountant/payment_adjuster/test_utils.rs +++ b/node/src/accountant/payment_adjuster/test_utils.rs @@ -2,238 +2,339 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; -use crate::accountant::payment_adjuster::disqualification_arbiter::{ - DisqualificationArbiter, DisqualificationGauge, -}; -use crate::accountant::payment_adjuster::inner::{PaymentAdjusterInner, PaymentAdjusterInnerReal}; -use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ - AdjustmentIterationResult, UnconfirmedAdjustment, WeightedPayable, -}; -use crate::accountant::payment_adjuster::service_fee_adjuster::ServiceFeeAdjuster; -use crate::accountant::payment_adjuster::PaymentAdjusterReal; -use crate::accountant::test_utils::{make_analyzed_account, make_non_guaranteed_qualified_payable}; -use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; -use crate::sub_lib::accountant::PaymentThresholds; -use crate::test_utils::make_wallet; -use itertools::Either; -use lazy_static::lazy_static; -use masq_lib::constants::MASQ_TOTAL_SUPPLY; -use masq_lib::logger::Logger; -use std::cell::RefCell; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime}; - -lazy_static! { - pub static ref MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR: u128 = - MASQ_TOTAL_SUPPLY as u128 * 10_u128.pow(18); - pub static ref ONE_MONTH_LONG_DEBT_SEC: u64 = 30 * 24 * 60 * 60; -} +// This basically says: visible only within the PaymentAdjuster module +pub(super) mod local_utils { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; + use crate::accountant::payment_adjuster::disqualification_arbiter::{ + DisqualificationArbiter, DisqualificationGauge, + }; + use crate::accountant::payment_adjuster::inner::PaymentAdjusterInner; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + AdjustmentIterationResult, UnconfirmedAdjustment, WeighedPayable, + }; + use crate::accountant::payment_adjuster::service_fee_adjuster::ServiceFeeAdjuster; + use crate::accountant::payment_adjuster::PaymentAdjusterReal; + use crate::accountant::test_utils::{ + make_meaningless_analyzed_account, make_meaningless_qualified_payable, + }; + use crate::accountant::{gwei_to_wei, AnalyzedPayableAccount, QualifiedPayableAccount}; + use crate::sub_lib::accountant::PaymentThresholds; + use crate::test_utils::make_wallet; + use itertools::Either; + use lazy_static::lazy_static; + use masq_lib::constants::MASQ_TOTAL_SUPPLY; + use masq_lib::logger::Logger; + use std::cell::RefCell; + use std::sync::{Arc, Mutex}; + use std::time::{Duration, SystemTime}; -pub fn make_initialized_subject( - now_opt: Option, - cw_service_fee_balance_minor_opt: Option, - criterion_calculator_mock_opt: Option, - largest_exceeding_balance_recently_qualified: Option, - logger_opt: Option, -) -> PaymentAdjusterReal { - let cw_service_fee_balance_minor = cw_service_fee_balance_minor_opt.unwrap_or(0); - let logger = logger_opt.unwrap_or(Logger::new("test")); - let mut subject = PaymentAdjusterReal::default(); - subject.logger = logger; - subject.inner = Box::new(PaymentAdjusterInnerReal::new( - now_opt.unwrap_or(SystemTime::now()), - None, - cw_service_fee_balance_minor, - largest_exceeding_balance_recently_qualified.unwrap_or(0), - )); - if let Some(calculator) = criterion_calculator_mock_opt { - subject.calculators = vec![Box::new(calculator)] - } - subject -} + lazy_static! { + pub static ref MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR: u128 = + multiply_by_quintillion(MASQ_TOTAL_SUPPLY as u128); + pub static ref ONE_MONTH_LONG_DEBT_SEC: u64 = 30 * 24 * 60 * 60; + } + + #[derive(Default)] + pub struct PaymentAdjusterBuilder { + start_with_inner_null: bool, + cw_service_fee_balance_minor_opt: Option, + mock_replacing_calculators_opt: Option, + max_debt_above_threshold_in_qualified_payables_minor_opt: Option, + transaction_limit_count_opt: Option, + logger_opt: Option, + } + + impl PaymentAdjusterBuilder { + pub fn build(self) -> PaymentAdjusterReal { + let mut payment_adjuster = PaymentAdjusterReal::default(); + let logger = self.logger_opt.unwrap_or(Logger::new("test")); + payment_adjuster.logger = logger; + if !self.start_with_inner_null { + payment_adjuster.inner.initialize_guts( + self.transaction_limit_count_opt, + self.cw_service_fee_balance_minor_opt.unwrap_or(0), + self.max_debt_above_threshold_in_qualified_payables_minor_opt + .unwrap_or(0), + ); + } + if let Some(calculator) = self.mock_replacing_calculators_opt { + payment_adjuster.calculators = vec![Box::new(calculator)] + } + payment_adjuster + } + + pub fn start_with_inner_null(mut self) -> Self { + self.start_with_inner_null = true; + self + } + + pub fn replace_calculators_with_mock( + mut self, + calculator_mock: CriterionCalculatorMock, + ) -> Self { + self.mock_replacing_calculators_opt = Some(calculator_mock); + self + } + + pub fn cw_service_fee_balance_minor(mut self, cw_service_fee_balance_minor: u128) -> Self { + self.cw_service_fee_balance_minor_opt = Some(cw_service_fee_balance_minor); + self + } + + pub fn max_debt_above_threshold_in_qualified_payables_minor( + mut self, + max_exceeding_part_of_debt: u128, + ) -> Self { + self.max_debt_above_threshold_in_qualified_payables_minor_opt = + Some(max_exceeding_part_of_debt); + self + } -pub fn make_extreme_payables( - months_of_debt_and_balance_minor: Either<(Vec, u128), Vec<(usize, u128)>>, - now: SystemTime, -) -> Vec { - let accounts_seeds: Vec<(usize, u128)> = match months_of_debt_and_balance_minor { - Either::Left((vec_of_months, constant_balance)) => vec_of_months + pub fn logger(mut self, logger: Logger) -> Self { + self.logger_opt = Some(logger); + self + } + + #[allow(dead_code)] + pub fn transaction_limit_count(mut self, tx_limit: u16) -> Self { + self.transaction_limit_count_opt = Some(tx_limit); + self + } + } + + pub fn make_mammoth_payables( + months_of_debt_and_balance_minor: Either<(Vec, u128), Vec<(usize, u128)>>, + now: SystemTime, + ) -> Vec { + // What is a mammoth like? Prehistoric, giant, and impossible to meet. Exactly as these payables. + let accounts_seeds: Vec<(usize, u128)> = match months_of_debt_and_balance_minor { + Either::Left((vec_of_months, constant_balance)) => vec_of_months + .into_iter() + .map(|months| (months, constant_balance)) + .collect(), + Either::Right(specific_months_and_specific_balance) => { + specific_months_and_specific_balance + } + }; + accounts_seeds .into_iter() - .map(|months| (months, constant_balance)) - .collect(), - Either::Right(specific_months_and_specific_balance) => specific_months_and_specific_balance, + .enumerate() + .map(|(idx, (months_count, balance_minor))| PayableAccount { + wallet: make_wallet(&format!("blah{}", idx)), + balance_wei: balance_minor, + last_paid_timestamp: now + .checked_sub(Duration::from_secs( + months_count as u64 * (*ONE_MONTH_LONG_DEBT_SEC), + )) + .unwrap(), + pending_payable_opt: None, + }) + .collect() + } + + pub(in crate::accountant::payment_adjuster) const PRESERVED_TEST_PAYMENT_THRESHOLDS: + PaymentThresholds = PaymentThresholds { + debt_threshold_gwei: 2_000_000, + maturity_threshold_sec: 1_000, + payment_grace_period_sec: 1_000, + permanent_debt_allowed_gwei: 1_000_000, + threshold_interval_sec: 500_000, + unban_below_gwei: 1_000_000, }; - accounts_seeds - .into_iter() - .enumerate() - .map(|(idx, (months_count, balance_minor))| PayableAccount { - wallet: make_wallet(&format!("blah{}", idx)), - balance_wei: balance_minor, - last_paid_timestamp: now - .checked_sub(Duration::from_secs( - months_count as u64 * (*ONE_MONTH_LONG_DEBT_SEC), - )) - .unwrap(), - pending_payable_opt: None, - }) - .collect() -} -pub(in crate::accountant::payment_adjuster) const PRESERVED_TEST_PAYMENT_THRESHOLDS: - PaymentThresholds = PaymentThresholds { - debt_threshold_gwei: 2_000_000, - maturity_threshold_sec: 1_000, - payment_grace_period_sec: 1_000, - permanent_debt_allowed_gwei: 1_000_000, - threshold_interval_sec: 500_000, - unban_below_gwei: 1_000_000, -}; - -pub fn make_non_guaranteed_unconfirmed_adjustment(n: u64) -> UnconfirmedAdjustment { - let qualified_account = make_non_guaranteed_qualified_payable(n); - let proposed_adjusted_balance_minor = - (qualified_account.bare_account.balance_wei / 2) * (n as f64).sqrt() as u128; - let disqualification_limit_minor = (3 * proposed_adjusted_balance_minor) / 4; - let analyzed_account = - AnalyzedPayableAccount::new(qualified_account, disqualification_limit_minor); - let weight = (n as u128).pow(3); - UnconfirmedAdjustment::new( - WeightedPayable::new(analyzed_account, weight), - proposed_adjusted_balance_minor, - ) -} + pub fn make_non_guaranteed_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; + let disqualification_limit_minor = (3 * proposed_adjusted_balance_minor) / 4; + let analyzed_account = + AnalyzedPayableAccount::new(qualified_account, disqualification_limit_minor); + let weight = multiply_by_billion(n as u128); + UnconfirmedAdjustment::new( + WeighedPayable::new(analyzed_account, weight), + proposed_adjusted_balance_minor, + ) + } -#[derive(Default)] -pub struct CriterionCalculatorMock { - calculate_params: Arc>>, - calculate_results: RefCell>, -} + #[derive(Default)] + pub struct CriterionCalculatorMock { + calculate_params: Arc>>, + calculate_results: RefCell>, + } -impl CriterionCalculator for CriterionCalculatorMock { - fn calculate( - &self, - account: &QualifiedPayableAccount, - _context: &dyn PaymentAdjusterInner, - ) -> u128 { - self.calculate_params.lock().unwrap().push(account.clone()); - self.calculate_results.borrow_mut().remove(0) + impl CriterionCalculator for CriterionCalculatorMock { + fn calculate( + &self, + account: &QualifiedPayableAccount, + _context: &PaymentAdjusterInner, + ) -> u128 { + self.calculate_params.lock().unwrap().push(account.clone()); + self.calculate_results.borrow_mut().remove(0) + } + + fn parameter_name(&self) -> &'static str { + "MOCKED CALCULATOR" + } } - fn parameter_name(&self) -> &'static str { - "MOCKED CALCULATOR" + impl CriterionCalculatorMock { + pub fn calculate_params( + mut self, + params: &Arc>>, + ) -> Self { + self.calculate_params = params.clone(); + self + } + pub fn calculate_result(self, result: u128) -> Self { + self.calculate_results.borrow_mut().push(result); + self + } } -} -impl CriterionCalculatorMock { - pub fn calculate_params(mut self, params: &Arc>>) -> Self { - self.calculate_params = params.clone(); - self + #[derive(Default)] + pub struct DisqualificationGaugeMock { + determine_limit_params: Arc>>, + determine_limit_results: RefCell>, } - pub fn calculate_result(self, result: u128) -> Self { - self.calculate_results.borrow_mut().push(result); - self + + impl DisqualificationGauge for DisqualificationGaugeMock { + fn determine_limit( + &self, + account_balance_wei: u128, + threshold_intercept_wei: u128, + permanent_debt_allowed_wei: u128, + ) -> u128 { + self.determine_limit_params.lock().unwrap().push(( + account_balance_wei, + threshold_intercept_wei, + permanent_debt_allowed_wei, + )); + self.determine_limit_results.borrow_mut().remove(0) + } } -} -#[derive(Default)] -pub struct DisqualificationGaugeMock { - determine_limit_params: Arc>>, - determine_limit_results: RefCell>, -} + impl DisqualificationGaugeMock { + pub fn determine_limit_params( + mut self, + params: &Arc>>, + ) -> Self { + self.determine_limit_params = params.clone(); + self + } -impl DisqualificationGauge for DisqualificationGaugeMock { - fn determine_limit( - &self, - account_balance_wei: u128, - threshold_intercept_wei: u128, - permanent_debt_allowed_wei: u128, - ) -> u128 { - self.determine_limit_params.lock().unwrap().push(( - account_balance_wei, - threshold_intercept_wei, - permanent_debt_allowed_wei, - )); - self.determine_limit_results.borrow_mut().remove(0) + pub fn determine_limit_result(self, result: u128) -> Self { + self.determine_limit_results.borrow_mut().push(result); + self + } } -} -impl DisqualificationGaugeMock { - pub fn determine_limit_params(mut self, params: &Arc>>) -> Self { - self.determine_limit_params = params.clone(); - self + #[derive(Default)] + pub struct ServiceFeeAdjusterMock { + perform_adjustment_by_service_fee_params: Arc, u128)>>>, + perform_adjustment_by_service_fee_results: RefCell>, } - pub fn determine_limit_result(self, result: u128) -> Self { - self.determine_limit_results.borrow_mut().push(result); - self + impl ServiceFeeAdjuster for ServiceFeeAdjusterMock { + fn perform_adjustment_by_service_fee( + &self, + weighed_accounts: Vec, + _disqualification_arbiter: &DisqualificationArbiter, + remaining_cw_service_fee_balance_minor: u128, + _logger: &Logger, + ) -> AdjustmentIterationResult { + self.perform_adjustment_by_service_fee_params + .lock() + .unwrap() + .push((weighed_accounts, remaining_cw_service_fee_balance_minor)); + self.perform_adjustment_by_service_fee_results + .borrow_mut() + .remove(0) + } } -} -#[derive(Default)] -pub struct ServiceFeeAdjusterMock { - perform_adjustment_by_service_fee_params: Arc, u128)>>>, - perform_adjustment_by_service_fee_results: RefCell>, -} -impl ServiceFeeAdjuster for ServiceFeeAdjusterMock { - fn perform_adjustment_by_service_fee( - &self, - weighted_accounts: Vec, - _disqualification_arbiter: &DisqualificationArbiter, - unallocated_cw_service_fee_balance_minor: u128, - _logger: &Logger, - ) -> AdjustmentIterationResult { - self.perform_adjustment_by_service_fee_params - .lock() - .unwrap() - .push((weighted_accounts, unallocated_cw_service_fee_balance_minor)); - self.perform_adjustment_by_service_fee_results - .borrow_mut() - .remove(0) + impl ServiceFeeAdjusterMock { + pub fn perform_adjustment_by_service_fee_params( + mut self, + params: &Arc, u128)>>>, + ) -> Self { + self.perform_adjustment_by_service_fee_params = params.clone(); + self + } + + pub fn perform_adjustment_by_service_fee_result( + self, + result: AdjustmentIterationResult, + ) -> Self { + self.perform_adjustment_by_service_fee_results + .borrow_mut() + .push(result); + self + } } -} -impl ServiceFeeAdjusterMock { - pub fn perform_adjustment_by_service_fee_params( - mut self, - params: &Arc, u128)>>>, - ) -> Self { - self.perform_adjustment_by_service_fee_params = params.clone(); - self + // = 1 gwei + pub fn multiply_by_billion(num: u128) -> u128 { + gwei_to_wei(num) } - pub fn perform_adjustment_by_service_fee_result( - self, - result: AdjustmentIterationResult, - ) -> Self { - self.perform_adjustment_by_service_fee_results - .borrow_mut() - .push(result); - self + // = 1 MASQ + pub fn multiply_by_quintillion(num: u128) -> u128 { + multiply_by_billion(multiply_by_billion(num)) } -} -pub fn multiple_by_billion(num: u128) -> u128 { - num * 10_u128.pow(9) -} -pub fn make_analyzed_account_by_wallet(wallet_address_segment: &str) -> AnalyzedPayableAccount { - let num = u64::from_str_radix(wallet_address_segment, 16).unwrap(); - let wallet = make_wallet(wallet_address_segment); - let mut account = make_analyzed_account(num); - account.qualified_as.bare_account.wallet = wallet; - account -} + // = 1 gwei + pub fn multiply_by_billion_concise(num: f64) -> u128 { + multiple_by(num, 9, "billion") + } + + // = 1 MASQ + pub fn multiply_by_quintillion_concise(num: f64) -> u128 { + multiple_by(num, 18, "quintillion") + } + + fn multiple_by( + num_in_concise_form: f64, + desired_increase_in_magnitude: usize, + mathematical_name: &str, + ) -> u128 { + if (num_in_concise_form * 1000.0).fract() != 0.0 { + panic!("Multiplying by {mathematical_name}: It's allowed only when applied on numbers with three \ + digits after the decimal point at maximum!") + } + let significant_digits = (num_in_concise_form * 1000.0) as u128; + significant_digits * 10_u128.pow(desired_increase_in_magnitude as u32 - 3) + } + + pub fn make_meaningless_analyzed_account_by_wallet( + wallet_address_segment: &str, + ) -> AnalyzedPayableAccount { + let num = u64::from_str_radix(wallet_address_segment, 16).unwrap(); + let wallet = make_wallet(wallet_address_segment); + let mut account = make_meaningless_analyzed_account(num); + account.qualified_as.bare_account.wallet = wallet; + account + } -pub fn make_weighed_account(n: u64) -> WeightedPayable { - WeightedPayable::new(make_analyzed_account(n), 123456789) + pub fn make_meaningless_weighed_account(n: u64) -> WeighedPayable { + WeighedPayable::new(make_meaningless_analyzed_account(n), 123456 * n as u128) + } } -// Should stay test only! -impl From for AnalyzedPayableAccount { - fn from(qualified_account: QualifiedPayableAccount) -> Self { - let disqualification_limit = - DisqualificationArbiter::default().calculate_disqualification_edge(&qualified_account); - AnalyzedPayableAccount::new(qualified_account, disqualification_limit) +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( + qualified_account: Vec, + ) -> Vec { + qualified_account + .into_iter() + .map(|account| { + let disqualification_limit = + DisqualificationArbiter::default().calculate_disqualification_edge(&account); + AnalyzedPayableAccount::new(account, disqualification_limit) + }) + .collect() } } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs index a25630009..eea1265c5 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_null.rs @@ -4,7 +4,7 @@ use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockch use crate::sub_lib::wallet::Wallet; use ethereum_types::U256; use masq_lib::logger::Logger; -use masq_lib::percentage::Percentage; +use masq_lib::percentage::PurePercentage; #[derive(Clone)] pub struct BlockchainAgentNull { @@ -28,14 +28,14 @@ impl BlockchainAgent for BlockchainAgentNull { 0 } - fn agreed_fee_per_computation_unit(&self) -> u64 { - self.log_function_call("agreed_fee_per_computation_unit()"); + fn gas_price(&self) -> u64 { + self.log_function_call("gas_price()"); 0 } - fn agreed_transaction_fee_margin(&self) -> Percentage { - self.log_function_call("agreed_transaction_fee_margin()"); - Percentage::new(0) + fn gas_price_margin(&self) -> PurePercentage { + self.log_function_call("gas_price_margin()"); + PurePercentage::try_from(0).expect("0 should cause no issue") } fn consuming_wallet(&self) -> &Wallet { @@ -85,7 +85,7 @@ mod tests { use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::sub_lib::wallet::Wallet; use masq_lib::logger::Logger; - use masq_lib::percentage::Percentage; + use masq_lib::percentage::PurePercentage; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use web3::types::U256; @@ -163,29 +163,29 @@ mod tests { } #[test] - fn null_agent_agreed_fee_per_computation_unit() { + fn null_agent_gas_price() { init_test_logging(); - let test_name = "null_agent_agreed_fee_per_computation_unit"; + let test_name = "null_agent_gas_price"; let mut subject = BlockchainAgentNull::new(); subject.logger = Logger::new(test_name); - let result = subject.agreed_fee_per_computation_unit(); + let result = subject.gas_price(); assert_eq!(result, 0); - assert_error_log(test_name, "agreed_fee_per_computation_unit") + assert_error_log(test_name, "gas_price") } #[test] - fn null_agent_agreed_transaction_fee_margin() { + fn null_agent_gas_price_margin() { init_test_logging(); - let test_name = "null_agent_agreed_transaction_fee_margin"; + let test_name = "null_agent_gas_price_margin"; let mut subject = BlockchainAgentNull::new(); subject.logger = Logger::new(test_name); - let result = subject.agreed_transaction_fee_margin(); + let result = subject.gas_price_margin(); - assert_eq!(result, Percentage::new(0)); - assert_error_log(test_name, "agreed_transaction_fee_margin") + assert_eq!(result, PurePercentage::try_from(0).unwrap()); + assert_error_log(test_name, "gas_price_margin") } #[test] diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs index cc4caf32f..dc7d33c64 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/agent_web3.rs @@ -6,15 +6,15 @@ use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::accountant::gwei_to_wei; -use crate::blockchain::blockchain_interface::blockchain_interface_web3::TRANSACTION_FEE_MARGIN; -use masq_lib::percentage::Percentage; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; +use masq_lib::percentage::PurePercentage; use web3::types::U256; #[derive(Debug, Clone)] pub struct BlockchainAgentWeb3 { gas_price_gwei: u64, gas_limit_const_part: u64, - agreed_transaction_fee_margin: Percentage, + gas_price_margin: PurePercentage, maximum_added_gas_margin: u64, consuming_wallet: Wallet, consuming_wallet_balances: ConsumingWalletBalances, @@ -38,12 +38,12 @@ impl BlockchainAgent for BlockchainAgentWeb3 { .service_fee_balance_in_minor_units } - fn agreed_fee_per_computation_unit(&self) -> u64 { + fn gas_price(&self) -> u64 { self.gas_price_gwei } - fn agreed_transaction_fee_margin(&self) -> Percentage { - self.agreed_transaction_fee_margin + fn gas_price_margin(&self) -> PurePercentage { + self.gas_price_margin } fn consuming_wallet(&self) -> &Wallet { @@ -67,12 +67,12 @@ impl BlockchainAgentWeb3 { consuming_wallet_balances: ConsumingWalletBalances, pending_transaction_id: U256, ) -> Self { - let agreed_transaction_fee_margin = *TRANSACTION_FEE_MARGIN; + let gas_price_margin = *TX_FEE_MARGIN_IN_PERCENT; let maximum_added_gas_margin = WEB3_MAXIMAL_GAS_LIMIT_MARGIN; Self { gas_price_gwei, gas_limit_const_part, - agreed_transaction_fee_margin, + gas_price_margin, consuming_wallet, maximum_added_gas_margin, consuming_wallet_balances, @@ -87,11 +87,8 @@ mod tests { BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::make_wallet; - - use crate::accountant::gwei_to_wei; use web3::types::U256; #[test] @@ -118,7 +115,7 @@ mod tests { pending_transaction_id, ); - assert_eq!(subject.agreed_fee_per_computation_unit(), gas_price_gwei); + assert_eq!(subject.gas_price(), gas_price_gwei); assert_eq!(subject.consuming_wallet(), &consuming_wallet); assert_eq!( subject.transaction_fee_balance_minor(), @@ -154,10 +151,6 @@ mod tests { agent.maximum_added_gas_margin, WEB3_MAXIMAL_GAS_LIMIT_MARGIN ); - let expected_result: u128 = { - let gwei_amount = ((77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) as u128) * 244; - gwei_to_wei(gwei_amount) - }; - assert_eq!(result, expected_result); + assert_eq!(result, 19789620000000000); } } diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs index 649218c42..6cea72ebb 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/blockchain_agent.rs @@ -2,7 +2,7 @@ use crate::arbitrary_id_stamp_in_trait; use crate::sub_lib::wallet::Wallet; -use masq_lib::percentage::Percentage; +use masq_lib::percentage::PurePercentage; use web3::types::U256; // Table of chains by @@ -25,8 +25,8 @@ pub trait BlockchainAgent: Send { fn estimated_transaction_fee_per_transaction_minor(&self) -> u128; fn transaction_fee_balance_minor(&self) -> U256; fn service_fee_balance_minor(&self) -> u128; - fn agreed_fee_per_computation_unit(&self) -> u64; - fn agreed_transaction_fee_margin(&self) -> Percentage; + fn gas_price(&self) -> u64; + fn gas_price_margin(&self) -> PurePercentage; fn consuming_wallet(&self) -> &Wallet; fn pending_transaction_id(&self) -> U256; diff --git a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs index 506a94bee..95ac9240c 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/mod.rs @@ -6,7 +6,7 @@ pub mod blockchain_agent; pub mod msgs; pub mod test_utils; -use crate::accountant::payment_adjuster::AdjustmentAnalysis; +use crate::accountant::payment_adjuster::AdjustmentAnalysisReport; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; use crate::accountant::scanners::Scanner; @@ -36,19 +36,21 @@ pub trait SolvencySensitivePaymentInstructor { setup: PreparedAdjustment, logger: &Logger, ) -> Option; + + fn cancel_scan(&mut self, logger: &Logger); } pub struct PreparedAdjustment { pub agent: Box, pub response_skeleton_opt: Option, - pub adjustment_analysis: AdjustmentAnalysis, + pub adjustment_analysis: AdjustmentAnalysisReport, } impl PreparedAdjustment { pub fn new( agent: Box, response_skeleton_opt: Option, - adjustment_analysis: AdjustmentAnalysis, + adjustment_analysis: AdjustmentAnalysisReport, ) -> Self { Self { agent, 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 6bb667c0f..ff1f6d3d7 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 @@ -7,7 +7,7 @@ use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::{arbitrary_id_stamp_in_trait_impl, set_arbitrary_id_stamp_in_mock_impl}; use ethereum_types::U256; -use masq_lib::percentage::Percentage; +use masq_lib::percentage::PurePercentage; use std::cell::RefCell; #[derive(Default)] @@ -15,8 +15,8 @@ pub struct BlockchainAgentMock { estimated_transaction_fee_per_transaction_minor_results: RefCell>, transaction_fee_balance_minor_results: RefCell>, service_fee_balance_minor_results: RefCell>, - agreed_fee_per_computation_unit_results: RefCell>, - agreed_transaction_fee_margin: RefCell>, + gas_price_results: RefCell>, + gas_price_margin: RefCell>, consuming_wallet_result_opt: Option, pending_transaction_id_results: RefCell>, arbitrary_id_stamp_opt: Option, @@ -41,14 +41,12 @@ impl BlockchainAgent for BlockchainAgentMock { .remove(0) } - fn agreed_fee_per_computation_unit(&self) -> u64 { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .remove(0) + fn gas_price(&self) -> u64 { + self.gas_price_results.borrow_mut().remove(0) } - fn agreed_transaction_fee_margin(&self) -> Percentage { - self.agreed_transaction_fee_margin.borrow_mut().remove(0) + fn gas_price_margin(&self) -> PurePercentage { + self.gas_price_margin.borrow_mut().remove(0) } fn consuming_wallet(&self) -> &Wallet { @@ -88,15 +86,13 @@ impl BlockchainAgentMock { self } - pub fn agreed_fee_per_computation_unit_result(self, result: u64) -> Self { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .push(result); + pub fn gas_price_result(self, result: u64) -> Self { + self.gas_price_results.borrow_mut().push(result); self } - pub fn agreed_transaction_fee_margin_result(self, result: Percentage) -> Self { - self.agreed_transaction_fee_margin.borrow_mut().push(result); + pub fn gas_price_margin_result(self, result: PurePercentage) -> Self { + self.gas_price_margin.borrow_mut().push(result); self } diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index f722e1911..70d7345b8 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -131,7 +131,7 @@ pub struct ScannerCommon { initiated_at_opt: Option, // TODO The thresholds probably shouldn't be in ScannerCommon because the PendingPayableScanner // does not need it - pub payment_thresholds: Rc, + payment_thresholds: Rc, } impl ScannerCommon { @@ -190,7 +190,7 @@ pub struct PayableScanner { pub payable_dao: Box, pub pending_payable_dao: Box, pub payable_inspector: PayableInspector, - pub payment_adjuster: RefCell>, + pub payment_adjuster: Box, } impl Scanner for PayableScanner { @@ -274,8 +274,7 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { match self .payment_adjuster - .borrow() - .search_for_indispensable_adjustment(unprotected, &*msg.agent) + .consider_adjustment(unprotected, &*msg.agent) { Ok(processed) => { let either = match processed { @@ -300,16 +299,7 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { } Err(e) => { if e.insolvency_detected() { - warning!( - logger, - "Insolvency detected led to an analysis of feasibility for making payments \ - adjustment, however, giving no satisfactory solution. Please be advised that \ - your balances can cover neither reasonable portion of any of those payables \ - recently qualified for an imminent payment. You must add more funds into your \ - consuming wallet in order to stay off delinquency bans that your creditors may \ - apply against you otherwise. Details: {}.", - e - ) + warning!(logger, "{}. Details: {}.", Self::ADD_MORE_FUNDS_URGE, e) } else { unimplemented!("This situation is not possible yet, but may be in the future") } @@ -323,24 +313,28 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { setup: PreparedAdjustment, logger: &Logger, ) -> Option { - let now = SystemTime::now(); - match self - .payment_adjuster - .borrow_mut() - .adjust_payments(setup, now) - { + match self.payment_adjuster.adjust_payments(setup) { Ok(instructions) => Some(instructions), Err(e) => { warning!( logger, - "Payment adjustment has not produced any executable payments. Please add funds \ - into your consuming wallet in order to avoid bans from your creditors. Details: {}", + "Payment adjustment has not produced any executable payments. {}. Details: {}", + Self::ADD_MORE_FUNDS_URGE, e ); None } } } + + 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." + ); + self.mark_as_ended(logger) + } } impl MultistagePayableScanner for PayableScanner {} @@ -358,7 +352,7 @@ impl PayableScanner { payable_dao, pending_payable_dao, payable_inspector, - payment_adjuster: RefCell::new(payment_adjuster), + payment_adjuster, } } @@ -367,21 +361,10 @@ impl PayableScanner { non_pending_payables: Vec, logger: &Logger, ) -> Vec { + let now = SystemTime::now(); let qualified_payables = non_pending_payables .into_iter() - .flat_map(|account| { - self.payable_exceeded_threshold(&account, SystemTime::now()) - .map(|payment_threshold_intercept| { - let creditor_thresholds = CreditorThresholds::new(gwei_to_wei( - self.common.payment_thresholds.permanent_debt_allowed_gwei, - )); - QualifiedPayableAccount::new( - account, - payment_threshold_intercept, - creditor_thresholds, - ) - }) - }) + .flat_map(|account| self.try_qualify_account(account, now)) .collect(); match logger.debug_enabled() { false => qualified_payables, @@ -392,16 +375,29 @@ impl PayableScanner { } } + fn try_qualify_account( + &self, + account: PayableAccount, + now: SystemTime, + ) -> Option { + let intercept_opt = self.payable_exceeded_threshold(&account, now); + + intercept_opt.map(|payment_threshold_intercept| { + let creditor_thresholds = CreditorThresholds::new(gwei_to_wei( + self.common.payment_thresholds.permanent_debt_allowed_gwei, + )); + QualifiedPayableAccount::new(account, payment_threshold_intercept, creditor_thresholds) + }) + } + fn payable_exceeded_threshold( &self, account: &PayableAccount, now: SystemTime, ) -> Option { - self.payable_inspector.payable_exceeded_threshold( - account, - &self.common.payment_thresholds, - now, - ) + let payment_thresholds = self.common.payment_thresholds.as_ref(); + self.payable_inspector + .payable_exceeded_threshold(account, payment_thresholds, now) } fn separate_existent_and_nonexistent_fingerprints<'a>( @@ -462,9 +458,9 @@ impl PayableScanner { fn is_symmetrical( sent_payables_hashes: HashSet, - fingerptint_hashes: HashSet, + fingerprints_hashes: HashSet, ) -> bool { - sent_payables_hashes == fingerptint_hashes + sent_payables_hashes == fingerprints_hashes } fn mark_pending_payable(&self, sent_payments: &[&PendingPayable], logger: &Logger) { @@ -584,6 +580,11 @@ impl PayableScanner { fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { obfuscated.expose_vector() } + + 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"; } pub struct PendingPayableScanner { @@ -812,11 +813,7 @@ impl PendingPayableScanner { records due to {:?}", serialize_hashes(&fingerprints), e ) } else { - self.add_percent_to_the_total_of_paid_payable( - &fingerprints, - serialize_hashes, - logger, - ); + self.add_to_the_total_of_paid_payable(&fingerprints, serialize_hashes, logger); let rowids = fingerprints .iter() .map(|fingerprint| fingerprint.rowid) @@ -835,7 +832,7 @@ impl PendingPayableScanner { } } - fn add_percent_to_the_total_of_paid_payable( + fn add_to_the_total_of_paid_payable( &mut self, fingerprints: &[PendingPayableFingerprint], serialize_hashes: fn(&[PendingPayableFingerprint]) -> String, @@ -1147,7 +1144,7 @@ mod tests { }; use crate::accountant::test_utils::{ make_custom_payment_thresholds, make_payable_account, make_pending_payable_fingerprint, - make_receivable_account, make_unqualified_and_qualified_payables, BannedDaoFactoryMock, + make_qualified_and_unqualified_payables, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, @@ -1194,14 +1191,15 @@ mod tests { #[test] fn scanners_struct_can_be_constructed_with_the_respective_scanners() { - let payable_dao_factory = PayableDaoFactoryMock::new() + let payable_dao_factory = PayableDaoFactoryMock::default() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()); - let pending_payable_dao_factory = PendingPayableDaoFactoryMock::new() + let pending_payable_dao_factory = PendingPayableDaoFactoryMock::default() .make_result(PendingPayableDaoMock::new()) .make_result(PendingPayableDaoMock::new()); let receivable_dao = ReceivableDaoMock::new(); - let receivable_dao_factory = ReceivableDaoFactoryMock::new().make_result(receivable_dao); + let receivable_dao_factory = + ReceivableDaoFactoryMock::default().make_result(receivable_dao); let banned_dao_factory = BannedDaoFactoryMock::new().make_result(BannedDaoMock::new()); let set_params_arc = Arc::new(Mutex::new(vec![])); let config_dao_mock = ConfigDaoMock::new() @@ -1325,11 +1323,14 @@ mod tests { let test_name = "payable_scanner_can_initiate_a_scan"; let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_unqualified_and_qualified_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = 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(), + ))) .build(); let result = subject.begin_scan(now, None, &Logger::new(test_name)); @@ -1358,11 +1359,14 @@ mod tests { fn payable_scanner_throws_error_when_a_scan_is_already_running() { let now = SystemTime::now(); let (_, _, all_non_pending_payables) = - make_unqualified_and_qualified_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = 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(), + ))) .build(); let _result = subject.begin_scan(now, None, &Logger::new("test")); @@ -1380,11 +1384,14 @@ mod tests { fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_unqualified_and_qualified_payables(now, &PaymentThresholds::default()); + make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); let payable_dao = 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(), + ))) .build(); let result = subject.begin_scan(now, None, &Logger::new("test")); @@ -2180,6 +2187,9 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_inspector(PayableInspector::new(Box::new( + PayableThresholdsGaugeReal::default(), + ))) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; @@ -2210,6 +2220,9 @@ mod tests { }; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_inspector(PayableInspector::new(Box::new( + PayableThresholdsGaugeReal::default(), + ))) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified"; let logger = Logger::new(test_name); @@ -2250,6 +2263,9 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_inspector(PayableInspector::new(Box::new( + PayableThresholdsGaugeReal::default(), + ))) .build(); let logger = Logger::new(test_name); @@ -2262,7 +2278,7 @@ mod tests { } #[test] - fn sniff_out_alarming_payables_and_maybe_log_them_generates_and_uses_correct_timestamp() { + fn sniff_out_alarming_payables_and_maybe_log_them_computes_debt_age_from_correct_now() { let payment_thresholds = PaymentThresholds { debt_threshold_gwei: 10_000_000_000, maturity_threshold_sec: 100, @@ -2272,10 +2288,10 @@ mod tests { unban_below_gwei: 0, }; let wallet = make_wallet("abc"); - // It is important to have a payable matching the declining part of the thresholds, also it - // will be more believable if the slope is steep because then one second can make the bigger - // difference in the intercept value, which is the value this test compare in order to - // conclude a pass + // 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 + // compares for carrying out the conclusion let debt_age = payment_thresholds.maturity_threshold_sec + (payment_thresholds.threshold_interval_sec / 2); let payable = PayableAccount { @@ -2286,6 +2302,9 @@ mod tests { }; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_inspector(PayableInspector::new(Box::new( + PayableThresholdsGaugeReal::default(), + ))) .build(); let intercept_before = subject .payable_exceeded_threshold(&payable, SystemTime::now()) @@ -2301,8 +2320,16 @@ mod tests { .unwrap(); assert_eq!(result.len(), 1); assert_eq!(&result[0].bare_account.wallet, &wallet); - assert!(intercept_before >= result[0].payment_threshold_intercept_minor && result[0].payment_threshold_intercept_minor >= intercept_after, - "Tested intercept {} does not lie between two nows {} and {} while we assume the act generates third timestamp of presence", result[0].payment_threshold_intercept_minor, intercept_before, intercept_after + assert!( + intercept_before >= result[0].payment_threshold_intercept_minor + && result[0].payment_threshold_intercept_minor >= intercept_after, + "Tested intercept {} does not lie between two referring intercepts derived from two \ + calls of now(), intercept before: {} and after: {}, while the act is supposed to \ + generate its own, third timestamp used to compute an intercept somewhere in \ + the middle", + result[0].payment_threshold_intercept_minor, + intercept_before, + intercept_after ) } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 92d192eb3..f4a286091 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -13,8 +13,9 @@ 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::{ - AdjustmentAnalysis, PaymentAdjuster, PaymentAdjusterError, + AdjustmentAnalysisResult, PaymentAdjuster, PaymentAdjusterError, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::{ @@ -56,7 +57,6 @@ use itertools::Either; use masq_lib::logger::Logger; use masq_lib::messages::ScanType; use masq_lib::ui_gateway::NodeToUiMessage; -use masq_lib::utils::convert_collection; use rusqlite::{Connection, OpenFlags, Row}; use std::any::type_name; use std::cell::RefCell; @@ -103,25 +103,25 @@ pub fn make_payable_account_with_wallet_and_balance_and_timestamp_opt( } pub struct AccountantBuilder { - config: Option, - logger: Option, - payable_dao_factory: Option, - receivable_dao_factory: Option, - pending_payable_dao_factory: Option, - banned_dao_factory: Option, - config_dao_factory: Option, + config_opt: Option, + logger_opt: Option, + payable_dao_factory_opt: Option, + receivable_dao_factory_opt: Option, + pending_payable_dao_factory_opt: Option, + banned_dao_factory_opt: Option, + config_dao_factory_opt: Option, } impl Default for AccountantBuilder { fn default() -> Self { Self { - config: None, - logger: None, - payable_dao_factory: None, - receivable_dao_factory: None, - pending_payable_dao_factory: None, - banned_dao_factory: None, - config_dao_factory: None, + config_opt: None, + logger_opt: None, + payable_dao_factory_opt: None, + receivable_dao_factory_opt: None, + pending_payable_dao_factory_opt: None, + banned_dao_factory_opt: None, + config_dao_factory_opt: None, } } } @@ -239,12 +239,12 @@ const RECEIVABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 2] = impl AccountantBuilder { pub fn bootstrapper_config(mut self, config: BootstrapperConfig) -> Self { - self.config = Some(config); + self.config_opt = Some(config); self } pub fn logger(mut self, logger: Logger) -> Self { - self.logger = Some(logger); + self.logger_opt = Some(logger); self } @@ -255,8 +255,7 @@ impl AccountantBuilder { Self::create_or_update_factory( specially_configured_daos, PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - &mut self.pending_payable_dao_factory, - PendingPayableDaoFactoryMock::new(), + &mut self.pending_payable_dao_factory_opt, ); self } @@ -268,8 +267,7 @@ impl AccountantBuilder { Self::create_or_update_factory( specially_configured_daos, PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - &mut self.payable_dao_factory, - PayableDaoFactoryMock::new(), + &mut self.payable_dao_factory_opt, ); self } @@ -281,8 +279,7 @@ impl AccountantBuilder { Self::create_or_update_factory( specially_configured_daos, RECEIVABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - &mut self.receivable_dao_factory, - ReceivableDaoFactoryMock::new(), + &mut self.receivable_dao_factory_opt, ); self } @@ -290,65 +287,59 @@ impl AccountantBuilder { fn create_or_update_factory( dao_set: Vec>, dao_initialization_order_in_regard_to_accountant: [DestinationMarker; N], - factory_field_in_builder: &mut Option, - dao_factory_mock: DAOFactory, + existing_dao_factory_mock_opt: &mut Option, ) where DAO: Default, - DAOFactory: DaoFactoryWithMakeReplace, + DAOFactory: DaoFactoryWithMakeReplace + Default, { - let make_queue_uncast = fill_vacancies_with_given_or_default_daos( + let finished_make_queue: Vec> = fill_vacancies_with_given_or_default_daos( dao_initialization_order_in_regard_to_accountant, dao_set, ); - let finished_make_queue: Vec> = make_queue_uncast - .into_iter() - .map(|elem| elem as Box) - .collect(); - - let ready_factory = match factory_field_in_builder.take() { + let ready_factory = match existing_dao_factory_mock_opt.take() { Some(existing_factory) => { existing_factory.replace_make_results(finished_make_queue); existing_factory } None => { - let new_factory = dao_factory_mock; + let new_factory = DAOFactory::default(); new_factory.replace_make_results(finished_make_queue); new_factory } }; - factory_field_in_builder.replace(ready_factory); + existing_dao_factory_mock_opt.replace(ready_factory); } pub fn config_dao(mut self, config_dao: ConfigDaoMock) -> Self { - self.config_dao_factory = Some(ConfigDaoFactoryMock::new().make_result(config_dao)); + self.config_dao_factory_opt = Some(ConfigDaoFactoryMock::new().make_result(config_dao)); self } pub fn build(self) -> Accountant { - let config = self.config.unwrap_or(make_bc_with_defaults()); - let payable_dao_factory = self.payable_dao_factory.unwrap_or( + let config = self.config_opt.unwrap_or(make_bc_with_defaults()); + let payable_dao_factory = self.payable_dao_factory_opt.unwrap_or( PayableDaoFactoryMock::new() .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()) .make_result(PayableDaoMock::new()), ); - let receivable_dao_factory = self.receivable_dao_factory.unwrap_or( + let receivable_dao_factory = self.receivable_dao_factory_opt.unwrap_or( ReceivableDaoFactoryMock::new() .make_result(ReceivableDaoMock::new()) .make_result(ReceivableDaoMock::new()), ); - let pending_payable_dao_factory = self.pending_payable_dao_factory.unwrap_or( + let pending_payable_dao_factory = self.pending_payable_dao_factory_opt.unwrap_or( PendingPayableDaoFactoryMock::new() .make_result(PendingPayableDaoMock::new()) .make_result(PendingPayableDaoMock::new()) .make_result(PendingPayableDaoMock::new()), ); let banned_dao_factory = self - .banned_dao_factory + .banned_dao_factory_opt .unwrap_or(BannedDaoFactoryMock::new().make_result(BannedDaoMock::new())); let config_dao_factory = self - .config_dao_factory + .config_dao_factory_opt .unwrap_or(ConfigDaoFactoryMock::new().make_result(ConfigDaoMock::new())); let mut accountant = Accountant::new( config, @@ -360,7 +351,7 @@ impl AccountantBuilder { config_dao_factory: Box::new(config_dao_factory), }, ); - if let Some(logger) = self.logger { + if let Some(logger) = self.logger_opt { accountant.logger = logger; } @@ -373,6 +364,12 @@ pub struct PayableDaoFactoryMock { make_results: RefCell>>, } +impl Default for PayableDaoFactoryMock { + fn default() -> Self { + Self::new() + } +} + impl PayableDaoFactory for PayableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { @@ -415,6 +412,12 @@ pub struct PendingPayableDaoFactoryMock { make_results: RefCell>>, } +impl Default for PendingPayableDaoFactoryMock { + fn default() -> Self { + Self::new() + } +} + impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { @@ -457,6 +460,12 @@ pub struct ReceivableDaoFactoryMock { make_results: RefCell>>, } +impl Default for ReceivableDaoFactoryMock { + fn default() -> Self { + Self::new() + } +} + impl ReceivableDaoFactory for ReceivableDaoFactoryMock { fn make(&self) -> Box { if self.make_results.borrow().len() == 0 { @@ -1100,7 +1109,7 @@ impl PayableScannerBuilder { pending_payable_dao: PendingPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), payable_inspector: PayableInspector::new(Box::new( - PayableThresholdsGaugeReal::default(), + PayableThresholdsGaugeMock::default(), )), payment_adjuster: PaymentAdjusterMock::default(), } @@ -1276,7 +1285,7 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } -pub fn make_unqualified_and_qualified_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( @@ -1314,11 +1323,8 @@ pub fn make_unqualified_and_qualified_payables( pending_payable_opt: None, }, ]; - let qualified_payable_accounts = make_guaranteed_qualified_payables( - payable_accounts_to_qualify.clone(), - payment_thresholds, - now, - ); + let qualified_payable_accounts = + make_qualified_payables(payable_accounts_to_qualify.clone(), payment_thresholds, now); let mut all_non_pending_payables = Vec::new(); all_non_pending_payables.extend(payable_accounts_to_qualify); @@ -1462,71 +1468,50 @@ pub fn trick_rusqlite_with_read_only_conn( #[derive(Default)] pub struct PaymentAdjusterMock { - search_for_indispensable_adjustment_params: - Arc, ArbitraryIdStamp)>>>, - search_for_indispensable_adjustment_results: RefCell< - Vec, AdjustmentAnalysis>, PaymentAdjusterError>>, - >, - adjust_payments_params: Arc>>, + consider_adjustment_params: Arc, ArbitraryIdStamp)>>>, + consider_adjustment_results: RefCell>, + adjust_payments_params: Arc>>, adjust_payments_results: RefCell>>, } impl PaymentAdjuster for PaymentAdjusterMock { - fn search_for_indispensable_adjustment( + fn consider_adjustment( &self, qualified_payables: Vec, agent: &dyn BlockchainAgent, - ) -> Result, AdjustmentAnalysis>, PaymentAdjusterError> - { - self.search_for_indispensable_adjustment_params + ) -> AdjustmentAnalysisResult { + self.consider_adjustment_params .lock() .unwrap() .push((qualified_payables, agent.arbitrary_id_stamp())); - self.search_for_indispensable_adjustment_results - .borrow_mut() - .remove(0) + self.consider_adjustment_results.borrow_mut().remove(0) } fn adjust_payments( - &mut self, + &self, setup: PreparedAdjustment, - now: SystemTime, ) -> Result { - self.adjust_payments_params - .lock() - .unwrap() - .push((setup, now)); + self.adjust_payments_params.lock().unwrap().push(setup); self.adjust_payments_results.borrow_mut().remove(0) } } impl PaymentAdjusterMock { - pub fn search_for_indispensable_adjustment_params( + pub fn consider_adjustment_params( mut self, params: &Arc, ArbitraryIdStamp)>>>, ) -> Self { - self.search_for_indispensable_adjustment_params = params.clone(); + self.consider_adjustment_params = params.clone(); self } - pub fn search_for_indispensable_adjustment_result( - self, - result: Result< - Either, AdjustmentAnalysis>, - PaymentAdjusterError, - >, - ) -> Self { - self.search_for_indispensable_adjustment_results - .borrow_mut() - .push(result); + pub fn consider_adjustment_result(self, result: AdjustmentAnalysisResult) -> Self { + self.consider_adjustment_results.borrow_mut().push(result); self } - pub fn adjust_payments_params( - mut self, - params: &Arc>>, - ) -> Self { + pub fn adjust_payments_params(mut self, params: &Arc>>) -> Self { self.adjust_payments_params = params.clone(); self } @@ -1560,6 +1545,10 @@ macro_rules! formal_traits_for_payable_mid_scan_msg_handling { ) -> Option { intentionally_blank!() } + + fn cancel_scan(&mut self, _logger: &Logger) { + intentionally_blank!() + } } }; } @@ -1735,40 +1724,45 @@ impl ScanSchedulers { } } -pub fn make_non_guaranteed_qualified_payable(n: u64) -> QualifiedPayableAccount { - // Without guarantee that the generated payable would cross the given thresholds +pub fn make_meaningless_qualified_payable(n: u64) -> QualifiedPayableAccount { + // It's not guaranteed that the payables would cross the given thresholds. + let coefficient = (n as f64).sqrt().floor() as u64; + let permanent_deb_allowed_minor = gwei_to_wei(coefficient); + let payment_threshold_intercept = 7_u128 * gwei_to_wei::(n) / 10_u128; QualifiedPayableAccount::new( make_payable_account(n), - n as u128 * 12345, - CreditorThresholds::new(111_111_111), + payment_threshold_intercept, + CreditorThresholds::new(permanent_deb_allowed_minor), ) } -pub fn make_analyzed_account(n: u64) -> AnalyzedPayableAccount { - AnalyzedPayableAccount::new(make_non_guaranteed_qualified_payable(n), 123456789) +pub fn make_meaningless_analyzed_account(n: u64) -> AnalyzedPayableAccount { + let qualified_account = make_meaningless_qualified_payable(n); + let disqualification_limit = 85 * qualified_account.payment_threshold_intercept_minor / 100; + AnalyzedPayableAccount::new(qualified_account, disqualification_limit) } -pub fn make_guaranteed_qualified_payables( +pub fn make_qualified_payables( payables: Vec, payment_thresholds: &PaymentThresholds, now: SystemTime, ) -> Vec { - try_making_guaranteed_qualified_payables(payables, payment_thresholds, now, true) + try_to_make_guaranteed_qualified_payables(payables, payment_thresholds, now, true) } -pub fn make_guaranteed_analyzed_payables( +pub fn make_analyzed_payables( payables: Vec, payment_thresholds: &PaymentThresholds, now: SystemTime, ) -> Vec { - convert_collection(make_guaranteed_qualified_payables( + convert_qualified_into_analyzed_payables_in_test(make_qualified_payables( payables, payment_thresholds, now, )) } -pub fn try_making_guaranteed_qualified_payables( +pub fn try_to_make_guaranteed_qualified_payables( payables: Vec, payment_thresholds: &PaymentThresholds, now: SystemTime, diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 2b71af003..07b8ea50d 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -521,7 +521,7 @@ mod tests { use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; use crate::accountant::scanners::test_utils::protect_qualified_payables_in_test; use crate::accountant::test_utils::{ - make_non_guaranteed_qualified_payable, make_pending_payable_fingerprint, + make_meaningless_qualified_payable, make_pending_payable_fingerprint, }; use crate::blockchain::bip32::Bip32EncryptionKeyProvider; use crate::blockchain::blockchain_interface::blockchain_interface_null::BlockchainInterfaceNull; @@ -664,8 +664,8 @@ mod tests { let persistent_configuration = PersistentConfigurationMock::default() .set_arbitrary_id_stamp(persistent_config_id_stamp); let qualified_payables = vec![ - make_non_guaranteed_qualified_payable(111), - make_non_guaranteed_qualified_payable(222), + make_meaningless_qualified_payable(111), + make_meaningless_qualified_payable(222), ]; let subject = BlockchainBridge::new( Box::new(blockchain_interface), @@ -739,7 +739,7 @@ mod tests { subject.scan_error_subs_opt = Some(scan_error_recipient); let request = QualifiedPayablesMessage { protected_qualified_payables: protect_qualified_payables_in_test(vec![ - make_non_guaranteed_qualified_payable(1234), + make_meaningless_qualified_payable(1234), ]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11, @@ -786,7 +786,7 @@ mod tests { ); let request = QualifiedPayablesMessage { protected_qualified_payables: protect_qualified_payables_in_test(vec![ - make_non_guaranteed_qualified_payable(12345), + make_meaningless_qualified_payable(12345), ]), response_skeleton_opt: None, }; diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 842c203a7..580e3d606 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -37,7 +37,7 @@ use web3::types::{ H160, H256, U256, }; use web3::{BatchTransport, Error, Web3}; -use masq_lib::percentage::Percentage; +use masq_lib::percentage::PurePercentage; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible, RpcPayablesFailure}; use crate::sub_lib::blockchain_interface_web3::{compute_gas_limit, transaction_data_web3, web3_gas_limit_const_part}; @@ -71,7 +71,7 @@ pub const REQUESTS_IN_PARALLEL: usize = 1; lazy_static! { // TODO In the future, we'll replace this by a dynamical value of the user's choice. - pub static ref TRANSACTION_FEE_MARGIN: Percentage = Percentage::new(15); + pub static ref TX_FEE_MARGIN_IN_PERCENT: PurePercentage = PurePercentage::try_from(15).expect("Value below 100 should cause no issue"); } pub struct BlockchainInterfaceWeb3 @@ -278,7 +278,7 @@ where accounts: &[PayableAccount], ) -> Result, PayableTransactionError> { let consuming_wallet = agent.consuming_wallet(); - let gas_price = agent.agreed_fee_per_computation_unit(); + let gas_price = agent.gas_price(); let pending_nonce = agent.pending_transaction_id(); debug!( @@ -612,8 +612,8 @@ mod tests { use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_FEE_MARGIN, - TRANSACTION_LITERAL, + BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, + TX_FEE_MARGIN_IN_PERCENT, }; use crate::blockchain::blockchain_interface::test_utils::{ test_blockchain_interface_is_connected_and_functioning, LowBlockchainIntMock, @@ -659,7 +659,7 @@ mod tests { }; use crate::sub_lib::blockchain_interface_web3::web3_gas_limit_const_part; use indoc::indoc; - use masq_lib::percentage::Percentage; + use masq_lib::percentage::PurePercentage; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; @@ -701,7 +701,10 @@ mod tests { assert_eq!(CONTRACT_ABI, contract_abi_expected); assert_eq!(TRANSACTION_LITERAL, transaction_literal_expected); assert_eq!(REQUESTS_IN_PARALLEL, 1); - assert_eq!(*TRANSACTION_FEE_MARGIN, Percentage::new(15)); + assert_eq!( + *TX_FEE_MARGIN_IN_PERCENT, + PurePercentage::try_from(15).unwrap() + ); } #[test] @@ -1058,8 +1061,11 @@ mod tests { transaction_fee_balance ); assert_eq!(result.service_fee_balance_minor(), masq_balance.as_u128()); - assert_eq!(result.agreed_transaction_fee_margin(), Percentage::new(15)); - assert_eq!(result.agreed_fee_per_computation_unit(), 50); + assert_eq!( + result.gas_price_margin(), + PurePercentage::try_from(15).unwrap() + ); + assert_eq!(result.gas_price(), 50); assert_eq!( result.estimated_transaction_fee_per_transaction_minor(), 3666400000000000 @@ -2167,7 +2173,7 @@ mod tests { Box::new( BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(gas_price_gwei) + .gas_price_result(gas_price_gwei) .pending_transaction_id_result(nonce), ) } diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 32ea22f54..7bf9d4124 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::unshared_test_utils::standard_dir_for_test_input_data; +use crate::test_utils::standard_dir_for_test_input_data; use masq_lib::logger::Logger; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::{to_string, NeighborhoodModeLight}; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 0decd8d9b..a3b279eca 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -51,6 +51,7 @@ use serde_derive::{Deserialize, Serialize}; use std::collections::btree_set::BTreeSet; use std::collections::HashSet; use std::convert::From; +use std::env::current_dir; use std::fmt::Debug; use std::hash::Hash; @@ -58,7 +59,7 @@ use std::io::ErrorKind; use std::io::Read; use std::iter::repeat; use std::net::{Shutdown, TcpStream}; - +use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::thread; @@ -526,6 +527,17 @@ 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"); + } + working_dir + .join("src") + .join("test_utils") + .join("test_input_data") +} + #[cfg(test)] pub mod unshared_test_utils { use crate::accountant::DEFAULT_PENDING_TOO_LONG_SEC; @@ -554,14 +566,11 @@ pub mod unshared_test_utils { use lazy_static::lazy_static; use masq_lib::messages::{ToMessageBody, UiCrashRequest}; use masq_lib::multi_config::MultiConfig; - #[cfg(not(feature = "no_test_share"))] - use masq_lib::test_utils::utils::MutexIncrementInset; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::slice_of_strs_to_vec_of_strings; use std::any::TypeId; use std::cell::RefCell; use std::collections::HashMap; - use std::env::current_dir; use std::num::ParseIntError; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::path::{Path, PathBuf}; @@ -809,14 +818,6 @@ pub mod unshared_test_utils { .collect() } - pub fn standard_dir_for_test_input_data() -> PathBuf { - current_dir() - .unwrap() - .join("src") - .join("test_utils") - .join("input_data") - } - pub mod system_killer_actor { use super::*; @@ -964,6 +965,7 @@ pub mod unshared_test_utils { pub mod arbitrary_id_stamp { use super::*; + use masq_lib::test_utils::utils::MutexIncrementInset; //The issues we are to solve might look as follows: diff --git a/node/src/test_utils/input_data/database_version_0_sqls.txt b/node/src/test_utils/test_input_data/database_version_0_sqls.txt similarity index 100% rename from node/src/test_utils/input_data/database_version_0_sqls.txt rename to node/src/test_utils/test_input_data/database_version_0_sqls.txt diff --git a/node/src/test_utils/test_input_data/smart_contract_for_on_blockchain_test b/node/src/test_utils/test_input_data/smart_contract_for_on_blockchain_test new file mode 100644 index 000000000..91e6e1dd2 --- /dev/null +++ b/node/src/test_utils/test_input_data/smart_contract_for_on_blockchain_test @@ -0,0 +1,60 @@ + +608060405234801561001057600080fd5b5060038054600160a060020a031916331790819055604051600160a060020a0391909116906000907f8be0 +079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a3610080336b01866de34549d620d8000000640100000000610b94 +61008582021704565b610156565b600160a060020a038216151561009a57600080fd5b6002546100b490826401000000006109a461013d8202170456 +5b600255600160a060020a0382166000908152602081905260409020546100e790826401000000006109a461013d82021704565b600160a060020a03 +83166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a +4df523b3ef9281900390910190a35050565b60008282018381101561014f57600080fd5b9392505050565b610c6a806101656000396000f300608060 +4052600436106100fb5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde038114610100 +578063095ea7b31461018a57806318160ddd146101c257806323b872dd146101e95780632ff2e9dc14610213578063313ce567146102285780633950 +93511461025357806342966c681461027757806370a0823114610291578063715018a6146102b257806379cc6790146102c75780638da5cb5b146102 +eb5780638f32d59b1461031c57806395d89b4114610331578063a457c2d714610346578063a9059cbb1461036a578063dd62ed3e1461038e578063f2 +fde38b146103b5575b600080fd5b34801561010c57600080fd5b506101156103d6565b60408051602080825283518183015283519192839290830191 +85019080838360005b8381101561014f578181015183820152602001610137565b50505050905090810190601f16801561017c578082038051600183 +6020036101000a031916815260200191505b509250505060405180910390f35b34801561019657600080fd5b506101ae600160a060020a0360043516 +602435610436565b604080519115158252519081900360200190f35b3480156101ce57600080fd5b506101d7610516565b6040805191825251908190 +0360200190f35b3480156101f557600080fd5b506101ae600160a060020a036004358116906024351660443561051c565b34801561021f57600080fd +5b506101d76105b9565b34801561023457600080fd5b5061023d6105c9565b6040805160ff9092168252519081900360200190f35b34801561025f57 +600080fd5b506101ae600160a060020a03600435166024356105ce565b34801561028357600080fd5b5061028f60043561067e565b005b3480156102 +9d57600080fd5b506101d7600160a060020a036004351661068b565b3480156102be57600080fd5b5061028f6106a6565b3480156102d357600080fd +5b5061028f600160a060020a0360043516602435610710565b3480156102f757600080fd5b5061030061071e565b60408051600160a060020a039092 +168252519081900360200190f35b34801561032857600080fd5b506101ae61072d565b34801561033d57600080fd5b5061011561073e565b34801561 +035257600080fd5b506101ae600160a060020a0360043516602435610775565b34801561037657600080fd5b506101ae600160a060020a0360043516 +6024356107c0565b34801561039a57600080fd5b506101d7600160a060020a03600435811690602435166107d6565b3480156103c157600080fd5b50 +61028f600160a060020a0360043516610801565b606060405190810160405280602481526020017f486f7420746865206e657720746f6b656e20796f +75277265206c6f6f6b696e6781526020017f20666f720000000000000000000000000000000000000000000000000000000081525081565b60008115 +8061044c575061044a33846107d6565b155b151561050557604080517f08c379a0000000000000000000000000000000000000000000000000000000 +00815260206004820152604160248201527f55736520696e637265617365417070726f76616c206f7220646563726561736560448201527f41707072 +6f76616c20746f2070726576656e7420646f75626c652d7370656e6460648201527f2e00000000000000000000000000000000000000000000000000 +000000000000608482015290519081900360a40190fd5b61050f838361081d565b9392505050565b60025490565b600160a060020a03831660009081 +5260016020908152604080832033845290915281205482111561054c57600080fd5b600160a060020a03841660009081526001602090815260408083 +20338452909152902054610580908363ffffffff61089b16565b600160a060020a038516600090815260016020908152604080832033845290915290 +20556105af8484846108b2565b5060019392505050565b6b01866de34549d620d800000081565b601281565b6000600160a060020a03831615156105 +e557600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff6109a416565b33 +6000818152600160209081526040808320600160a060020a0389168085529083529281902085905580519485525191937f8c5be1e5ebec7d5bd14f71 +427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929081900390910190a350600192915050565b61068833826109b6565b50565b600160a060020a +031660009081526020819052604090205490565b6106ae61072d565b15156106b957600080fd5b600354604051600091600160a060020a0316907f8b +e0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a36003805473ffffffffffffffffffffffffffffffffffffffff +19169055565b61071a8282610a84565b5050565b600354600160a060020a031690565b600354600160a060020a0316331490565b6040805180820190 +9152600381527f484f540000000000000000000000000000000000000000000000000000000000602082015281565b6000600160a060020a03831615 +1561078c57600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff61089b16 +565b60006107cd3384846108b2565b50600192915050565b600160a060020a0391821660009081526001602090815260408083209390941682529190 +9152205490565b61080961072d565b151561081457600080fd5b61068881610b16565b6000600160a060020a038316151561083457600080fd5b3360 +00818152600160209081526040808320600160a060020a03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f +71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b600080838311156108ab57600080fd5b505090 +0390565b600160a060020a0383166000908152602081905260409020548111156108d757600080fd5b600160a060020a03821615156108ec57600080 +fd5b600160a060020a038316600090815260208190526040902054610915908263ffffffff61089b16565b600160a060020a03808516600090815260 +208190526040808220939093559084168152205461094a908263ffffffff6109a416565b600160a060020a0380841660008181526020818152604091 +82902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190 +a3505050565b60008282018381101561050f57600080fd5b600160a060020a03821615156109cb57600080fd5b600160a060020a0382166000908152 +602081905260409020548111156109f057600080fd5b600254610a03908263ffffffff61089b16565b600255600160a060020a038216600090815260 +208190526040902054610a2f908263ffffffff61089b16565b600160a060020a03831660008181526020818152604080832094909455835185815293 +5191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35050565b600160a060020a038216 +6000908152600160209081526040808320338452909152902054811115610ab457600080fd5b600160a060020a038216600090815260016020908152 +6040808320338452909152902054610ae8908263ffffffff61089b16565b600160a060020a0383166000908152600160209081526040808320338452 +90915290205561071a82826109b6565b600160a060020a0381161515610b2b57600080fd5b600354604051600160a060020a038084169216907f8be0 +079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a36003805473ffffffffffffffffffffffffffffffffffffffff +1916600160a060020a0392909216919091179055565b600160a060020a0382161515610ba957600080fd5b600254610bbc908263ffffffff6109a416 +565b600255600160a060020a038216600090815260208190526040902054610be8908263ffffffff6109a416565b600160a060020a03831660008181 +52602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92 +81900390910190a350505600a165627a7a72305820d4ad56dfe541fec48c3ecb02cebad565a998dfca7774c0c4f4b1f4a8e2363a590029 diff --git a/port_exposer/.gitignore b/port_exposer/.gitignore new file mode 100644 index 000000000..e0264b089 --- /dev/null +++ b/port_exposer/.gitignore @@ -0,0 +1,5 @@ + +## File-based project format: +*.iws +*.iml +*.ipr \ No newline at end of file