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/automap/Cargo.lock b/automap/Cargo.lock index c970dc200..fdc0b7294 100644 --- a/automap/Cargo.lock +++ b/automap/Cargo.lock @@ -1063,6 +1063,7 @@ dependencies = [ "lazy_static", "log 0.4.17", "nix", + "num", "regex", "serde", "serde_derive", @@ -1205,6 +1206,40 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1215,11 +1250,34 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg 1.1.0", ] 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/dns_utility/Cargo.lock b/dns_utility/Cargo.lock index 52a3ec6b3..6151b467c 100644 --- a/dns_utility/Cargo.lock +++ b/dns_utility/Cargo.lock @@ -866,6 +866,7 @@ dependencies = [ "lazy_static", "log 0.4.17", "nix", + "num", "regex", "serde", "serde_derive", @@ -1008,6 +1009,81 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "num_cpus" version = "1.15.0" 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/Cargo.toml b/masq_lib/Cargo.toml index f87de81a1..332ff1efc 100644 --- a/masq_lib/Cargo.toml +++ b/masq_lib/Cargo.toml @@ -17,6 +17,7 @@ ethereum-types = "0.9.0" itertools = "0.10.1" lazy_static = "1.4.0" log = "0.4.8" +num = "=0.4.0" regex = "1.5.4" serde = "1.0.133" serde_derive = "1.0.133" diff --git a/masq_lib/src/lib.rs b/masq_lib/src/lib.rs index e5232b221..b3a8b2902 100644 --- a/masq_lib/src/lib.rs +++ b/masq_lib/src/lib.rs @@ -20,6 +20,7 @@ pub mod command; pub mod constants; pub mod crash_point; pub mod data_version; +pub mod percentage; pub mod shared_schema; pub mod test_utils; pub mod type_obfuscation; diff --git a/masq_lib/src/percentage.rs b/masq_lib/src/percentage.rs new file mode 100644 index 000000000..f290fb74c --- /dev/null +++ b/masq_lib/src/percentage.rs @@ -0,0 +1,741 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use num::CheckedAdd; +use num::CheckedSub; +use num::{CheckedDiv, CheckedMul, Integer}; +use std::any::type_name; +use std::fmt::Debug; +use std::ops::Rem; + +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); + +// Designed to store values from 0 to 100 and offer a set of handy methods for PurePercentage +// operations over a wide variety of integer types. It is also to look after the least significant +// digit on the resulted number in order to avoid the effect of a loss on precision that genuinely +// comes with division on integers if a remainder is left over. The percents are always represented +// by an unsigned integer. On the contrary, the numbers that it is applied on can take on both +// positive and negative values. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PurePercentage { + degree: u8, +} + +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 + )), + } + } +} + +trait PurePercentageInternalMethods +where + Self: Sized, +{ + fn _of(&self, num: N) -> N; + fn __check_zero_and_maybe_return_it(&self, num: N) -> Option; + fn __abs(num: N, is_signed: bool) -> N; + fn __derive_rounding_increment(remainder: N) -> N; + fn _increase_by_percent_for(&self, num: N) -> N; + fn _decrease_by_percent_for(&self, num: N) -> N; + fn __handle_upper_overflow(&self, num: N) -> N; +} + +impl PurePercentageInternalMethods for PurePercentage +where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, +{ + fn _of(&self, num: N) -> N { + if let Some(zero) = self.__check_zero_and_maybe_return_it(num) { + return zero; + } + + let product_before_final_div = match N::try_from(self.degree as i8) + .expect("Each integer has 100") + .checked_mul(&num) + { + Some(num) => num, + None => return self.__handle_upper_overflow(num), + }; + + let (base, remainder) = base_and_rem_from_div_100(product_before_final_div); + + base + Self::__derive_rounding_increment(remainder) + } + + fn __check_zero_and_maybe_return_it(&self, num: N) -> Option { + let zero = N::try_from(0).expect("Each integer has 0"); + if num == zero || N::try_from(self.degree as i8).expect("Each integer has 100") == zero { + Some(zero) + } else { + None + } + } + + fn __abs(num: N, is_negative: bool) -> N { + if is_negative { + N::try_from(-1) + .expect("Negative 1 must be possible for a confirmed signed integer") + .checked_mul(&num) + .expect("Must be possible for these low values") + } else { + num + } + } + + // This function helps to correct the last digit of the resulting integer to be as close + // as possible to the hypothetical fractional number, if we could go beyond the decimal point. + fn __derive_rounding_increment(remainder: N) -> N { + let is_negative = remainder < N::try_from(0).expect("Each integer has 0"); + let is_minor = + Self::__abs(remainder, is_negative) < N::try_from(50).expect("Each integer has 50"); + let addition = match (is_negative, is_minor) { + (false, true) => 0, + (false, false) => 1, + (true, true) => 0, + (true, false) => -1, + }; + N::try_from(addition).expect("Each integer has 1, or -1 if signed") + } + + fn _increase_by_percent_for(&self, num: N) -> N { + let to_add = self._of(num); + num.checked_add(&to_add).unwrap_or_else(|| { + panic!( + "Overflowed during addition of {} percent, that is an extra {:?} for {:?} of type {}.", + self.degree, + to_add, + num, + type_name::() + ) + }) + } + + fn _decrease_by_percent_for(&self, num: N) -> N { + let to_subtract = self._of(num); + num.checked_sub(&to_subtract) + .expect("Mathematically impossible") + } + + fn __handle_upper_overflow(&self, num: N) -> N { + let (base, remainder) = base_and_rem_from_div_100(num); + let percents = N::try_from(self.degree as i8).expect("Each integer has 100"); + let percents_of_base = base * percents; + let (percents_of_remainder, nearly_lost_tail) = + base_and_rem_for_ensured_i16(remainder, percents); + let final_rounding_element = Self::__derive_rounding_increment(nearly_lost_tail); + + percents_of_base + percents_of_remainder + final_rounding_element + } +} + +impl PurePercentage { + pub fn of(&self, num: N) -> N + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + self._of(num) + } + + pub fn increase_by_percent_for(&self, num: N) -> N + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + self._increase_by_percent_for(num) + } + + pub fn decrease_by_percent_for(&self, num: N) -> N + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + self._decrease_by_percent_for(num) + } +} + +fn base_and_rem_for_ensured_i16(a: N, b: N) -> (N, N) +where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, +{ + let num = i16::try_from(a) + .expect("Remainder: Each integer can go up to 100, or down to -100 if signed") + * i16::try_from(b) + .expect("Percents: Each integer can go up to 100, or down to -100 if signed"); + + let (base, remainder) = base_and_rem_from_div_100(num); + + ( + N::try_from(base as i8) + .expect("Base: Each integer can go up to 100, or down to -100 if signed"), + N::try_from(remainder as i8) + .expect("Remainder: Each integer can go up to 100, or down to -100 if signed"), + ) +} + +fn base_and_rem_from_div_100(num: N) -> (N, N) +where + N: PercentageInteger, + >::Error: Debug, +{ + let hundred = N::try_from(100i8).expect("Each integer has 100"); + let modulo = num % hundred; + (num / hundred, modulo) +} + +// This is a wider type that allows to specify cumulative percents of more than only 100. +// The expected use of this would look like requesting percents meaning possibly multiples of 100%, +// roughly, of a certain base number. Similarly to the PurePercentage type, also signed numbers +// would be accepted. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LoosePercentage { + multiplier_of_100_percent: u32, + degrees_from_remainder: PurePercentage, +} + +impl LoosePercentage { + pub fn new(percents: u32) -> Self { + let multiples_of_100_percent = percents / 100; + let remainder = (percents % 100) as u8; + let degrees_from_remainder = + PurePercentage::try_from(remainder).expect("Should never happen."); + Self { + multiplier_of_100_percent: multiples_of_100_percent, + degrees_from_remainder, + } + } + + // If this returns an overflow error, you may want to precede this by converting the base + // number to a larger integer + pub fn of(&self, num: N) -> Result + where + N: PercentageInteger + TryFrom, + >::Error: Debug, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + let multiplier = match N::try_from(self.multiplier_of_100_percent) { + Ok(n) => n, + Err(e) => { + return Err(BaseTypeOverflow { + msg: format!( + "Couldn't init multiplier {} to type {} due to {:?}.", + self.multiplier_of_100_percent, + type_name::(), + e + ), + }) + } + }; + + let wholes = match num.checked_mul(&multiplier) { + Some(n) => n, + None => { + return Err(BaseTypeOverflow { + msg: format!( + "Multiplication failed between {:?} and {:?} for type {}.", + num, + multiplier, + type_name::() + ), + }) + } + }; + + let remainder = self.degrees_from_remainder.of(num); + + match wholes.checked_add(&remainder) { + Some(res) => Ok(res), + None => Err(BaseTypeOverflow { + msg: format!( + "Final addition failed on {:?} and {:?} for type {}.", + wholes, + remainder, + type_name::() + ), + }), + } + } + + // Note that functions like 'add_percents_to' or 'subtract_percents_from' don't need to be + // implemented here, even though they are at the 'PurePercentage'. You can substitute them + // simply by querying 100 + or 100 - +} + +#[derive(Debug, PartialEq, Eq)] +pub struct BaseTypeOverflow { + msg: String, +} + +#[cfg(test)] +mod tests { + use crate::percentage::{ + BaseTypeOverflow, LoosePercentage, PercentageInteger, PurePercentage, + PurePercentageInternalMethods, + }; + use std::fmt::Debug; + + #[test] + fn percentage_is_implemented_for_all_rust_integers() { + let subject = PurePercentage::try_from(50).unwrap(); + + assert_positive_integer_compatibility(&subject, u8::MAX, 128); + assert_positive_integer_compatibility(&subject, u16::MAX, 32768); + assert_positive_integer_compatibility(&subject, u32::MAX, 2147483648); + assert_positive_integer_compatibility(&subject, u64::MAX, 9223372036854775808); + assert_positive_integer_compatibility( + &subject, + u128::MAX, + 170141183460469231731687303715884105728, + ); + assert_negative_integer_compatibility(&subject, i8::MIN, -64); + assert_negative_integer_compatibility(&subject, i16::MIN, -16384); + assert_negative_integer_compatibility(&subject, i32::MIN, -1073741824); + assert_negative_integer_compatibility(&subject, i64::MIN, -4611686018427387904); + assert_negative_integer_compatibility( + &subject, + i128::MIN, + -85070591730234615865843651857942052864, + ); + } + + fn assert_positive_integer_compatibility( + subject: &PurePercentage, + num: N, + expected_literal_num: N, + ) where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + assert_against_literal_value(subject, num, expected_literal_num); + + let trivially_calculated_half = num / N::try_from(2).unwrap(); + // Widening the bounds to compensate the extra rounding + let one = N::try_from(1).unwrap(); + assert!( + trivially_calculated_half <= expected_literal_num + && expected_literal_num <= (trivially_calculated_half + one), + "We expected {:?} to be {:?} or {:?}", + expected_literal_num, + trivially_calculated_half, + trivially_calculated_half + one + ) + } + + fn assert_negative_integer_compatibility( + subject: &PurePercentage, + num: N, + expected_literal_num: N, + ) where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + assert_against_literal_value(subject, num, expected_literal_num); + + let trivially_calculated_half = num / N::try_from(2).unwrap(); + // Widening the bounds to compensate the extra rounding + let one = N::try_from(1).unwrap(); + assert!( + trivially_calculated_half >= expected_literal_num + && expected_literal_num >= trivially_calculated_half - one, + "We expected {:?} to be {:?} or {:?}", + expected_literal_num, + trivially_calculated_half, + trivially_calculated_half - one + ) + } + + fn assert_against_literal_value(subject: &PurePercentage, num: N, expected_literal_num: N) + where + N: PercentageInteger, + >::Error: Debug, + i16: TryFrom, + >::Error: Debug, + { + let percents_of_num = subject.of(num); + + assert_eq!( + percents_of_num, expected_literal_num, + "Expected {:?}, but was {:?}", + expected_literal_num, percents_of_num + ); + } + + #[test] + 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 base_value = 100; + let act = |percent, base| PurePercentage::try_from(percent).unwrap().of(base); + let expected_values = (0..=100).collect::>(); + + test_end_to_end(act, base_value, expected_values) + } + + #[test] + fn pure_percentage_end_to_end_test_for_signed() { + let base_value = -100; + let act = |percent, base| PurePercentage::try_from(percent).unwrap().of(base); + let expected_values = (-100..=0).rev().collect::>(); + + test_end_to_end(act, base_value, expected_values) + } + + fn test_end_to_end(act: F, base: i8, expected_values: Vec) + where + F: Fn(u8, i8) -> i8, + { + let range = 0_u8..=100; + + let round_returned_range = range + .into_iter() + .map(|percent| act(percent, base)) + .collect::>(); + + assert_eq!(round_returned_range, expected_values) + } + + #[test] + fn only_numbers_up_to_100_are_accepted() { + (101..=u8::MAX) + .map(|num| (PurePercentage::try_from(num), num)) + .for_each(|(res, num)| { + assert_eq!( + 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![ + 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_to_works_for_last_but_one_digit() { + [ + (49, 0), + (50, 1), + (51, 1), + (5, 0), + (99,1), + (0,0) + ] + .into_iter() + .for_each( + |(num, expected_abs_result)| { + let result = PurePercentage::__derive_rounding_increment(num); + assert_eq!( + result, + expected_abs_result, + "Unsigned number {} was identified for rounding as {:?}, but it should've been {:?}", + num, + result, + expected_abs_result + ); + let signed = num as i64 * -1; + let result = PurePercentage::__derive_rounding_increment(signed); + let expected_neg_result = expected_abs_result * -1; + assert_eq!( + result, + expected_neg_result, + "Signed number {} was identified for rounding as {:?}, but it should've been {:?}", + signed, + result, + expected_neg_result + ) + }, + ) + } + + #[test] + fn increase_by_percent_for_works() { + let subject = PurePercentage::try_from(13).unwrap(); + + let unsigned = subject.increase_by_percent_for(100); + let signed = subject.increase_by_percent_for(-100); + + assert_eq!(unsigned, 113); + assert_eq!(signed, -113) + } + + #[test] + #[should_panic(expected = "Overflowed during addition of 1 percent, that is \ + an extra 184467440737095516 for 18446744073709551615 of type u64.")] + fn increase_by_percent_for_hits_overflow() { + let _ = PurePercentage::try_from(1) + .unwrap() + .increase_by_percent_for(u64::MAX); + } + + #[test] + fn decrease_by_percent_for_works() { + let subject = PurePercentage::try_from(55).unwrap(); + + let unsigned = subject.decrease_by_percent_for(100); + let signed = subject.decrease_by_percent_for(-100); + + assert_eq!(unsigned, 45); + assert_eq!(signed, -45) + } + + #[test] + fn preventing_early_upper_overflow() { + // 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 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 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 base_value = 100; + let act = |percent, base| LoosePercentage::new(percent as u32).of(base).unwrap(); + let expected_values = (0..=100).collect::>(); + + test_end_to_end(act, base_value, expected_values) + } + + #[test] + fn loose_percentage_end_to_end_test_for_standard_values_signed() { + let base_value = -100; + let act = |percent, base| LoosePercentage::new(percent as u32).of(base).unwrap(); + let expected_values = (-100..=0).rev().collect::>(); + + test_end_to_end(act, base_value, expected_values) + } + + 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 { + msg: "Couldn't init multiplier 256 to type u8 due to TryFromIntError(())." + .to_string() + }) + ) + } + + #[test] + fn loose_percentage_hits_limit_at_multiplication() { + let percents = 200; + let subject = LoosePercentage::new(percents); + + let result: Result = subject.of(u8::MAX); + + assert_eq!( + result, + Err(BaseTypeOverflow { + msg: "Multiplication failed between 255 and 2 for type u8.".to_string() + }) + ) + } + + #[test] + fn loose_percentage_hits_limit_at_addition_from_remainder() { + let percents = 101; + let subject = LoosePercentage::new(percents); + + let result: Result = subject.of(u8::MAX); + + assert_eq!( + result, + Err(BaseTypeOverflow { + msg: "Final addition failed on 255 and 3 for type u8.".to_string() + }) + ) + } +} 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/masq_lib/src/utils.rs b/masq_lib/src/utils.rs index 8d563ef37..7758c18ef 100644 --- a/masq_lib/src/utils.rs +++ b/masq_lib/src/utils.rs @@ -364,6 +364,13 @@ pub fn type_name_of(_examined: T) -> &'static str { std::any::type_name::() } +pub fn convert_collection(inputs: Vec) -> Vec +where + Product: From, +{ + inputs.into_iter().map(Product::from).collect() +} + pub trait MutabilityConflictHelper where T: 'static, @@ -376,7 +383,7 @@ where F: FnOnce(&T, &mut Self) -> Self::Result, { //TODO we should seriously think about rewriting this in well tested unsafe code, - // Rust is unnecessarily strict as for this conflicting situation + // Rust is unnecessarily strict in this conflicting situation let helper = self.helper_access().take().expectv("helper"); let result = closure(&helper, self); self.helper_access().replace(helper); @@ -455,7 +462,7 @@ macro_rules! as_any_mut_in_trait_impl { #[macro_export] macro_rules! test_only_use { - ($($use_clause: item),+) => { + ($($use_clause: item)+) => { $( #[cfg(test)] $use_clause @@ -468,8 +475,8 @@ macro_rules! hashmap { () => { ::std::collections::HashMap::new() }; - ($($key:expr => $val:expr,)+) => { - hashmap!($($key => $val),+) + ($($key:expr => $value:expr,)+) => { + hashmap!($($key => $value),+) }; ($($key:expr => $value:expr),+) => { { 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/Dockerfile b/multinode_integration_tests/docker/blockchain/amd64_linux/Dockerfile similarity index 63% rename from multinode_integration_tests/docker/blockchain/Dockerfile rename to multinode_integration_tests/docker/blockchain/amd64_linux/Dockerfile index 027eb7a27..5413c9c8d 100644 --- a/multinode_integration_tests/docker/blockchain/Dockerfile +++ b/multinode_integration_tests/docker/blockchain/amd64_linux/Dockerfile @@ -1,7 +1,8 @@ # Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -FROM trufflesuite/ganache-cli:v6.7.0 -ADD ./entrypoint.sh /app/ +FROM trufflesuite/ganache-cli:v6.12.2 + +ADD ./amd64_linux/entrypoint.sh /app/ EXPOSE 18545 diff --git a/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh b/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh new file mode 100755 index 000000000..28d960392 --- /dev/null +++ b/multinode_integration_tests/docker/blockchain/amd64_linux/entrypoint.sh @@ -0,0 +1,18 @@ +#!/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 ',' that assigns a certain initial balance.) This same principle of initialization needs to be +# regarded, during the test setup, and applied with both the transaction fee (wei of ETH) and the service fee (MASQ). +# While on the transaction fee it's a choice done by us, with the latter, there probably isn't any other solution given +# the mechanism how the deployment of the blockchain smart contract generates the entire token supply only on +# the account of the contract owner's wallet from where it must be sent out to other wallets if needed. + +node /app/ganache-core.docker.cli.js \ + -p 18545 \ + --networkId 2 \ + --verbose \ + --mnemonic "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle lamp absent write kind term toddler sphere ripple idle dragon curious hold" \ + --defaultBalanceEther 0 \ + --account '0xd4670b314ecb5e6b44b7fbe625ed746522c906316e66df31be64194ee6189188,10000000000000000000000' \ No newline at end of file diff --git a/multinode_integration_tests/docker/blockchain/arm64_linux/Dockerfile b/multinode_integration_tests/docker/blockchain/arm64_linux/Dockerfile new file mode 100644 index 000000000..d54dfef4d --- /dev/null +++ b/multinode_integration_tests/docker/blockchain/arm64_linux/Dockerfile @@ -0,0 +1,10 @@ +# Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +# This is version v6.12.2 according to the official ganache versioning +FROM --platform=linux/arm64 nutrina/ganache-cli:0.3 + +ADD ./arm64_linux/entrypoint.sh /app/ + +EXPOSE 18545 + +ENTRYPOINT /app/entrypoint.sh diff --git a/multinode_integration_tests/docker/blockchain/arm64_linux/entrypoint.sh b/multinode_integration_tests/docker/blockchain/arm64_linux/entrypoint.sh new file mode 100755 index 000000000..b014955f7 --- /dev/null +++ b/multinode_integration_tests/docker/blockchain/arm64_linux/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Copyright (c) 2024, 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 ',' that assigns a certain initial balance.) This same principle of initialization needs to be +# regarded, during the test setup, and applied with both the transaction fee (wei of ETH) and the service fee (MASQ). +# While on the transaction fee it's a choice done by us, with the latter, there probably isn't any other solution given +# the mechanism how the deployment of the blockchain smart contract generates the entire token supply only on +# the account of the contract owner's wallet from where it must be sent out to other wallets if needed. + +ganache-cli \ + -h 0.0.0.0 \ + -p 18545 \ + --networkId 2 \ + --verbose \ + -m "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle lamp absent write kind term toddler sphere ripple idle dragon curious hold" \ + --defaultBalanceEther 0 \ + --account '0xd4670b314ecb5e6b44b7fbe625ed746522c906316e66df31be64194ee6189188,10000000000000000000000' diff --git a/multinode_integration_tests/docker/blockchain/build.sh b/multinode_integration_tests/docker/blockchain/build.sh index 8ecfa5025..e8cec53d0 100755 --- a/multinode_integration_tests/docker/blockchain/build.sh +++ b/multinode_integration_tests/docker/blockchain/build.sh @@ -1,4 +1,16 @@ #!/bin/bash -evx # Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -docker build -t ganache-cli . +arch=`dpkg --print-architecture` + +if [[ $arch == "amd64" ]]; then + + echo "Building ganache-cli image for linux/amd64 architecture" + docker build -t ganache-cli . -f amd64_linux/Dockerfile + +elif [[ $arch == "arm64" ]]; then + + echo "Building ganache-cli image for linux/arm64 architecture" + docker build -t ganache-cli . -f arm64_linux/Dockerfile + +fi diff --git a/multinode_integration_tests/docker/blockchain/entrypoint.sh b/multinode_integration_tests/docker/blockchain/entrypoint.sh deleted file mode 100755 index f9d6cc220..000000000 --- a/multinode_integration_tests/docker/blockchain/entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -node /app/ganache-core.docker.cli.js -p 18545 --networkId 2 --verbose --mnemonic "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle lamp absent write kind term toddler sphere ripple idle dragon curious hold" diff --git a/multinode_integration_tests/src/blockchain.rs b/multinode_integration_tests/src/blockchain.rs index aecb91e22..fbbc55665 100644 --- a/multinode_integration_tests/src/blockchain.rs +++ b/multinode_integration_tests/src/blockchain.rs @@ -6,26 +6,31 @@ use crate::utils::UrlHolder; use node_lib::test_utils; use std::net::{IpAddr, Ipv4Addr}; -pub struct BlockchainServer<'a> { - 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 77b82054c..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; @@ -126,6 +126,7 @@ pub struct NodeStartupConfig { pub consuming_wallet_info: ConsumingWalletInfo, pub rate_pack: RatePack, pub payment_thresholds: PaymentThresholds, + pub gas_price_opt: Option, pub firewall_opt: Option, pub memory_opt: Option, pub fake_public_key_opt: Option, @@ -158,6 +159,7 @@ impl NodeStartupConfig { consuming_wallet_info: ConsumingWalletInfo::None, rate_pack: DEFAULT_RATE_PACK, payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, + gas_price_opt: None, firewall_opt: None, memory_opt: None, fake_public_key_opt: None, @@ -205,6 +207,10 @@ impl NodeStartupConfig { args.push(format!("\"{}\"", self.rate_pack)); args.push("--payment-thresholds".to_string()); args.push(format!("\"{}\"", self.payment_thresholds)); + if let Some(price) = self.gas_price_opt { + args.push("--gas-price".to_string()); + args.push(price.to_string()); + } if let EarningWalletInfo::Address(ref address) = self.earning_wallet_info { args.push("--earning-wallet".to_string()); args.push(address.to_string()); @@ -421,6 +427,7 @@ pub struct NodeStartupConfigBuilder { consuming_wallet_info: ConsumingWalletInfo, rate_pack: RatePack, payment_thresholds: PaymentThresholds, + gas_price_opt: Option, firewall: Option, memory: Option, fake_public_key: Option, @@ -477,6 +484,7 @@ impl NodeStartupConfigBuilder { consuming_wallet_info: ConsumingWalletInfo::None, rate_pack: DEFAULT_RATE_PACK, payment_thresholds: *DEFAULT_PAYMENT_THRESHOLDS, + gas_price_opt: None, firewall: None, memory: None, fake_public_key: None, @@ -503,6 +511,7 @@ impl NodeStartupConfigBuilder { consuming_wallet_info: config.consuming_wallet_info.clone(), rate_pack: config.rate_pack, payment_thresholds: config.payment_thresholds, + gas_price_opt: config.gas_price_opt, firewall: config.firewall_opt.clone(), memory: config.memory_opt.clone(), fake_public_key: config.fake_public_key_opt.clone(), @@ -596,6 +605,11 @@ impl NodeStartupConfigBuilder { self } + pub fn gas_price(mut self, value: u64) -> Self { + self.gas_price_opt = Some(value); + self + } + // This method is currently disabled. See multinode_integration_tests/docker/Dockerfile. pub fn open_firewall_port(mut self, port: u16) -> Self { if self.firewall.is_none() { @@ -616,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 } @@ -660,6 +674,7 @@ impl NodeStartupConfigBuilder { consuming_wallet_info: self.consuming_wallet_info, rate_pack: self.rate_pack, payment_thresholds: self.payment_thresholds, + gas_price_opt: self.gas_price_opt, firewall_opt: self.firewall, memory_opt: self.memory, fake_public_key_opt: self.fake_public_key, @@ -763,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(); } @@ -1206,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, @@ -1384,6 +1406,7 @@ mod tests { threshold_interval_sec: 10, unban_below_gwei: 60, }, + gas_price_opt: Some(151), firewall_opt: Some(Firewall { ports_to_open: vec![HTTP_PORT, TLS_PORT], }), @@ -1468,7 +1491,17 @@ mod tests { permanent_debt_allowed_gwei: 50, unban_below_gwei: 60 } - ) + ); + assert_eq!( + result.rate_pack, + RatePack { + routing_byte_rate: 10, + routing_service_rate: 20, + exit_byte_rate: 30, + exit_service_rate: 40, + } + ); + assert_eq!(result.gas_price_opt, Some(151)) } #[test] @@ -1499,6 +1532,7 @@ mod tests { threshold_interval_sec: 2592000, unban_below_gwei: 490000000, }; + let gas_price = 233; let subject = NodeStartupConfigBuilder::standard() .neighborhood_mode("consume-only") @@ -1506,6 +1540,7 @@ mod tests { .ip(IpAddr::from_str("1.3.5.7").unwrap()) .neighbor(one_neighbor.clone()) .neighbor(another_neighbor.clone()) + .gas_price(gas_price) .rate_pack(rate_pack) .payment_thresholds(payment_thresholds) .consuming_wallet_info(default_consuming_wallet_info()) @@ -1532,6 +1567,8 @@ mod tests { "\"1|90|3|250\"", "--payment-thresholds", "\"10000000000|1200|1200|490000000|2592000|490000000\"", + "--gas-price", + "233", "--consuming-private-key", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "--chain", 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 5e9b50347..b121d4148 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -1,222 +1,94 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use bip39::{Language, Mnemonic, Seed}; -use futures::Future; + +use crate::verify_bill_payment_utils::utils::{ + test_body, to_wei, AssertionsValues, Debt, DebtsSpecs, FinalServiceFeeBalancesByServingNodes, + NodeProfile, TestInputBuilder, UiPorts, WholesomeConfig, +}; +use itertools::Itertools; use masq_lib::blockchains::chains::Chain; -use masq_lib::constants::WEIS_IN_GWEI; -use masq_lib::utils::{derivation_path, NeighborhoodModeLight}; -use multinode_integration_tests_lib::blockchain::BlockchainServer; +use masq_lib::messages::FromMessageBody; +use masq_lib::messages::ToMessageBody; +use masq_lib::messages::{ScanType, UiScanRequest, UiScanResponse}; +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, 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::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::wallet::Wallet; -use node_lib::test_utils; -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, TransactionParameters}; -use web3::Web3; +use std::time::Duration; +use std::u128; + +mod verify_bill_payment_utils; #[test] -fn verify_bill_payment() { - 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 deriv_path = derivation_path(0, 0); - let seed = make_seed(); - let (contract_owner_wallet, _) = make_node_wallet(&seed, &deriv_path); - 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); - assert_balances( - &contract_owner_wallet, - &blockchain_interface, - "99998043204000000000", - "472000000000000000000000000", - ); +fn payments_processed_fully_as_balances_were_sufficient() { + // Note: besides the main objectives of this test, it relies on (and so it proves) the premise + // that each Node, after it achieves an effective connectivity as making a route is enabled, + // activates the accountancy module whereas the first cycle of scanners is unleashed. That's + // 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_592_000, + threshold_interval_sec: 2_500_000, debt_threshold_gwei: 1_000_000_000, - payment_grace_period_sec: 86_400, - maturity_threshold_sec: 86_400, + payment_grace_period_sec: 85_000, + maturity_threshold_sec: 85_000, permanent_debt_allowed_gwei: 10_000_000, unban_below_gwei: 10_000_000, }; - let (consuming_config, _) = - build_config(&blockchain_server, &seed, payment_thresholds, deriv_path); - - let (serving_node_1_config, serving_node_1_wallet) = build_config( - &blockchain_server, - &seed, - payment_thresholds, - derivation_path(0, 1), - ); - let (serving_node_2_config, serving_node_2_wallet) = build_config( - &blockchain_server, - &seed, - payment_thresholds, - derivation_path(0, 2), - ); - let (serving_node_3_config, serving_node_3_wallet) = build_config( - &blockchain_server, - &seed, - payment_thresholds, - derivation_path(0, 3), - ); - - let amount = 10 * payment_thresholds.permanent_debt_allowed_gwei as u128 * WEIS_IN_GWEI as u128; - - 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_payable_dao = PayableDaoReal::new(consuming_node_connection); - open_all_file_permissions(consuming_node_path.clone().into()); - assert_eq!( - format!("{}", &contract_owner_wallet), - "0x5a4d5df91d0124dec73dbd112f82d6077ccab47d" - ); - assert_eq!( - format!("{}", &serving_node_1_wallet), - "0x7a3cf474962646b18666b5a5be597bb0af013d81" - ); - assert_eq!( - format!("{}", &serving_node_2_wallet), - "0x0bd8bc4b8aba5d8abf13ea78a6668ad0e9985ad6" - ); - assert_eq!( - format!("{}", &serving_node_3_wallet), - "0xb329c8b029a2d3d217e71bc4d188e8e1a4a8b924" - ); - let now = SystemTime::now(); - consuming_payable_dao - .more_money_payable(now, &serving_node_1_wallet, amount) - .unwrap(); - consuming_payable_dao - .more_money_payable(now, &serving_node_2_wallet, amount) - .unwrap(); - consuming_payable_dao - .more_money_payable(now, &serving_node_3_wallet, amount) - .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(), &contract_owner_wallet, amount) - .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(), &contract_owner_wallet, amount) - .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), + 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_input = TestInputBuilder::default() + .consuming_node_initial_service_fee_balance_minor( + consuming_node_initial_service_fee_balance_minor, ) - .unwrap(); - let serving_node_3_receivable_dao = ReceivableDaoReal::new(serving_node_3_connection); - serving_node_3_receivable_dao - .more_money_receivable(SystemTime::now(), &contract_owner_wallet, amount) - .unwrap(); - open_all_file_permissions(serving_node_3_path.clone().into()); - - expire_payables(consuming_node_path.into()); - expire_receivables(serving_node_1_path.into()); - expire_receivables(serving_node_2_path.into()); - expire_receivables(serving_node_3_path.into()); - - assert_balances( - &contract_owner_wallet, - &blockchain_interface, - "99998043204000000000", - "472000000000000000000000000", - ); - - assert_balances( - &serving_node_1_wallet, - &blockchain_interface, - "100000000000000000000", - "0", - ); - - assert_balances( - &serving_node_2_wallet, - &blockchain_interface, - "100000000000000000000", - "0", - ); + .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, + ), + }; - assert_balances( - &serving_node_3_wallet, - &blockchain_interface, - "100000000000000000000", - "0", + test_body( + test_input, + assertions_values, + stimulate_consuming_node_to_pay_for_test_with_sufficient_funds, + activating_serving_nodes_for_test_with_sufficient_funds, ); +} - let real_consuming_node = - cluster.start_named_real_node(&consuming_node_name, consuming_node_index, consuming_config); - for _ in 0..6 { +fn stimulate_consuming_node_to_pay_for_test_with_sufficient_funds( + cluster: &mut MASQNodeCluster, + real_consuming_node: &MASQRealNode, + _wholesome_config: &WholesomeConfig, +) { + // 1 + 4 Nodes should be enough to compose a route, right? + for _ in 0..4 { cluster.start_real_node( NodeStartupConfigBuilder::standard() .chain(Chain::Dev) @@ -224,233 +96,224 @@ fn verify_bill_payment() { .build(), ); } +} - let now = Instant::now(); - while !consuming_payable_dao.non_pending_payables().is_empty() - && now.elapsed() < Duration::from_secs(10) - { - thread::sleep(Duration::from_millis(400)); - } - - assert_balances( - &contract_owner_wallet, - &blockchain_interface, - "99997886466000000000", - "471999999700000000000000000", - ); - - assert_balances( - &serving_node_1_wallet, - &blockchain_interface, - "100000000000000000000", - amount.to_string().as_str(), - ); - - assert_balances( - &serving_node_2_wallet, - &blockchain_interface, - "100000000000000000000", - amount.to_string().as_str(), - ); - - assert_balances( - &serving_node_3_wallet, - &blockchain_interface, - "100000000000000000000", - amount.to_string().as_str(), - ); +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(); - let serving_node_1 = cluster.start_named_real_node( - &serving_node_1_name, - serving_node_1_index, - serving_node_1_config, - ); - let serving_node_2 = cluster.start_named_real_node( - &serving_node_2_name, - serving_node_2_index, - serving_node_2_config, - ); - let serving_node_3 = cluster.start_named_real_node( - &serving_node_3_name, - serving_node_3_index, - serving_node_3_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(), - ); + // Should be enough additional Nodes to provide the full connectivity + for _ in 0..3 { + let _ = cluster.start_real_node(auxiliary_node_config.clone()); } - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_1_receivable_dao.account_status(&contract_owner_wallet) { - status.balance_wei == 0 - } else { - false - } - }); - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_2_receivable_dao.account_status(&contract_owner_wallet) { - status.balance_wei == 0 - } else { - false - } - }); - test_utils::wait_for(Some(1000), Some(15000), || { - if let Some(status) = serving_node_3_receivable_dao.account_status(&contract_owner_wallet) { - status.balance_wei == 0 - } else { - false - } - }); + serving_nodes.try_into().unwrap() } -fn make_init_config(chain: Chain) -> DbInitializationConfig { - DbInitializationConfig::create_or_migrate(ExternalData::new( - chain, - NeighborhoodModeLight::Standard, - None, - )) -} - -fn assert_balances( - wallet: &Wallet, - blockchain_interface: &BlockchainInterfaceWeb3, - expected_eth_balance: &str, - expected_token_balance: &str, -) { - 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!( - format!("{}", eth_balance), - String::from(expected_eth_balance), - "Actual EthBalance {} doesn't much with expected {}", - eth_balance, - expected_eth_balance - ); - 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_dec_str(expected_token_balance).unwrap(), - "Actual TokenBalance {} doesn't match with expected {}", - token_balance, - expected_token_balance - ); +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(); + let debt_array = debts.try_into().unwrap(); + DebtsSpecs::new(debt_array) } -fn deploy_smart_contract(wallet: &Wallet, web3: &Web3, chain: Chain) -> Address { - let data = "608060405234801561001057600080fd5b5060038054600160a060020a031916331790819055604051600160a060020a0391909116906000907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a3610080336b01866de34549d620d8000000640100000000610b9461008582021704565b610156565b600160a060020a038216151561009a57600080fd5b6002546100b490826401000000006109a461013d82021704565b600255600160a060020a0382166000908152602081905260409020546100e790826401000000006109a461013d82021704565b600160a060020a0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b60008282018381101561014f57600080fd5b9392505050565b610c6a806101656000396000f3006080604052600436106100fb5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde038114610100578063095ea7b31461018a57806318160ddd146101c257806323b872dd146101e95780632ff2e9dc14610213578063313ce56714610228578063395093511461025357806342966c681461027757806370a0823114610291578063715018a6146102b257806379cc6790146102c75780638da5cb5b146102eb5780638f32d59b1461031c57806395d89b4114610331578063a457c2d714610346578063a9059cbb1461036a578063dd62ed3e1461038e578063f2fde38b146103b5575b600080fd5b34801561010c57600080fd5b506101156103d6565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561014f578181015183820152602001610137565b50505050905090810190601f16801561017c5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561019657600080fd5b506101ae600160a060020a0360043516602435610436565b604080519115158252519081900360200190f35b3480156101ce57600080fd5b506101d7610516565b60408051918252519081900360200190f35b3480156101f557600080fd5b506101ae600160a060020a036004358116906024351660443561051c565b34801561021f57600080fd5b506101d76105b9565b34801561023457600080fd5b5061023d6105c9565b6040805160ff9092168252519081900360200190f35b34801561025f57600080fd5b506101ae600160a060020a03600435166024356105ce565b34801561028357600080fd5b5061028f60043561067e565b005b34801561029d57600080fd5b506101d7600160a060020a036004351661068b565b3480156102be57600080fd5b5061028f6106a6565b3480156102d357600080fd5b5061028f600160a060020a0360043516602435610710565b3480156102f757600080fd5b5061030061071e565b60408051600160a060020a039092168252519081900360200190f35b34801561032857600080fd5b506101ae61072d565b34801561033d57600080fd5b5061011561073e565b34801561035257600080fd5b506101ae600160a060020a0360043516602435610775565b34801561037657600080fd5b506101ae600160a060020a03600435166024356107c0565b34801561039a57600080fd5b506101d7600160a060020a03600435811690602435166107d6565b3480156103c157600080fd5b5061028f600160a060020a0360043516610801565b606060405190810160405280602481526020017f486f7420746865206e657720746f6b656e20796f75277265206c6f6f6b696e6781526020017f20666f720000000000000000000000000000000000000000000000000000000081525081565b600081158061044c575061044a33846107d6565b155b151561050557604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604160248201527f55736520696e637265617365417070726f76616c206f7220646563726561736560448201527f417070726f76616c20746f2070726576656e7420646f75626c652d7370656e6460648201527f2e00000000000000000000000000000000000000000000000000000000000000608482015290519081900360a40190fd5b61050f838361081d565b9392505050565b60025490565b600160a060020a038316600090815260016020908152604080832033845290915281205482111561054c57600080fd5b600160a060020a0384166000908152600160209081526040808320338452909152902054610580908363ffffffff61089b16565b600160a060020a03851660009081526001602090815260408083203384529091529020556105af8484846108b2565b5060019392505050565b6b01866de34549d620d800000081565b601281565b6000600160a060020a03831615156105e557600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff6109a416565b336000818152600160209081526040808320600160a060020a0389168085529083529281902085905580519485525191937f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929081900390910190a350600192915050565b61068833826109b6565b50565b600160a060020a031660009081526020819052604090205490565b6106ae61072d565b15156106b957600080fd5b600354604051600091600160a060020a0316907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908390a36003805473ffffffffffffffffffffffffffffffffffffffff19169055565b61071a8282610a84565b5050565b600354600160a060020a031690565b600354600160a060020a0316331490565b60408051808201909152600381527f484f540000000000000000000000000000000000000000000000000000000000602082015281565b6000600160a060020a038316151561078c57600080fd5b336000908152600160209081526040808320600160a060020a0387168452909152902054610619908363ffffffff61089b16565b60006107cd3384846108b2565b50600192915050565b600160a060020a03918216600090815260016020908152604080832093909416825291909152205490565b61080961072d565b151561081457600080fd5b61068881610b16565b6000600160a060020a038316151561083457600080fd5b336000818152600160209081526040808320600160a060020a03881680855290835292819020869055805186815290519293927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a350600192915050565b600080838311156108ab57600080fd5b5050900390565b600160a060020a0383166000908152602081905260409020548111156108d757600080fd5b600160a060020a03821615156108ec57600080fd5b600160a060020a038316600090815260208190526040902054610915908263ffffffff61089b16565b600160a060020a03808516600090815260208190526040808220939093559084168152205461094a908263ffffffff6109a416565b600160a060020a038084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008282018381101561050f57600080fd5b600160a060020a03821615156109cb57600080fd5b600160a060020a0382166000908152602081905260409020548111156109f057600080fd5b600254610a03908263ffffffff61089b16565b600255600160a060020a038216600090815260208190526040902054610a2f908263ffffffff61089b16565b600160a060020a038316600081815260208181526040808320949094558351858152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35050565b600160a060020a0382166000908152600160209081526040808320338452909152902054811115610ab457600080fd5b600160a060020a0382166000908152600160209081526040808320338452909152902054610ae8908263ffffffff61089b16565b600160a060020a038316600090815260016020908152604080832033845290915290205561071a82826109b6565b600160a060020a0381161515610b2b57600080fd5b600354604051600160a060020a038084169216907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a36003805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a0392909216919091179055565b600160a060020a0382161515610ba957600080fd5b600254610bbc908263ffffffff6109a416565b600255600160a060020a038216600090815260208190526040902054610be8908263ffffffff6109a416565b600160a060020a0383166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a350505600a165627a7a72305820d4ad56dfe541fec48c3ecb02cebad565a998dfca7774c0c4f4b1f4a8e2363a590029".from_hex::>().unwrap(); - let gas_price = 2_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::try_from(0).expect("Internal error"), - data: Bytes(data), - chain_id: Some(chain.rec().num_chain_id), +#[test] +fn payments_were_adjusted_due_to_insufficient_balances() { + let payment_thresholds = PaymentThresholds { + threshold_interval_sec: 2_500_000, + debt_threshold_gwei: 100_000_000, + payment_grace_period_sec: 85_000, + maturity_threshold_sec: 85_000, + permanent_debt_allowed_gwei: 10_000_000, + unban_below_gwei: 1_000_000, }; - - let signed_tx = web3 - .accounts() - .sign_transaction( - tx, - &wallet - .prepare_secp256k1_secret() - .expect("wallet without secret"), + // Assuming all Nodes rely on the same set of payment thresholds + let owed_to_serv_node_1_minor = to_wei(payment_thresholds.debt_threshold_gwei + 5_000_000); + let owed_to_serv_node_2_minor = to_wei(payment_thresholds.debt_threshold_gwei + 20_000_000); + // Account of Node 3 will be a victim of tx fee insufficiency and will drop out, as its debt + // is the heaviest, implying the smallest weight evaluated and the smallest priority compared to + // the two others. + let owed_to_serv_node_3_minor = to_wei(payment_thresholds.debt_threshold_gwei + 60_000_000); + let enough_balance_for_serving_node_1_and_2 = + owed_to_serv_node_1_minor + owed_to_serv_node_2_minor; + let missing_portion_to_the_full_amount = to_wei(2_345_678); + let consuming_node_initial_service_fee_balance_minor = + enough_balance_for_serving_node_1_and_2 - missing_portion_to_the_full_amount; + let gas_price_major = 60; + let tx_fee_needed_to_pay_for_one_payment_major = { + // We'll need littler funds, but we can stand mild inaccuracy from assuming the use of + // 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_costs.as_slice(), + )) + .unwrap() + }; + let transaction_fee_margin = PurePercentage::try_from(15).unwrap(); + transaction_fee_margin.increase_by_percent_for(gas_limit_dev_chain * gas_price_major) + }; + let affordable_payments_count_by_tx_fee = 2; + let tx_fee_needed_to_pay_for_one_payment_minor: u128 = + to_wei(tx_fee_needed_to_pay_for_one_payment_major); + let consuming_node_transaction_fee_balance_minor = + affordable_payments_count_by_tx_fee * tx_fee_needed_to_pay_for_one_payment_minor; + let test_input = TestInputBuilder::default() + .ui_ports(UiPorts::new( + find_free_port(), + find_free_port(), + find_free_port(), + find_free_port(), + )) + // Should be enough only for two payments, the least significant one will fall out + .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, ) - .wait() - .expect("transaction preparation failed"); - - 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 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::(); + .debts_config(DebtsSpecs::new([ + // This account will be the most significant and will deserve the full balance + Debt::new( + owed_to_serv_node_1_minor, + 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. + 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(); - ( - Wallet::from(Bip32EncryptionKeyProvider::from_key(extended_priv_key)), - secret, - ) -} + 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 + 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 + 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, + ), + }; -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 + test_body( + test_input, + assertions_values, + stimulate_consuming_node_to_pay_for_test_with_insufficient_funds, + activating_serving_nodes_for_test_with_insufficient_funds, + ); } -fn build_config( - server_url_holder: &dyn UrlHolder, - seed: &Seed, - payment_thresholds: PaymentThresholds, - wallet_derivation_path: String, -) -> (NodeStartupConfig, Wallet) { - let (node_wallet, node_secret) = make_node_wallet(seed, wallet_derivation_path.as_str()); - let config = 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() - ))) - .build(); - (config, node_wallet) +fn stimulate_consuming_node_to_pay_for_test_with_insufficient_funds( + _cluster: &mut MASQNodeCluster, + real_consuming_node: &MASQRealNode, + wholesome_config: &WholesomeConfig, +) { + process_scan_request_to_node( + &real_consuming_node, + wholesome_config + .consuming_node + .node_profile + .ui_port() + .expect("UI port missing"), + ScanType::Payables, + 1111, + ) } -fn expire_payables(path: PathBuf) { - let conn = DbInitializerReal::default() - .initialize(&path, DbInitializationConfig::panic_on_migration()) - .unwrap(); - let mut statement = conn - .prepare("update payable set last_paid_timestamp = 0 where pending_payable_rowid is null") - .unwrap(); - statement.execute([]).unwrap(); - - let mut config_stmt = conn - .prepare("update config set value = '0' where name = 'start_block'") - .unwrap(); - config_stmt.execute([]).unwrap(); +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() + .enumerate() + .map(|(idx, serving_node_attributes)| { + let node_config = serving_node_attributes + .common + .startup_config_opt + .borrow_mut() + .take() + .unwrap(); + 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 + .node_profile + .ui_port() + .expect("ui port missing"); + + process_scan_request_to_node( + &serving_node, + ui_port, + ScanType::Receivables, + (idx * 111) as u64, + ); + + serving_node + }) + .collect(); + real_nodes.try_into().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(); +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..cdace9d32 --- /dev/null +++ b/multinode_integration_tests/tests/verify_bill_payment_utils/utils.rs @@ -0,0 +1,1031 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use bip39::{Language, Mnemonic, Seed}; +use ethereum_types::H256; +use futures::Future; +use itertools::Itertools; +use lazy_static::lazy_static; +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; +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::test_input_data_standard_dir; +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, TransactionReceipt, + TransactionRequest, +}; +use web3::Web3; + +pub type StimulateConsumingNodePayments = fn(&mut MASQNodeCluster, &MASQRealNode, &WholesomeConfig); + +pub type StartServingNodesAndLetThemActivateTheirAccountancy = + fn(&mut MASQNodeCluster, &WholesomeConfig) -> [MASQRealNode; 3]; + +pub fn test_body( + test_inputs: TestInput, + assertions_values: AssertionsValues, + stimulate_consuming_node_payments: StimulateConsumingNodePayments, + start_serving_nodes_and_let_them_activate_their_accountancy: StartServingNodesAndLetThemActivateTheirAccountancy, +) { + // It's important to prevent the blockchain server handle being dropped too early + let (mut cluster, global_values, _blockchain_server) = establish_test_frame(test_inputs); + let consuming_node = global_values.prepare_consuming_node(&mut cluster); + let serving_nodes_array = global_values.prepare_serving_nodes(&mut cluster); + global_values.set_up_consuming_node_db(&serving_nodes_array, &consuming_node); + global_values.set_up_serving_nodes_databases(&serving_nodes_array, &consuming_node); + let wholesome_config = WholesomeConfig::new(global_values, consuming_node, serving_nodes_array); + wholesome_config.assert_expected_wallet_addresses(); + let cn_common = &wholesome_config.consuming_node.common; + let real_consuming_node = cluster.start_named_real_node( + &cn_common.prepared_node.node_docker_name, + cn_common.prepared_node.index, + cn_common.startup_config_opt.borrow_mut().take().unwrap(), + ); + + stimulate_consuming_node_payments(&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_let_them_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) +} + +pub struct TestInput { + // The contract owner wallet is populated with 100 ETH as defined in the set of commands with + // which we start up the Ganache server. + // + // 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 TestInputBuilder { + ui_ports_opt: Option, + consuming_node_initial_tx_fee_balance_minor_opt: Option, + consuming_node_initial_service_fee_balance_minor_opt: Option, + debts_config_opt: Option, + payment_thresholds_all_nodes_opt: Option, + consuming_node_gas_price_major_opt: Option, +} + +impl TestInputBuilder { + pub fn ui_ports(mut self, ports: UiPorts) -> 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_major_opt = Some(gas_price); + self + } + + pub fn build(self) -> TestInput { + 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_major_opt, + initial_tx_fee_balance_minor_opt: self.consuming_node_initial_tx_fee_balance_minor_opt, + initial_service_fee_balance_minor: self + .consuming_node_initial_service_fee_balance_minor_opt + .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)), + }; + + TestInput { + 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 ui_ports_as_opt = ui_ports.serving_nodes.into_iter().map(Some).collect_vec(); + let serving_nodes_array: [Option; 3] = ui_ports_as_opt.try_into().unwrap(); + (Some(ui_ports.consuming_node), serving_nodes_array) + } + None => Default::default(), + } + } +} + +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_wallet: Wallet, + seed: Seed, +} + +impl BlockchainParams { + fn new( + chain: Chain, + server_url: String, + blockchain_interface: &ExtendedBlockchainInterface, + ) -> Self { + let seed = make_seed(); + let (contract_owner_wallet, _) = + make_node_wallet_and_private_key(&seed, &derivation_path(0, 0)); + let contract_owner_addr = + blockchain_interface.deploy_smart_contract(&contract_owner_wallet, chain); + + assert_eq!( + contract_owner_addr, + chain.rec().contract, + "Either the contract has been modified or Ganache is not accurately mimicking Ethereum. \ + Resulted contact addr {:?} doesn't much what's expected: {:?}", + contract_owner_addr, + chain.rec().contract + ); + + BlockchainParams { + chain, + server_url, + contract_owner_wallet, + seed, + } + } +} + +struct ExtendedBlockchainInterface { + node_standard_interface: Box, + raw_interface: RawBlockchainInterface, +} + +impl ExtendedBlockchainInterface { + fn new(chain: Chain, server_url: &str) -> Self { + let (event_loop_handle, http) = + Http::with_max_parallel(&server_url, REQUESTS_IN_PARALLEL).unwrap(); + let web3 = Web3::new(http.clone()); + let raw_interface = RawBlockchainInterface::new(web3); + let node_standard_interface = + Box::new(BlockchainInterfaceWeb3::new(http, event_loop_handle, chain)); + Self { + node_standard_interface, + raw_interface, + } + } + + fn deploy_smart_contract(&self, wallet: &Wallet, chain: Chain) -> Address { + let contract = load_contract_in_bytes(); + let tx = TransactionParameters { + nonce: Some(ethereum_types::U256::zero()), + to: None, + gas: *GAS_LIMIT, + gas_price: Some(*GAS_PRICE), + value: ethereum_types::U256::zero(), + data: Bytes(contract), + chain_id: Some(chain.rec().num_chain_id), + }; + let signed_tx = self.raw_interface.await_sign_transaction(tx, wallet); + match self.raw_interface.await_send_raw_transaction(signed_tx) { + Ok(tx_hash) => match self.raw_interface.await_transaction_receipt(tx_hash) { + Ok(Some(tx_receipt)) => tx_receipt.contract_address.unwrap(), + Ok(None) => panic!("Contract deployment failed Ok(None)"), + Err(e) => panic!("Contract deployment failed {:?}", e), + }, + Err(e) => panic!("Contract deployment failed {:?}", e), + } + } + + fn transfer_transaction_fee_amount_to_address( + &self, + from_wallet: &Wallet, + to_wallet: &Wallet, + amount_minor: u128, + transaction_nonce: u64, + ) { + let tx = TransactionRequest { + from: from_wallet.address(), + to: Some(to_wallet.address()), + gas: Some(*GAS_LIMIT), + gas_price: Some(*GAS_PRICE), + value: Some(ethereum_types::U256::from(amount_minor)), + data: None, + nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), + condition: None, + }; + match self + .raw_interface + .await_unlock_account(from_wallet.address(), "", None) + { + Ok(was_successful) => { + if was_successful { + eprintln!("Account {} unlocked for a single transaction", from_wallet) + } else { + panic!( + "Couldn't unlock account {} for the purpose of signing the next transaction", + from_wallet + ) + } + } + Err(e) => panic!( + "Attempt to unlock account {:?} failed at {:?}", + from_wallet.address(), + e + ), + } + match self.raw_interface.await_send_transaction(tx) { + Ok(tx_hash) => eprintln!( + "Transaction {:?} of {} wei of ETH was sent from wallet {:?} to {:?}", + tx_hash, amount_minor, from_wallet, to_wallet + ), + Err(e) => panic!("Transaction for token transfer failed {:?}", e), + } + } + + fn transfer_service_fee_amount_to_address( + &self, + contract_addr: Address, + from_wallet: &Wallet, + to_wallet: &Wallet, + amount_minor: u128, + transaction_nonce: u64, + chain: Chain, + ) { + let data = transaction_data_web3(to_wallet, amount_minor); + let tx = TransactionParameters { + nonce: Some(ethereum_types::U256::try_from(transaction_nonce).expect("Internal error")), + to: Some(contract_addr), + gas: *GAS_LIMIT, + gas_price: Some(*GAS_PRICE), + value: ethereum_types::U256::zero(), + data: Bytes(data.to_vec()), + chain_id: Some(chain.rec().num_chain_id), + }; + let signed_tx = self.raw_interface.await_sign_transaction(tx, from_wallet); + match &self.raw_interface.await_send_raw_transaction(signed_tx) { + Ok(tx_hash) => eprintln!( + "Transaction {:?} of {} wei of MASQ was sent from wallet {} to {}", + tx_hash, amount_minor, from_wallet, to_wallet + ), + Err(e) => panic!("Transaction for token transfer failed {:?}", e), + } + } + + fn single_balance_assertion( + &self, + wallet: &Wallet, + expected_balance: u128, + asserted_balance: AssertedBalance, + ) { + let balance_fetcher = match asserted_balance { + AssertedBalance::TransactionFee => LowBlockchainInt::get_transaction_fee_balance, + AssertedBalance::ServiceFee => LowBlockchainInt::get_service_fee_balance, + }; + let lower_blockchain_int = self.node_standard_interface.lower_interface(); + let actual_balance = balance_fetcher(lower_blockchain_int, &wallet) + .unwrap_or_else(|_| panic!("Failed to retrieve {:?} for {}", asserted_balance, wallet)); + assert_eq!( + actual_balance, + web3::types::U256::from(expected_balance), + "Actual {:?} {} doesn't much with expected {} for {}", + asserted_balance, + actual_balance, + expected_balance, + wallet + ); + } + + fn assert_balances( + &self, + wallet: &Wallet, + expected_tx_fee_balance: u128, + expected_service_fee_balance: u128, + ) { + self.single_balance_assertion( + wallet, + expected_tx_fee_balance, + AssertedBalance::TransactionFee, + ); + + self.single_balance_assertion( + wallet, + expected_service_fee_balance, + AssertedBalance::ServiceFee, + ); + } +} + +#[derive(Debug, Clone, Copy)] +enum AssertedBalance { + TransactionFee, + ServiceFee, +} + +lazy_static! { + static ref GAS_PRICE: ethereum_types::U256 = + 50_u64.try_into().expect("Gas price, internal error"); + static ref GAS_LIMIT: ethereum_types::U256 = + 1_000_000_u64.try_into().expect("Gas limit, internal error"); +} + +struct RawBlockchainInterface { + web3_transport: Web3, +} + +impl RawBlockchainInterface { + fn new(web3_transport: Web3) -> Self { + Self { web3_transport } + } + fn await_unlock_account( + &self, + address: Address, + password: &str, + duration: Option, + ) -> Result { + self.web3_transport + .personal() + .unlock_account(address, password, duration) + .wait() + } + fn await_send_transaction(&self, tx: TransactionRequest) -> Result { + self.web3_transport.eth().send_transaction(tx).wait() + } + + fn await_sign_transaction( + &self, + tx: TransactionParameters, + signing_wallet: &Wallet, + ) -> SignedTransaction { + let secret = &signing_wallet + .prepare_secp256k1_secret() + .expect("wallet without secret"); + self.web3_transport + .accounts() + .sign_transaction(tx, secret) + .wait() + .expect("transaction preparation failed") + } + + fn await_send_raw_transaction( + &self, + tx: SignedTransaction, + ) -> Result { + self.web3_transport + .eth() + .send_raw_transaction(tx.raw_transaction) + .wait() + } + + fn await_transaction_receipt( + &self, + tx_hash: H256, + ) -> Result, web3::error::Error> { + self.web3_transport + .eth() + .transaction_receipt(tx_hash) + .wait() + } +} + +pub struct GlobalValues { + pub test_inputs: TestInput, + pub blockchain_params: BlockchainParams, + pub now_in_common: SystemTime, + blockchain_interface: ExtendedBlockchainInterface, +} + +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(debts: [Debt; 3]) -> Self { + 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: TestInput, +) -> (MASQNodeCluster, GlobalValues, BlockchainServer) { + let now = SystemTime::now(); + let cluster = match MASQNodeCluster::start() { + Ok(cluster) => cluster, + Err(e) => panic!("{}", e), + }; + let (blockchain_server, server_url) = start_blockchain_server(); + let chain = cluster.chain(); + let blockchain_interface = ExtendedBlockchainInterface::new(chain, &server_url); + let blockchain_params = BlockchainParams::new(chain, server_url, &blockchain_interface); + let global_values = GlobalValues { + test_inputs, + blockchain_params, + blockchain_interface, + now_in_common: now, + }; + (cluster, global_values, blockchain_server) +} + +fn start_blockchain_server() -> (BlockchainServer, String) { + let blockchain_server = BlockchainServer::new("ganache-cli"); + blockchain_server.start(); + blockchain_server.wait_until_ready(); + let url = blockchain_server.url().to_string(); + (blockchain_server, url) +} + +const MNEMONIC_PHRASE: &str = + "timber cage wide hawk phone shaft pattern movie army dizzy hen tackle \ + lamp absent write kind term toddler sphere ripple idle dragon curious hold"; + +fn make_seed() -> Seed { + let mnemonic = Mnemonic::from_phrase(MNEMONIC_PHRASE, Language::English).unwrap(); + Seed::new(&mnemonic, "") +} + +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 = test_input_data_standard_dir().join("verify_bill_payments_smart_contract"); + let mut file = File::open(file_path).expect("couldn't acquire a file handle"); + let mut data = String::new(); + file.read_to_string(&mut data).unwrap(); + let data = data + .chars() + .filter(|char| !char.is_whitespace()) + .collect::(); + data.from_hex::>() + .expect("contract contains non-hexadecimal characters") +} + +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) -> 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); + self.blockchain_interface + .transfer_transaction_fee_amount_to_address( + &self.blockchain_params.contract_owner_wallet, + &consuming_node_wallet, + initial_transaction_fee_balance, + 1, + ); + self.blockchain_interface + .transfer_service_fee_amount_to_address( + self.blockchain_params.contract_owner_wallet.address(), + &self.blockchain_params.contract_owner_wallet, + &consuming_node_wallet, + initial_service_fee_balance_minor, + 2, + self.blockchain_params.chain, + ); + + self.blockchain_interface.assert_balances( + &consuming_node_wallet, + 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(); + self.blockchain_interface + .assert_balances(&serving_node.earning_wallet, 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.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_interfaces = &self.global_values.blockchain_interface; + blockchain_interfaces.assert_balances( + &self.consuming_node.consuming_wallet, + 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)| { + blockchain_interfaces.assert_balances( + &serving_node.earning_wallet, + 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.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 UiPorts { + consuming_node: u16, + serving_nodes: [u16; 3], +} + +impl UiPorts { + 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 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.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( + node_profile: ServingNodeProfile, + prepared_node: PreparedNodeInfo, + config: NodeStartupConfig, + earning_wallet: Wallet, + receivable_dao: ReceivableDaoReal, + ) -> Self { + let common = NodeAttributesCommon::new(prepared_node, config); + Self { + node_profile, + common, + earning_wallet, + receivable_dao, + } + } +} diff --git a/node/Cargo.lock b/node/Cargo.lock index 04806d093..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", ] @@ -1834,6 +1834,7 @@ dependencies = [ "lazy_static", "log 0.4.18", "nix 0.23.1", + "num", "regex", "serde", "serde_derive", @@ -2539,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", ] @@ -2629,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", ] @@ -2653,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]] @@ -3269,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", ] @@ -3314,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", ] @@ -3532,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", ] @@ -3543,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", ] @@ -3636,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", ] @@ -4334,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", ] @@ -4414,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", ] @@ -4438,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", ] @@ -4448,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", @@ -4729,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/Cargo.toml b/node/Cargo.toml index 4ae89971d..3b39e5af1 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,7 +15,6 @@ automap = { path = "../automap"} backtrace = "0.3.57" base64 = "0.13.0" bytes = "0.4.12" -time = {version = "0.3.11", features = [ "macros" ]} clap = "2.33.3" crossbeam-channel = "0.5.1" dirs = "4.0.0" @@ -44,6 +43,7 @@ rlp = "0.4.6" rpassword = "5.0.1" rusqlite = {version = "0.28.0", features = ["bundled","functions"]} rustc-hex = "2.1.0" +secp256k1secrets = {package = "secp256k1", version = "0.17.2"} serde = "1.0.136" serde_derive = "1.0.136" serde_json = "1.0.79" @@ -51,6 +51,7 @@ serde_cbor = "0.11.2" sha1 = "0.6.0" sodiumoxide = "0.2.2" sysinfo = "0.21.1" +time = {version = "0.3.11", features = [ "macros" ]} tiny-bip39 = "0.8.2" tiny-hderive = "0.3.0" thousands = "0.2.0" @@ -60,11 +61,10 @@ toml = "0.5.8" trust-dns = "0.17.0" trust-dns-resolver = "0.12.0" unindent = "0.1.7" +uuid = "0.7.4" variant_count = "1.1.0" web3 = {version = "0.11.0", default-features = false, features = ["http", "tls"]} websocket = {version = "0.26.2", default-features = false, features = ["async", "sync"]} -secp256k1secrets = {package = "secp256k1", version = "0.17.2"} -uuid = "0.7.4" [target.'cfg(target_os = "macos")'.dependencies] system-configuration = "0.4.0" diff --git a/node/src/accountant/db_access_objects/receivable_dao.rs b/node/src/accountant/db_access_objects/receivable_dao.rs index 9b71a3939..0a7ccf6e5 100644 --- a/node/src/accountant/db_access_objects/receivable_dao.rs +++ b/node/src/accountant/db_access_objects/receivable_dao.rs @@ -201,10 +201,10 @@ impl ReceivableDao for ReceivableDaoReal { from receivable r left outer join banned b on r.wallet_address = b.wallet_address where - r.last_received_timestamp < :sugg_and_grace - and ((r.balance_high_b > slope_drop_high_bytes(:debt_threshold, :slope, :sugg_and_grace - r.last_received_timestamp)) - or ((r.balance_high_b = slope_drop_high_bytes(:debt_threshold, :slope, :sugg_and_grace - r.last_received_timestamp)) - and (r.balance_low_b > slope_drop_low_bytes(:debt_threshold, :slope, :sugg_and_grace - r.last_received_timestamp)))) + r.last_received_timestamp < :maturity_and_grace + and ((r.balance_high_b > slope_drop_high_bytes(:debt_threshold, :slope, :maturity_and_grace - r.last_received_timestamp)) + or ((r.balance_high_b = slope_drop_high_bytes(:debt_threshold, :slope, :maturity_and_grace - r.last_received_timestamp)) + and (r.balance_low_b > slope_drop_low_bytes(:debt_threshold, :slope, :maturity_and_grace - r.last_received_timestamp)))) and ((r.balance_high_b > :permanent_debt_allowed_high_b) or ((r.balance_high_b = 0) and (r.balance_low_b > :permanent_debt_allowed_low_b))) and b.wallet_address is null " @@ -216,7 +216,7 @@ impl ReceivableDao for ReceivableDaoReal { named_params! { ":debt_threshold": checked_conversion::(payment_thresholds.debt_threshold_gwei), ":slope": slope, - ":sugg_and_grace": payment_thresholds.sugg_and_grace(to_time_t(now)), + ":maturity_and_grace": payment_thresholds.maturity_and_grace(to_time_t(now)), ":permanent_debt_allowed_high_b": permanent_debt_allowed_high_b, ":permanent_debt_allowed_low_b": permanent_debt_allowed_low_b }, @@ -329,11 +329,11 @@ impl ReceivableDaoReal { // The plus signs are intended. 'Subtraction' provided by the '.wei_change()' causes x of u128 // to become -x of i128 which produces a negative i64 integer in the column for the high bytes let main_sql = "update receivable set balance_high_b = balance_high_b + :balance_high_b, \ - balance_low_b = balance_low_b + :balance_low_b, last_received_timestamp = :last_received \ + balance_low_b = balance_low_b + :balance_low_b, last_received_timestamp = :last_received_timestamp \ where wallet_address = :wallet"; let update_clause_with_compensated_overflow = "update receivable set balance_high_b = :balance_high_b, \ - balance_low_b = :balance_low_b, last_received_timestamp = :last_received \ + balance_low_b = :balance_low_b, last_received_timestamp = :last_received_timestamp \ where wallet_address = :wallet"; match received_payments.iter().try_for_each(|received_payment| { @@ -346,7 +346,10 @@ impl ReceivableDaoReal { WeiChangeDirection::Subtraction, )) .other_params(vec![ParamByUse::BeforeAndAfterOverflow( - DisplayableRusqliteParamPair::new(":last_received", &last_received_timestamp), + DisplayableRusqliteParamPair::new( + ":last_received_timestamp", + &last_received_timestamp, + ), )]) .build(); @@ -1074,12 +1077,12 @@ mod tests { assert_eq!(&prepare_params[0..3], &[ "update receivable set balance_high_b = balance_high_b + :balance_high_b, balance_low_b \ - = balance_low_b + :balance_low_b, last_received_timestamp = :last_received where wallet_address \ - = :wallet", + = balance_low_b + :balance_low_b, last_received_timestamp = :last_received_timestamp \ + where wallet_address = :wallet", "select balance_high_b, balance_low_b from receivable where wallet_address = \ '0x0000000000000000000000000000000000616263'", "update receivable set balance_high_b = :balance_high_b, balance_low_b = :balance_low_b, \ - last_received_timestamp = :last_received where wallet_address = :wallet" + last_received_timestamp = :last_received_timestamp where wallet_address = :wallet" ]); // The first transaction did not affect the db, was rolled back let account_status = receivable_dao.account_status(&first_wallet); @@ -1207,17 +1210,17 @@ mod tests { not_delinquent_inside_grace_period.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei + 1); not_delinquent_inside_grace_period.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) + 2); + from_time_t(payment_thresholds.maturity_and_grace(now) + 2); let mut not_delinquent_after_grace_below_slope = make_receivable_account(2345, false); not_delinquent_after_grace_below_slope.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 2); not_delinquent_after_grace_below_slope.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_time_t(payment_thresholds.maturity_and_grace(now) - 1); let mut delinquent_above_slope_after_grace = make_receivable_account(3456, true); delinquent_above_slope_after_grace.balance_wei = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); delinquent_above_slope_after_grace.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 2); + from_time_t(payment_thresholds.maturity_and_grace(now) - 2); let mut not_delinquent_below_slope_before_stop = make_receivable_account(4567, false); not_delinquent_below_slope_before_stop.balance_wei = gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 1); @@ -1264,11 +1267,11 @@ mod tests { let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(105); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_time_t(payment_thresholds.maturity_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(105); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_time_t(payment_thresholds.maturity_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_shallow_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1296,11 +1299,11 @@ mod tests { let mut not_delinquent = make_receivable_account(1234, false); not_delinquent.balance_wei = gwei_to_wei(600); not_delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 25); + from_time_t(payment_thresholds.maturity_and_grace(now) - 25); let mut delinquent = make_receivable_account(2345, true); delinquent.balance_wei = gwei_to_wei(600); delinquent.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 75); + from_time_t(payment_thresholds.maturity_and_grace(now) - 75); let home_dir = ensure_node_home_directory_exists("accountant", "new_delinquencies_steep_slope"); let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); @@ -1328,11 +1331,11 @@ mod tests { let mut existing_delinquency = make_receivable_account(1234, true); existing_delinquency.balance_wei = gwei_to_wei(250); existing_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_time_t(payment_thresholds.maturity_and_grace(now) - 1); let mut new_delinquency = make_receivable_account(2345, true); new_delinquency.balance_wei = gwei_to_wei(250); new_delinquency.last_received_timestamp = - from_time_t(payment_thresholds.sugg_and_grace(now) - 1); + from_time_t(payment_thresholds.maturity_and_grace(now) - 1); let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_does_not_find_existing_delinquencies", @@ -1374,7 +1377,7 @@ mod tests { #[test] fn new_delinquencies_handles_too_young_debts_causing_slope_parameter_to_be_negative() { - //situation where sugg_and_grace makes more time than the age of the debt + // Situation where maturity_and_grace makes longer period of time than the debt age let home_dir = ensure_node_home_directory_exists( "receivable_dao", "new_delinquencies_handles_too_young_debts_causing_slope_parameter_to_be_negative", @@ -1388,16 +1391,16 @@ mod tests { unban_below_gwei: 0, }; let now = to_time_t(SystemTime::now()); - let sugg_and_grace = payment_thresholds.sugg_and_grace(now); + let maturity_and_grace = payment_thresholds.maturity_and_grace(now); let too_young_new_delinquency = ReceivableAccount { wallet: make_wallet("abc123"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace + 1), + last_received_timestamp: from_time_t(maturity_and_grace + 1), }; let ok_new_delinquency = ReceivableAccount { wallet: make_wallet("aaa999"), balance_wei: 123_456_789_101_112, - last_received_timestamp: from_time_t(sugg_and_grace - 1), + last_received_timestamp: from_time_t(maturity_and_grace - 1), }; let conn = make_connection_with_our_defined_sqlite_functions(&home_dir); add_receivable_account(&conn, &too_young_new_delinquency); diff --git a/node/src/accountant/db_access_objects/utils.rs b/node/src/accountant/db_access_objects/utils.rs index 8b78bb5f4..03ff00504 100644 --- a/node/src/accountant/db_access_objects/utils.rs +++ b/node/src/accountant/db_access_objects/utils.rs @@ -383,13 +383,14 @@ impl ThresholdUtils { .debt_threshold_gwei, so that the numerator will be no greater than -10^9 (-gwei_to_wei(1)), and the denominator must be less than or equal to 10^9. - These restrictions do not seem over-strict, since having .permanent_debt_allowed greater - than or equal to .debt_threshold_gwei would result in chaos, and setting - .threshold_interval_sec over 10^9 would mean continuing to declare debts delinquent after - more than 31 years. - - If payment_thresholds are ever configurable by the user, these validations should be done - on the values before they are accepted. + These restrictions do not seem overly strict for having .permanent_debt_allowed greater + 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. + + 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 bd444ef8f..a3a27257e 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -3,7 +3,7 @@ pub mod db_access_objects; pub mod db_big_integer; pub mod financials; -pub mod payment_adjuster; +mod payment_adjuster; pub mod scanners; #[cfg(test)] @@ -13,7 +13,9 @@ use core::fmt::Debug; use masq_lib::constants::{SCAN_ERROR, WEIS_IN_GWEI}; use std::cell::{Ref, RefCell}; -use crate::accountant::db_access_objects::payable_dao::{PayableDao, PayableDaoError}; +use crate::accountant::db_access_objects::payable_dao::{ + PayableAccount, PayableDao, PayableDaoError, +}; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayableDao; use crate::accountant::db_access_objects::receivable_dao::{ReceivableDao, ReceivableDaoError}; use crate::accountant::db_access_objects::utils::{ @@ -57,14 +59,15 @@ use itertools::Either; use itertools::Itertools; use masq_lib::crash_point::CrashPoint; use masq_lib::logger::Logger; -use masq_lib::messages::UiFinancialsResponse; -use masq_lib::messages::{FromMessageBody, ToMessageBody, UiFinancialsRequest}; +use masq_lib::messages::{ + FromMessageBody, ToMessageBody, UiFinancialsRequest, UiFinancialsResponse, UiScanResponse, +}; use masq_lib::messages::{ QueryResults, ScanType, UiFinancialStatistics, UiPayableAccount, UiReceivableAccount, UiScanRequest, }; use masq_lib::ui_gateway::MessageTarget::ClientId; -use masq_lib::ui_gateway::{MessageBody, MessagePath}; +use masq_lib::ui_gateway::{MessageBody, MessagePath, MessageTarget}; use masq_lib::ui_gateway::{NodeFromUiMessage, NodeToUiMessage}; use masq_lib::utils::ExpectValue; use std::any::type_name; @@ -127,6 +130,55 @@ pub struct ReceivedPayments { pub response_skeleton_opt: Option, } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct QualifiedPayableAccount { + pub bare_account: PayableAccount, + pub payment_threshold_intercept_minor: u128, + pub creditor_thresholds: CreditorThresholds, +} + +impl QualifiedPayableAccount { + pub fn new( + bare_account: PayableAccount, + payment_threshold_intercept_minor: u128, + creditor_thresholds: CreditorThresholds, + ) -> QualifiedPayableAccount { + Self { + bare_account, + payment_threshold_intercept_minor, + creditor_thresholds, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AnalyzedPayableAccount { + pub qualified_as: QualifiedPayableAccount, + pub disqualification_limit_minor: u128, +} + +impl AnalyzedPayableAccount { + pub fn new(qualified_as: QualifiedPayableAccount, disqualification_limit_minor: u128) -> Self { + AnalyzedPayableAccount { + qualified_as, + disqualification_limit_minor, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct CreditorThresholds { + pub permanent_debt_allowed_minor: u128, +} + +impl CreditorThresholds { + pub fn new(permanent_debt_allowed_minor: u128) -> Self { + Self { + permanent_debt_allowed_minor, + } + } +} + #[derive(Debug, Message, PartialEq)] pub struct SentPayables { pub payment_procedure_result: Result, PayableTransactionError>, @@ -213,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) } } @@ -341,7 +393,7 @@ pub trait SkeletonOptHolder { #[derive(Debug, PartialEq, Eq, Message, Clone)] pub struct RequestTransactionReceipts { - pub pending_payable: Vec, + pub pending_payables: Vec, pub response_skeleton_opt: Option, } @@ -623,17 +675,17 @@ impl Accountant { self.logger, "MsgId {}: Accruing debt to {} for consuming {} exited bytes", msg_id, - msg.exit.earning_wallet, - msg.exit.payload_size + msg.exit_service.earning_wallet, + msg.exit_service.payload_size ); self.record_service_consumed( - msg.exit.service_rate, - msg.exit.byte_rate, + msg.exit_service.service_rate, + msg.exit_service.byte_rate, msg.timestamp, - msg.exit.payload_size, - &msg.exit.earning_wallet, + msg.exit_service.payload_size, + &msg.exit_service.earning_wallet, ); - msg.routing.iter().for_each(|routing_service| { + msg.routing_services.iter().for_each(|routing_service| { debug!( self.logger, "MsgId {}: Accruing debt to {} for consuming {} routed bytes", @@ -651,27 +703,60 @@ impl Accountant { }) } - fn handle_payable_payment_setup(&mut self, msg: BlockchainAgentWithContextMessage) { - let blockchain_bridge_instructions = match self + fn send_outbound_payments_instructions(&mut self, msg: BlockchainAgentWithContextMessage) { + let response_skeleton_opt = msg.response_skeleton_opt; + 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) { - Ok(Either::Left(finalized_msg)) => finalized_msg, - Ok(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) } - Err(_e) => todo!("be completed by GH-711"), - }; - self.outbound_payments_instructions_sub_opt - .as_ref() - .expect("BlockchainBridge is unbound") - .try_send(blockchain_bridge_instructions) - .expect("BlockchainBridge is dead") - //TODO implement send point for ScanError; be completed by GH-711 + } + } + + fn handle_obstruction(&mut self, response_skeleton_opt: Option) { + self.scanners.payable.cancel_scan(&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") + } } fn handle_financials(&self, msg: &UiFinancialsRequest, client_id: u64, context_id: u64) { @@ -993,19 +1078,25 @@ 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::Adjustment; + use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; + use crate::accountant::payment_adjuster::{ + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, + TransactionFeeImmoderateInsufficiency, + }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGaugeReal; + use crate::accountant::scanners::test_utils::protect_qualified_payables_in_test; use crate::accountant::scanners::BeginScanError; use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, - BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, NullScanner, - PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, - PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, - ReceivableDaoMock, ScannerMock, + 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, }; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; @@ -1034,7 +1125,7 @@ mod tests { }; use crate::test_utils::{make_paying_wallet, make_wallet}; use actix::{Arbiter, System}; - use ethereum_types::U64; + use ethereum_types::{U256, U64}; use ethsign_crypto::Keccak256; use itertools::Itertools; use log::Level; @@ -1051,7 +1142,9 @@ mod tests { use masq_lib::test_utils::logging::TestLogHandler; use masq_lib::test_utils::utils::ensure_node_home_directory_exists; use masq_lib::ui_gateway::MessagePath::Conversation; - use masq_lib::ui_gateway::{MessageBody, MessagePath, NodeFromUiMessage, NodeToUiMessage}; + use masq_lib::ui_gateway::{ + MessageBody, MessagePath, MessageTarget, NodeFromUiMessage, NodeToUiMessage, + }; use std::any::TypeId; use std::ops::{Add, Sub}; use std::sync::Arc; @@ -1086,23 +1179,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 @@ -1300,16 +1393,16 @@ mod tests { #[test] fn scan_payables_request() { let config = bc_from_earning_wallet(make_wallet("some_wallet_address")); - let payable_account = PayableAccount { + let now = SystemTime::now(); + let payable = PayableAccount { wallet: make_wallet("wallet"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), - last_paid_timestamp: SystemTime::now().sub(Duration::from_secs( + last_paid_timestamp: now.sub(Duration::from_secs( (DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 1) as u64, )), pending_payable_opt: None, }; - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(vec![payable_account.clone()]); + let payable_dao = PayableDaoMock::new().non_pending_payables_result(vec![payable.clone()]); let subject = AccountantBuilder::default() .bootstrapper_config(config) .payable_daos(vec![ForPayableScanner(payable_dao)]) @@ -1334,10 +1427,14 @@ mod tests { System::current().stop(); system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + let expected_qualified_payables = + make_qualified_payables(vec![payable], &DEFAULT_PAYMENT_THRESHOLDS, now); assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable_account]), + protected_qualified_payables: protect_qualified_payables_in_test( + expected_qualified_payables + ), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1393,21 +1490,32 @@ 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 to 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 is_adjustment_required_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)) .start() .recipient(); let mut subject = AccountantBuilder::default().build(); + let account_1 = make_payable_account(44_444); + let account_2 = make_payable_account(333_333); + let qualified_payables = vec![ + { + 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() - .is_adjustment_required_params(&is_adjustment_required_params_arc) - .is_adjustment_required_result(Ok(None)); + .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(); @@ -1415,14 +1523,13 @@ mod tests { subject.outbound_payments_instructions_sub_opt = Some(instructions_recipient); subject.logger = Logger::new(test_name); let subject_addr = subject.start(); - let account_1 = make_payable_account(44_444); - let account_2 = make_payable_account(333_333); let system = System::new("test"); - let agent_id_stamp = ArbitraryIdStamp::new(); - let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); - let accounts = vec![account_1, account_2]; + 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_payables_in_test(accounts.clone()), + protected_qualified_payables, agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1433,31 +1540,19 @@ mod tests { subject_addr.try_send(msg).unwrap(); system.run(); - let mut is_adjustment_required_params = is_adjustment_required_params_arc.lock().unwrap(); - let (blockchain_agent_with_context_msg_actual, logger_clone) = - is_adjustment_required_params.remove(0); - assert_eq!( - blockchain_agent_with_context_msg_actual.protected_qualified_payables, - protect_payables_in_test(accounts.clone()) - ); - assert_eq!( - blockchain_agent_with_context_msg_actual.response_skeleton_opt, - Some(ResponseSkeleton { - client_id: 1234, - context_id: 4321, - }) - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .arbitrary_id_stamp(), - agent_id_stamp - ); - assert!(is_adjustment_required_params.is_empty()); + let mut consider_adjustment_params = consider_adjustment_params_arc.lock().unwrap(); + let (actual_qualified_payables, actual_agent_id_stamp) = + consider_adjustment_params.remove(0); + assert_eq!(actual_qualified_payables, qualified_payables); + assert_eq!(actual_agent_id_stamp, expected_agent_id_stamp); + 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); - assert_eq!(payments_instructions.affordable_accounts, accounts); + assert_eq!( + payments_instructions.affordable_accounts, + vec![account_1, account_2] + ); assert_eq!( payments_instructions.response_skeleton_opt, Some(ResponseSkeleton { @@ -1467,28 +1562,15 @@ mod tests { ); assert_eq!( payments_instructions.agent.arbitrary_id_stamp(), - agent_id_stamp + expected_agent_id_stamp ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) - // adjust_payments() did not need a prepared result which means it wasn't reached - // because otherwise this test would've panicked - } - - fn test_use_of_the_same_logger(logger_clone: &Logger, test_name: &str) { - let experiment_msg = format!("DEBUG: {test_name}: hello world"); - let log_handler = TestLogHandler::default(); - log_handler.exists_no_log_containing(&experiment_msg); - debug!(logger_clone, "hello world"); - log_handler.exists_log_containing(&experiment_msg); } #[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 to 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![])); @@ -1498,29 +1580,30 @@ mod tests { .start() .recipient(); let mut subject = AccountantBuilder::default().build(); - let unadjusted_account_1 = make_payable_account(111_111); - let unadjusted_account_2 = make_payable_account(222_222); - let adjusted_account_1 = PayableAccount { - balance_wei: gwei_to_wei(55_550_u64), - ..unadjusted_account_1.clone() - }; - let adjusted_account_2 = PayableAccount { - balance_wei: gwei_to_wei(100_000_u64), - ..unadjusted_account_2.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, }; + let unadjusted_qualified_accounts = vec![unadjusted_account_1, unadjusted_account_2]; let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = protect_payables_in_test(vec![ - unadjusted_account_1.clone(), - unadjusted_account_2.clone(), - ]); - let msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: initial_unadjusted_accounts.clone(), + let protected_qualified_payables = + protect_qualified_payables_in_test(unadjusted_qualified_accounts.clone()); + let payable_payments_setup_msg = BlockchainAgentWithContextMessage { + protected_qualified_payables, agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; @@ -1535,10 +1618,14 @@ mod tests { agent: Box::new(agent), response_skeleton_opt: Some(response_skeleton), }; + let analyzed_accounts = + convert_qualified_p_into_analyzed_p(unadjusted_qualified_accounts.clone()); + let adjustment_analysis = + AdjustmentAnalysisReport::new(Adjustment::ByServiceFee, analyzed_accounts.clone()); let payment_adjuster = PaymentAdjusterMock::default() - .is_adjustment_required_result(Ok(Some(Adjustment::MasqToken))) + .consider_adjustment_result(Ok(Either::Right(adjustment_analysis))) .adjust_payments_params(&adjust_payments_params_arc) - .adjust_payments_result(payments_instructions); + .adjust_payments_result(Ok(payments_instructions)); let payable_scanner = PayableScannerBuilder::new() .payment_adjuster(payment_adjuster) .build(); @@ -1548,53 +1635,208 @@ mod tests { let subject_addr = subject.start(); let system = System::new("test"); - subject_addr.try_send(msg).unwrap(); + 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, logger_clone) = - adjust_payments_params.remove(0); - assert_eq!(actual_prepared_adjustment.adjustment, Adjustment::MasqToken); + let actual_prepared_adjustment = adjust_payments_params.remove(0); assert_eq!( - actual_prepared_adjustment - .original_setup_msg - .protected_qualified_payables, - initial_unadjusted_accounts + actual_prepared_adjustment.adjustment_analysis.adjustment, + Adjustment::ByServiceFee ); assert_eq!( - actual_prepared_adjustment - .original_setup_msg - .agent - .arbitrary_id_stamp(), - agent_id_stamp_first_phase + actual_prepared_adjustment.adjustment_analysis.accounts, + analyzed_accounts ); - assert!( - before <= captured_now && captured_now <= after, - "captured timestamp should have been between {:?} and {:?} but was {:?}", - before, - after, - captured_now + assert_eq!( + actual_prepared_adjustment.agent.arbitrary_id_stamp(), + agent_id_stamp_first_phase ); assert!(adjust_payments_params.is_empty()); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); - let payments_instructions = + let actual_payments_instructions = blockchain_bridge_recording.get_record::(0); assert_eq!( - payments_instructions.affordable_accounts, + actual_payments_instructions.affordable_accounts, affordable_accounts ); assert_eq!( - payments_instructions.response_skeleton_opt, + actual_payments_instructions.response_skeleton_opt, Some(response_skeleton) ); assert_eq!( - payments_instructions.agent.arbitrary_id_stamp(), + actual_payments_instructions.agent.arbitrary_id_stamp(), agent_id_stamp_second_phase ); assert_eq!(blockchain_bridge_recording.len(), 1); - test_use_of_the_same_logger(&logger_clone, test_name) + } + + fn test_payment_adjuster_error_during_different_stages( + test_name: &str, + payment_adjuster: PaymentAdjusterMock, + ) { + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let blockchain_bridge_recipient = blockchain_bridge.start().recipient(); + let (ui_gateway, _, ui_gateway_recording_arc) = make_recorder(); + let ui_gateway_recipient = ui_gateway + .system_stop_conditions(match_every_type_id!(NodeToUiMessage)) + .start() + .recipient(); + let mut subject = AccountantBuilder::default().build(); + let response_skeleton = ResponseSkeleton { + client_id: 12, + context_id: 55, + }; + let payable_scanner = PayableScannerBuilder::new() + .payment_adjuster(payment_adjuster) + .build(); + subject.outbound_payments_instructions_sub_opt = Some(blockchain_bridge_recipient); + subject.ui_message_sub_opt = Some(ui_gateway_recipient); + subject.logger = Logger::new(test_name); + subject.scanners.payable = Box::new(payable_scanner); + subject.scanners.payable.mark_as_started(SystemTime::now()); + let subject_addr = subject.start(); + let account = make_payable_account(111_111); + let qualified_payable = + QualifiedPayableAccount::new(account, 123456, CreditorThresholds::new(111111)); + let system = System::new(test_name); + let agent = BlockchainAgentMock::default(); + let protected_qualified_payables = + protect_qualified_payables_in_test(vec![qualified_payable]); + let msg = BlockchainAgentWithContextMessage::new( + protected_qualified_payables, + Box::new(agent), + Some(response_skeleton), + ); + let assertion_message = AssertionsMessage { + assertions: Box::new(|accountant: &mut Accountant| { + assert_eq!(accountant.scanners.payable.scan_started_at(), None) // meaning the scan was called off + }), + }; + + subject_addr.try_send(msg).unwrap(); + + subject_addr.try_send(assertion_message).unwrap(); + assert_eq!(system.run(), 0); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!(blockchain_bridge_recording.len(), 0); + let ui_gateway_recording = ui_gateway_recording_arc.lock().unwrap(); + let response_to_user: &NodeToUiMessage = ui_gateway_recording.get_record(0); + assert_eq!( + response_to_user, + &NodeToUiMessage { + target: MessageTarget::ClientId(12), + body: UiScanResponse {}.tmb(55) + } + ) + } + + #[test] + 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_during_stage_one_the_insolvency_check"; + let payment_adjuster = PaymentAdjusterMock::default().consider_adjustment_result(Err( + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 1, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: gwei_to_wei(60_u64 * 55_000), + cw_transaction_fee_balance_minor: gwei_to_wei(123_u64), + }), + service_fee_opt: None, + }, + }, + )); + + 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}: Add more funds into your consuming wallet to become able to repay \ + already matured debts as the creditors would respond by a delinquency ban otherwise. \ + Details: Current transaction fee balance is not enough to pay a single payment. Number \ + of canceled payments: 1. Transaction fee per payment: 3,300,000,000,000,000 wei, while \ + the wallet contains: 123,000,000,000 wei." + )); + log_handler + .exists_log_containing(&format!("INFO: {test_name}: The Payables scan ended in")); + log_handler.exists_log_containing(&format!( + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." + )); + } + + #[test] + 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_during_stage_two_adjustment_went_wrong"; + let payment_adjuster = PaymentAdjusterMock::default() + .consider_adjustment_result(Ok(Either::Right(AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + vec![make_meaningless_analyzed_account(123)], + )))) + .adjust_payments_result(Err(PaymentAdjusterError::RecursionEliminatedAllAccounts)); + + test_payment_adjuster_error_during_different_stages(test_name, payment_adjuster); + + let log_handler = TestLogHandler::new(); + log_handler.exists_log_containing(&format!( + "WARN: {test_name}: Payment adjustment has not produced any executable payments. Add \ + more funds into your consuming wallet to become able to repay already matured debts as \ + the creditors would respond by a delinquency ban otherwise. Details: The \ + payments adjusting process failed to find any combination of payables that can be paid \ + immediately with the finances provided" + )); + log_handler + .exists_log_containing(&format!("INFO: {test_name}: The Payables scan ended in")); + log_handler.exists_log_containing(&format!( + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." + )); + } + + #[test] + fn payment_adjuster_error_is_not_reported_to_ui_if_scan_not_manually_requested() { + init_test_logging(); + 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().consider_adjustment_result(Err( + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 20, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 40_000_000_000, + cw_transaction_fee_balance_minor: U256::from(123), + }), + service_fee_opt: None, + }, + }, + )); + let payable_scanner = PayableScannerBuilder::new() + .payment_adjuster(payment_adjuster) + .build(); + subject.logger = Logger::new(test_name); + subject.scanners.payable = Box::new(payable_scanner); + 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( + protected_payables, + Box::new(blockchain_agent), + None, + ); + + subject.send_outbound_payments_instructions(msg); + + // No NodeUiMessage was sent because there is no `response_skeleton`. It is evident by + // the fact that the test didn't blow up even though UIGateway is unbound + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Payable scanner is unable to generate payment instructions. \ + It looks like only the user can resolve this issue." + )); } #[test] @@ -1643,7 +1885,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &RequestTransactionReceipts { - pending_payable: vec![fingerprint], + pending_payables: vec![fingerprint], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, context_id: 4321, @@ -1791,12 +2033,12 @@ mod tests { } #[test] - fn accountant_sends_initial_payable_payments_msg_when_qualified_payable_found() { + fn accountant_sends_qualified_payables_msg_when_qualified_payable_found() { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); let (qualified_payables, _, all_non_pending_payables) = - make_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( @@ -1825,7 +2067,9 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + protected_qualified_payables: protect_qualified_payables_in_test( + qualified_payables + ), response_skeleton_opt: None, } ); @@ -2086,7 +2330,7 @@ mod tests { .begin_scan_params(&begin_scan_params_arc) .begin_scan_result(Err(BeginScanError::NothingToProcess)) .begin_scan_result(Ok(RequestTransactionReceipts { - pending_payable: vec![], + pending_payables: vec![], response_skeleton_opt: None, })) .stop_the_system_after_last_msg(); @@ -2158,9 +2402,9 @@ mod tests { .begin_scan_params(&begin_scan_params_arc) .begin_scan_result(Err(BeginScanError::NothingToProcess)) .begin_scan_result(Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![make_payable_account( - 123, - )]), + protected_qualified_payables: protect_qualified_payables_in_test(vec![ + make_meaningless_qualified_payable(123), + ]), response_skeleton_opt: None, })) .stop_the_system_after_last_msg(); @@ -2335,37 +2579,41 @@ mod tests { payable_scan_interval: Duration::from_secs(50_000), receivable_scan_interval: Duration::from_secs(50_000), }); - let now = to_time_t(SystemTime::now()); - let qualified_payables = vec![ - // slightly above minimum balance, to the right of the curve (time intersection) + let now = SystemTime::now(); + let now_t = to_time_t(now); + let payables = vec![ + // Slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( DEFAULT_PAYMENT_THRESHOLDS.permanent_debt_allowed_gwei + 1, ), last_paid_timestamp: from_time_t( - now - checked_conversion::( - DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec - + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec - + 10, - ), + now_t + - checked_conversion::( + DEFAULT_PAYMENT_THRESHOLDS.threshold_interval_sec + + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + + 10, + ), ), pending_payable_opt: None, }, - // slightly above the curve (balance intersection), to the right of minimum time + // Slightly above the curve (balance intersection), to the right of the minimum time PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), last_paid_timestamp: from_time_t( - now - checked_conversion::( - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, - ), + now_t + - checked_conversion::( + DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec + 10, + ), ), pending_payable_opt: None, }, ]; - let payable_dao = - PayableDaoMock::default().non_pending_payables_result(qualified_payables.clone()); + let qualified_payables = + 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 .system_stop_conditions(match_every_type_id!(QualifiedPayablesMessage)); @@ -2386,13 +2634,15 @@ mod tests { send_start_message!(accountant_subs); - system.run(); + assert_eq!(system.run(), 0); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( message, &QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(qualified_payables), + protected_qualified_payables: protect_qualified_payables_in_test( + qualified_payables + ), response_skeleton_opt: None, } ); @@ -2535,7 +2785,7 @@ mod tests { assert_eq!( received_msg, &RequestTransactionReceipts { - pending_payable: vec![payable_fingerprint_1, payable_fingerprint_2], + pending_payables: vec![payable_fingerprint_1, payable_fingerprint_2], response_skeleton_opt: None, } ); @@ -2847,14 +3097,14 @@ mod tests { subject_addr .try_send(ReportServicesConsumedMessage { timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: earning_wallet_exit.clone(), payload_size: 1200, service_rate: 120, byte_rate: 30, }, routing_payload_size: 3456, - routing: vec![ + routing_services: vec![ RoutingServiceConsumed { earning_wallet: earning_wallet_routing_1.clone(), service_rate: 42, @@ -2944,14 +3194,14 @@ mod tests { let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: foreign_wallet.clone(), payload_size: 1234, service_rate: 45, byte_rate: 10, }, routing_payload_size: 3333, - routing: vec![RoutingServiceConsumed { + routing_services: vec![RoutingServiceConsumed { earning_wallet: consuming_wallet.clone(), service_rate: 42, byte_rate: 6, @@ -2986,14 +3236,14 @@ mod tests { let timestamp = SystemTime::now(); let report_message = ReportServicesConsumedMessage { timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: foreign_wallet.clone(), payload_size: 1234, service_rate: 45, byte_rate: 10, }, routing_payload_size: 3333, - routing: vec![RoutingServiceConsumed { + routing_services: vec![RoutingServiceConsumed { earning_wallet: earning_wallet.clone(), service_rate: 42, byte_rate: 6, @@ -3026,14 +3276,14 @@ mod tests { let config = bc_from_wallets(consuming_wallet.clone(), make_wallet("own earning wallet")); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: consuming_wallet.clone(), payload_size: 1234, service_rate: 42, byte_rate: 24, }, routing_payload_size: 3333, - routing: vec![], + routing_services: vec![], }; let more_money_payable_params_arc = @@ -3056,14 +3306,14 @@ mod tests { let config = bc_from_earning_wallet(earning_wallet.clone()); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: earning_wallet.clone(), payload_size: 1234, service_rate: 42, byte_rate: 24, }, routing_payload_size: 3333, - routing: vec![], + routing_services: vec![], }; let more_money_payable_params_arc = @@ -3174,15 +3424,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 @@ -3261,7 +3511,12 @@ mod tests { last_paid_timestamp: past_payable_timestamp_2, pending_payable_opt: None, }; - let pending_payable_scan_interval = 200; // should be slightly less than 1/5 of the time until shutting the system + let qualified_payables = make_qualified_payables( + vec![account_1.clone(), account_2.clone()], + &DEFAULT_PAYMENT_THRESHOLDS, + now, + ); + let pending_payable_scan_interval = 200; // Should be slightly less than 1/5 of the time until shutting the system let payable_dao_for_payable_scanner = PayableDaoMock::new() .non_pending_payables_params(&non_pending_payables_params_arc) .non_pending_payables_result(vec![account_1, account_2]) @@ -3348,10 +3603,10 @@ mod tests { .increment_scan_attempts_result(Ok(())) .increment_scan_attempts_result(Ok(())) .mark_failures_params(&mark_failure_params_arc) - // we don't have a better solution yet, so we mark this down + // We don't have a better solution yet, so we just mark the error down in the database .mark_failures_result(Ok(())) .delete_fingerprints_params(&delete_record_params_arc) - // this is used during confirmation of the successful one + // This is used during confirmation of the successful transaction .delete_fingerprints_result(Ok(())); pending_payable_dao_for_pending_payable_scanner .have_return_all_errorless_fingerprints_shut_down_the_system = true; @@ -3360,16 +3615,23 @@ mod tests { .start(move |_| { let mut subject = AccountantBuilder::default() .bootstrapper_config(bootstrapper_config) - .payable_daos(vec![ - ForPayableScanner(payable_dao_for_payable_scanner), - ForPendingPayableScanner(payable_dao_for_pending_payable_scanner), - ]) - .pending_payable_daos(vec![ - ForPayableScanner(pending_payable_dao_for_payable_scanner), - ForPendingPayableScanner(pending_payable_dao_for_pending_payable_scanner), - ]) + .payable_daos(vec![ForPendingPayableScanner( + payable_dao_for_pending_payable_scanner, + )]) + .pending_payable_daos(vec![ForPendingPayableScanner( + pending_payable_dao_for_pending_payable_scanner, + )]) .build(); subject.scanners.receivable = Box::new(NullScanner::new()); + let payment_adjuster = PaymentAdjusterMock::default() + .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_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) + .payment_adjuster(payment_adjuster) + .build(); + subject.scanners.payable = Box::new(payable_scanner); let notify_later_half_mock = NotifyLaterHandleMock::default() .notify_later_params(¬ify_later_scan_for_pending_payable_arc_cloned) .capture_msg_and_let_it_fly_on(); @@ -3408,7 +3670,7 @@ mod tests { assert_eq!(second_payable.1, rowid_for_account_2); let return_all_errorless_fingerprints_params = return_all_errorless_fingerprints_params_arc.lock().unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary + // It varies with machines, and sometimes we manage more cycles than necessary assert!(return_all_errorless_fingerprints_params.len() >= 5); let non_pending_payables_params = non_pending_payables_params_arc.lock().unwrap(); assert_eq!(*non_pending_payables_params, vec![()]); // because we disabled further scanning for payables @@ -3458,7 +3720,7 @@ mod tests { notify_later_scan_for_pending_payable_params_arc .lock() .unwrap(); - // it varies with machines and sometimes we manage more cycles than necessary + // It varies with machines, and sometimes we manage more cycles than necessary let vector_of_first_five_cycles = notify_later_check_for_confirmation .drain(0..=4) .collect_vec(); diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs deleted file mode 100644 index 88ee13e74..000000000 --- a/node/src/accountant/payment_adjuster.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; -use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; -use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; -use masq_lib::logger::Logger; -use std::time::SystemTime; - -pub trait PaymentAdjuster { - fn search_for_indispensable_adjustment( - &self, - msg: &BlockchainAgentWithContextMessage, - logger: &Logger, - ) -> Result, AnalysisError>; - - fn adjust_payments( - &self, - setup: PreparedAdjustment, - now: SystemTime, - logger: &Logger, - ) -> OutboundPaymentsInstructions; - - as_any_ref_in_trait!(); -} - -pub struct PaymentAdjusterReal {} - -impl PaymentAdjuster for PaymentAdjusterReal { - fn search_for_indispensable_adjustment( - &self, - _msg: &BlockchainAgentWithContextMessage, - _logger: &Logger, - ) -> Result, AnalysisError> { - Ok(None) - } - - fn adjust_payments( - &self, - _setup: PreparedAdjustment, - _now: SystemTime, - _logger: &Logger, - ) -> OutboundPaymentsInstructions { - todo!("this function is dead until the card GH-711 is played") - } - - as_any_ref_in_trait_impl!(); -} - -impl PaymentAdjusterReal { - pub fn new() -> Self { - Self {} - } -} - -impl Default for PaymentAdjusterReal { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Adjustment { - MasqToken, - TransactionFeeCurrency { limiting_count: u16 }, - Both, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum AnalysisError {} - -#[cfg(test)] -mod tests { - use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::BlockchainAgentWithContextMessage; - use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::test_utils::make_payable_account; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - - #[test] - fn search_for_indispensable_adjustment_always_returns_none() { - init_test_logging(); - let test_name = "is_adjustment_required_always_returns_none"; - let mut payable = make_payable_account(111); - payable.balance_wei = 100_000_000; - let agent = BlockchainAgentMock::default(); - let setup_msg = BlockchainAgentWithContextMessage { - protected_qualified_payables: protect_payables_in_test(vec![payable]), - agent: Box::new(agent), - response_skeleton_opt: None, - }; - let logger = Logger::new(test_name); - let subject = PaymentAdjusterReal::new(); - - let result = subject.search_for_indispensable_adjustment(&setup_msg, &logger); - - assert_eq!(result, Ok(None)); - TestLogHandler::default().exists_no_log_containing(test_name); - // Nobody in this test asked about the wallet balances and the transaction fee - // requirement, yet we got through with the final None. - // How do we know? The mock agent didn't blow up while missing these - // results - } -} diff --git a/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs b/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs new file mode 100644 index 000000000..45226e199 --- /dev/null +++ b/node/src/accountant/payment_adjuster/criterion_calculators/balance_calculator.rs @@ -0,0 +1,80 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; +use crate::accountant::payment_adjuster::inner::PaymentAdjusterInner; +use crate::accountant::QualifiedPayableAccount; + +#[derive(Default)] +pub struct BalanceCriterionCalculator {} + +impl CriterionCalculator for BalanceCriterionCalculator { + 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; + let diff = largest - this_account; + + // We invert the magnitude of smaller debts, so they weight the most + largest + diff + } + + fn parameter_name(&self) -> &'static str { + "BALANCE" + } +} + +#[cfg(test)] +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::PaymentAdjusterInner; + use crate::accountant::payment_adjuster::miscellaneous::helper_functions::find_largest_exceeding_balance; + 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() { + let subject = BalanceCriterionCalculator::default(); + + let result = subject.parameter_name(); + + assert_eq!(result, "BALANCE") + } + + #[test] + fn balance_criterion_calculator_works() { + let analyzed_accounts = [50, 100, 2_222] + .into_iter() + .enumerate() + .map(|(idx, n)| { + let mut basic_analyzed_payable = make_meaningless_analyzed_account(idx as u64); + basic_analyzed_payable.qualified_as.bare_account.balance_wei = + multiply_by_billion(n); + basic_analyzed_payable + .qualified_as + .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 = PaymentAdjusterInner::default(); + payment_adjuster_inner.initialize_guts(None, 123456789, largest_exceeding_balance); + let subject = BalanceCriterionCalculator::default(); + + let calculated_values = analyzed_accounts + .iter() + .map(|analyzed_account| { + subject.calculate(&analyzed_account.qualified_as, &payment_adjuster_inner) + }) + .collect::>(); + + let expected_values = vec![4_384_000_000_000, 4_336_000_000_000, 2_216_000_000_000]; + calculated_values + .into_iter() + .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 new file mode 100644 index 000000000..b28e7c1a5 --- /dev/null +++ b/node/src/accountant/payment_adjuster/criterion_calculators/mod.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod balance_calculator; + +use crate::accountant::payment_adjuster::inner::PaymentAdjusterInner; +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: &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 new file mode 100644 index 000000000..1992b9667 --- /dev/null +++ b/node/src/accountant/payment_adjuster/disqualification_arbiter.rs @@ -0,0 +1,551 @@ +// 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, +}; +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 masq_lib::logger::Logger; + +pub struct DisqualificationArbiter { + disqualification_gauge: Box, +} + +impl Default for DisqualificationArbiter { + fn default() -> Self { + Self::new(Box::new(DisqualificationGaugeReal::default())) + } +} + +impl DisqualificationArbiter { + pub fn new(disqualification_gauge: Box) -> Self { + Self { + disqualification_gauge, + } + } + + pub fn calculate_disqualification_edge( + &self, + qualified_payable: &QualifiedPayableAccount, + ) -> u128 { + let balance = qualified_payable.bare_account.balance_wei; + let intercept = qualified_payable.payment_threshold_intercept_minor; + let permanent_debt_allowed = qualified_payable + .creditor_thresholds + .permanent_debt_allowed_minor; + + self.disqualification_gauge + .determine_limit(balance, intercept, permanent_debt_allowed) + } + + pub fn find_an_account_to_disqualify_in_this_iteration( + &self, + unconfirmed_adjustments: &[UnconfirmedAdjustment], + logger: &Logger, + ) -> 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; + + try_finding_an_account_to_disqualify_diagnostics( + &disqualification_suspected_accounts, + wallet, + ); + + debug!( + logger, + "Found accounts {:?} applying for disqualification", + disqualification_suspected_accounts, + ); + + info_log_for_disqualified_account(logger, account_to_disqualify); + + wallet + } + + fn list_accounts_nominated_for_disqualification( + unconfirmed_adjustments: &[UnconfirmedAdjustment], + ) -> Vec { + unconfirmed_adjustments + .iter() + .flat_map(|adjustment_info| { + let disqualification_limit = adjustment_info.disqualification_limit_minor(); + let proposed_adjusted_balance = adjustment_info.proposed_adjusted_balance_minor; + + if proposed_adjusted_balance < disqualification_limit { + account_nominated_for_disqualification_diagnostics( + adjustment_info, + proposed_adjusted_balance, + disqualification_limit, + ); + + let suspected_account: DisqualificationSuspectedAccount = + adjustment_info.into(); + + Some(suspected_account) + } else { + None + } + }) + .collect() + } + + 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 { + pub wallet: Address, + pub weight: u128, + // 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 +{ + fn from(unconfirmed_account: &'unconfirmed_accounts UnconfirmedAdjustment) -> Self { + DisqualificationSuspectedAccount { + wallet: unconfirmed_account.wallet(), + 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(), + } + } +} + +pub trait DisqualificationGauge { + fn determine_limit( + &self, + account_balance_wei: u128, + threshold_intercept_wei: u128, + permanent_debt_allowed_wei: u128, + ) -> u128; +} + +#[derive(Default)] +pub struct DisqualificationGaugeReal {} + +impl DisqualificationGauge for DisqualificationGaugeReal { + fn determine_limit( + &self, + account_balance_minor: u128, + 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; + } + Self::determine_adequate_minimal_payment( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ) + } +} + +impl DisqualificationGaugeReal { + 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_acceptable_payment = exceeding_threshold + permanent_debt_allowed_minor; + + let is_debt_growing_fast = minimal_acceptable_payment + >= Self::FIRST_QUALIFICATION_CONDITION_COEFFICIENT * considered_forgiven; + + let situated_on_the_left_half_of_the_slope = considered_forgiven + >= Self::SECOND_QUALIFICATION_CONDITION_COEFFICIENT * permanent_debt_allowed_minor; + + is_debt_growing_fast && situated_on_the_left_half_of_the_slope + } + + 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) + // + // Y axis - debt size + // + // | + // | 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' + // +---------------------------+---+---------------------+ X axis - time + // 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::payment_adjuster::disqualification_arbiter::{ + DisqualificationArbiter, DisqualificationGauge, DisqualificationGaugeReal, + DisqualificationSuspectedAccount, + }; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::UnconfirmedAdjustment; + use crate::accountant::payment_adjuster::test_utils::local_utils::{ + make_meaningless_unconfirmed_adjustment, make_meaningless_weighed_account, + }; + use itertools::Itertools; + use masq_lib::logger::Logger; + use masq_lib::utils::convert_collection; + + #[test] + fn constants_are_correct() { + 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_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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, true) + } + + #[test] + 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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, true) + } + + #[test] + 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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, true) + } + + #[test] + 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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, true) + } + + #[test] + 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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, false) + } + + #[test] + 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_thicker_margin( + account_balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, false) + } + + #[test] + fn calculate_disqualification_edge_in_the_horizontal_thresholds_area() { + let balance_minor = 30_000_000_000; + let threshold_intercept_minor = 4_000_000_000; + let permanent_debt_allowed_minor = 4_000_000_000; + let subject = DisqualificationGaugeReal::default(); + + let result = subject.determine_limit( + balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, 30_000_000_000) + } + + #[test] + fn calculate_disqualification_edge_in_the_tilted_thresholds_area_with_normal_margin() { + let balance_minor = 6_000_000_000; + let threshold_intercept_minor = 4_000_000_000; + let permanent_debt_allowed_minor = 1_000_000_000; + let subject = DisqualificationGaugeReal::default(); + + let result = subject.determine_limit( + balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!(result, (6_000_000_000 - 4_000_000_000) + 1_000_000_000) + } + + #[test] + fn calculate_disqualification_edge_in_the_tilted_thresholds_area_with_double_margin() { + let balance_minor = 30_000_000_000; + let threshold_intercept_minor = 4_000_000_000; + let permanent_debt_allowed_minor = 1_000_000_000; + let subject = DisqualificationGaugeReal::default(); + + let result = subject.determine_limit( + balance_minor, + threshold_intercept_minor, + permanent_debt_allowed_minor, + ); + + assert_eq!( + result, + (30_000_000_000 - 4_000_000_000) + (2 * 1_000_000_000) + ) + } + + #[test] + fn list_accounts_nominated_for_disqualification_ignores_adjustment_even_to_the_dsq_limit() { + let mut account = make_meaningless_unconfirmed_adjustment(444); + account.proposed_adjusted_balance_minor = 1_000_000_000; + account + .weighed_account + .analyzed_account + .disqualification_limit_minor = 1_000_000_000; + let accounts = vec![account]; + + let result = + DisqualificationArbiter::list_accounts_nominated_for_disqualification(&accounts); + + assert!(result.is_empty()) + } + + #[test] + fn find_account_with_smallest_weight_works_for_unequal_weights() { + let adjustments = make_unconfirmed_adjustments(vec![1004, 1000, 1002, 1001]); + let dsq_suspected_accounts = make_dsq_suspected_accounts(&adjustments); + + let result = + DisqualificationArbiter::find_account_with_smallest_weight(&dsq_suspected_accounts); + + let expected_result = &dsq_suspected_accounts[1]; + assert_eq!(result, expected_result) + } + + #[test] + fn find_account_with_smallest_weight_for_equal_weights_chooses_the_first_of_the_same_size() { + let adjustments = make_unconfirmed_adjustments(vec![1111, 1113, 1111]); + let dsq_suspected_accounts = make_dsq_suspected_accounts(&adjustments); + + let result = + DisqualificationArbiter::find_account_with_smallest_weight(&dsq_suspected_accounts); + + let expected_result = &dsq_suspected_accounts[0]; + assert_eq!(result, expected_result) + } + + #[test] + fn only_account_with_the_smallest_weight_will_be_disqualified_in_single_iteration() { + 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 result = subject.find_an_account_to_disqualify_in_this_iteration( + &unconfirmed_adjustments, + &Logger::new("test"), + ); + + assert_eq!(result, wallet_3); + } + + fn make_unconfirmed_adjustments(weights: Vec) -> Vec { + weights + .into_iter() + .enumerate() + .map(|(idx, weight)| { + let mut account = make_meaningless_unconfirmed_adjustment(idx as u64); + account.weighed_account.weight = weight; + account + }) + .collect() + } + + fn make_dsq_suspected_accounts( + accounts: &[UnconfirmedAdjustment], + ) -> Vec { + 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 new file mode 100644 index 000000000..24daf4910 --- /dev/null +++ b/node/src/accountant/payment_adjuster/inner.rs @@ -0,0 +1,277 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use std::cell::RefCell; + +pub struct PaymentAdjusterInner { + initialized_guts_opt: RefCell>, +} + +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, + remaining_cw_service_fee_balance_minor: u128, +} + +impl GutsOfPaymentAdjusterInner { + pub fn new( + transaction_count_limit_opt: Option, + cw_service_fee_balance_minor: u128, + max_debt_above_threshold_in_qualified_payables_minor: u128, + ) -> Self { + Self { + transaction_count_limit_opt, + max_debt_above_threshold_in_qualified_payables_minor, + original_cw_service_fee_balance_minor: cw_service_fee_balance_minor, + remaining_cw_service_fee_balance_minor: cw_service_fee_balance_minor, + } + } +} + +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, + ); + + self.initialized_guts_opt + .borrow_mut() + .replace(initialized_guts); + } + + 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, + ) + } + + pub fn transaction_count_limit_opt(&self) -> Option { + self.get_value("transaction_count_limit_opt", |guts_ref| { + guts_ref.transaction_count_limit_opt + }) + } + 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 + }) + } + 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 + }) + } + pub fn subtract_from_remaining_cw_service_fee_balance_minor(&self, subtrahend: u128) { + let updated_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_cw_balance, + ) + } + + pub fn invalidate_guts(&self) { + self.initialized_guts_opt.replace(None); + } + + fn get_value(&self, method: &str, getter: F) -> T + where + F: FnOnce(&GutsOfPaymentAdjusterInner) -> T, + { + let guts_borrowed_opt = self.initialized_guts_opt.borrow(); + + let guts_ref = guts_borrowed_opt + .as_ref() + .unwrap_or_else(|| Self::uninitialized_panic(method)); + + getter(guts_ref) + } + + 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 uninitialized_panic(method: &str) -> ! { + panic!( + "PaymentAdjusterInner is uninitialized. It was identified during the execution of \ + '{method}()'" + ) + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::payment_adjuster::inner::{ + GutsOfPaymentAdjusterInner, PaymentAdjusterInner, + }; + use std::panic::{catch_unwind, AssertUnwindSafe}; + + #[test] + 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 max_debt_above_threshold_in_qualified_payables_minor = 44_555_666; + + subject.initialize_guts( + tx_count_limit_opt, + cw_service_fee_balance, + 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!( + 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!( + read_original_cw_service_fee_balance_minor, + cw_service_fee_balance + ); + assert_eq!( + read_remaining_cw_service_fee_balance_minor, + cw_service_fee_balance + ); + } + + #[test] + fn subtracting_remaining_cw_service_fee_balance_works() { + let initial_cw_service_fee_balance_minor = 123_123_678_678; + let subject = PaymentAdjusterInner::default(); + subject.initialize_guts(None, initial_cw_service_fee_balance_minor, 12345); + let amount_to_subtract = 555_666_777; + + subject.subtract_from_remaining_cw_service_fee_balance_minor(amount_to_subtract); + + 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] + 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, + })); + + subject.invalidate_guts(); + + let guts_removed = subject.initialized_guts_opt.borrow().is_none(); + assert_eq!(guts_removed, true) + } + + #[test] + 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); + }, + ) + } + + 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 new file mode 100644 index 000000000..f6dabec53 --- /dev/null +++ b/node/src/accountant/payment_adjuster/logging_and_diagnostics/diagnostics.rs @@ -0,0 +1,221 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use masq_lib::constants::WALLET_ADDRESS_LENGTH; +use std::fmt::Debug; + +const RUN_DIAGNOSTICS_FOR_DEVS: bool = false; + +pub const DIAGNOSTICS_MIDDLE_COLUMN_WIDTH: usize = 58; + +#[macro_export] +macro_rules! diagnostics { + // Displays only a description of an event + ($description: literal) => { + diagnostics( + None::String>, + $description, + None::String> + ) + }; + // Displays a brief description and values from a collection + ($description: literal, $debuggable_collection: expr) => { + collection_diagnostics($description, $debuggable_collection) + }; + // Displays a brief description and formatted literal with arguments + ($description: literal, $($formatted_values: tt)*) => { + diagnostics( + None::String>, + $description, + Some(|| format!($($formatted_values)*)) + ) + }; + // Displays an account by wallet address, brief description and formatted literal with arguments + ($wallet_address: expr, $description: expr, $($formatted_values: tt)*) => { + diagnostics( + Some(||format!("{:?}", $wallet_address)), + $description, + Some(|| format!($($formatted_values)*)) + ) + }; +} + +// Intended to be used through the overloaded macro diagnostics!() for better clearness +// and differentiation from the primary functionality +pub fn diagnostics( + subject_renderer_opt: Option, + description: &str, + value_renderer_opt: Option, +) where + F1: FnOnce() -> String, + F2: FnOnce() -> String, +{ + if RUN_DIAGNOSTICS_FOR_DEVS { + let subject_column_length = if subject_renderer_opt.is_some() { + WALLET_ADDRESS_LENGTH + 2 + } else { + 0 + }; + let subject = no_text_or_by_renderer(subject_renderer_opt); + let values = no_text_or_by_renderer(value_renderer_opt); + let description_length = DIAGNOSTICS_MIDDLE_COLUMN_WIDTH; + eprintln!( + "\n{:(renderer_opt: Option) -> String +where + F: FnOnce() -> String, +{ + if let Some(renderer) = renderer_opt { + renderer() + } else { + "".to_string() + } +} + +// Should be used via the macro diagnostics!() for better clearness and differentiation from +// the prime functionality +pub fn collection_diagnostics( + label: &str, + accounts: &[DebuggableAccount], +) { + if RUN_DIAGNOSTICS_FOR_DEVS { + eprintln!("{}", label); + accounts + .iter() + .for_each(|account| eprintln!("{:?}", account)); + } +} + +pub mod ordinary_diagnostic_functions { + use crate::accountant::payment_adjuster::criterion_calculators::CriterionCalculator; + use crate::accountant::payment_adjuster::diagnostics; + use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationSuspectedAccount; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeighedPayable, + }; + use thousands::Separable; + use web3::types::Address; + + pub fn diagnostics_for_accounts_above_disqualification_limit( + account_info: &UnconfirmedAdjustment, + disqualification_limit: u128, + ) { + diagnostics!( + &account_info.wallet(), + "THRIVING COMPETITOR FOUND", + "Disqualification limit: {}, proposed balance: {}", + disqualification_limit.separate_with_commas(), + account_info + .proposed_adjusted_balance_minor + .separate_with_commas() + ); + } + + pub fn account_nominated_for_disqualification_diagnostics( + account_info: &UnconfirmedAdjustment, + proposed_adjusted_balance: u128, + disqualification_edge: u128, + ) { + diagnostics!( + account_info.wallet(), + "ACCOUNT NOMINATED FOR DISQUALIFICATION FOR INSIGNIFICANCE AFTER ADJUSTMENT", + "Proposed: {}, disqualification limit: {}", + proposed_adjusted_balance.separate_with_commas(), + disqualification_edge.separate_with_commas() + ); + } + + pub fn minimal_acceptable_balance_assigned_diagnostics( + weighed_account: &WeighedPayable, + disqualification_limit: u128, + ) { + diagnostics!( + weighed_account.wallet(), + "MINIMAL ACCEPTABLE BALANCE ASSIGNED", + "Used disqualification limit for given account {}", + disqualification_limit.separate_with_commas() + ) + } + + pub fn exhausting_cw_balance_diagnostics( + non_finalized_account_info: &AdjustedAccountBeforeFinalization, + possible_extra_addition: u128, + ) { + diagnostics!( + "EXHAUSTING CW ON PAYMENT", + "Account {} from proposed {} to the possible maximum of {}", + non_finalized_account_info.original_account.wallet, + non_finalized_account_info.proposed_adjusted_balance_minor, + non_finalized_account_info.proposed_adjusted_balance_minor + possible_extra_addition + ); + } + + pub fn not_exhausting_cw_balance_diagnostics( + non_finalized_account_info: &AdjustedAccountBeforeFinalization, + ) { + diagnostics!( + "FULLY EXHAUSTED CW, PASSING ACCOUNT OVER", + "Account {} with original balance {} must be finalized with proposed {}", + non_finalized_account_info.original_account.wallet, + non_finalized_account_info.original_account.balance_wei, + non_finalized_account_info.proposed_adjusted_balance_minor + ); + } + + pub fn proposed_adjusted_balance_diagnostics( + account: &WeighedPayable, + proposed_adjusted_balance: u128, + ) { + diagnostics!( + account.wallet(), + "PROPOSED ADJUSTED BALANCE", + "{}", + proposed_adjusted_balance.separate_with_commas() + ); + } + + pub fn try_finding_an_account_to_disqualify_diagnostics( + disqualification_suspected_accounts: &[DisqualificationSuspectedAccount], + wallet: Address, + ) { + diagnostics!( + "PICKED DISQUALIFIED ACCOUNT", + "Picked {} from nominated accounts {:?}", + wallet, + disqualification_suspected_accounts + ); + } + + pub fn calculated_criterion_and_weight_diagnostics( + wallet: Address, + calculator: &dyn CriterionCalculator, + criterion: u128, + added_in_the_sum: u128, + ) { + const FIRST_COLUMN_WIDTH: usize = 30; + + diagnostics!( + wallet, + "PARTIAL CRITERION CALCULATED", + "For {:, + adjusted_accounts: &[PayableAccount], +) -> String { + let excluded_wallets_and_balances = + preprocess_excluded_accounts(&original_account_balances_mapped, adjusted_accounts); + let excluded_accounts_summary_opt = excluded_wallets_and_balances.is_empty().not().then(|| { + write_title_and_summary( + &excluded_accounts_title(), + &format_summary_for_excluded_accounts(&excluded_wallets_and_balances), + ) + }); + let included_accounts_summary = write_title_and_summary( + &included_accounts_title(), + &format_summary_for_included_accounts(&original_account_balances_mapped, adjusted_accounts), + ); + concatenate_summaries(included_accounts_summary, excluded_accounts_summary_opt) +} + +fn included_accounts_title() -> String { + format!( + "{:, + adjusted_accounts: &[PayableAccount], +) -> String { + adjusted_accounts + .iter() + .sorted_by(|account_a, account_b| { + // Sorting in descending order + Ord::cmp(&account_b.balance_wei, &account_a.balance_wei) + }) + .map(|account| format_single_included_account(account, original_account_balances_mapped)) + .join("\n") +} + +fn format_single_included_account( + processed_account: &PayableAccount, + original_account_balances_mapped: &HashMap, +) -> String { + let original_balance = original_account_balances_mapped + .get(&processed_account.wallet.address()) + .expect("The hashmap should contain every wallet"); + + format!( + "{} {}\n{:^length$} {}", + processed_account.wallet, + 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(); + original_account_balances_mapped + .iter() + .fold(vec![], |mut acc, (wallet, original_balance)| { + if !adjusted_accounts_wallets.contains(wallet) { + acc.push((*wallet, *original_balance)); + } + acc + }) +} + +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, + ] + .into_iter() + .flatten() + .join("\n") +} + +pub fn info_log_for_disqualified_account( + logger: &Logger, + account: &DisqualificationSuspectedAccount, +) { + info!( + logger, + "Ready payment to {:?} was eliminated to spare MASQ for those higher prioritized. {} wei \ + owed at the moment.", + account.wallet, + account.initial_account_balance_minor.separate_with_commas(), + ) +} + +pub fn log_adjustment_by_service_fee_is_required( + logger: &Logger, + payables_sum: u128, + cw_service_fee_balance: u128, +) { + warning!( + logger, + "Mature payables amount to {} MASQ wei while the consuming wallet holds only {} wei. \ + Adjustment in their count or balances is necessary.", + payables_sum.separate_with_commas(), + cw_service_fee_balance.separate_with_commas() + ); + info!(logger, "{}", REFILL_RECOMMENDATION) +} + +pub fn log_insufficient_transaction_fee_balance( + logger: &Logger, + requested_tx_count: u16, + feasible_tx_count: u16, + txn_fee_required_per_txn_minor: u128, + transaction_fee_minor: U256, +) { + warning!( + logger, + "Transaction fee balance of {} wei cannot cover the anticipated {} wei for {} \ + transactions. Maximal count is set to {}. Adjustment must be performed.", + transaction_fee_minor.separate_with_commas(), + (requested_tx_count as u128 * txn_fee_required_per_txn_minor).separate_with_commas(), + requested_tx_count, + feasible_tx_count + ); + info!(logger, "{}", REFILL_RECOMMENDATION) +} + +pub fn log_transaction_fee_adjustment_ok_but_by_service_fee_undoable(logger: &Logger) { + error!(logger, "{}", LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY) +} + +#[cfg(test)] +mod tests { + use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::{ + LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY, REFILL_RECOMMENDATION, + }; + + #[test] + fn constants_are_correct() { + assert_eq!( + REFILL_RECOMMENDATION, + "Please be aware that abandoning your debts is going to result in delinquency bans. \ + To consume services without limitations, you will need to place more funds into \ + your consuming wallet." + ); + assert_eq!( + LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY, + "Passed successfully adjustment by transaction fee, then rechecked the service fee \ + balance to be applied on the adjusted set, but discovered a shortage of \ + MASQ not to suffice even for a single transaction. Operation is aborting." + ) + } +} diff --git a/node/src/accountant/payment_adjuster/logging_and_diagnostics/mod.rs b/node/src/accountant/payment_adjuster/logging_and_diagnostics/mod.rs new file mode 100644 index 000000000..3f96fc9fe --- /dev/null +++ b/node/src/accountant/payment_adjuster/logging_and_diagnostics/mod.rs @@ -0,0 +1,3 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod diagnostics; +pub mod log_functions; diff --git a/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs b/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs new file mode 100644 index 000000000..dbd17bffb --- /dev/null +++ b/node/src/accountant/payment_adjuster/miscellaneous/account_stages_conversions.rs @@ -0,0 +1,159 @@ +// Copyright (c) 2024, 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::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::minimal_acceptable_balance_assigned_diagnostics; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + AdjustedAccountBeforeFinalization, UnconfirmedAdjustment, WeighedPayable, +}; +use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::DisqualificationLimitProvidingAccount; +use crate::accountant::QualifiedPayableAccount; + +// Accounts that pass through the checks in PA and dart to BlockchainBridge right away +impl From for PayableAccount { + fn from(qualified_payable: QualifiedPayableAccount) -> Self { + qualified_payable.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; + account.balance_wei = non_finalized_adjustment.proposed_adjusted_balance_minor; + account + } +} + +// 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.weighed_account + } +} + +// 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.weighed_account.weight; + let original_account = unconfirmed_adjustment + .weighed_account + .analyzed_account + .qualified_as + .bare_account; + + AdjustedAccountBeforeFinalization::new( + original_account, + weight, + proposed_adjusted_balance_minor, + ) + } +} + +// 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 adjusted_balance = weighed_account.disqualification_limit(); + minimal_acceptable_balance_assigned_diagnostics(&weighed_account, adjusted_balance); + let weight = weighed_account.weight; + let original_account = weighed_account.analyzed_account.qualified_as.bare_account; + AdjustedAccountBeforeFinalization::new(original_account, weight, adjusted_balance) + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + 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() { + let mut original_payable_account = make_payable_account(123); + original_payable_account.balance_wei = 200_000_000; + let non_finalized_account = AdjustedAccountBeforeFinalization::new( + original_payable_account.clone(), + 666777, + 123_456_789, + ); + + let result = PayableAccount::from(non_finalized_account); + + original_payable_account.balance_wei = 123_456_789; + assert_eq!(result, original_payable_account) + } + + 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_meaningless_qualified_payable(111), + garbage_disqualification_limit, + ); + analyzed_account.qualified_as.bare_account = payable_account; + WeighedPayable::new(analyzed_account, garbage_weight) + } + + #[test] + fn conversation_between_weighed_payable_and_standard_payable_account() { + let original_payable_account = make_payable_account(345); + let weighed_account = prepare_weighed_account(original_payable_account.clone()); + + let result = PayableAccount::from(weighed_account); + + assert_eq!(result, original_payable_account) + } + + #[test] + fn conversion_between_weighed_payable_and_non_finalized_account() { + let original_payable_account = make_payable_account(123); + let mut weighed_account = prepare_weighed_account(original_payable_account.clone()); + weighed_account + .analyzed_account + .disqualification_limit_minor = 200_000_000; + weighed_account.weight = 78910; + + let result = AdjustedAccountBeforeFinalization::from(weighed_account); + + let expected_result = + AdjustedAccountBeforeFinalization::new(original_payable_account, 78910, 200_000_000); + assert_eq!(result, expected_result) + } + + #[test] + 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 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, + 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 new file mode 100644 index 000000000..23156e7ae --- /dev/null +++ b/node/src/accountant/payment_adjuster/miscellaneous/data_structures.rs @@ -0,0 +1,162 @@ +// 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::AnalyzedPayableAccount; +use web3::types::{Address, U256}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WeighedPayable { + pub analyzed_account: AnalyzedPayableAccount, + pub weight: u128, +} + +impl WeighedPayable { + pub fn new(analyzed_account: AnalyzedPayableAccount, weight: u128) -> Self { + Self { + analyzed_account, + weight, + } + } + + pub fn wallet(&self) -> Address { + self.analyzed_account + .qualified_as + .bare_account + .wallet + .address() + } + + 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: Vec, + pub remaining_undecided_accounts: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AdjustedAccountBeforeFinalization { + pub original_account: PayableAccount, + pub weight: u128, + pub proposed_adjusted_balance_minor: u128, +} + +impl AdjustedAccountBeforeFinalization { + pub fn new( + original_account: PayableAccount, + weight: u128, + proposed_adjusted_balance_minor: u128, + ) -> Self { + Self { + original_account, + weight, + proposed_adjusted_balance_minor, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct UnconfirmedAdjustment { + pub weighed_account: WeighedPayable, + pub proposed_adjusted_balance_minor: u128, +} + +impl UnconfirmedAdjustment { + pub fn new(weighed_account: WeighedPayable, proposed_adjusted_balance_minor: u128) -> Self { + Self { + weighed_account, + proposed_adjusted_balance_minor, + } + } + + pub fn wallet(&self) -> Address { + self.weighed_account.wallet() + } + + pub fn initial_balance_minor(&self) -> u128 { + self.weighed_account.initial_balance_minor() + } + + pub fn disqualification_limit_minor(&self) -> u128 { + self.weighed_account + .analyzed_account + .disqualification_limit_minor + } +} + +pub struct AffordableAndRequiredTxCounts { + pub affordable: u16, + pub required: u16, +} + +impl AffordableAndRequiredTxCounts { + pub fn new(max_possible_tx_count: U256, number_of_accounts: usize) -> Self { + AffordableAndRequiredTxCounts { + affordable: u16::try_from(max_possible_tx_count).unwrap_or(u16::MAX), + required: u16::try_from(number_of_accounts).unwrap_or(u16::MAX), + } + } +} + +pub enum AccountsByFinalization { + Unexhausted(Vec), + Finalized(Vec), +} + +#[cfg(test)] +mod tests { + use crate::accountant::payment_adjuster::miscellaneous::data_structures::AffordableAndRequiredTxCounts; + use ethereum_types::U256; + + #[test] + fn there_is_u16_ceiling_for_possible_tx_count() { + let corrections_from_u16_max = [-3_i8, -1, 0, 1, 10]; + let prepared_input_numbers = corrections_from_u16_max + .into_iter() + .map(plus_minus_correction_for_u16_max) + .map(U256::from); + let result = prepared_input_numbers + .map(|max_possible_tx_count| { + let detected_tx_counts = + AffordableAndRequiredTxCounts::new(max_possible_tx_count, 123); + detected_tx_counts.affordable + }) + .collect::>(); + + assert_eq!( + result, + vec![u16::MAX - 3, u16::MAX - 1, u16::MAX, u16::MAX, u16::MAX] + ) + } + + #[test] + fn there_is_u16_ceiling_for_required_number_of_accounts() { + let corrections_from_u16_max = [-9_i8, -1, 0, 1, 5]; + let right_input_numbers = corrections_from_u16_max + .into_iter() + .map(plus_minus_correction_for_u16_max); + let result = right_input_numbers + .map(|required_tx_count_usize| { + let detected_tx_counts = + AffordableAndRequiredTxCounts::new(U256::from(123), required_tx_count_usize); + detected_tx_counts.required + }) + .collect::>(); + + assert_eq!( + result, + vec![u16::MAX - 9, u16::MAX - 1, u16::MAX, u16::MAX, u16::MAX] + ) + } + + fn plus_minus_correction_for_u16_max(correction: i8) -> usize { + if correction < 0 { + 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 new file mode 100644 index 000000000..0a276cf17 --- /dev/null +++ b/node/src/accountant/payment_adjuster/miscellaneous/helper_functions.rs @@ -0,0 +1,449 @@ +// 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::{AccountsByFinalization, AdjustedAccountBeforeFinalization, WeighedPayable}; +use crate::accountant::{AnalyzedPayableAccount}; +use itertools::{Itertools}; + +pub fn no_affordable_accounts_found(accounts: &AccountsByFinalization) -> bool { + match accounts { + AccountsByFinalization::Finalized(vector) => vector.is_empty(), + AccountsByFinalization::Unexhausted(vector) => vector.is_empty(), + } +} + +pub fn sum_as(collection: &[T], arranger: F) -> N +where + N: Sum, + F: Fn(&T) -> N, +{ + collection.iter().map(arranger).sum::() +} + +pub fn eliminate_accounts_by_tx_fee_limit( + weighed_accounts: Vec, + affordable_transaction_count: u16, +) -> Vec { + let sorted_accounts = sort_in_descending_order_by_weights(weighed_accounts); + + diagnostics!( + "ACCOUNTS CUTBACK FOR TRANSACTION FEE", + "Keeping {} out of {} accounts. Dumping these accounts: {:?}", + affordable_transaction_count, + sorted_accounts.len(), + sorted_accounts + .iter() + .skip(affordable_transaction_count as usize) + ); + + sorted_accounts + .into_iter() + .take(affordable_transaction_count as usize) + .collect() +} + +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)) + .collect() +} + +pub fn compute_mul_coefficient_preventing_fractional_numbers( + cw_service_fee_balance_minor: u128, +) -> u128 { + u128::MAX / cw_service_fee_balance_minor +} + +pub fn find_largest_exceeding_balance(qualified_accounts: &[AnalyzedPayableAccount]) -> u128 { + let diffs = qualified_accounts + .iter() + .map(|account| { + account + .qualified_as + .bare_account + .balance_wei + .checked_sub(account.qualified_as.payment_threshold_intercept_minor) + .expect("should be: balance > intercept!") + }) + .collect::>(); + *diffs.iter().max().expect("No account found") +} + +pub fn exhaust_cw_balance_entirely( + approved_accounts: Vec, + original_cw_service_fee_balance_minor: u128, +) -> Vec { + let adjusted_balances_total: u128 = sum_as(&approved_accounts, |account_info| { + account_info.proposed_adjusted_balance_minor + }); + + let cw_remaining = original_cw_service_fee_balance_minor + .checked_sub(adjusted_balances_total) + .unwrap_or_else(|| { + panic!( + "Remainder should've been a positive number but wasn't after {} - {}", + original_cw_service_fee_balance_minor, adjusted_balances_total + ) + }); + + let init = ConsumingWalletExhaustingStatus::new(cw_remaining); + approved_accounts + .into_iter() + .sorted_by(|info_a, info_b| Ord::cmp(&info_b.weight, &info_a.weight)) + .fold( + init, + run_cw_exhausting_on_possibly_sub_optimal_adjusted_balances, + ) + .accounts_finalized_so_far +} + +fn run_cw_exhausting_on_possibly_sub_optimal_adjusted_balances( + status: ConsumingWalletExhaustingStatus, + non_finalized_account: AdjustedAccountBeforeFinalization, +) -> ConsumingWalletExhaustingStatus { + if !status.is_cw_exhausted_to_0() { + let balance_gap_minor = non_finalized_account + .original_account + .balance_wei + .checked_sub(non_finalized_account.proposed_adjusted_balance_minor) + .unwrap_or_else(|| { + panic!( + "Proposed balance should never be bigger than the original one. Proposed: \ + {}, original: {}", + non_finalized_account.proposed_adjusted_balance_minor, + non_finalized_account.original_account.balance_wei + ) + }); + let possible_extra_addition = if balance_gap_minor < status.remaining_cw_balance { + balance_gap_minor + } else { + status.remaining_cw_balance + }; + + exhausting_cw_balance_diagnostics(&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); + + status.add(non_finalized_account) + } +} + +struct ConsumingWalletExhaustingStatus { + remaining_cw_balance: u128, + accounts_finalized_so_far: Vec, +} + +impl ConsumingWalletExhaustingStatus { + fn new(remaining_cw_balance: u128) -> Self { + Self { + remaining_cw_balance, + accounts_finalized_so_far: vec![], + } + } + + 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 + } + + 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 { + let finalized_account = PayableAccount::from(non_finalized_account_info); + self.accounts_finalized_so_far.push(finalized_account); + self + } +} +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + AccountsByFinalization, AdjustedAccountBeforeFinalization, + }; + use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ + compute_mul_coefficient_preventing_fractional_numbers, eliminate_accounts_by_tx_fee_limit, + exhaust_cw_balance_entirely, find_largest_exceeding_balance, no_affordable_accounts_found, + ConsumingWalletExhaustingStatus, + }; + 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::Itertools; + use std::time::SystemTime; + + #[test] + fn no_affordable_accounts_found_returns_true_for_non_finalized_accounts() { + let result = no_affordable_accounts_found(&AccountsByFinalization::Unexhausted(vec![])); + + assert_eq!(result, true) + } + + #[test] + fn no_affordable_accounts_found_returns_false_for_non_finalized_accounts() { + let result = no_affordable_accounts_found(&AccountsByFinalization::Unexhausted(vec![ + AdjustedAccountBeforeFinalization::new(make_payable_account(456), 5678, 1234), + ])); + + assert_eq!(result, false) + } + + #[test] + fn no_affordable_accounts_found_returns_true_for_finalized_accounts() { + let result = no_affordable_accounts_found(&AccountsByFinalization::Finalized(vec![])); + + assert_eq!(result, true) + } + + #[test] + fn no_affordable_accounts_found_returns_false_for_finalized_accounts() { + let result = no_affordable_accounts_found(&AccountsByFinalization::Finalized(vec![ + make_payable_account(123), + ])); + + assert_eq!(result, false) + } + + #[test] + fn find_largest_exceeding_balance_works() { + 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_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, 5_000_000_000 - 1_999_999_999) + } + + #[test] + 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_meaningless_weighed_account(456); + account_2.weight = 999_999_999; + let mut account_3 = make_meaningless_weighed_account(789); + account_3.weight = 999_999_999; + let mut account_4 = make_meaningless_weighed_account(1011); + account_4.weight = 1_000_000_001; + let affordable_transaction_count = 2; + + let result = eliminate_accounts_by_tx_fee_limit( + vec![account_1.clone(), account_2, account_3, account_4.clone()], + affordable_transaction_count, + ); + + let expected_result = vec![account_4, account_1]; + assert_eq!(result, expected_result) + } + + #[test] + fn compute_mul_coefficient_preventing_fractional_numbers_works() { + let cw_service_fee_balance_minor = 12345678; + + let result = + compute_mul_coefficient_preventing_fractional_numbers(cw_service_fee_balance_minor); + + 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( + wallet: &Wallet, + original_balance: u128, + weight: u128, + proposed_adjusted_balance: u128, + ) -> AdjustedAccountBeforeFinalization { + let garbage_last_paid_timestamp = SystemTime::now(); + let payable_account = PayableAccount { + wallet: wallet.clone(), + balance_wei: original_balance, + last_paid_timestamp: garbage_last_paid_timestamp, + pending_payable_opt: None, + }; + AdjustedAccountBeforeFinalization::new(payable_account, weight, proposed_adjusted_balance) + } + + fn assert_payable_accounts_after_adjustment_finalization( + actual_accounts: Vec, + expected_account_parts: Vec<(Wallet, u128)>, + ) { + let actual_accounts_simplified_and_sorted = actual_accounts + .into_iter() + .map(|account| (account.wallet.address(), account.balance_wei)) + .sorted() + .collect::>(); + let expected_account_parts_sorted = expected_account_parts + .into_iter() + .map(|(expected_wallet, expected_balance)| { + (expected_wallet.address(), expected_balance) + }) + .sorted() + .collect::>(); + assert_eq!( + actual_accounts_simplified_and_sorted, + expected_account_parts_sorted + ) + } + + #[test] + fn exhaustive_status_is_constructed_properly() { + let cw_remaining_balance = 45678; + + let result = ConsumingWalletExhaustingStatus::new(cw_remaining_balance); + + assert_eq!(result.remaining_cw_balance, cw_remaining_balance); + assert_eq!(result.accounts_finalized_so_far, vec![]) + } + + #[test] + 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; + let weight_1 = 2_000_000_000; + let wallet_2 = make_wallet("def"); + let original_requested_balance_2 = 33_500_000_000; + let proposed_adjusted_balance_2 = 33_487_999_999; + let weight_2 = 6_000_000_000; + let wallet_3 = make_wallet("ghi"); + let original_requested_balance_3 = 41_000_000; + let proposed_adjusted_balance_3 = 40_980_000; + let weight_3 = 20_000_000_000; + let original_cw_balance = original_requested_balance_1 + + original_requested_balance_2 + + original_requested_balance_3 + + 5000; + let non_finalized_adjusted_accounts = vec![ + make_non_finalized_adjusted_account( + &wallet_1, + original_requested_balance_1, + weight_1, + proposed_adjusted_balance_1, + ), + make_non_finalized_adjusted_account( + &wallet_2, + original_requested_balance_2, + weight_2, + proposed_adjusted_balance_2, + ), + make_non_finalized_adjusted_account( + &wallet_3, + original_requested_balance_3, + weight_3, + proposed_adjusted_balance_3, + ), + ]; + + let result = + exhaust_cw_balance_entirely(non_finalized_adjusted_accounts, original_cw_balance); + + let expected_resulted_balances = vec![ + (wallet_1, original_requested_balance_1), + (wallet_2, original_requested_balance_2), + (wallet_3, original_requested_balance_3), + ]; + assert_payable_accounts_after_adjustment_finalization(result, expected_resulted_balances) + } + + #[test] + fn three_non_exhaustive_accounts_with_one_completely_refilled_one_partly_one_not_at_all() { + // The smallest proposed adjusted balance gets refilled first, and then gradually on... + let wallet_1 = make_wallet("abc"); + let original_requested_balance_1 = 41_000_000; + let proposed_adjusted_balance_1 = 39_700_000; + let weight_1 = 38_000_000_000; + let wallet_2 = make_wallet("def"); + let original_requested_balance_2 = 33_500_000_000; + let proposed_adjusted_balance_2 = 32_487_999_999; + let weight_2 = 25_000_000_000; + let wallet_3 = make_wallet("ghi"); + let original_requested_balance_3 = 50_000_000_000; + let proposed_adjusted_balance_3 = 43_000_000_000; + let weight_3 = 38_000_000; + let original_cw_balance = original_requested_balance_1 + + proposed_adjusted_balance_2 + + proposed_adjusted_balance_3 + + 222_000_000; + let non_finalized_adjusted_accounts = vec![ + make_non_finalized_adjusted_account( + &wallet_1, + original_requested_balance_1, + weight_1, + proposed_adjusted_balance_1, + ), + make_non_finalized_adjusted_account( + &wallet_2, + original_requested_balance_2, + weight_2, + proposed_adjusted_balance_2, + ), + make_non_finalized_adjusted_account( + &wallet_3, + original_requested_balance_3, + weight_3, + proposed_adjusted_balance_3, + ), + ]; + + let result = + exhaust_cw_balance_entirely(non_finalized_adjusted_accounts, original_cw_balance); + + let expected_resulted_balances = vec![ + (wallet_1, original_requested_balance_1), + (wallet_2, proposed_adjusted_balance_2 + 222_000_000), + (wallet_3, proposed_adjusted_balance_3), + ]; + let check_sum: u128 = expected_resulted_balances + .iter() + .map(|(_, balance)| balance) + .sum(); + assert_payable_accounts_after_adjustment_finalization(result, expected_resulted_balances); + assert_eq!(check_sum, original_cw_balance) + } +} diff --git a/node/src/accountant/payment_adjuster/miscellaneous/mod.rs b/node/src/accountant/payment_adjuster/miscellaneous/mod.rs new file mode 100644 index 000000000..4d9c39e16 --- /dev/null +++ b/node/src/accountant/payment_adjuster/miscellaneous/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +mod account_stages_conversions; +pub mod data_structures; +pub mod helper_functions; diff --git a/node/src/accountant/payment_adjuster/mod.rs b/node/src/accountant/payment_adjuster/mod.rs new file mode 100644 index 000000000..1a9a7c6bc --- /dev/null +++ b/node/src/accountant/payment_adjuster/mod.rs @@ -0,0 +1,2638 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +// If possible, keep these modules private +mod criterion_calculators; +mod disqualification_arbiter; +mod inner; +mod logging_and_diagnostics; +mod miscellaneous; +#[cfg(test)] +mod non_unit_tests; +mod preparatory_analyser; +mod service_fee_adjuster; +// Intentionally public +#[cfg(test)] +pub mod test_utils; + +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +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; +use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::{collection_diagnostics, diagnostics}; +use crate::accountant::payment_adjuster::disqualification_arbiter::{ + DisqualificationArbiter, +}; +use crate::accountant::payment_adjuster::inner::{ + PaymentAdjusterInner, +}; +use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::{ + accounts_before_and_after_debug, +}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{AccountsByFinalization, AdjustedAccountBeforeFinalization, WeighedPayable}; +use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ + eliminate_accounts_by_tx_fee_limit, + exhaust_cw_balance_entirely, find_largest_exceeding_balance, + sum_as, no_affordable_accounts_found, +}; +use crate::accountant::payment_adjuster::preparatory_analyser::{LaterServiceFeeErrorFactory, PreparatoryAnalyzer}; +use crate::accountant::payment_adjuster::service_fee_adjuster::{ + ServiceFeeAdjuster, ServiceFeeAdjusterReal, +}; +use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::PreparedAdjustment; +use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; +use crate::diagnostics; +use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use itertools::Either; +use masq_lib::logger::Logger; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use thousands::Separable; +use variant_count::VariantCount; +use web3::types::{Address, U256}; +use masq_lib::utils::convert_collection; +use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::DisqualificationLimitProvidingAccount; + +// PaymentAdjuster is a recursive and scalable algorithm that inspects payments under conditions +// of acute insolvency. Each parameter that participates in the determination of the optimized +// asset allocation should have its own calculator. You can easily maintain the range of evaluated +// parameters by removing or adding your own calculator. These calculators, placed in a vector, +// make the heart of the algorithm. +// +// For parameters that can't be derived from an account, there is still a way to provide such values +// up into the calculator. This can be achieved via the PaymentAdjusterInner. + +// Algorithm description: +// +// It begins with accounts getting weights from the criteria calculators. The weighting is inverse +// to the debt size, accounts with smaller debts are prioritized so that we can satisfy as many +// accounts as possible and avoid the same number of bans. +// +// If it is necessary to adjust the set by the transaction fee, the accounts are sorted +// by the weights and only accounts that fit together under the limit are kept in. The adjustment +// by service fee follows up (or it may take the first place if the need for the previous step was +// missing). + +// Here comes the recursive part. The algorithm iterates through the weighted accounts and assigns +// a proportional portion of the available means to them. Since the initial stage of the Payment- +// Adjuster, where accounts were tested on causing insolvency, they've remained equipped with +// a computed parameter of the so-called disqualification limit. This limit determines +// if the weight-derived assignment of the money is too low or enough. If it is below the limit, +// the account is removed from consideration. However, only a single account with the smallest +// weight is eliminated per each recursion. + +// The pool of money remains the same, but the set of accounts is reduced. The recursion repeats. +// If none of the accounts disqualifies, it means all accounts were proposed with enough money. +// Although the proposals may exceed the disqualification limits, only the value of the limit is +// dedicated to the selected accounts. + +// The remaining assets are later allocated to the accounts based on the order by their weights, +// exhausting them fully one account after another up their 100% allocation until there is any +// money that can be distributed. + +// In the end, the accounts are shaped back as a PayableAccount and returned. + +pub type AdjustmentAnalysisResult = + Result, PaymentAdjusterError>; + +pub type IntactOriginalAccounts = Vec; + +pub trait PaymentAdjuster { + fn consider_adjustment( + &self, + qualified_payables: Vec, + agent: &dyn BlockchainAgent, + ) -> AdjustmentAnalysisResult; + + fn adjust_payments( + &self, + setup: PreparedAdjustment, + ) -> Result; +} + +pub struct PaymentAdjusterReal { + analyzer: PreparatoryAnalyzer, + disqualification_arbiter: DisqualificationArbiter, + service_fee_adjuster: Box, + calculators: Vec>, + inner: PaymentAdjusterInner, + logger: Logger, +} + +impl PaymentAdjuster for PaymentAdjusterReal { + fn consider_adjustment( + &self, + qualified_payables: Vec, + agent: &dyn BlockchainAgent, + ) -> AdjustmentAnalysisResult { + let disqualification_arbiter = &self.disqualification_arbiter; + let logger = &self.logger; + + self.analyzer + .analyze_accounts(agent, disqualification_arbiter, qualified_payables, logger) + } + + fn adjust_payments( + &self, + setup: PreparedAdjustment, + ) -> 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 max_debt_above_threshold_in_qualified_payables_minor = + find_largest_exceeding_balance(&analyzed_payables); + + self.initialize_inner( + required_adjustment, + initial_service_fee_balance_minor, + max_debt_above_threshold_in_qualified_payables_minor, + ); + + let sketched_debug_log_opt = self.sketch_debug_log_opt(&analyzed_payables); + + let affordable_accounts = self.run_adjustment(analyzed_payables)?; + + self.complete_debug_log_if_enabled(sketched_debug_log_opt, &affordable_accounts); + + self.inner.invalidate_guts(); + + Ok(OutboundPaymentsInstructions::new( + Either::Right(affordable_accounts), + agent, + response_skeleton_opt, + )) + } +} + +impl Default for PaymentAdjusterReal { + fn default() -> Self { + Self::new() + } +} + +impl PaymentAdjusterReal { + pub fn new() -> Self { + Self { + analyzer: PreparatoryAnalyzer::new(), + disqualification_arbiter: DisqualificationArbiter::default(), + service_fee_adjuster: Box::new(ServiceFeeAdjusterReal::default()), + calculators: vec![Box::new(BalanceCriterionCalculator::default())], + inner: PaymentAdjusterInner::default(), + logger: Logger::new("PaymentAdjuster"), + } + } + + fn initialize_inner( + &self, + required_adjustment: Adjustment, + initial_service_fee_balance_minor: u128, + max_debt_above_threshold_in_qualified_payables_minor: u128, + ) { + let transaction_fee_limitation_opt = match required_adjustment { + Adjustment::BeginByTransactionFee { + transaction_count_limit, + } => Some(transaction_count_limit), + Adjustment::ByServiceFee => None, + }; + + self.inner.initialize_guts( + transaction_fee_limitation_opt, + initial_service_fee_balance_minor, + max_debt_above_threshold_in_qualified_payables_minor, + ) + } + + fn run_adjustment( + &self, + analyzed_accounts: Vec, + ) -> Result, PaymentAdjusterError> { + 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::RecursionEliminatedAllAccounts); + } + + match processed_accounts { + AccountsByFinalization::Unexhausted(unexhausted_accounts) => { + Ok(exhaust_cw_balance_entirely( + unexhausted_accounts, + self.inner.original_cw_service_fee_balance_minor(), + )) + } + AccountsByFinalization::Finalized(accounts) => Ok(accounts), + } + } + + fn resolve_initial_adjustment_dispatch( + &self, + weighed_payables: Vec, + ) -> Result { + if let Some(limit) = self.inner.transaction_count_limit_opt() { + return self.begin_with_adjustment_by_transaction_fee(weighed_payables, limit); + } + + Ok(AccountsByFinalization::Unexhausted( + self.propose_possible_adjustment_recursively(weighed_payables), + )) + } + + fn begin_with_adjustment_by_transaction_fee( + &self, + weighed_accounts: Vec, + transaction_count_limit: u16, + ) -> Result { + diagnostics!( + "\nBEGINNING WITH ADJUSTMENT BY TRANSACTION FEE FOR ACCOUNTS:", + &weighed_accounts + ); + + let error_factory = LaterServiceFeeErrorFactory::new(&weighed_accounts); + + let weighed_accounts_affordable_by_transaction_fee = + eliminate_accounts_by_tx_fee_limit(weighed_accounts, transaction_count_limit); + + let cw_service_fee_balance_minor = self.inner.original_cw_service_fee_balance_minor(); + + if self.analyzer.recheck_if_service_fee_adjustment_is_needed( + &weighed_accounts_affordable_by_transaction_fee, + cw_service_fee_balance_minor, + error_factory, + &self.logger, + )? { + let final_set_before_exhausting_cw_balance = self + .propose_possible_adjustment_recursively( + weighed_accounts_affordable_by_transaction_fee, + ); + + Ok(AccountsByFinalization::Unexhausted( + final_set_before_exhausting_cw_balance, + )) + } else { + let accounts_not_needing_adjustment = + convert_collection(weighed_accounts_affordable_by_transaction_fee); + + Ok(AccountsByFinalization::Finalized( + accounts_not_needing_adjustment, + )) + } + } + + fn propose_possible_adjustment_recursively( + &self, + weighed_accounts: Vec, + ) -> Vec { + diagnostics!( + "\nUNRESOLVED ACCOUNTS IN CURRENT ITERATION:", + &weighed_accounts + ); + + let disqualification_arbiter = &self.disqualification_arbiter; + 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, + remaining_cw_service_fee_balance, + logger, + ); + + let decided_accounts = current_iteration_result.decided_accounts; + let remaining_undecided_accounts = current_iteration_result.remaining_undecided_accounts; + + if remaining_undecided_accounts.is_empty() { + return decided_accounts; + } + + if !decided_accounts.is_empty() { + self.adjust_remaining_cw_balance_down(&decided_accounts) + } + + let merged = if self.is_cw_balance_enough(&remaining_undecided_accounts) { + Self::merge_accounts( + decided_accounts, + convert_collection(remaining_undecided_accounts), + ) + } else { + Self::merge_accounts( + decided_accounts, + self.propose_possible_adjustment_recursively(remaining_undecided_accounts), + ) + }; + + diagnostics!( + "\nFINAL SET OF ADJUSTED ACCOUNTS IN CURRENT ITERATION:", + &merged + ); + + merged + } + + fn is_cw_balance_enough(&self, remaining_undecided_accounts: &[WeighedPayable]) -> bool { + let remaining_cw_service_fee_balance = self.inner.remaining_cw_service_fee_balance_minor(); + let minimum_sum_required: u128 = sum_as(remaining_undecided_accounts, |weighed_account| { + weighed_account.disqualification_limit() + }); + minimum_sum_required <= remaining_cw_service_fee_balance + } + + 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 { + self.apply_criteria(self.calculators.as_slice(), accounts) + } + + fn apply_criteria( + &self, + criteria_calculators: &[Box], + qualified_accounts: Vec, + ) -> Vec { + qualified_accounts + .into_iter() + .map(|payable| { + let weight = + criteria_calculators + .iter() + .fold(0_u128, |weight, criterion_calculator| { + 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.address(), + criterion_calculator.as_ref(), + new_criterion, + summed_up, + ); + + summed_up + }); + + WeighedPayable::new(payable, weight) + }) + .collect() + } + + fn adjust_remaining_cw_balance_down( + &self, + decided_accounts: &[AdjustedAccountBeforeFinalization], + ) { + let subtrahend_total: u128 = sum_as(decided_accounts, |account| { + account.proposed_adjusted_balance_minor + }); + self.inner + .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 + .remaining_cw_service_fee_balance_minor() + .separate_with_commas() + ) + } + + fn sketch_debug_log_opt( + &self, + qualified_payables: &[AnalyzedPayableAccount], + ) -> Option> { + self.logger.debug_enabled().then(|| { + qualified_payables + .iter() + .map(|payable| { + ( + payable.qualified_as.bare_account.wallet.address(), + payable.qualified_as.bare_account.balance_wei, + ) + }) + .collect() + }) + } + + fn complete_debug_log_if_enabled( + &self, + 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, fully_processed_accounts) + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Adjustment { + ByServiceFee, + BeginByTransactionFee { transaction_count_limit: u16 }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AdjustmentAnalysisReport { + pub adjustment: Adjustment, + pub accounts: Vec, +} + +impl AdjustmentAnalysisReport { + pub fn new(adjustment: Adjustment, accounts: Vec) -> Self { + AdjustmentAnalysisReport { + adjustment, + accounts, + } + } +} + +#[derive(Debug, PartialEq, Eq, VariantCount)] +pub enum PaymentAdjusterError { + AbsoluteFeeInsufficiency { + number_of_accounts: usize, + detection_phase: DetectionPhase, + }, + // AbsolutelyInsufficientBalance { + // number_of_accounts: usize, + // transaction_fee_opt: Option, + // service_fee_opt: Option, + // }, + // AbsolutelyInsufficientServiceFeeBalancePostTxFeeAdjustment { + // original_number_of_accounts: usize, + // number_of_accounts: usize, + // original_total_service_fee_required_minor: u128, + // cw_service_fee_balance_minor: u128, + // }, + RecursionEliminatedAllAccounts, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum DetectionPhase { + InitialCheck { + transaction_fee_opt: Option, + service_fee_opt: Option, + }, + PostTxFeeAdjustment { + original_number_of_accounts: usize, + original_total_service_fee_required_minor: u128, + cw_service_fee_balance_minor: u128, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct TransactionFeeImmoderateInsufficiency { + pub per_transaction_requirement_minor: u128, + pub cw_transaction_fee_balance_minor: U256, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ServiceFeeImmoderateInsufficiency { + pub total_service_fee_required_minor: u128, + pub cw_service_fee_balance_minor: u128, +} + +impl PaymentAdjusterError { + pub fn insolvency_detected(&self) -> bool { + match self { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + detection_phase, .. + } => match detection_phase { + DetectionPhase::InitialCheck { .. } => true, + DetectionPhase::PostTxFeeAdjustment { .. } => true, + }, + PaymentAdjusterError::RecursionEliminatedAllAccounts => true, + // We haven't needed to worry about this matter, yet this is rather a future alarm that + // will draw attention after somebody adds a possibility for an error not necessarily + // implying that an insolvency was detected before. At the moment, each error occurs + // only alongside an actual insolvency. (Hint: There might be consequences for + // the wording of the error message whose forming takes place back out, nearer to the + // Accountant's general area) + } + } +} + +impl Display for PaymentAdjusterError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase + } => { + match detection_phase { + DetectionPhase::InitialCheck { + transaction_fee_opt, + service_fee_opt + } => + match (transaction_fee_opt, service_fee_opt) { + (Some(transaction_fee_check_summary), None) => + write!( + f, + "Current transaction fee balance is not enough to pay a single payment. \ + Number of canceled payments: {}. Transaction fee per payment: {} wei, while \ + the wallet contains: {} wei", + number_of_accounts, + transaction_fee_check_summary.per_transaction_requirement_minor.separate_with_commas(), + transaction_fee_check_summary.cw_transaction_fee_balance_minor.separate_with_commas() + ), + (None, Some(service_fee_check_summary)) => + write!( + f, + "Current service fee balance is not enough to pay a single payment. \ + Number of canceled payments: {}. Total amount required: {} wei, while the wallet \ + contains: {} wei", + number_of_accounts, + service_fee_check_summary.total_service_fee_required_minor.separate_with_commas(), + service_fee_check_summary.cw_service_fee_balance_minor.separate_with_commas()), + (Some(transaction_fee_check_summary), Some(service_fee_check_summary)) => + write!( + f, + "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, + transaction_fee_check_summary.per_transaction_requirement_minor.separate_with_commas(), + transaction_fee_check_summary.cw_transaction_fee_balance_minor.separate_with_commas(), + service_fee_check_summary.total_service_fee_required_minor.separate_with_commas(), + service_fee_check_summary.cw_service_fee_balance_minor.separate_with_commas() + ), + (None, None) => unreachable!("This error contains no specifications") + }, + DetectionPhase::PostTxFeeAdjustment { original_number_of_accounts, original_total_service_fee_required_minor, cw_service_fee_balance_minor } + => write!(f, "The original set with {} accounts was adjusted down to {} due to \ + transaction fee. The new set was tested on service fee later again and did not \ + pass. Original required amount of service fee: {} wei, while the wallet \ + contains {} wei.", + original_number_of_accounts, + number_of_accounts, + original_total_service_fee_required_minor.separate_with_commas(), + cw_service_fee_balance_minor.separate_with_commas() + )}}, + PaymentAdjusterError::RecursionEliminatedAllAccounts => write!( + f, + "The payments adjusting process failed to find any combination of payables that \ + can be paid immediately with the finances provided." + ), + } + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::db_access_objects::payable_dao::PayableAccount; + 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::{ + AccountsByFinalization, AdjustmentIterationResult, WeighedPayable, + }; + use crate::accountant::payment_adjuster::miscellaneous::helper_functions::{ + find_largest_exceeding_balance, sum_as, + }; + use crate::accountant::payment_adjuster::service_fee_adjuster::illustrative_util::illustrate_why_we_need_to_prevent_exceeding_the_original_value; + use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; + use crate::accountant::payment_adjuster::test_utils::local_utils::{ + make_mammoth_payables, make_meaningless_analyzed_account_by_wallet, multiply_by_billion, + multiply_by_billion_concise, multiply_by_quintillion, multiply_by_quintillion_concise, + CriterionCalculatorMock, PaymentAdjusterBuilder, ServiceFeeAdjusterMock, + MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, PRESERVED_TEST_PAYMENT_THRESHOLDS, + }; + use crate::accountant::payment_adjuster::{ + Adjustment, AdjustmentAnalysisReport, DetectionPhase, 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_analyzed_payables, make_meaningless_analyzed_account, make_payable_account, + make_qualified_payables, + }; + use crate::accountant::{ + AnalyzedPayableAccount, CreditorThresholds, QualifiedPayableAccount, ResponseSkeleton, + }; + 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, Itertools}; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + 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::{Address, U256}; + + #[test] + #[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 subject = PaymentAdjusterReal::new(); + + let _ = subject.inner.remaining_cw_service_fee_balance_minor(); + } + + #[test] + fn consider_adjustment_happy_path() { + init_test_logging(); + 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 { + 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 { + 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.increase_by_percent_for(base_value); + multiply_by_billion(with_margin) + }; + // Transaction fee balance > payments + let input_3 = make_input_for_initial_check_tests( + None, + Some(TestConfigForTransactionFees { + gas_price_major: 100, + number_of_accounts: 6, + 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 { + gas_price_major: 100, + number_of_accounts: 6, + tx_computation_units: 53_000, + cw_transaction_fee_balance_minor: transaction_fee_balance_exactly_required_minor, + }), + ); + + [input_1, input_2, input_3, input_4] + .into_iter() + .enumerate() + .for_each(|(idx, (qualified_payables, agent))| { + assert_eq!( + subject.consider_adjustment(qualified_payables.clone(), &*agent), + Ok(Either::Left(qualified_payables)), + "failed for tested input number {:?}", + idx + 1 + ) + }); + + TestLogHandler::new().exists_no_log_containing(&format!("WARN: {test_name}:")); + } + + #[test] + fn consider_adjustment_sad_path_for_transaction_fee() { + init_test_logging(); + 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 (qualified_payables, agent) = make_input_for_initial_check_tests( + None, + Some(TestConfigForTransactionFees { + gas_price_major: 100, + number_of_accounts, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor: TX_FEE_MARGIN_IN_PERCENT + .increase_by_percent_for(multiply_by_billion(100 * 3 * 55_000)) + - 1, + }), + ); + + let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); + + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); + assert_eq!( + result, + 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 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 \ + delinquency bans. To consume services without limitations, you will need to \ + place more funds into your consuming wallet." + )); + } + + #[test] + fn consider_adjustment_sad_path_for_service_fee_balance() { + init_test_logging(); + 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 { + payable_account_balances_minor: vec![ + multiply_by_billion(85), + multiply_by_billion(15) + 1, + ], + cw_balance_minor: multiply_by_billion(100), + }), + None, + ); + + let result = subject.consider_adjustment(qualified_payables.clone(), &*agent); + + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); + assert_eq!( + result, + Ok(Either::Right(AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + analyzed_payables + ))) + ); + let log_handler = TestLogHandler::new(); + 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. To consume services without limitations, you will need to \ + place more funds into your consuming wallet." + )); + } + + #[test] + 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.increase_by_percent_for(base_minor) + }; + let cw_transaction_fee_balance_minor = tx_fee_exactly_required_for_single_tx - 1; + let (qualified_payables, agent) = make_input_for_initial_check_tests( + Some(TestConfigForServiceFeeBalances { + payable_account_balances_minor: vec![multiply_by_billion(123)], + cw_balance_minor: multiply_by_billion(444), + }), + Some(TestConfigForTransactionFees { + gas_price_major: 100, + number_of_accounts, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor, + }), + ); + + let result = subject.consider_adjustment(qualified_payables, &*agent); + + let per_transaction_requirement_minor = { + let base_minor = multiply_by_billion(55_000 * 100); + TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(base_minor) + }; + assert_eq!( + result, + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor, + cw_transaction_fee_balance_minor: cw_transaction_fee_balance_minor.into(), + }), + service_fee_opt: None + } + }) + ); + } + + #[test] + 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 { + 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, boxed_agent) = + make_input_for_initial_check_tests(service_fee_balances_config_opt, None); + let analyzed_accounts = convert_qualified_p_into_analyzed_p(qualified_payables.clone()); + let minimal_disqualification_limit = analyzed_accounts + .iter() + .map(|account| account.disqualification_limit_minor) + .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.consider_adjustment(qualified_payables, &agent); + + assert_eq!( + result, + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 3, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: multiply_by_billion(920), + cw_service_fee_balance_minor: actual_insufficient_cw_service_fee_balance + }) + } + }) + ); + } + + #[test] + 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 { + payable_account_balances_minor: vec![ + multiply_by_billion(200), + multiply_by_billion(300), + ], + cw_balance_minor: 0, + }), + Some(TestConfigForTransactionFees { + gas_price_major: 123, + number_of_accounts, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor: 0, + }), + ); + + let result = subject.consider_adjustment(qualified_payables, &*agent); + + let per_transaction_requirement_minor = + TX_FEE_MARGIN_IN_PERCENT.increase_by_percent_for(55_000 * multiply_by_billion(123)); + assert_eq!( + result, + Err(PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor, + cw_transaction_fee_balance_minor: U256::zero(), + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: multiply_by_billion(500), + cw_service_fee_balance_minor: 0 + }) + } + }) + ); + } + + #[test] + fn payment_adjuster_error_implements_display() { + let inputs = vec![ + ( + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 4, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: multiply_by_billion(70_000), + cw_transaction_fee_balance_minor: U256::from(90_000), + }), + service_fee_opt: None + } + }, + "Current transaction fee balance is not enough to pay a single payment. Number of \ + canceled payments: 4. Transaction fee per payment: 70,000,000,000,000 wei, while \ + the wallet contains: 90,000 wei", + ), + ( + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 5, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 6_000_000_000, + cw_service_fee_balance_minor: 333_000_000, + }) + } + }, + "Current service fee balance is not enough to pay a single payment. Number of \ + canceled payments: 5. Total amount required: 6,000,000,000 wei, while the wallet \ + contains: 333,000,000 wei", + ), + ( + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 5, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 5_000_000_000, + cw_transaction_fee_balance_minor: U256::from(3_000_000_000_u64) + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 7_000_000_000, + cw_service_fee_balance_minor: 100_000_000 + }) + } + }, + "Neither transaction fee nor service fee balance is enough to pay a single payment. \ + Number of payments considered: 5. Transaction fee per payment: 5,000,000,000 wei, \ + 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::AbsoluteFeeInsufficiency { + number_of_accounts: 3, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 6, + original_total_service_fee_required_minor: 1234567891011, + cw_service_fee_balance_minor: 333333, + } + }, + "The original set with 6 accounts was adjusted down to 3 due to transaction fee. \ + The new set was tested on service fee later again and did not pass. Original \ + required amount of service fee: 1,234,567,891,011 wei, while the wallet contains \ + 333,333 wei."), + ( + PaymentAdjusterError::RecursionEliminatedAllAccounts, + "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(); + inputs + .into_iter() + .for_each(|(error, expected_msg)| assert_eq!(error.to_string(), expected_msg)); + assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 3) + } + + #[test] + #[should_panic( + expected = "internal error: entered unreachable code: This error contains no \ + specifications" + )] + fn error_message_for_input_referring_to_no_issues_cannot_be_made() { + let _ = PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 0, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: None, + }, + } + .to_string(); + } + + #[test] + fn we_can_say_if_error_occurred_after_insolvency_was_detected() { + let inputs = vec![ + PaymentAdjusterError::RecursionEliminatedAllAccounts, + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 0, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 0, + cw_transaction_fee_balance_minor: Default::default(), + }), + service_fee_opt: None, + }, + }, + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 0, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: None, + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }), + }, + }, + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 0, + detection_phase: DetectionPhase::InitialCheck { + transaction_fee_opt: Some(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: 0, + cw_transaction_fee_balance_minor: Default::default(), + }), + service_fee_opt: Some(ServiceFeeImmoderateInsufficiency { + total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }), + }, + }, + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts: 0, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 0, + original_total_service_fee_required_minor: 0, + cw_service_fee_balance_minor: 0, + }, + }, + ]; + let inputs_count = inputs.len(); + let results = inputs + .into_iter() + .map(|err| err.insolvency_detected()) + .collect::>(); + assert_eq!(results, vec![true, true, true, true, true]); + assert_eq!(inputs_count, PaymentAdjusterError::VARIANT_COUNT + 3) + } + + #[test] + fn adjusted_balance_threats_to_outgrow_the_original_account_but_is_capped_by_disqualification_limit( + ) { + 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 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 = 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 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 result = subject.resolve_initial_adjustment_dispatch(weighed_payables.clone()); + + let mut accounts = if let AccountsByFinalization::Unexhausted(accounts) = result.unwrap() { + accounts + } else { + panic!("We expected unexhausted accounts but got those already finalized") + }; + // This shows how the weights can turn tricky for which it's important to have a hard upper + // limit, chosen quite down, as the disqualification limit, for optimisation. In its + // extremity, the naked algorithm of the reallocation of funds could have granted a value + // above the original debt size, which is clearly unfair. + illustrate_why_we_need_to_prevent_exceeding_the_original_value( + cw_service_fee_balance_minor, + weighed_payables.clone(), + wallet_2.address(), + balance_2, + ); + 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 = accounts.remove(0); + assert_eq!(&first_returned_account.original_account, payable_account_2); + assert_eq!( + first_returned_account.proposed_adjusted_balance_minor, + disqualification_limit_2 + ); + let second_returned_account = accounts.remove(0); + assert_eq!(&second_returned_account.original_account, payable_account_1); + assert_eq!( + second_returned_account.proposed_adjusted_balance_minor, + disqualification_limit_1 + ); + assert!(accounts.is_empty()); + } + + #[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(); + // 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: common_unimportant_age_for_accounts, + pending_payable_opt: None, + }; + let balance_2 = multiply_by_quintillion_concise(0.002); + let account_2 = PayableAccount { + wallet: make_wallet("def"), + balance_wei: balance_2, + last_paid_timestamp: common_unimportant_age_for_accounts, + pending_payable_opt: None, + }; + let balance_3 = multiply_by_quintillion_concise(0.005); + let account_3 = PayableAccount { + wallet: make_wallet("ghi"), + balance_wei: balance_3, + 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_qualified_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); + let calculator_mock = CriterionCalculatorMock::default() + .calculate_result(multiply_by_quintillion(2)) + .calculate_result(0) + .calculate_result(0); + let mut subject = PaymentAdjusterBuilder::default() + .start_with_inner_null() + .logger(Logger::new(test_name)) + .build(); + subject.calculators.push(Box::new(calculator_mock)); + let cw_service_fee_balance_minor = balance_2; + let disqualification_arbiter = &subject.disqualification_arbiter; + let agent_for_analysis = BlockchainAgentMock::default() + .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); + let analysis_result = subject.analyzer.analyze_accounts( + &agent_for_analysis, + disqualification_arbiter, + qualified_payables, + &subject.logger, + ); + // 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!( + "We expected to be let it for an adjustments with AnalyzedAccounts but got: {:?}", + x + ), + }; + 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); + + let err = match result { + Err(e) => e, + Ok(ok) => panic!( + "we expected to get an error, but it was ok: {:?}", + ok.affordable_accounts + ), + }; + assert_eq!(err, PaymentAdjusterError::RecursionEliminatedAllAccounts) + } + + #[test] + 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_flooded_with_enough_money_suddenly"; + let now = SystemTime::now(); + // 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: common_age_for_accounts_as_unimportant, + pending_payable_opt: None, + }; + let balance_2 = multiply_by_quintillion(60); + let account_2 = PayableAccount { + wallet: make_wallet("def"), + balance_wei: balance_2, + last_paid_timestamp: common_age_for_accounts_as_unimportant, + pending_payable_opt: None, + }; + let balance_3 = multiply_by_quintillion(40); + let account_3 = PayableAccount { + wallet: make_wallet("ghi"), + balance_wei: balance_3, + 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_analyzed_payables(payables, &PRESERVED_TEST_PAYMENT_THRESHOLDS, now); + let calculator_mock = CriterionCalculatorMock::default() + // 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 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_minor); + Box::new(mock) + }; + let adjustment_setup = PreparedAdjustment { + agent, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + analyzed_accounts, + ), + response_skeleton_opt: None, + }; + + 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); + // 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 overloaded_by_mammoth_debts_to_see_if_we_can_pass_through_without_blowing_up() { + init_test_logging(); + let test_name = + "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 algorithm, especially for + // the calculated criteria of over accounts + let extreme_payables = { + let debt_age_in_months = vec![120, 120, 120]; + make_mammoth_payables( + Either::Left(( + debt_age_in_months, + *MAX_POSSIBLE_SERVICE_FEE_BALANCE_IN_MINOR, + )), + 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_minor = 1_000; + let agent = { + let mock = BlockchainAgentMock::default() + .service_fee_balance_minor_result(cw_service_fee_balance_minor); + Box::new(mock) + }; + let adjustment_setup = PreparedAdjustment { + agent, + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + analyzed_payables, + ), + response_skeleton_opt: None, + }; + + 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::RecursionEliminatedAllAccounts); + let expected_log = |wallet: &str| { + format!( + "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(); + [ + "0x000000000000000000000000000000626c616830", + "0x000000000000000000000000000000626c616831", + "0x000000000000000000000000000000626c616832", + ] + .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 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 + } + + 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(&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 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 = "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, 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(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_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: AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + analyzed_payables.clone().into(), + ), + response_skeleton_opt: None, + }; + + let result = subject.adjust_payments(adjustment_setup).unwrap(); + + 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 = + 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!( + result.affordable_accounts, + expected_criteria_computation_output + ); + 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(); + 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 +| +| Original +| Adjusted +| +|0x0000000000000000000000000000000000676869 {} +| {} +|0x0000000000000000000000000000000000646566 {} +| {} +|0x0000000000000000000000000000000000616263 {} +| {}", + balance_account_3.separate_with_commas(), + expected_adjusted_balance_3.separate_with_commas(), + balance_account_2.separate_with_commas(), + expected_adjusted_balance_2.separate_with_commas(), + balance_account_1.separate_with_commas(), + expected_adjusted_balance_1.separate_with_commas() + ); + TestLogHandler::new().exists_log_containing(&log_msg.replace("|", "")); + test_inner_was_reset_to_null(subject) + } + + #[test] + fn only_transaction_fee_causes_limitations_and_the_service_fee_balance_suffices() { + init_test_logging(); + let test_name = + "only_transaction_fee_causes_limitations_and_the_service_fee_balance_suffices"; + 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(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: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, + }, + analyzed_payables.clone().into(), + ), + response_skeleton_opt: None, + }; + + let result = subject.adjust_payments(adjustment_setup).unwrap(); + + // The account 1 takes the first place for its weight being the biggest + 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!( + "DEBUG: {test_name}: \n\ +|Payable Account Balance Wei +| +| Original +| Adjusted +| +|0x0000000000000000000000000000000000676869 222,222,222,000,000,000 +| 222,222,222,000,000,000 +|0x0000000000000000000000000000000000616263 111,000,000,000,000,000 +| 111,000,000,000,000,000 +| +|Ruled Out Accounts Original +| +|0x0000000000000000000000000000000000646566 300,000,000,000,000,000" + ); + TestLogHandler::new().exists_log_containing(&log_msg.replace("|", "")); + test_inner_was_reset_to_null(subject) + } + + #[test] + 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 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(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) + .service_fee_balance_minor_result(cw_service_fee_balance_minor); + let response_skeleton_opt = Some(ResponseSkeleton { + 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: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, + }, + analyzed_payables.clone().into(), + ), + response_skeleton_opt, + }; + + 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 = + 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); + assert_eq!(result.response_skeleton_opt, response_skeleton_opt); + assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); + test_inner_was_reset_to_null(subject) + } + + #[test] + fn only_service_fee_balance_limits_the_payments_count() { + init_test_logging(); + let test_name = "only_service_fee_balance_limits_the_payments_count"; + // Account to be adjusted to keep as much as it is left in the cw balance + 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_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_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(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) + .service_fee_balance_minor_result(service_fee_balance_in_minor_units); + let response_skeleton_opt = Some(ResponseSkeleton { + client_id: 11, + context_id: 234, + }); + let adjustment_setup = PreparedAdjustment { + agent: Box::new(agent), + adjustment_analysis: AdjustmentAnalysisReport::new( + Adjustment::ByServiceFee, + analyzed_payables.clone().into(), + ), + response_skeleton_opt, + }; + + 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 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); + assert_eq!( + result.response_skeleton_opt, + Some(ResponseSkeleton { + client_id: 11, + context_id: 234 + }) + ); + assert_eq!(result.agent.arbitrary_id_stamp(), agent_id_stamp); + TestLogHandler::new().exists_log_containing(&format!( + "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) + } + + #[test] + 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 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_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_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(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: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, + }, + analyzed_payables.clone().into(), + ), + response_skeleton_opt: None, + }; + + let result = subject.adjust_payments(adjustment_setup).unwrap(); + + 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); + let log_msg = format!( + "DEBUG: {test_name}: \n\ +|Payable Account Balance Wei +| +| Original +| Adjusted +| +|0x0000000000000000000000000000000000616263 100,000,000,000,000,000,000 +| 90,000,000,000,000,000,000 +| +|Ruled Out Accounts Original +| +|0x0000000000000000000000000000000000646566 500,000,000,000,000,000,000 +|0x0000000000000000000000000000000000676869 250,000,000,000,000,000,000" + ); + TestLogHandler::new().exists_log_containing(&log_msg.replace("|", "")); + test_inner_was_reset_to_null(subject) + } + + #[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_p_into_analyzed_p(qualified_payables); + let analyzed_accounts: [AnalyzedPayableAccount; 3] = analyzed_accounts.try_into().unwrap(); + let disqualification_limits: QuantifiedDisqualificationLimits = (&analyzed_accounts).into(); + (analyzed_accounts, disqualification_limits) + } + + 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_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_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); + // 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: AdjustmentAnalysisReport::new( + Adjustment::BeginByTransactionFee { + transaction_count_limit, + }, + analyzed_payables.into(), + ), + response_skeleton_opt: None, + }; + + 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::AbsoluteFeeInsufficiency { + number_of_accounts: 2, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts: 3, + original_total_service_fee_required_minor: balance_account_1 + + balance_account_2 + + balance_account_3, + cw_service_fee_balance_minor + } + } + ); + TestLogHandler::new().assert_logs_contain_in_order(vec![ + &format!( + "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 \ + result in delinquency bans. To consume services without limitations, you \ + will need to place more funds into your consuming wallet.", + ), + &format!( + "ERROR: {test_name}: {}", + LATER_DETECTED_SERVICE_FEE_SEVERE_SCARCITY + ), + ]); + } + + struct TestConfigForServiceFeeBalances { + 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 { + gas_price_major: u64, + number_of_accounts: usize, + tx_computation_units: u64, + cw_transaction_fee_balance_minor: u128, + } + + fn make_input_for_initial_check_tests( + service_fee_config_opt: Option, + tx_fee_config_opt: Option, + ) -> (Vec, Box) { + 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 = 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 = 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 default_transaction_fee_config( + accounts_count_from_sf_config: usize, + ) -> TestConfigForTransactionFees { + TestConfigForTransactionFees { + gas_price_major: 120, + number_of_accounts: accounts_count_from_sf_config, + tx_computation_units: 55_000, + cw_transaction_fee_balance_minor: u128::MAX, + } + } + + fn prepare_payable_accounts_from( + balances_or_desired_accounts_count: Either>, + ) -> Vec { + match balances_or_desired_accounts_count { + Either::Left(desired_accounts_count) => (0..desired_accounts_count) + .map(|idx| make_payable_account(idx as u64)) + .collect(), + Either::Right(balances_of_accounts_minor) => balances_of_accounts_minor + .into_iter() + .enumerate() + .map(|(idx, balance)| { + let mut account = make_payable_account(idx as u64); + account.balance_wei = balance; + account + }) + .collect(), + } + } + + fn prepare_qualified_payables( + payable_accounts: Vec, + ) -> Vec { + payable_accounts + .into_iter() + .map(|payable| { + let balance = payable.balance_wei; + QualifiedPayableAccount { + bare_account: payable, + payment_threshold_intercept_minor: (balance / 10) * 7, + creditor_thresholds: CreditorThresholds { + permanent_debt_allowed_minor: (balance / 10) * 7, + }, + } + }) + .collect() + } + + fn prepare_agent( + cw_transaction_fee_minor: u128, + tx_computation_units: u64, + gas_price: u64, + cw_service_fee_balance_minor: u128, + ) -> Box { + let estimated_transaction_fee_per_transaction_minor = + multiply_by_billion((tx_computation_units * gas_price) as u128); + + let blockchain_agent = BlockchainAgentMock::default() + .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, + ); + + 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() + })) + .unwrap_err(); + let panic_msg = err.downcast_ref::().unwrap(); + assert_eq!( + panic_msg, + "PaymentAdjusterInner is uninitialized. It was identified during the execution of \ + 'original_cw_service_fee_balance_minor()'" + ) + } + + // 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() { + let now = SystemTime::now(); + let payment_adjuster = PaymentAdjusterReal::default(); + let qualified_payable = QualifiedPayableAccount { + bare_account: PayableAccount { + wallet: make_wallet("abc"), + 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: multiply_by_billion(20_000), + creditor_thresholds: CreditorThresholds::new(multiply_by_billion(10_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 = 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| { + // Testing a bigger gap between the values of the different calculators (we don't + // want to use previous_result != current_result because that could also mean + // a difference by one or a similarly negligible value) + let slightly_less_than_current = (current_result * 97) / 100; + let slightly_more_than_current = (current_result * 103) / 100; + assert_ne!(current_result, 0); + assert!( + previous_result <= slightly_less_than_current + || previous_result >= slightly_more_than_current + ); + current_result + }); + } + + struct CalculatorTestScenario { + payable: QualifiedPayableAccount, + expected_weight: u128, + } + + type InputMatrixConfigurator = fn( + (QualifiedPayableAccount, QualifiedPayableAccount, SystemTime), + ) -> 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 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![ + // This puts only the first calculator on test, the BalanceCalculator... + { + let mut account_1 = nominal_account_1; + account_1.bare_account.balance_wei += 123456789; + let mut account_2 = nominal_account_2; + 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... + ] + }; + + test_calculators_reactivity(input_matrix) + } + + #[derive(Clone, Copy)] + struct TemplateComputedWeight { + common_weight: u128, + } + + struct ExpectedWeightWithWallet { + wallet: Address, + 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 = 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_ACCOUNT_WEIGHT + ); + let mut template_accounts = template_accounts.to_vec(); + let mut pop_account = || template_accounts.remove(0); + let nominal_account_1 = pop_account(); + let nominal_account_2 = pop_account(); + let input_matrix = input_matrix_configurator((nominal_account_1, nominal_account_2, now)); + assert_eq!( + input_matrix.len(), + calculators_count, + "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, + cw_service_fee_balance_minor, + template_computed_weight, + ) + } + + fn prepare_nominal_data_before_loading_actual_test_input( + now: SystemTime, + cw_service_fee_balance_minor: u128, + ) -> ([QualifiedPayableAccount; 2], TemplateComputedWeight) { + let template_accounts = initialize_template_accounts(now); + let template_weight = compute_common_weight_for_templates( + template_accounts.clone(), + cw_service_fee_balance_minor, + ); + (template_accounts, template_weight) + } + + fn initialize_template_accounts(now: SystemTime) -> [QualifiedPayableAccount; 2] { + let make_qualified_payable = |wallet| QualifiedPayableAccount { + bare_account: PayableAccount { + wallet, + 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: multiply_by_quintillion_concise(0.012), + creditor_thresholds: CreditorThresholds::new(multiply_by_quintillion_concise(0.001)), + }; + + [ + make_qualified_payable(make_wallet("abc")), + make_qualified_payable(make_wallet("def")), + ] + } + + fn compute_common_weight_for_templates( + template_accounts: [QualifiedPayableAccount; 2], + cw_service_fee_balance_minor: u128, + ) -> TemplateComputedWeight { + let template_results = exercise_production_code_to_get_weighed_accounts( + template_accounts.to_vec(), + cw_service_fee_balance_minor, + ); + let templates_common_weight = template_results + .iter() + .map(|account| account.weight) + .reduce(|previous, current| { + assert_eq!(previous, current); + current + }) + .unwrap(); + // Formal test if the value is different from zero, + // and ideally much bigger than that + assert!(1_000_000_000_000 < templates_common_weight); + TemplateComputedWeight { + common_weight: templates_common_weight, + } + } + + fn exercise_production_code_to_get_weighed_accounts( + qualified_payables: Vec, + cw_service_fee_balance_minor: u128, + ) -> Vec { + let analyzed_payables = convert_qualified_p_into_analyzed_p(qualified_payables); + let max_debt_above_threshold_in_qualified_payables_minor = + find_largest_exceeding_balance(&analyzed_payables); + let mut subject = PaymentAdjusterBuilder::default() + .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 + .perform_adjustment_by_service_fee_params(&perform_adjustment_by_service_fee_params_arc) + // 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: 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_weighed_accounts_extraction( + result, + perform_adjustment_by_service_fee_params_arc, + cw_service_fee_balance_minor, + ) + } + + fn less_important_constant_assertions_and_weighed_accounts_extraction( + actual_result: Result, PaymentAdjusterError>, + perform_adjustment_by_service_fee_params_arc: Arc, u128)>>>, + cw_service_fee_balance_minor: u128, + ) -> 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::RecursionEliminatedAllAccounts) + ); + let mut perform_adjustment_by_service_fee_params = + perform_adjustment_by_service_fee_params_arc.lock().unwrap(); + 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()); + weighed_accounts + } + + fn test_accounts_from_input_matrix( + input_matrix: Vec<[CalculatorTestScenario; 2]>, + cw_service_fee_balance_minor: u128, + template_computed_weight: TemplateComputedWeight, + ) { + fn prepare_inputs_with_expected_weights( + particular_calculator_scenario: CalculatorTestScenario, + ) -> (QualifiedPayableAccount, ExpectedWeightWithWallet) { + 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 + .into_iter() + .map(|test_case| { + test_case + .into_iter() + .map(prepare_inputs_with_expected_weights) + .collect::>() + }) + .for_each(|qualified_payables_and_their_expected_weights| { + let (qualified_payments, expected_computed_weights): (Vec<_>, Vec<_>) = + qualified_payables_and_their_expected_weights + .into_iter() + .unzip(); + + let actual_weighed_accounts = exercise_production_code_to_get_weighed_accounts( + qualified_payments, + cw_service_fee_balance_minor, + ); + + assert_results( + actual_weighed_accounts, + expected_computed_weights, + template_computed_weight, + ) + }); + } + + fn make_comparison_hashmap( + weighed_accounts: Vec, + ) -> HashMap { + let feeding_iterator = weighed_accounts + .into_iter() + .map(|account| (account.wallet(), account)); + HashMap::from_iter(feeding_iterator) + } + + fn assert_results( + weighed_accounts: Vec, + expected_computed_weights: Vec, + template_computed_weight: TemplateComputedWeight, + ) { + 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 = weighed_accounts_as_hash_map + .get(&wallet) + .unwrap_or_else(|| panic!("Account for wallet {:?} disappeared", wallet)); + assert_ne!( + actual_account.weight, template_computed_weight.common_weight, + "Weight is exactly the same as that one from the template. The inputs \ + (modifications in the template accounts) are supposed to cause the weight to \ + evaluated differently." + ); + assert_eq!( + actual_account.weight, + expected_account_weight.weight, + "Computed weight {} differs from what was expected {}", + actual_account.weight.separate_with_commas(), + expected_account_weight.weight.separate_with_commas() + ); + 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." + ); + 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 new file mode 100644 index 000000000..49bcddcce --- /dev/null +++ b/node/src/accountant/payment_adjuster/non_unit_tests/mod.rs @@ -0,0 +1,1351 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; +use crate::accountant::payment_adjuster::miscellaneous::helper_functions::sum_as; +use crate::accountant::payment_adjuster::preparatory_analyser::accounts_abstraction::BalanceProvidingAccount; +use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; +use crate::accountant::payment_adjuster::test_utils::local_utils::PRESERVED_TEST_PAYMENT_THRESHOLDS; +use crate::accountant::payment_adjuster::{ + Adjustment, AdjustmentAnalysisReport, DetectionPhase, 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; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + PayableInspector, PayableThresholdsGaugeReal, +}; +use crate::accountant::test_utils::{ + make_single_qualified_payable_opt, try_to_make_qualified_payables, +}; +use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; +use crate::blockchain::blockchain_interface::blockchain_interface_web3::TX_FEE_MARGIN_IN_PERCENT; +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::test_utils::utils::ensure_node_home_directory_exists; +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::{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. It generates possibly an overwhelming number of scenarios that + // the PaymentAdjuster could be given to sort them out, as realistic as it can get, while its + // nature of randomness offers chances to have a dense range of combinations that a human fails + // to even try imagining. The hypothesis is that some of those might be corner cases whose + // trickiness wasn't recognized when the functionality was still at design. This test is to + // 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 others are lost quite early as legitimate errors that the PaymentAdjuster can detect, + // which would prevent finishing the search for a given scenario. + + // When the test reaches its end, it produces important output in a text file, located: + // node/generated/test/payment_adjuster/tests/home/loading_test_output.txt + + // 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) 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 + // their inconsistent proportionality towards the balances. These percents represent a payment + // coverage of the initial debts. But why don't they correspond with ascending balances? There's + // a principle to equip account low balances with the biggest weights. True. However, it doesn't + // need to be reflected so clearly, though. The adjustment depends heavily on a so-called + // "disqualification limit". Besides other purposes, this value affects that the payment won't + // require the entire amount but only its portion. That inherently will do for the payer to stay + // unbanned. In bulky accounts, this until-some-time forgiven portion stands only as a fraction + // of a whole. Small accounts, however, if it can be applied (as opposed to the account having + // to be excluded) might get shrunk a lot, and therefore many percents are to be reported as + // missing. This is what the numbers like 99% and 90% illustrate. That said, the letter account + // comes across as it should take precedence for its expectedly larger weight and gain at the + // expanse of the other, but the percents speak otherwise. Yet it's correct. The interpretation + // is the key. (Caution: this test displays its output with those accounts sorted). + + // CW service fee balance: 32,041,461,894,055,482 wei + // Portion of CW balance used: 100% + // Maximal txn count due to CW txn fee balance: UNLIMITED + // Used PaymentThresholds: DEFAULTED + // 2000000|1000|1000|1000000|500000|1000000 + // _____________________________________________________________________________________________ + // 1,988,742,049,305,843 wei | 236,766 s | 100 % + // 21,971,010,542,100,729 wei | 472,884 s | 99 % << << << << + // 4,726,030,753,976,563 wei | 395,377 s | 95 % << << << << + // 3,995,577,830,314,875 wei | 313,396 s | 90 % << << << << + // 129,594,971,536,673,815 wei | 343,511 s | X + + // In the code, we select and pale up accounts so that the picked balance isn't the full range, + // but still enough. The disqualification limit draws the cut. Only if the wallet isn't all + // dried up, after the accounts to keep are determined, while iterated over again, accounts + // sorted by descending weights are given more of it one by one, the maximum they can absorb, + // until there is still something to spend. + + let now = SystemTime::now(); + let mut gn = thread_rng(); + 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 output_collector = TestOverallOutputCollector::new(invalidly_generated_scenarios); + + struct FirstStageOutput { + output_collector: TestOverallOutputCollector, + allowed_scenarios: Vec, + } + + let init = FirstStageOutput { + output_collector, + allowed_scenarios: vec![], + }; + let first_stage_output = scenarios + .into_iter() + .fold(init, |mut output_collector, scenario| { + // We care only about the service fee balance check, parameters for transaction fee can + // be worked into the scenarios later. + let qualified_payables = scenario + .prepared_adjustment + .adjustment_analysis + .accounts + .iter() + .map(|account| account.qualified_as.clone()) + .collect(); + 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 { + Either::Left(_) => panic!( + "Wrong test setup. This test is designed to generate scenarios with \ + balances always insufficient in some way!" + ), + Either::Right(_) => (), + }; + Some(scenario) + } + Err(_) => { + output_collector + .output_collector + .scenarios_denied_before_adjustment_started += 1; + None + } + }; + + match allowed_scenario_opt { + Some(scenario) => output_collector.allowed_scenarios.push(scenario), + None => (), + } + + output_collector + }); + + let second_stage_scenarios = first_stage_output.allowed_scenarios; + let test_overall_output_collector = first_stage_output.output_collector; + let (test_overall_output_collector, scenario_adjustment_results) = + second_stage_scenarios.into_iter().fold( + (test_overall_output_collector, vec![]), + |(test_overall_output_collector_in_fold, mut scenario_results), scenario| { + let prepared_adjustment = scenario.prepared_adjustment; + let account_infos = + preserve_account_infos(&prepared_adjustment.adjustment_analysis.accounts, now); + + let required_adjustment = + prepared_adjustment.adjustment_analysis.adjustment.clone(); + let cw_service_fee_balance_minor = + prepared_adjustment.agent.service_fee_balance_minor(); + + let payment_adjuster_result = subject.adjust_payments(prepared_adjustment); + + let (t_o_c_i_f, scenario_result) = administrate_single_scenario_result( + test_overall_output_collector_in_fold, + payment_adjuster_result, + account_infos, + scenario.applied_thresholds, + required_adjustment, + cw_service_fee_balance_minor, + ); + + scenario_results.push(scenario_result); + + (t_o_c_i_f, scenario_results) + }, + ); + + render_results_to_file_and_attempt_basic_assertions( + scenario_adjustment_results, + number_of_requested_scenarios, + test_overall_output_collector, + ) +} + +fn generate_scenarios( + gn: &mut ThreadRng, + now: SystemTime, + number_of_scenarios: usize, +) -> Vec { + (0..number_of_scenarios) + .flat_map(|_| try_making_single_valid_scenario(gn, now)) + .collect() +} + +fn try_making_single_valid_scenario( + gn: &mut ThreadRng, + now: SystemTime, +) -> Option { + let accounts_count = generate_non_zero_usize(gn, 25) + 1; + + 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_p_into_analyzed_p(qualified_payables); + let agent = make_agent(cw_service_fee_balance); + let adjustment = make_adjustment(gn, analyzed_accounts.len()); + let prepared_adjustment = PreparedAdjustment::new( + Box::new(agent), + None, + AdjustmentAnalysisReport::new(adjustment, analyzed_accounts), + ); + Some(PreparedAdjustmentAndThresholds { + prepared_adjustment, + applied_thresholds, + }) +} + +fn make_payable_account( + wallet: Wallet, + thresholds: &PaymentThresholds, + now: SystemTime, + gn: &mut ThreadRng, +) -> PayableAccount { + 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, + balance_wei: service_fee_balance_minor, + last_paid_timestamp, + pending_payable_opt: None, + } +} + +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, + ) +} + +fn generate_highly_randomized_payable_account_balance( + gn: &mut ThreadRng, + thresholds: &PaymentThresholds, +) -> u128 { + // This seems overcomplicated, damn. Yet it's a result of good simple intentions. I wanted + // to ensure the occurrence of accounts with balances of different magnitudes to be generated + // for a single scenario. This was crucial to me so much that I didn't stop myself from writing + // this fishy-looking piece of code. + // This setup worked well for the randomness I needed, a lot significantly more compared to + // what the default number generator from this library seemed to be able to provide only. + // Using some nesting, it scattered the distribution better and allowed me to have accounts + // with diverse balances. + const COEFFICIENT_A: usize = 100; + const COEFFICIENT_B: usize = 2; + const COEFFICIENT_C: usize = 3; + const COEFFICIENT_D: usize = 4; + const COEFFICIENT_E: usize = 5; + + let mut generate_u128 = || generate_non_zero_usize(gn, COEFFICIENT_A) as u128; + + let parameter_a = generate_u128(); + let parameter_b = generate_u128(); + let parameter_c = generate_u128(); + let parameter_d = generate_u128(); + let parameter_e = generate_u128(); + let parameter_f = generate_u128(); + + let mut apply_arbitrary_variable_exponent = + |parameter: u128, up_to: usize| parameter.pow(generate_non_zero_usize(gn, up_to) as u32); + + let a_b_c_d_e_f = parameter_a + * apply_arbitrary_variable_exponent(parameter_b, COEFFICIENT_B) + * apply_arbitrary_variable_exponent(parameter_c, COEFFICIENT_C) + * apply_arbitrary_variable_exponent(parameter_d, COEFFICIENT_D) + * apply_arbitrary_variable_exponent(parameter_e, COEFFICIENT_E) + * parameter_f; + + thresholds.permanent_debt_allowed_gwei as u128 + a_b_c_d_e_f +} + +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_qualified_payables( + payable_accounts, + &PaymentThresholds::default(), + now, + false, + ), + AppliedThresholds::CommonButRandomized { common_thresholds } => { + try_to_make_qualified_payables(payable_accounts, common_thresholds, now, false) + } + AppliedThresholds::RandomizedForEachAccount { + individual_thresholds, + } => make_qualified_payables_for_individualized_thresholds( + payable_accounts, + now, + &payment_inspector, + individual_thresholds, + ), + } +} + +fn make_qualified_payables_for_individualized_thresholds( + payable_accounts: Vec, + now: SystemTime, + payment_inspector: &PayableInspector, + individual_thresholds: &HashMap, +) -> Vec { + let vec_of_thresholds = individual_thresholds.values().collect_vec(); + let zipped = payable_accounts.into_iter().zip(vec_of_thresholds.iter()); + zipped + .flat_map(|(qualified_payable, thresholds)| { + make_single_qualified_payable_opt( + qualified_payable, + &payment_inspector, + &thresholds, + false, + now, + ) + }) + .collect() +} + +fn try_generating_qualified_payables_and_cw_balance( + gn: &mut ThreadRng, + accounts_count: usize, + now: SystemTime, +) -> Option<(u128, Vec, AppliedThresholds)> { + let (payables, applied_thresholds) = + make_payables_according_to_thresholds_setup(gn, accounts_count, now); + + let qualified_payables = + try_make_qualified_payables_by_applied_thresholds(payables, &applied_thresholds, now); + + 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, + applied_thresholds, + )) + } +} + +fn pick_appropriate_cw_service_fee_balance( + gn: &mut ThreadRng, + qualified_payables: &[QualifiedPayableAccount], + accounts_count: usize, +) -> u128 { + // Values picked empirically + const COEFFICIENT_A: usize = 1000; + const COEFFICIENT_B: usize = 2; + + let balance_average = sum_as(qualified_payables, |account| { + account.initial_balance_minor() + }) / accounts_count as u128; + + let max_pieces = accounts_count * COEFFICIENT_A; + let number_of_pieces = + generate_usize(gn, max_pieces - COEFFICIENT_B) as u128 + COEFFICIENT_B as u128; + + balance_average / COEFFICIENT_A as u128 * number_of_pieces +} + +fn make_payables_according_to_thresholds_setup( + gn: &mut ThreadRng, + accounts_count: usize, + now: SystemTime, +) -> (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, &PaymentThresholds::default(), now) + } + AppliedThresholds::CommonButRandomized { common_thresholds } => { + make_payables_with_common_thresholds(gn, wallets, common_thresholds, now) + } + AppliedThresholds::RandomizedForEachAccount { + individual_thresholds, + } => make_payables_with_individual_thresholds(gn, wallets, individual_thresholds, now), + }; + + (payables, nominated_thresholds) +} + +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, prepared_wallets: &[Wallet]) -> AppliedThresholds { + let be_defaulted = generate_boolean(gn); + if be_defaulted { + return AppliedThresholds::Defaulted; + } + + let be_same_for_all_accounts = generate_boolean(gn); + if be_same_for_all_accounts { + return AppliedThresholds::CommonButRandomized { + common_thresholds: return_single_randomized_thresholds(gn), + }; + } + + let individual_thresholds = prepared_wallets + .iter() + .map(|wallet| (wallet.address(), return_single_randomized_thresholds(gn))) + .collect::>(); + AppliedThresholds::RandomizedForEachAccount { + individual_thresholds, + } +} + +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); + PaymentThresholds { + debt_threshold_gwei, + maturity_threshold_sec: generate_range(gn, 100, 10_000), + payment_grace_period_sec: 0, + permanent_debt_allowed_gwei, + threshold_interval_sec: generate_range(gn, 1000, 100_000), + unban_below_gwei: permanent_debt_allowed_gwei, + } +} + +fn make_agent(cw_service_fee_balance: u128) -> BlockchainAgentMock { + BlockchainAgentMock::default() + // We don't care about this check in this test + .transaction_fee_balance_minor_result(U256::from(u128::MAX)) + // ...as well as we don't here + .estimated_transaction_fee_per_transaction_minor_result(1) + // Used in the entry check + .service_fee_balance_minor_result(cw_service_fee_balance) + // For evaluation preparations in the test + .service_fee_balance_minor_result(cw_service_fee_balance) + // For PaymentAdjuster itself + .service_fee_balance_minor_result(cw_service_fee_balance) + .gas_price_margin_result(*TX_FEE_MARGIN_IN_PERCENT) +} + +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 transaction_count_limit = + u16::try_from(generate_non_zero_usize(gn, accounts_count)).unwrap(); + Adjustment::BeginByTransactionFee { + transaction_count_limit, + } + } else { + Adjustment::ByServiceFee + } +} + +fn administrate_single_scenario_result( + mut test_overall_output_collector: TestOverallOutputCollector, + payment_adjuster_result: Result, + account_infos: Vec, + used_thresholds: AppliedThresholds, + required_adjustment: Adjustment, + cw_service_fee_balance_minor: u128, +) -> (TestOverallOutputCollector, ScenarioResult) { + let common = CommonScenarioInfo { + cw_service_fee_balance_minor, + required_adjustment, + payment_thresholds: used_thresholds, + }; + let reinterpreted_result = match payment_adjuster_result { + Ok(outbound_payment_instructions) => { + 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, adjusted_accounts_within_this_scenario) = + prepare_interpretable_adjustment_results(merged); + + look_after_adjustment_statistics( + &mut test_overall_output_collector, + adjusted_accounts_within_this_scenario, + ); + + let (partially_sorted_interpretable_adjustments, no_accounts_eliminated) = + sort_interpretable_adjustments(interpretable_adjustments); + + Ok(SuccessfulAdjustment { + common, + portion_of_cw_cumulatively_used_percents, + partially_sorted_interpretable_adjustments, + no_accounts_eliminated, + }) + } + Err(adjuster_error) => Err(FailedAdjustment { + common, + account_infos, + adjuster_error, + }), + }; + + ( + test_overall_output_collector, + ScenarioResult::new(reinterpreted_result), + ) +} + +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(|info| { + let adjusted_account_opt = accounts_hashmap.remove(&info.wallet); + (info, adjusted_account_opt) + }) + .collect() +} + +fn prepare_interpretable_adjustment_results( + merged: Vec<(AccountInfo, Option)>, +) -> (Vec, usize) { + merged.into_iter().fold( + (vec![], 0), + |(mut interpretable_accounts, adjusted_accounts_so_far), + (info, maybe_eliminated_account)| { + let (interpretable_account, was_this_account_eliminated) = + InterpretableAccountAdjustmentResult::new(info, maybe_eliminated_account); + interpretable_accounts.push(interpretable_account); + ( + interpretable_accounts, + adjusted_accounts_so_far + if was_this_account_eliminated { 1 } else { 0 }, + ) + }, + ) +} + +fn look_after_adjustment_statistics( + test_overall_output_collector: &mut TestOverallOutputCollector, + adjusted_accounts_within_this_scenario: usize, +) { + if adjusted_accounts_within_this_scenario > 1 { + test_overall_output_collector.scenarios_with_more_than_one_adjustment += 1 + } + test_overall_output_collector.adjusted_account_count_total += + adjusted_accounts_within_this_scenario; +} + +enum PercentPortionOfCWUsed { + Percents(u8), + LessThanOnePercent, +} + +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::LessThanOnePercent + } else { + PercentPortionOfCWUsed::Percents(percents) + } + } + + 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, +} + +impl ScenarioResult { + fn new(result: Result) -> Self { + Self { result } + } +} + +struct SuccessfulAdjustment { + common: CommonScenarioInfo, + portion_of_cw_cumulatively_used_percents: PercentPortionOfCWUsed, + partially_sorted_interpretable_adjustments: Vec, + no_accounts_eliminated: bool, +} + +struct FailedAdjustment { + common: CommonScenarioInfo, + account_infos: Vec, + adjuster_error: PaymentAdjusterError, +} + +fn preserve_account_infos( + accounts: &[AnalyzedPayableAccount], + now: SystemTime, +) -> Vec { + accounts + .iter() + .map(|account| AccountInfo { + 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) + .unwrap() + .as_secs(), + }) + .collect() +} + +fn render_results_to_file_and_attempt_basic_assertions( + scenario_results: Vec, + number_of_requested_scenarios: usize, + output_collector: TestOverallOutputCollector, +) { + let file_dir = ensure_node_home_directory_exists("payment_adjuster", "tests"); + 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(output_collector, |acc, scenario_result| { + do_final_processing_of_single_scenario(&mut output_file, acc, scenario_result) + }); + 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 = + 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 ({})", + total_scenarios_handled_including_invalid_ones, number_of_requested_scenarios + ); + // Only some of the generated scenarios are acceptable, don't be surprised by the waste. That's + // expected given the nature of the generator and the requirements on the payable accounts + // so that they are picked up and let in the PaymentAdjuster. We'll be better off truly faithful + // to the use case and the expected conditions. Therefore, we insist on making "guaranteed" + // QualifiedPayableAccounts out of PayableAccount which is where we take the losses. + let actual_entry_check_pass_percentage = 100 + - ((output_collector.scenarios_denied_before_adjustment_started * 100) + / total_scenarios_evaluated); + const REQUIRED_ENTRY_CHECK_PASS_PERCENTAGE: usize = 50; + assert!( + 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, + actual_entry_check_pass_percentage + ); + 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_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 + ); +} + +fn introduction(file: &mut File) { + write_thick_dividing_line(file); + write_thick_dividing_line(file); + let page_width = PAGE_WIDTH; + file.write_fmt(format_args!( + "{:^page_width$}\n", + "There is a short summary at the tail" + )) + .unwrap(); + write_thick_dividing_line(file) +} + +fn write_brief_test_summary_at_file_s_tail( + file: &mut File, + output_collector: &TestOverallOutputCollector, + scenarios_requested: usize, + scenarios_evaluated: usize, +) { + write_thick_dividing_line(file); + file.write_fmt(format_args!( + "\n\ + Scenarios\n\ + Requested:............................. {}\n\ + Actually evaluated:.................... {}\n\n\ + Successful:............................ {}\n\ + Successes with no accounts eliminated:. {}\n\n\ + Transaction fee / mixed adjustments:... {}\n\ + Bills fulfillment distribution:\n\ + {}\n\n\ + Plain service fee adjustments:......... {}\n\ + Bills fulfillment distribution:\n\ + {}\n\n\ + Individual accounts adjusted:.......... {}\n\ + Scenarios with more than one account\n\ + adjusted:.............................. {}\n\n\ + Unsuccessful\n\ + Caught by the entry check:............. {}\n\ + With 'RecursionEliminatedAllAccounts':.... {}\n\ + With late insufficient balance errors:. {}\n\n\ + Legend\n\ + Adjusted balances are highlighted\n\ + 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(), + output_collector + .fulfillment_distribution_for_transaction_fee_adjustments + .render_in_two_lines(), + output_collector + .fulfillment_distribution_for_service_fee_adjustments + .total_scenarios(), + output_collector + .fulfillment_distribution_for_service_fee_adjustments + .render_in_two_lines(), + output_collector.adjusted_account_count_total, + output_collector.scenarios_with_more_than_one_adjustment, + output_collector.scenarios_denied_before_adjustment_started, + output_collector.all_accounts_eliminated, + output_collector.late_immoderately_insufficient_service_fee_balance, + NON_EXHAUSTED_ACCOUNT_MARKER + )) + .unwrap() +} + +fn do_final_processing_of_single_scenario( + file: &mut File, + mut output_collector: TestOverallOutputCollector, + scenario: ScenarioResult, +) -> TestOverallOutputCollector { + match scenario.result { + Ok(positive) => { + if positive.no_accounts_eliminated { + output_collector.with_no_accounts_eliminated += 1 + } + if matches!( + positive.common.required_adjustment, + Adjustment::BeginByTransactionFee { .. } + ) { + output_collector + .fulfillment_distribution_for_transaction_fee_adjustments + .collected_fulfillment_percentages + .push( + positive + .portion_of_cw_cumulatively_used_percents + .as_plain_number(), + ) + } + 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 + .as_plain_number(), + ) + } + render_positive_scenario(file, positive); + output_collector.oks += 1; + output_collector + } + Err(negative) => { + match negative.adjuster_error { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + ref detection_phase, + .. + } => match detection_phase { + DetectionPhase::InitialCheck { .. } => { + panic!("Such errors should be already filtered out") + } + DetectionPhase::PostTxFeeAdjustment { .. } => { + output_collector.late_immoderately_insufficient_service_fee_balance += 1 + } + }, + PaymentAdjusterError::RecursionEliminatedAllAccounts => { + output_collector.all_accounts_eliminated += 1 + } + } + render_negative_scenario(file, negative); + output_collector + } + } +} + +fn render_scenario_header( + file: &mut File, + scenario_common: &CommonScenarioInfo, + 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 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, + scenario_common.resolve_affordable_tx_count_by_tx_fee(), + scenario_common.resolve_thresholds_description() + )) + .unwrap(); +} + +fn render_positive_scenario(file: &mut File, result: SuccessfulAdjustment) { + render_scenario_header( + file, + &result.common, + result.portion_of_cw_cumulatively_used_percents, + ); + + let adjusted_accounts = result.partially_sorted_interpretable_adjustments; + + render_accounts( + file, + &adjusted_accounts, + &result.common.payment_thresholds, + |file, account, individual_thresholds_opt| { + single_account_output( + file, + individual_thresholds_opt, + &account.info, + account.bill_coverage_in_percentage_opt, + ) + }, + ) +} + +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: &[Account], + used_thresholds: &AppliedThresholds, + mut render_account: F, +) where + Account: AccountWithWallet, + F: FnMut(&mut File, &Account, Option<&PaymentThresholds>), +{ + let individual_thresholds_opt = if let AppliedThresholds::RandomizedForEachAccount { + individual_thresholds, + } = used_thresholds + { + Some(individual_thresholds) + } else { + None + }; + + accounts + .iter() + .map(|account| { + ( + account, + 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(); +} + +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 = 34; +const AGE_COLUMN_WIDTH: usize = 8; +const STARTING_GAP: usize = 6; + +fn single_account_output( + file: &mut File, + 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; + 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_fulfilment_status_graphically( + bill_coverage_in_percentage_opt: Option, +) -> String { + match bill_coverage_in_percentage_opt { + Some(percentage) => { + let highlighting = if percentage != 100 { + NON_EXHAUSTED_ACCOUNT_MARKER + } else { + "" + }; + format!("{} %{:>shift$}", percentage, highlighting, shift = 40) + } + None => "X".to_string(), + } +} + +fn write_error(file: &mut File, error: PaymentAdjusterError) { + file.write_fmt(format_args!( + "Scenario resulted in a failure: {:?}\n", + error + )) + .unwrap() +} + +fn write_thick_dividing_line(file: &mut dyn Write) { + write_ln_made_of(file, '=') +} + +fn write_thin_dividing_line(file: &mut dyn Write) { + write_ln_made_of(file, '_') +} + +const PAGE_WIDTH: usize = 120; + +fn write_ln_made_of(file: &mut dyn Write, char: char) { + let _ = file + .write_fmt(format_args!("{}\n", char.to_string().repeat(PAGE_WIDTH))) + .unwrap(); +} + +fn sort_interpretable_adjustments( + interpretable_adjustments: Vec, +) -> (Vec, bool) { + let (finished, eliminated): ( + Vec, + Vec, + ) = interpretable_adjustments + .into_iter() + .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.bill_coverage_in_percentage_opt, + result_a.info.initially_requested_service_fee_minor, + ), + &( + 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_b.info.initially_requested_service_fee_minor, + &result_a.info.initially_requested_service_fee_minor, + ) + }); + let all_results = finished_sorted.chain(eliminated_sorted).collect(); + (all_results, were_no_accounts_eliminated) +} + +fn generate_range(gn: &mut ThreadRng, low: T, up_to: T) -> T +where + T: SampleUniform + PartialOrd, +{ + gn.gen_range(low..up_to) +} + +fn generate_non_zero_usize(gn: &mut ThreadRng, up_to: usize) -> usize { + generate_range(gn, 1, up_to) +} + +fn generate_usize(gn: &mut ThreadRng, up_to: usize) -> usize { + generate_range(gn, 0, up_to) +} + +fn generate_boolean(gn: &mut ThreadRng) -> bool { + gn.gen() +} + +struct TestOverallOutputCollector { + invalidly_generated_scenarios: usize, + // First stage: entry check + // ____________________________________ + scenarios_denied_before_adjustment_started: usize, + // Second stage: proper adjustments + // ____________________________________ + oks: usize, + with_no_accounts_eliminated: usize, + adjusted_account_count_total: usize, + scenarios_with_more_than_one_adjustment: usize, + fulfillment_distribution_for_transaction_fee_adjustments: PercentageFulfillmentDistribution, + fulfillment_distribution_for_service_fee_adjustments: PercentageFulfillmentDistribution, + // Errors + all_accounts_eliminated: usize, + late_immoderately_insufficient_service_fee_balance: usize, +} + +impl TestOverallOutputCollector { + fn new(invalidly_generated_scenarios: usize) -> Self { + Self { + invalidly_generated_scenarios, + scenarios_denied_before_adjustment_started: 0, + oks: 0, + with_no_accounts_eliminated: 0, + adjusted_account_count_total: 0, + scenarios_with_more_than_one_adjustment: 0, + fulfillment_distribution_for_transaction_fee_adjustments: Default::default(), + fulfillment_distribution_for_service_fee_adjustments: Default::default(), + all_accounts_eliminated: 0, + 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)] +struct PercentageFulfillmentDistribution { + collected_fulfillment_percentages: Vec, +} + +impl PercentageFulfillmentDistribution { + fn render_in_two_lines(&self) -> String { + #[derive(Default)] + struct Ranges { + from_0_to_10: usize, + from_10_to_20: usize, + from_20_to_30: usize, + from_30_to_40: usize, + from_40_to_50: usize, + from_50_to_60: usize, + from_60_to_70: usize, + from_70_to_80: usize, + from_80_to_90: usize, + from_90_to_100: usize, + } + + let full_count = self.collected_fulfillment_percentages.len(); + let ranges_populated = self.collected_fulfillment_percentages.iter().fold( + Ranges::default(), + |mut ranges, current| { + match current { + 0..=9 => ranges.from_0_to_10 += 1, + 10..=19 => ranges.from_10_to_20 += 1, + 20..=29 => ranges.from_20_to_30 += 1, + 30..=39 => ranges.from_30_to_40 += 1, + 40..=49 => ranges.from_40_to_50 += 1, + 50..=59 => ranges.from_50_to_60 += 1, + 60..=69 => ranges.from_60_to_70 += 1, + 70..=79 => ranges.from_70_to_80 += 1, + 80..=89 => ranges.from_80_to_90 += 1, + 90..=100 => ranges.from_90_to_100 += 1, + _ => panic!("Shouldn't happen"), + } + ranges + }, + ); + let digits = 6.max(full_count.to_string().len()); + format!( + "Percentage ranges\n\ + {:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|\ + {:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}\n\ + {:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|\ + {:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}|{:^digits$}", + "0-9", + "10-19", + "20-29", + "30-39", + "40-49", + "50-59", + "60-69", + "70-79", + "80-89", + "90-100", + ranges_populated.from_0_to_10, + ranges_populated.from_10_to_20, + ranges_populated.from_20_to_30, + ranges_populated.from_30_to_40, + ranges_populated.from_40_to_50, + ranges_populated.from_50_to_60, + ranges_populated.from_60_to_70, + ranges_populated.from_70_to_80, + ranges_populated.from_80_to_90, + ranges_populated.from_90_to_100 + ) + } + + fn total_scenarios(&self) -> usize { + self.collected_fulfillment_percentages.len() + } +} + +struct PreparedAdjustmentAndThresholds { + prepared_adjustment: PreparedAdjustment, + applied_thresholds: AppliedThresholds, +} + +struct CommonScenarioInfo { + cw_service_fee_balance_minor: u128, + required_adjustment: Adjustment, + 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 InterpretableAccountAdjustmentResult { + info: AccountInfo, + // Account was eliminated from payment if None + bill_coverage_in_percentage_opt: Option, +} + +impl AccountWithWallet for InterpretableAccountAdjustmentResult { + fn wallet(&self) -> Address { + self.info.wallet + } +} + +impl InterpretableAccountAdjustmentResult { + fn new(info: AccountInfo, maybe_eliminated_payable: Option) -> (Self, bool) { + let (bill_coverage_in_percentage_opt, was_this_account_adjusted) = + match &maybe_eliminated_payable { + Some(payable) => { + let percents_of_bill_coverage = Self::percents_of_bill_coverage(&info, payable); + ( + Some(percents_of_bill_coverage), + percents_of_bill_coverage != 100, + ) + } + None => (None, false), + }; + ( + InterpretableAccountAdjustmentResult { + info, + bill_coverage_in_percentage_opt, + }, + was_this_account_adjusted, + ) + } + + fn percents_of_bill_coverage(info: &AccountInfo, payable: &PayableAccount) -> u8 { + let percentage = (payable.balance_wei * 100) / info.initially_requested_service_fee_minor; + u8::try_from(percentage).unwrap() + } +} + +struct AccountInfo { + wallet: Address, + initially_requested_service_fee_minor: u128, + debt_age_s: u64, +} + +impl AccountWithWallet for AccountInfo { + fn wallet(&self) -> Address { + self.wallet + } +} + +enum AppliedThresholds { + Defaulted, + CommonButRandomized { + common_thresholds: PaymentThresholds, + }, + RandomizedForEachAccount { + individual_thresholds: HashMap, + }, +} diff --git a/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs b/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs new file mode 100644 index 000000000..4ac4fb25a --- /dev/null +++ b/node/src/accountant/payment_adjuster/preparatory_analyser/accounts_abstraction.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2024, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::payment_adjuster::miscellaneous::data_structures::WeighedPayable; +use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; + +pub trait BalanceProvidingAccount { + fn initial_balance_minor(&self) -> u128; +} + +impl BalanceProvidingAccount for WeighedPayable { + fn initial_balance_minor(&self) -> u128 { + self.analyzed_account.initial_balance_minor() + } +} + +impl BalanceProvidingAccount for AnalyzedPayableAccount { + fn initial_balance_minor(&self) -> u128 { + self.qualified_as.initial_balance_minor() + } +} + +impl BalanceProvidingAccount for QualifiedPayableAccount { + fn initial_balance_minor(&self) -> u128 { + self.bare_account.balance_wei + } +} + +pub trait DisqualificationLimitProvidingAccount { + fn disqualification_limit(&self) -> u128; +} + +impl DisqualificationLimitProvidingAccount for WeighedPayable { + fn disqualification_limit(&self) -> u128 { + self.analyzed_account.disqualification_limit() + } +} + +impl DisqualificationLimitProvidingAccount for AnalyzedPayableAccount { + fn disqualification_limit(&self) -> u128 { + self.disqualification_limit_minor + } +} diff --git a/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs new file mode 100644 index 000000000..d16d8c3b5 --- /dev/null +++ b/node/src/accountant/payment_adjuster/preparatory_analyser/mod.rs @@ -0,0 +1,715 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod accounts_abstraction; + +use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; +use crate::accountant::payment_adjuster::logging_and_diagnostics::log_functions::{ + log_adjustment_by_service_fee_is_required, log_insufficient_transaction_fee_balance, + log_transaction_fee_adjustment_ok_but_by_service_fee_undoable, +}; +use crate::accountant::payment_adjuster::miscellaneous::data_structures::{ + 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, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, + ServiceFeeImmoderateInsufficiency, TransactionFeeImmoderateInsufficiency, +}; +use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; +use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; +use ethereum_types::U256; +use itertools::Either; +use masq_lib::logger::Logger; +use masq_lib::percentage::PurePercentage; + +type TxCount = u16; + +pub struct PreparatoryAnalyzer {} + +impl PreparatoryAnalyzer { + pub fn new() -> Self { + Self {} + } + + pub fn analyze_accounts( + &self, + agent: &dyn BlockchainAgent, + disqualification_arbiter: &DisqualificationArbiter, + qualified_payables: Vec, + logger: &Logger, + ) -> Result, AdjustmentAnalysisReport>, PaymentAdjusterError> + { + let number_of_accounts = qualified_payables.len(); + let cw_transaction_fee_balance_minor = agent.transaction_fee_balance_minor(); + let required_tx_fee_per_transaction_minor = + agent.estimated_transaction_fee_per_transaction_minor(); + 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, + gas_price_margin, + required_tx_fee_per_transaction_minor, + number_of_accounts, + logger, + ); + + let cw_service_fee_balance_minor = agent.service_fee_balance_minor(); + let is_service_fee_adjustment_needed = Self::is_service_fee_adjustment_needed( + &qualified_payables, + cw_service_fee_balance_minor, + logger, + ); + + if matches!(transaction_fee_check_result, Ok(None)) && !is_service_fee_adjustment_needed { + return Ok(Either::Left(qualified_payables)); + } + + let prepared_accounts = Self::pre_process_accounts_for_adjustments( + qualified_payables, + disqualification_arbiter, + ); + + let service_fee_check_result = if is_service_fee_adjustment_needed { + let error_factory = EarlyServiceFeeErrorFactory::default(); + + Self::check_adjustment_possibility( + &prepared_accounts, + cw_service_fee_balance_minor, + error_factory, + ) + } else { + Ok(()) + }; + + let transaction_fee_limitation_opt = Self::handle_errors_if_present( + number_of_accounts, + transaction_fee_check_result, + service_fee_check_result, + )?; + + let adjustment = match transaction_fee_limitation_opt { + None => Adjustment::ByServiceFee, + Some(transaction_count_limit) => Adjustment::BeginByTransactionFee { + transaction_count_limit, + }, + }; + + Ok(Either::Right(AdjustmentAnalysisReport::new( + adjustment, + prepared_accounts, + ))) + } + + fn handle_errors_if_present( + number_of_accounts: usize, + transaction_fee_check_result: Result< + Option, + TransactionFeeImmoderateInsufficiency, + >, + service_fee_check_result: Result<(), ServiceFeeImmoderateInsufficiency>, + ) -> Result, PaymentAdjusterError> { + let construct_error = + |tx_fee_check_err_opt: Option, + service_fee_check_err_opt: Option| { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase: DetectionPhase::InitialCheck { + 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, + weighed_accounts: &[WeighedPayable], + cw_service_fee_balance_minor: u128, + error_factory: LaterServiceFeeErrorFactory, + logger: &Logger, + ) -> Result { + if Self::is_service_fee_adjustment_needed( + weighed_accounts, + cw_service_fee_balance_minor, + logger, + ) { + if let Err(e) = Self::check_adjustment_possibility( + weighed_accounts, + cw_service_fee_balance_minor, + error_factory, + ) { + log_transaction_fee_adjustment_ok_but_by_service_fee_undoable(logger); + Err(e) + } else { + Ok(true) + } + } else { + Ok(false) + } + } + + fn determine_transaction_count_limit_by_transaction_fee( + &self, + cw_transaction_fee_balance_minor: U256, + 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 = + gas_price_margin.increase_by_percent_for(per_transaction_requirement_minor); + + let verified_tx_counts = Self::transaction_counts_verification( + cw_transaction_fee_balance_minor, + per_txn_requirement_minor_with_margin, + number_of_qualified_accounts, + ); + + let max_tx_count_we_can_afford = verified_tx_counts.affordable; + let required_tx_count = verified_tx_counts.required; + + if max_tx_count_we_can_afford == 0 { + Err(TransactionFeeImmoderateInsufficiency { + per_transaction_requirement_minor: per_txn_requirement_minor_with_margin, + cw_transaction_fee_balance_minor, + }) + } else if max_tx_count_we_can_afford >= required_tx_count { + Ok(None) + } else { + log_insufficient_transaction_fee_balance( + logger, + required_tx_count, + max_tx_count_we_can_afford, + per_txn_requirement_minor_with_margin, + cw_transaction_fee_balance_minor, + ); + + Ok(Some(max_tx_count_we_can_afford)) + } + } + + fn transaction_counts_verification( + cw_transaction_fee_balance_minor: U256, + txn_fee_required_per_txn_minor: u128, + number_of_qualified_accounts: usize, + ) -> AffordableAndRequiredTxCounts { + let max_possible_tx_count_u256 = + cw_transaction_fee_balance_minor / U256::from(txn_fee_required_per_txn_minor); + + AffordableAndRequiredTxCounts::new(max_possible_tx_count_u256, number_of_qualified_accounts) + } + + fn check_adjustment_possibility( + prepared_accounts: &[AnalyzableAccounts], + cw_service_fee_balance_minor: u128, + service_fee_error_factory: ErrorFactory, + ) -> Result<(), Error> + where + AnalyzableAccounts: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, + ErrorFactory: ServiceFeeErrorFactory, + { + let lowest_disqualification_limit = + Self::find_lowest_disqualification_limit(prepared_accounts); + + // We can do little in this area but stepping in if the cw balance is zero or nearly + // zero with the assumption that the debt with the lowest disqualification limit in + // the set fits in the available balance. If it doesn't, we're not going to bother + // the payment adjuster by that work, so it'll abort, and no payments will come out. + if lowest_disqualification_limit <= cw_service_fee_balance_minor { + Ok(()) + } else { + let err = + service_fee_error_factory.make(prepared_accounts, cw_service_fee_balance_minor); + Err(err) + } + } + + fn pre_process_accounts_for_adjustments( + accounts: Vec, + disqualification_arbiter: &DisqualificationArbiter, + ) -> Vec { + accounts + .into_iter() + .map(|account| { + let disqualification_limit = + disqualification_arbiter.calculate_disqualification_edge(&account); + AnalyzedPayableAccount::new(account, disqualification_limit) + }) + .collect() + } + + fn compute_total_service_fee_required(payables: &[Account]) -> u128 + where + Account: BalanceProvidingAccount, + { + sum_as(payables, |account| account.initial_balance_minor()) + } + + fn is_service_fee_adjustment_needed( + qualified_payables: &[Account], + cw_service_fee_balance_minor: u128, + logger: &Logger, + ) -> bool + where + Account: BalanceProvidingAccount, + { + let service_fee_totally_required_minor = + 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( + logger, + service_fee_totally_required_minor, + cw_service_fee_balance_minor, + ) + }) + .is_some() + } + + fn find_lowest_disqualification_limit(accounts: &[Account]) -> u128 + where + Account: DisqualificationLimitProvidingAccount, + { + accounts + .iter() + .map(|account| account.disqualification_limit()) + .min() + .expect("No account to consider") + } +} + +pub trait ServiceFeeErrorFactory +where + AnalyzableAccount: BalanceProvidingAccount, +{ + fn make(&self, accounts: &[AnalyzableAccount], cw_service_fee_balance_minor: u128) -> Error; +} + +#[derive(Default)] +pub struct EarlyServiceFeeErrorFactory {} + +impl ServiceFeeErrorFactory + for EarlyServiceFeeErrorFactory +{ + fn make( + &self, + 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, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct LaterServiceFeeErrorFactory { + original_number_of_accounts: usize, + original_total_service_fee_required_minor: u128, +} + +impl LaterServiceFeeErrorFactory { + pub fn new(unadjusted_accounts: &[WeighedPayable]) -> Self { + let original_number_of_accounts = unadjusted_accounts.len(); + let original_total_service_fee_required_minor = sum_as(unadjusted_accounts, |account| { + account.initial_balance_minor() + }); + Self { + original_number_of_accounts, + original_total_service_fee_required_minor, + } + } +} + +impl ServiceFeeErrorFactory for LaterServiceFeeErrorFactory { + fn make( + &self, + current_set_of_accounts: &[WeighedPayable], + cw_service_fee_balance_minor: u128, + ) -> PaymentAdjusterError { + let number_of_accounts = current_set_of_accounts.len(); + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + cw_service_fee_balance_minor, + original_number_of_accounts: self.original_number_of_accounts, + original_total_service_fee_required_minor: self + .original_total_service_fee_required_minor, + }, + } + } +} + +#[cfg(test)] +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::{ + EarlyServiceFeeErrorFactory, LaterServiceFeeErrorFactory, PreparatoryAnalyzer, + ServiceFeeErrorFactory, + }; + use crate::accountant::payment_adjuster::test_utils::local_utils::{ + make_meaningless_weighed_account, multiply_by_billion, multiply_by_billion_concise, + DisqualificationGaugeMock, + }; + use crate::accountant::payment_adjuster::{ + Adjustment, AdjustmentAnalysisReport, DetectionPhase, PaymentAdjusterError, + ServiceFeeImmoderateInsufficiency, + }; + use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; + use crate::accountant::test_utils::make_meaningless_qualified_payable; + use crate::accountant::QualifiedPayableAccount; + 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}; + use std::fmt::Debug; + use std::sync::{Arc, Mutex}; + use thousands::Separable; + use web3::types::U256; + + fn test_adjustment_possibility_nearly_rejected( + test_name: &str, + disqualification_gauge: DisqualificationGaugeMock, + original_accounts: [QualifiedPayableAccount; 2], + cw_service_fee_balance_minor: u128, + ) { + init_test_logging(); + let determine_limit_params_arc = Arc::new(Mutex::new(vec![])); + 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 = make_populated_blockchain_agent(cw_service_fee_balance_minor); + + let result = subject.analyze_accounts( + &blockchain_agent, + &disqualification_arbiter, + original_accounts.clone().to_vec(), + &Logger::new(test_name), + ); + + 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]; + let account_2 = &original_accounts[1]; + let expected_params = vec![ + ( + account_1.bare_account.balance_wei, + account_1.payment_threshold_intercept_minor, + account_1.creditor_thresholds.permanent_debt_allowed_minor, + ), + ( + account_2.bare_account.balance_wei, + account_2.payment_threshold_intercept_minor, + account_2.creditor_thresholds.permanent_debt_allowed_minor, + ), + ]; + assert_eq!(&determine_limit_params[0..2], expected_params); + TestLogHandler::new().exists_log_containing(&format!( + "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_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_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(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( + "adjustment_possibility_nearly_rejected_when_cw_balance_slightly_bigger", + disqualification_gauge, + original_accounts, + cw_service_fee_balance, + ) + } + + #[test] + fn adjustment_possibility_nearly_rejected_when_cw_balance_equal() { + 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(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( + "adjustment_possibility_nearly_rejected_when_cw_balance_equal", + disqualification_gauge, + original_accounts, + cw_service_fee_balance, + ) + } + + fn test_not_enough_even_for_the_smallest_account_error< + ErrorFactory, + Error, + EnsureAccountsRightType, + PrepareExpectedError, + AnalyzableAccount, + >( + error_factory: ErrorFactory, + ensure_account_right_type: EnsureAccountsRightType, + prepare_expected_error: PrepareExpectedError, + ) where + EnsureAccountsRightType: FnOnce(Vec) -> Vec, + PrepareExpectedError: FnOnce(usize, u128, u128) -> Error, + ErrorFactory: ServiceFeeErrorFactory, + Error: Debug + PartialEq, + AnalyzableAccount: DisqualificationLimitProvidingAccount + BalanceProvidingAccount, + { + 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 rightly_typed_accounts = ensure_account_right_type(supplied_accounts); + + let result = PreparatoryAnalyzer::check_adjustment_possibility( + &rightly_typed_accounts, + cw_service_fee_balance_minor, + error_factory, + ); + + let expected_error = prepare_expected_error( + supplied_accounts_count, + service_fee_total_of_the_known_set, + cw_service_fee_balance_minor, + ); + assert_eq!(result, Err(expected_error)) + } + + #[test] + fn not_enough_for_even_the_smallest_account_error_right_after_alarmed_tx_fee_check() { + let error_factory = EarlyServiceFeeErrorFactory::default(); + 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, + cw_service_fee_balance_minor, + } + }; + + test_not_enough_even_for_the_smallest_account_error( + error_factory, + ensure_accounts_right_type, + prepare_expected_error, + ) + } + + #[test] + fn not_enough_for_even_the_smallest_account_error_right_after_accounts_dumped_for_tx_fee() { + let original_accounts = vec![ + 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 original_total_service_fee_required_minor = sum_as(&original_accounts, |account| { + account.initial_balance_minor() + }); + let error_factory = LaterServiceFeeErrorFactory::new(&original_accounts); + let ensure_accounts_right_type = |accounts| accounts; + let prepare_expected_error = |number_of_accounts, _, cw_service_fee_balance_minor| { + PaymentAdjusterError::AbsoluteFeeInsufficiency { + number_of_accounts, + detection_phase: DetectionPhase::PostTxFeeAdjustment { + original_number_of_accounts, + cw_service_fee_balance_minor, + original_total_service_fee_required_minor, + }, + } + }; + + test_not_enough_even_for_the_smallest_account_error( + error_factory, + ensure_accounts_right_type, + prepare_expected_error, + ) + } + + #[test] + fn recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighed_payables() { + init_test_logging(); + let test_name = + "recheck_if_service_fee_adjustment_is_needed_works_nicely_for_weighed_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 = 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![weighed_account_1, weighed_account_2]; + let service_fee_totally_required_minor = balance_1 + balance_2; + let error_factory = LaterServiceFeeErrorFactory::new(&accounts); + let logger = Logger::new(test_name); + let subject = PreparatoryAnalyzer::new(); + + [ + (service_fee_totally_required_minor - 1, true), + (service_fee_totally_required_minor, false), + (service_fee_totally_required_minor + 1, false), + ] + .iter() + .for_each(|(service_fee_balance, adjustment_is_needed_expected)| { + let adjustment_is_needed_actual = subject + .recheck_if_service_fee_adjustment_is_needed( + &accounts, + *service_fee_balance, + error_factory.clone(), + &logger, + ) + .unwrap(); + assert_eq!(adjustment_is_needed_actual, *adjustment_is_needed_expected); + }); + + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Mature payables amount to {} MASQ wei while the consuming wallet \ + holds only {}", + service_fee_totally_required_minor.separate_with_commas(), + (service_fee_totally_required_minor - 1).separate_with_commas() + )); + } + + #[test] + fn construction_of_error_context_with_accounts_dumped_works() { + let balance_1 = 1234567; + 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_meaningless_weighed_account(345); + account_2 + .analyzed_account + .qualified_as + .bare_account + .balance_wei = balance_2; + let weighed_accounts = vec![account_1, account_2]; + + let result = LaterServiceFeeErrorFactory::new(&weighed_accounts); + + assert_eq!( + result, + LaterServiceFeeErrorFactory { + original_number_of_accounts: 2, + original_total_service_fee_required_minor: balance_1 + balance_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::>(); + popped_results + .into_iter() + .cycle() + .take(4) + .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 new file mode 100644 index 000000000..ab1c51d3d --- /dev/null +++ b/node/src/accountant/payment_adjuster/service_fee_adjuster.rs @@ -0,0 +1,364 @@ +// 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::{ + AdjustedAccountBeforeFinalization, AdjustmentIterationResult, UnconfirmedAdjustment, + 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, sum_as}; +use itertools::Either; +use masq_lib::logger::Logger; +use masq_lib::utils::convert_collection; +use std::vec; +use crate::accountant::payment_adjuster::logging_and_diagnostics::diagnostics::ordinary_diagnostic_functions::diagnostics_for_accounts_above_disqualification_limit; + +pub trait ServiceFeeAdjuster { + fn perform_adjustment_by_service_fee( + &self, + weighed_accounts: Vec, + disqualification_arbiter: &DisqualificationArbiter, + remaining_cw_service_fee_balance_minor: u128, + logger: &Logger, + ) -> AdjustmentIterationResult; +} + +#[derive(Default)] +pub struct ServiceFeeAdjusterReal {} + +impl ServiceFeeAdjuster for ServiceFeeAdjusterReal { + fn perform_adjustment_by_service_fee( + &self, + weighed_accounts: Vec, + disqualification_arbiter: &DisqualificationArbiter, + cw_service_fee_balance_minor: u128, + logger: &Logger, + ) -> AdjustmentIterationResult { + let unconfirmed_adjustments = + compute_unconfirmed_adjustments(weighed_accounts, cw_service_fee_balance_minor); + + let checked_accounts = Self::try_confirm_some_accounts(unconfirmed_adjustments); + + match checked_accounts { + Either::Left(only_accounts_below_disq_limit) => Self::disqualify_single_account( + disqualification_arbiter, + only_accounts_below_disq_limit, + logger, + ), + Either::Right(some_accounts_above_or_even_to_disq_limit) => { + some_accounts_above_or_even_to_disq_limit + } + } + } +} + +impl ServiceFeeAdjusterReal { + // 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, 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 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. + // + // However, if it begins to be clear that the remaining money doesn't allow keeping any + // additional account in the selection, there is the next step to come, where the already + // selected accounts are reviewed again in the order of their significance resolved from + // remembering their weights from the earlier processing, and the unused money is poured into, + // until all resources are used. + + fn try_confirm_some_accounts( + unconfirmed_adjustments: Vec, + ) -> Either, AdjustmentIterationResult> { + let (accounts_above_or_even_to_disq_limit, accounts_below_disq_limit) = + Self::filter_and_process_confirmable_accounts(unconfirmed_adjustments); + + if accounts_above_or_even_to_disq_limit.is_empty() { + Either::Left(accounts_below_disq_limit) + } else { + let remaining_undecided_accounts: Vec = + convert_collection(accounts_below_disq_limit); + let pre_processed_decided_accounts: Vec = + convert_collection(accounts_above_or_even_to_disq_limit); + Either::Right(AdjustmentIterationResult { + decided_accounts: pre_processed_decided_accounts, + remaining_undecided_accounts, + }) + } + } + + fn disqualify_single_account( + disqualification_arbiter: &DisqualificationArbiter, + unconfirmed_adjustments: Vec, + logger: &Logger, + ) -> AdjustmentIterationResult { + let disqualified_account_wallet = disqualification_arbiter + .find_an_account_to_disqualify_in_this_iteration(&unconfirmed_adjustments, logger); + + let remaining = unconfirmed_adjustments + .into_iter() + .filter(|account_info| account_info.wallet() != disqualified_account_wallet) + .collect(); + + let remaining_reverted = convert_collection(remaining); + + AdjustmentIterationResult { + decided_accounts: vec![], + remaining_undecided_accounts: remaining_reverted, + } + } + + fn filter_and_process_confirmable_accounts( + unconfirmed_adjustments: Vec, + ) -> ( + Vec, + Vec, + ) { + let init: (Vec, Vec) = (vec![], vec![]); + let fold_guts = + |(mut above_or_even_to_disq_limit, mut below_disq_limit): (Vec<_>, Vec<_>), + current: UnconfirmedAdjustment| { + let disqualification_limit = current.disqualification_limit_minor(); + if current.proposed_adjusted_balance_minor >= disqualification_limit { + diagnostics_for_accounts_above_disqualification_limit( + ¤t, + disqualification_limit, + ); + let mut adjusted = current; + adjusted.proposed_adjusted_balance_minor = disqualification_limit; + above_or_even_to_disq_limit.push(adjusted) + } else { + below_disq_limit.push(current) + } + (above_or_even_to_disq_limit, below_disq_limit) + }; + + let (accounts_above_or_even_to_disq_limit, accounts_below_disq_limit) = + unconfirmed_adjustments.into_iter().fold(init, fold_guts); + + let decided_accounts = if accounts_above_or_even_to_disq_limit.is_empty() { + vec![] + } else { + convert_collection(accounts_above_or_even_to_disq_limit) + }; + + (decided_accounts, accounts_below_disq_limit) + } +} + +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); + + let multiplication_coefficient = compute_mul_coefficient_preventing_fractional_numbers( + remaining_cw_service_fee_balance_minor, + ); + + let proportional_cw_fragment = compute_proportional_cw_fragment( + remaining_cw_service_fee_balance_minor, + weights_total, + multiplication_coefficient, + ); + + let compute_proposed_adjusted_balance = + |weight| weight * proportional_cw_fragment / multiplication_coefficient; + + weighed_accounts + .into_iter() + .map(|weighed_account| { + let proposed_adjusted_balance = + compute_proposed_adjusted_balance(weighed_account.weight); + + proposed_adjusted_balance_diagnostics(&weighed_account, proposed_adjusted_balance); + + 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 as to 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::local_utils::{ + make_meaningless_unconfirmed_adjustment, multiply_by_quintillion, + multiply_by_quintillion_concise, + }; + + #[test] + fn filter_and_process_confirmable_accounts_limits_them_by_their_disqualification_edges() { + let mut account_1 = make_meaningless_unconfirmed_adjustment(111); + let weight_1 = account_1.weighed_account.weight; + account_1 + .weighed_account + .analyzed_account + .qualified_as + .bare_account + .balance_wei = multiply_by_quintillion(2); + account_1 + .weighed_account + .analyzed_account + .disqualification_limit_minor = multiply_by_quintillion_concise(1.8); + account_1.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(3.0); + let mut account_2 = make_meaningless_unconfirmed_adjustment(222); + let weight_2 = account_2.weighed_account.weight; + account_2 + .weighed_account + .analyzed_account + .qualified_as + .bare_account + .balance_wei = multiply_by_quintillion(5); + account_2 + .weighed_account + .analyzed_account + .disqualification_limit_minor = multiply_by_quintillion_concise(4.2) - 1; + account_2.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(4.2); + let mut account_3 = make_meaningless_unconfirmed_adjustment(333); + account_3 + .weighed_account + .analyzed_account + .qualified_as + .bare_account + .balance_wei = multiply_by_quintillion(3); + account_3 + .weighed_account + .analyzed_account + .disqualification_limit_minor = multiply_by_quintillion(2) + 1; + account_3.proposed_adjusted_balance_minor = multiply_by_quintillion(2); + let mut account_4 = make_meaningless_unconfirmed_adjustment(444); + let weight_4 = account_4.weighed_account.weight; + account_4 + .weighed_account + .analyzed_account + .qualified_as + .bare_account + .balance_wei = multiply_by_quintillion_concise(1.5); + account_4 + .weighed_account + .analyzed_account + .disqualification_limit_minor = multiply_by_quintillion_concise(0.5); + account_4.proposed_adjusted_balance_minor = multiply_by_quintillion_concise(0.5); + let mut account_5 = make_meaningless_unconfirmed_adjustment(555); + account_5 + .weighed_account + .analyzed_account + .qualified_as + .bare_account + .balance_wei = multiply_by_quintillion(2); + account_5 + .weighed_account + .analyzed_account + .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(), + account_3.clone(), + account_4.clone(), + account_5.clone(), + ]; + + let (thriving_competitors, losing_competitors) = + 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 + .weighed_account + .analyzed_account + .qualified_as + .bare_account, + weight_1, + multiply_by_quintillion_concise(1.8), + ), + AdjustedAccountBeforeFinalization::new( + account_2 + .weighed_account + .analyzed_account + .qualified_as + .bare_account, + weight_2, + multiply_by_quintillion_concise(4.2) - 1, + ), + AdjustedAccountBeforeFinalization::new( + account_4 + .weighed_account + .analyzed_account + .qualified_as + .bare_account, + weight_4, + 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 secured a rule an account could never demand more + // than 100% of its size. + + // Later it was changed to a different policy, the so-called "outweighed" account is given + // automatically a balance equal to its disqualification limit. Still, it's quite likely + // some accounts will acquire slightly more by a distribution of the last bits of funds + // away out of the consuming wallet. + + // Here, though, the assertion illustrates what the latest policy intends to fight off, + // as the unprotected proposed adjusted balance rises over the original balance. + let proposed_adjusted_balance = unconfirmed_adjustments[1].proposed_adjusted_balance_minor; + assert!( + proposed_adjusted_balance > (original_balance_of_outweighed_account * 11 / 10), + "we expected the proposed balance to be unsound, bigger than the original balance \ + (at least 1.1 times more) which would be {} but it was {}", + original_balance_of_outweighed_account.separate_with_commas(), + proposed_adjusted_balance.separate_with_commas() + ); + } +} diff --git a/node/src/accountant/payment_adjuster/test_utils.rs b/node/src/accountant/payment_adjuster/test_utils.rs new file mode 100644 index 000000000..f85408b5c --- /dev/null +++ b/node/src/accountant/payment_adjuster/test_utils.rs @@ -0,0 +1,341 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +// 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}; + + 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 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_balances) => { + specific_months_and_specific_balances + } + }; + 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_meaningless_unconfirmed_adjustment(n: u64) -> UnconfirmedAdjustment { + let qualified_account = make_meaningless_qualified_payable(n); + let account_balance = qualified_account.bare_account.balance_wei; + let proposed_adjusted_balance_minor = (2 * account_balance) / 3; + 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>, + } + + 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" + } + } + + 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 + } + } + + #[derive(Default)] + pub struct DisqualificationGaugeMock { + determine_limit_params: Arc>>, + determine_limit_results: RefCell>, + } + + 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) + } + } + + impl DisqualificationGaugeMock { + pub fn determine_limit_params( + mut self, + params: &Arc>>, + ) -> Self { + self.determine_limit_params = params.clone(); + self + } + + pub fn determine_limit_result(self, result: u128) -> Self { + self.determine_limit_results.borrow_mut().push(result); + self + } + } + + #[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, + 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) + } + } + + 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 + } + } + + // = 1 gwei + pub fn multiply_by_billion(num: u128) -> u128 { + gwei_to_wei(num) + } + + // = 1 MASQ + pub fn multiply_by_quintillion(num: u128) -> u128 { + multiply_by_billion(multiply_by_billion(num)) + } + + // = 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_meaningless_weighed_account(n: u64) -> WeighedPayable { + WeighedPayable::new(make_meaningless_analyzed_account(n), 123456 * n as u128) + } +} + +pub mod exposed_utils { + use crate::accountant::payment_adjuster::disqualification_arbiter::DisqualificationArbiter; + use crate::accountant::{AnalyzedPayableAccount, QualifiedPayableAccount}; + + // Refrain from using this fn in the prod code + pub fn convert_qualified_p_into_analyzed_p( + 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 faf7d45cc..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 @@ -1,11 +1,10 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; - -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use ethereum_types::U256; use masq_lib::logger::Logger; +use masq_lib::percentage::PurePercentage; #[derive(Clone)] pub struct BlockchainAgentNull { @@ -14,24 +13,31 @@ pub struct BlockchainAgentNull { } impl BlockchainAgent for BlockchainAgentNull { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - self.log_function_call("estimated_transaction_fee_total()"); + fn estimated_transaction_fee_per_transaction_minor(&self) -> u128 { + self.log_function_call("estimated_transaction_fee_per_transaction_minor()"); 0 } - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - self.log_function_call("consuming_wallet_balances()"); - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero(), - } + fn transaction_fee_balance_minor(&self) -> U256 { + self.log_function_call("transaction_fee_balance_minor()"); + U256::zero() + } + + fn service_fee_balance_minor(&self) -> u128 { + self.log_function_call("service_fee_balance_minor()"); + 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 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 { self.log_function_call("consuming_wallet()"); &self.wallet @@ -77,11 +83,9 @@ impl Default for BlockchainAgentNull { mod tests { use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::agent_null::BlockchainAgentNull; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; - - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; - use masq_lib::logger::Logger; + use masq_lib::percentage::PurePercentage; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use web3::types::U256; @@ -120,48 +124,68 @@ mod tests { } #[test] - fn null_agent_estimated_transaction_fee_total() { + fn null_agent_estimated_transaction_fee_per_transaction_minor() { init_test_logging(); - let test_name = "null_agent_estimated_transaction_fee_total"; + let test_name = "null_agent_estimated_transaction_fee_per_transaction_minor"; let mut subject = BlockchainAgentNull::new(); subject.logger = Logger::new(test_name); - let result = subject.estimated_transaction_fee_total(4); + let result = subject.estimated_transaction_fee_per_transaction_minor(); assert_eq!(result, 0); - assert_error_log(test_name, "estimated_transaction_fee_total"); + assert_error_log(test_name, "estimated_transaction_fee_per_transaction_minor"); } #[test] - fn null_agent_consuming_wallet_balances() { + fn null_agent_consuming_transaction_fee_balance_minor() { init_test_logging(); - let test_name = "null_agent_consuming_wallet_balances"; + let test_name = "null_agent_consuming_transaction_fee_balance_minor"; let mut subject = BlockchainAgentNull::new(); subject.logger = Logger::new(test_name); - let result = subject.consuming_wallet_balances(); + let result = subject.transaction_fee_balance_minor(); - assert_eq!( - result, - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero() - } - ); - assert_error_log(test_name, "consuming_wallet_balances") + assert_eq!(result, U256::zero()); + assert_error_log(test_name, "transaction_fee_balance_minor") } #[test] - fn null_agent_agreed_fee_per_computation_unit() { + fn null_agent_service_fee_balance_minor() { init_test_logging(); - let test_name = "null_agent_agreed_fee_per_computation_unit"; + let test_name = "null_agent_service_fee_balance_minor"; let mut subject = BlockchainAgentNull::new(); subject.logger = Logger::new(test_name); - let result = subject.agreed_fee_per_computation_unit(); + let result = subject.service_fee_balance_minor(); assert_eq!(result, 0); - assert_error_log(test_name, "agreed_fee_per_computation_unit") + assert_error_log(test_name, "service_fee_balance_minor") + } + + #[test] + fn null_agent_gas_price() { + init_test_logging(); + let test_name = "null_agent_gas_price"; + let mut subject = BlockchainAgentNull::new(); + subject.logger = Logger::new(test_name); + + let result = subject.gas_price(); + + assert_eq!(result, 0); + assert_error_log(test_name, "gas_price") + } + + #[test] + fn null_agent_gas_price_margin() { + init_test_logging(); + let test_name = "null_agent_gas_price_margin"; + let mut subject = BlockchainAgentNull::new(); + subject.logger = Logger::new(test_name); + + let result = subject.gas_price_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 d54224352..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 @@ -5,12 +5,16 @@ use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockch 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::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, + gas_price_margin: PurePercentage, maximum_added_gas_margin: u64, consuming_wallet: Wallet, consuming_wallet_balances: ConsumingWalletBalances, @@ -18,20 +22,30 @@ pub struct BlockchainAgentWeb3 { } impl BlockchainAgent for BlockchainAgentWeb3 { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128 { + fn estimated_transaction_fee_per_transaction_minor(&self) -> u128 { let gas_price = self.gas_price_gwei as u128; let max_gas_limit = (self.maximum_added_gas_margin + self.gas_limit_const_part) as u128; - number_of_transactions as u128 * gas_price * max_gas_limit + gwei_to_wei(gas_price * max_gas_limit) } - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + fn transaction_fee_balance_minor(&self) -> U256 { self.consuming_wallet_balances + .transaction_fee_balance_in_minor_units } - fn agreed_fee_per_computation_unit(&self) -> u64 { + fn service_fee_balance_minor(&self) -> u128 { + self.consuming_wallet_balances + .service_fee_balance_in_minor_units + } + + fn gas_price(&self) -> u64 { self.gas_price_gwei } + fn gas_price_margin(&self) -> PurePercentage { + self.gas_price_margin + } + fn consuming_wallet(&self) -> &Wallet { &self.consuming_wallet } @@ -53,11 +67,14 @@ impl BlockchainAgentWeb3 { consuming_wallet_balances: ConsumingWalletBalances, pending_transaction_id: U256, ) -> Self { + 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, + gas_price_margin, consuming_wallet, - maximum_added_gas_margin: WEB3_MAXIMAL_GAS_LIMIT_MARGIN, + maximum_added_gas_margin, consuming_wallet_balances, pending_transaction_id, } @@ -70,10 +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 web3::types::U256; #[test] @@ -88,7 +103,7 @@ mod tests { let consuming_wallet = make_wallet("abcde"); let consuming_wallet_balances = ConsumingWalletBalances { transaction_fee_balance_in_minor_units: U256::from(456_789), - masq_token_balance_in_minor_units: U256::from(123_000_000), + service_fee_balance_in_minor_units: 123_000_000, }; let pending_transaction_id = U256::from(777); @@ -100,11 +115,15 @@ 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.consuming_wallet_balances(), - consuming_wallet_balances + subject.transaction_fee_balance_minor(), + consuming_wallet_balances.transaction_fee_balance_in_minor_units + ); + assert_eq!( + subject.service_fee_balance_minor(), + consuming_wallet_balances.service_fee_balance_in_minor_units ); assert_eq!(subject.pending_transaction_id(), pending_transaction_id) } @@ -114,27 +133,24 @@ mod tests { let consuming_wallet = make_wallet("efg"); let consuming_wallet_balances = ConsumingWalletBalances { transaction_fee_balance_in_minor_units: Default::default(), - masq_token_balance_in_minor_units: Default::default(), + service_fee_balance_in_minor_units: Default::default(), }; let nonce = U256::from(55); let agent = BlockchainAgentWeb3::new( - 444, + 244, 77_777, consuming_wallet, consuming_wallet_balances, nonce, ); - let result = agent.estimated_transaction_fee_total(3); + let result = agent.estimated_transaction_fee_per_transaction_minor(); assert_eq!(agent.gas_limit_const_part, 77_777); assert_eq!( agent.maximum_added_gas_margin, WEB3_MAXIMAL_GAS_LIMIT_MARGIN ); - assert_eq!( - result, - (3 * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) as u128 * 444 - ); + 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 3ded6a16b..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 @@ -1,8 +1,8 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::arbitrary_id_stamp_in_trait; -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; +use masq_lib::percentage::PurePercentage; use web3::types::U256; // Table of chains by @@ -22,9 +22,11 @@ use web3::types::U256; //* defaulted limit pub trait BlockchainAgent: Send { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128; - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances; - fn agreed_fee_per_computation_unit(&self) -> u64; + fn estimated_transaction_fee_per_transaction_minor(&self) -> u128; + fn transaction_fee_balance_minor(&self) -> U256; + fn service_fee_balance_minor(&self) -> u128; + 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 257c88fde..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,9 +6,11 @@ pub mod blockchain_agent; pub mod msgs; pub mod test_utils; -use crate::accountant::payment_adjuster::Adjustment; +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; +use crate::accountant::ResponseSkeleton; use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; use actix::Message; use itertools::Either; @@ -27,28 +29,33 @@ pub trait SolvencySensitivePaymentInstructor { &self, msg: BlockchainAgentWithContextMessage, logger: &Logger, - ) -> Result, String>; + ) -> Option>; fn perform_payment_adjustment( &self, setup: PreparedAdjustment, logger: &Logger, - ) -> OutboundPaymentsInstructions; + ) -> Option; + + fn cancel_scan(&mut self, logger: &Logger); } pub struct PreparedAdjustment { - pub original_setup_msg: BlockchainAgentWithContextMessage, - pub adjustment: Adjustment, + pub agent: Box, + pub response_skeleton_opt: Option, + pub adjustment_analysis: AdjustmentAnalysisReport, } impl PreparedAdjustment { pub fn new( - original_setup_msg: BlockchainAgentWithContextMessage, - adjustment: Adjustment, + agent: Box, + response_skeleton_opt: Option, + adjustment_analysis: AdjustmentAnalysisReport, ) -> Self { Self { - original_setup_msg, - adjustment, + agent, + response_skeleton_opt, + adjustment_analysis, } } } @@ -60,8 +67,9 @@ mod tests { impl Clone for PreparedAdjustment { fn clone(&self) -> Self { Self { - original_setup_msg: self.original_setup_msg.clone(), - adjustment: self.adjustment.clone(), + agent: self.agent.dup(), + response_skeleton_opt: self.response_skeleton_opt, + adjustment_analysis: self.adjustment_analysis.clone(), } } } 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 5de2a6b06..bf6acf656 100644 --- a/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs +++ b/node/src/accountant/scanners/mid_scan_msg_handling/payable_scanner/test_utils.rs @@ -3,37 +3,52 @@ #![cfg(test)] use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; 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::PurePercentage; use std::cell::RefCell; #[derive(Default)] pub struct BlockchainAgentMock { - consuming_wallet_balances_results: RefCell>, - agreed_fee_per_computation_unit_results: RefCell>, + estimated_transaction_fee_per_transaction_minor_results: RefCell>, + transaction_fee_balance_minor_results: RefCell>, + service_fee_balance_minor_results: RefCell>, + gas_price_results: RefCell>, + gas_price_margin_results: RefCell>, consuming_wallet_result_opt: Option, pending_transaction_id_results: RefCell>, arbitrary_id_stamp_opt: Option, } impl BlockchainAgent for BlockchainAgentMock { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - todo!("to be implemented by GH-711") + fn estimated_transaction_fee_per_transaction_minor(&self) -> u128 { + self.estimated_transaction_fee_per_transaction_minor_results + .borrow_mut() + .remove(0) } - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - todo!("to be implemented by GH-711") + fn transaction_fee_balance_minor(&self) -> U256 { + self.transaction_fee_balance_minor_results + .borrow_mut() + .remove(0) } - fn agreed_fee_per_computation_unit(&self) -> u64 { - self.agreed_fee_per_computation_unit_results + fn service_fee_balance_minor(&self) -> u128 { + self.service_fee_balance_minor_results .borrow_mut() .remove(0) } + fn gas_price(&self) -> u64 { + self.gas_price_results.borrow_mut().remove(0) + } + + fn gas_price_margin(&self) -> PurePercentage { + self.gas_price_margin_results.borrow_mut().remove(0) + } + fn consuming_wallet(&self) -> &Wallet { self.consuming_wallet_result_opt.as_ref().unwrap() } @@ -50,20 +65,37 @@ impl BlockchainAgent for BlockchainAgentMock { } impl BlockchainAgentMock { - pub fn consuming_wallet_balances_result(self, result: ConsumingWalletBalances) -> Self { - self.consuming_wallet_balances_results + pub fn estimated_transaction_fee_per_transaction_minor_result(self, result: u128) -> Self { + self.estimated_transaction_fee_per_transaction_minor_results .borrow_mut() .push(result); self } - pub fn agreed_fee_per_computation_unit_result(self, result: u64) -> Self { - self.agreed_fee_per_computation_unit_results + pub fn transaction_fee_balance_minor_result(self, result: U256) -> Self { + self.transaction_fee_balance_minor_results .borrow_mut() .push(result); self } + pub fn service_fee_balance_minor_result(self, result: u128) -> Self { + self.service_fee_balance_minor_results + .borrow_mut() + .push(result); + self + } + + pub fn gas_price_result(self, result: u64) -> Self { + self.gas_price_results.borrow_mut().push(result); + self + } + + pub fn gas_price_margin_result(self, result: PurePercentage) -> Self { + self.gas_price_margin_results.borrow_mut().push(result); + self + } + pub fn consuming_wallet_result(mut self, consuming_wallet_result: Wallet) -> Self { self.consuming_wallet_result_opt = Some(consuming_wallet_result); self diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index f1341ed10..3b50910af 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -13,18 +13,18 @@ use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableT }; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ debugging_summary_after_error_separation, err_msg_for_failure_with_expected_but_missing_fingerprints, - investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, - separate_errors, separate_rowids_and_hashes, PayableThresholdsGauge, - PayableThresholdsGaugeReal, PayableTransactingErrorEnum, PendingPayableMetadata, + investigate_debt_extremes, mark_pending_payable_fatal_error, payables_debug_summary, separate_errors, + separate_rowids_and_hashes, PayableThresholdsGaugeReal, PayableTransactingErrorEnum, + PendingPayableMetadata, PayableInspector }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{ elapsed_in_ms, handle_none_status, handle_status_with_failure, handle_status_with_success, PendingPayableScanReport, }; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; -use crate::accountant::PendingPayableId; +use crate::accountant::{CreditorThresholds, gwei_to_wei, PendingPayableId, QualifiedPayableAccount}; use crate::accountant::{ - comma_joined_stringifiable, gwei_to_wei, Accountant, ReceivedPayments, + comma_joined_stringifiable, Accountant, ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayables, }; @@ -76,6 +76,7 @@ impl Scanners { dao_factories.payable_dao_factory.make(), dao_factories.pending_payable_dao_factory.make(), Rc::clone(&payment_thresholds), + PayableInspector::new(Box::new(PayableThresholdsGaugeReal::default())), Box::new(PaymentAdjusterReal::new()), )); @@ -128,7 +129,9 @@ where pub struct ScannerCommon { initiated_at_opt: Option, - pub payment_thresholds: Rc, + // TODO The thresholds probably shouldn't be in ScannerCommon because the PendingPayableScanner + // does not need it + payment_thresholds: Rc, } impl ScannerCommon { @@ -186,7 +189,7 @@ pub struct PayableScanner { pub common: ScannerCommon, pub payable_dao: Box, pub pending_payable_dao: Box, - pub payable_threshold_gauge: Box, + pub payable_inspector: PayableInspector, pub payment_adjuster: Box, } @@ -265,22 +268,43 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { &self, msg: BlockchainAgentWithContextMessage, logger: &Logger, - ) -> Result, String> { + ) -> Option> { + let protected = msg.protected_qualified_payables; + let unprotected = self.expose_payables(protected); + match self .payment_adjuster - .search_for_indispensable_adjustment(&msg, logger) + .consider_adjustment(unprotected, &*msg.agent) { - Ok(None) => { - let protected = msg.protected_qualified_payables; - let unprotected = self.expose_payables(protected); - Ok(Either::Left(OutboundPaymentsInstructions::new( - unprotected, - msg.agent, - msg.response_skeleton_opt, - ))) + Ok(processed) => { + let either = match processed { + Either::Left(verified_payables) => { + Either::Left(OutboundPaymentsInstructions::new( + Either::Left(verified_payables), + msg.agent, + msg.response_skeleton_opt, + )) + } + Either::Right(adjustment_analysis) => { + let prepared_adjustment = PreparedAdjustment::new( + msg.agent, + msg.response_skeleton_opt, + adjustment_analysis, + ); + Either::Right(prepared_adjustment) + } + }; + + Some(either) + } + Err(e) => { + if e.insolvency_detected() { + warning!(logger, "{}. Details: {}.", Self::ADD_MORE_FUNDS_URGE, e) + } else { + unimplemented!("This situation is not possible yet, but may be in the future") + } + None } - Ok(Some(adjustment)) => Ok(Either::Right(PreparedAdjustment::new(msg, adjustment))), - Err(_e) => todo!("be implemented with GH-711"), } } @@ -288,9 +312,28 @@ impl SolvencySensitivePaymentInstructor for PayableScanner { &self, setup: PreparedAdjustment, logger: &Logger, - ) -> OutboundPaymentsInstructions { - let now = SystemTime::now(); - self.payment_adjuster.adjust_payments(setup, now, logger) + ) -> Option { + match self.payment_adjuster.adjust_payments(setup) { + Ok(instructions) => Some(instructions), + Err(e) => { + warning!( + logger, + "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 unable to generate payment instructions. It looks like only \ + the user can resolve this issue." + ); + self.mark_as_ended(logger) } } @@ -301,13 +344,14 @@ impl PayableScanner { payable_dao: Box, pending_payable_dao: Box, payment_thresholds: Rc, + payable_inspector: PayableInspector, payment_adjuster: Box, ) -> Self { Self { common: ScannerCommon::new(payment_thresholds), payable_dao, pending_payable_dao, - payable_threshold_gauge: Box::new(PayableThresholdsGaugeReal::default()), + payable_inspector, payment_adjuster, } } @@ -316,62 +360,44 @@ impl PayableScanner { &self, non_pending_payables: Vec, logger: &Logger, - ) -> Vec { - fn pass_payables_and_drop_points( - qp_tp: impl Iterator, - ) -> Vec { - let (payables, _) = qp_tp.unzip::<_, _, Vec, Vec<_>>(); - payables - } - - let qualified_payables_and_points_uncollected = - non_pending_payables.into_iter().flat_map(|account| { - self.payable_exceeded_threshold(&account, SystemTime::now()) - .map(|threshold_point| (account, threshold_point)) - }); + ) -> Vec { + let now = SystemTime::now(); + let qualified_payables = non_pending_payables + .into_iter() + .flat_map(|account| self.try_qualify_account(account, now)) + .collect(); match logger.debug_enabled() { - false => pass_payables_and_drop_points(qualified_payables_and_points_uncollected), + false => qualified_payables, true => { - let qualified_and_points_collected = - qualified_payables_and_points_uncollected.collect_vec(); - payables_debug_summary(&qualified_and_points_collected, logger); - pass_payables_and_drop_points(qualified_and_points_collected.into_iter()) + payables_debug_summary(&qualified_payables, logger); + qualified_payables } } } - fn payable_exceeded_threshold( + fn try_qualify_account( &self, - payable: &PayableAccount, + account: PayableAccount, now: SystemTime, - ) -> Option { - let debt_age = now - .duration_since(payable.last_paid_timestamp) - .expect("Internal error") - .as_secs(); - - if self.payable_threshold_gauge.is_innocent_age( - debt_age, - self.common.payment_thresholds.maturity_threshold_sec, - ) { - return None; - } + ) -> Option { + let intercept_opt = self.payable_exceeded_threshold(&account, now); - if self.payable_threshold_gauge.is_innocent_balance( - payable.balance_wei, - gwei_to_wei(self.common.payment_thresholds.permanent_debt_allowed_gwei), - ) { - return None; - } + 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) + }) + } - let threshold = self - .payable_threshold_gauge - .calculate_payout_threshold_in_gwei(&self.common.payment_thresholds, debt_age); - if payable.balance_wei > threshold { - Some(threshold) - } else { - None - } + fn payable_exceeded_threshold( + &self, + account: &PayableAccount, + now: SystemTime, + ) -> Option { + 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>( @@ -432,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) { @@ -547,13 +573,17 @@ impl PayableScanner { }; } - fn protect_payables(&self, payables: Vec) -> Obfuscated { + fn protect_payables(&self, payables: Vec) -> Obfuscated { Obfuscated::obfuscate_vector(payables) } - fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { + fn expose_payables(&self, obfuscated: Obfuscated) -> Vec { obfuscated.expose_vector() } + + const ADD_MORE_FUNDS_URGE: &'static str = + "Add more funds into your consuming wallet to become able to repay already matured debts \ + as the creditors would respond by a delinquency ban otherwise"; } pub struct PendingPayableScanner { @@ -589,7 +619,7 @@ impl Scanner for PendingP filtered_pending_payable.len() ); Ok(RequestTransactionReceipts { - pending_payable: filtered_pending_payable, + pending_payables: filtered_pending_payable, response_skeleton_opt, }) } @@ -752,8 +782,8 @@ impl PendingPayableScanner { Ok(_) => warning!( logger, "Broken transactions {} marked as an error. You should take over the care \ - of those to make sure your debts are going to be settled properly. At the moment, \ - there is no automated process fixing that without your assistance", + of those to make sure your debts are going to be settled properly. At the \ + moment, there is no automated process fixing that without your assistance", PendingPayableId::serialize_hashes_to_string(&ids) ), Err(e) => panic!( @@ -1099,26 +1129,30 @@ mod tests { use crate::accountant::db_access_objects::pending_payable_dao::{ PendingPayable, PendingPayableDaoError, TransactionHashes, }; - use crate::accountant::db_access_objects::utils::{from_time_t, to_time_t}; + use crate::accountant::db_access_objects::utils::{from_time_t, now_time_t, to_time_t}; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; - use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PendingPayableMetadata; + use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + PayableInspector, PayableThresholdsGauge, PayableThresholdsGaugeReal, + PendingPayableMetadata, + }; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::PendingPayableScanReport; - use crate::accountant::scanners::test_utils::protect_payables_in_test; + use crate::accountant::scanners::test_utils::protect_qualified_payables_in_test; use crate::accountant::scanners::{ BeginScanError, PayableScanner, PendingPayableScanner, ReceivableScanner, ScanSchedulers, Scanner, ScannerCommon, Scanners, }; use crate::accountant::test_utils::{ - make_custom_payment_thresholds, make_payable_account, make_payables, - make_pending_payable_fingerprint, make_receivable_account, BannedDaoFactoryMock, + make_custom_payment_thresholds, make_payable_account, make_pending_payable_fingerprint, + make_qualified_and_unqualified_payables, make_receivable_account, BannedDaoFactoryMock, BannedDaoMock, ConfigDaoFactoryMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PayableThresholdsGaugeMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, PendingPayableScannerBuilder, ReceivableDaoFactoryMock, ReceivableDaoMock, ReceivableScannerBuilder, }; use crate::accountant::{ - gwei_to_wei, PendingPayableId, ReceivedPayments, ReportTransactionReceipts, - RequestTransactionReceipts, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, + gwei_to_wei, CreditorThresholds, PendingPayableId, QualifiedPayableAccount, + ReceivedPayments, ReportTransactionReceipts, RequestTransactionReceipts, SentPayables, + DEFAULT_PENDING_TOO_LONG_SEC, }; use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; @@ -1156,14 +1190,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() @@ -1261,11 +1296,22 @@ mod tests { #[test] fn protected_payables_can_be_cast_from_and_back_to_vec_of_payable_accounts_by_payable_scanner() { - let initial_unprotected = vec![make_payable_account(123), make_payable_account(456)]; + let initial_unprotected = vec![ + QualifiedPayableAccount::new( + make_payable_account(123), + 123456789, + CreditorThresholds::new(11111111), + ), + QualifiedPayableAccount::new( + make_payable_account(456), + 987654321, + CreditorThresholds::new(22222222), + ), + ]; let subject = PayableScannerBuilder::new().build(); let protected = subject.protect_payables(initial_unprotected.clone()); - let again_unprotected: Vec = subject.expose_payables(protected); + let again_unprotected: Vec = subject.expose_payables(protected); assert_eq!(initial_unprotected, again_unprotected) } @@ -1276,11 +1322,12 @@ mod tests { let test_name = "payable_scanner_can_initiate_a_scan"; let now = SystemTime::now(); let (qualified_payable_accounts, _, all_non_pending_payables) = - make_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_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let result = subject.begin_scan(now, None, &Logger::new(test_name)); @@ -1290,7 +1337,7 @@ mod tests { assert_eq!( result, Ok(QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test( + protected_qualified_payables: protect_qualified_payables_in_test( qualified_payable_accounts.clone() ), response_skeleton_opt: None, @@ -1308,11 +1355,13 @@ mod tests { #[test] fn payable_scanner_throws_error_when_a_scan_is_already_running() { let now = SystemTime::now(); - let (_, _, all_non_pending_payables) = make_payables(now, &PaymentThresholds::default()); + let (_, _, all_non_pending_payables) = + 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_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let _result = subject.begin_scan(now, None, &Logger::new("test")); @@ -1330,11 +1379,12 @@ mod tests { fn payable_scanner_throws_error_in_case_no_qualified_payable_is_found() { let now = SystemTime::now(); let (_, unqualified_payable_accounts, _) = - make_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_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let result = subject.begin_scan(now, None, &Logger::new("test")); @@ -1993,7 +2043,7 @@ mod tests { .is_innocent_age_params(&is_innocent_age_params_arc) .is_innocent_age_result(true); let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + subject.payable_inspector = PayableInspector::new(Box::new(payable_thresholds_gauge)); let now = SystemTime::now(); let debt_age_s = 111_222; let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); @@ -2024,7 +2074,7 @@ mod tests { .is_innocent_balance_params(&is_innocent_balance_params_arc) .is_innocent_balance_result(true); let mut subject = PayableScannerBuilder::new().build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + subject.payable_inspector = PayableInspector::new(Box::new(payable_thresholds_gauge)); let now = SystemTime::now(); let debt_age_s = 3_456; let last_paid_timestamp = now.checked_sub(Duration::from_secs(debt_age_s)).unwrap(); @@ -2075,19 +2125,17 @@ mod tests { }; let payable_thresholds_gauge = PayableThresholdsGaugeMock::default() .is_innocent_age_params(&is_innocent_age_params_arc) - .is_innocent_age_result( - debt_age_s <= custom_payment_thresholds.maturity_threshold_sec as u64, - ) + .is_innocent_age_result(debt_age_s <= custom_payment_thresholds.maturity_threshold_sec) .is_innocent_balance_params(&is_innocent_balance_params_arc) .is_innocent_balance_result( balance <= gwei_to_wei(custom_payment_thresholds.permanent_debt_allowed_gwei), ) .calculate_payout_threshold_in_gwei_params(&calculate_payable_threshold_params_arc) - .calculate_payout_threshold_in_gwei_result(4567898); //made up value + .calculate_payout_threshold_in_gwei_result(4567898); // Made up value let mut subject = PayableScannerBuilder::new() .payment_thresholds(custom_payment_thresholds) .build(); - subject.payable_threshold_gauge = Box::new(payable_thresholds_gauge); + subject.payable_inspector = PayableInspector::new(Box::new(payable_thresholds_gauge)); let result = subject.payable_exceeded_threshold(&payable_account, now); @@ -2098,7 +2146,7 @@ mod tests { assert_eq!(debt_age_returned_innocent, debt_age_s); assert_eq!( curve_derived_time, - custom_payment_thresholds.maturity_threshold_sec as u64 + custom_payment_thresholds.maturity_threshold_sec ); let is_innocent_balance_params = is_innocent_balance_params_arc.lock().unwrap(); assert_eq!( @@ -2132,6 +2180,7 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified_and_the_threshold_value_is_returned"; @@ -2149,32 +2198,40 @@ mod tests { fn payable_with_debt_above_the_slope_is_qualified() { init_test_logging(); let payment_thresholds = PaymentThresholds::default(); + let now = SystemTime::now(); let debt = gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1); - let time = (payment_thresholds.maturity_threshold_sec + let debt_age = (payment_thresholds.maturity_threshold_sec + payment_thresholds.threshold_interval_sec - 1) as i64; - let qualified_payable = PayableAccount { + let payable = PayableAccount { wallet: make_wallet("wallet0"), balance_wei: debt, - last_paid_timestamp: from_time_t(time), + last_paid_timestamp: from_time_t(to_time_t(now) - debt_age), pending_payable_opt: None, }; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let test_name = "payable_with_debt_above_the_slope_is_qualified"; let logger = Logger::new(test_name); - let result = subject.sniff_out_alarming_payables_and_maybe_log_them( - vec![qualified_payable.clone()], - &logger, - ); + let result = + subject.sniff_out_alarming_payables_and_maybe_log_them(vec![payable.clone()], &logger); - assert_eq!(result, vec![qualified_payable]); + assert_eq!(result.len(), 1); + let expected_intercept = PayableThresholdsGaugeReal::default() + .calculate_payout_threshold_in_gwei(&payment_thresholds, debt_age as u64); + let expected_qualified_payable = QualifiedPayableAccount::new( + payable, + expected_intercept, + CreditorThresholds::new(gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei)), + ); + assert_eq!(&result[0], &expected_qualified_payable); TestLogHandler::new().exists_log_matching(&format!( "DEBUG: {}: Paying qualified debts:\n999,999,999,000,000,\ - 000 wei owed for \\d+ sec exceeds threshold: 500,000,000,000,000,000 wei; creditor: \ - 0x0000000000000000000000000077616c6c657430", + 000 wei owed for \\d+ sec exceeds threshold: 500,023,148,148,151,\\d{{3}} wei; \ + creditor: 0x0000000000000000000000000077616c6c657430", test_name )); } @@ -2195,6 +2252,7 @@ mod tests { }]; let subject = PayableScannerBuilder::new() .payment_thresholds(payment_thresholds) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) .build(); let logger = Logger::new(test_name); @@ -2206,6 +2264,60 @@ mod tests { .exists_no_log_containing(&format!("DEBUG: {test_name}: Paying qualified debts")); } + #[test] + 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, + payment_grace_period_sec: 0, + permanent_debt_allowed_gwei: 1_000_000_000, + threshold_interval_sec: 1_000, + unban_below_gwei: 0, + }; + let wallet = make_wallet("abc"); + // It is important to have a payable lying in the declining part of the thresholds, also + // it will be more believable the steeper we have the slope because then a single second + // can make a certain difference for the intercept value, which is the value this test + // compares for carrying out the conclusion + let debt_age = payment_thresholds.maturity_threshold_sec + + (payment_thresholds.threshold_interval_sec / 2); + let payable = PayableAccount { + wallet: wallet.clone(), + balance_wei: gwei_to_wei(12_000_000_000_u64), + last_paid_timestamp: from_time_t(now_time_t() - debt_age as i64), + pending_payable_opt: None, + }; + let subject = PayableScannerBuilder::new() + .payment_thresholds(payment_thresholds) + .payable_threshold_gauge(Box::new(PayableThresholdsGaugeReal::default())) + .build(); + let intercept_before = subject + .payable_exceeded_threshold(&payable, SystemTime::now()) + .unwrap(); + + let result = subject.sniff_out_alarming_payables_and_maybe_log_them( + vec![payable.clone()], + &Logger::new("test"), + ); + + let intercept_after = subject + .payable_exceeded_threshold(&payable, SystemTime::now()) + .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 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 + ) + } + #[test] fn pending_payable_scanner_can_initiate_a_scan() { init_test_logging(); @@ -2242,7 +2354,7 @@ mod tests { assert_eq!( result, Ok(RequestTransactionReceipts { - pending_payable: fingerprints, + pending_payables: fingerprints, response_skeleton_opt: None }) ); diff --git a/node/src/accountant/scanners/scanners_utils.rs b/node/src/accountant/scanners/scanners_utils.rs index 897015dec..748879593 100644 --- a/node/src/accountant/scanners/scanners_utils.rs +++ b/node/src/accountant/scanners/scanners_utils.rs @@ -6,7 +6,7 @@ pub mod payable_scanner_utils { use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableTransactingErrorEnum::{ LocallyCausedError, RemotelyCausedErrors, }; - use crate::accountant::{comma_joined_stringifiable, ProcessedPayableFallible, SentPayables}; + use crate::accountant::{comma_joined_stringifiable, gwei_to_wei, ProcessedPayableFallible, QualifiedPayableAccount, SentPayables}; use crate::sub_lib::accountant::PaymentThresholds; use crate::sub_lib::wallet::Wallet; use itertools::Itertools; @@ -26,7 +26,7 @@ pub mod payable_scanner_utils { RemotelyCausedErrors(Vec), } - //debugging purposes only + // Debug purposes only pub fn investigate_debt_extremes( timestamp: SystemTime, all_non_pending_payables: &[PayableAccount], @@ -119,7 +119,7 @@ pub mod payable_scanner_utils { batch_request_responses: &'a [ProcessedPayableFallible], logger: &'b Logger, ) -> (Vec<&'a PendingPayable>, Option>) { - //TODO maybe we can return not tuple but struct with remote_errors_opt member + //TODO maybe we can return not a tuple but struct with remote_errors_opt member let (oks, errs) = batch_request_responses .iter() .fold((vec![], vec![]), |acc, rpc_result| { @@ -166,7 +166,7 @@ pub mod payable_scanner_utils { } } - pub fn payables_debug_summary(qualified_accounts: &[(PayableAccount, u128)], logger: &Logger) { + pub fn payables_debug_summary(qualified_accounts: &[QualifiedPayableAccount], logger: &Logger) { if qualified_accounts.is_empty() { return; } @@ -174,16 +174,19 @@ pub mod payable_scanner_utils { let now = SystemTime::now(); qualified_accounts .iter() - .map(|(payable, threshold_point)| { + .map(|qualified_account| { + let account = &qualified_account.bare_account; let p_age = now - .duration_since(payable.last_paid_timestamp) + .duration_since(account.last_paid_timestamp) .expect("Payable time is corrupt"); format!( "{} wei owed for {} sec exceeds threshold: {} wei; creditor: {}", - payable.balance_wei.separate_with_commas(), + account.balance_wei.separate_with_commas(), p_age.as_secs(), - threshold_point.separate_with_commas(), - payable.wallet + qualified_account + .payment_threshold_intercept_minor + .separate_with_commas(), + account.wallet ) }) .join("\n") @@ -272,6 +275,58 @@ pub mod payable_scanner_utils { ids_of_payments.into_iter().unzip() } + pub struct PayableInspector { + payable_threshold_gauge: Box, + } + + impl PayableInspector { + pub fn new(payable_threshold_gauge: Box) -> Self { + Self { + payable_threshold_gauge, + } + } + pub fn payable_exceeded_threshold( + &self, + payable: &PayableAccount, + payment_thresholds: &PaymentThresholds, + now: SystemTime, + ) -> Option { + let debt_age = now + .duration_since(payable.last_paid_timestamp) + .expect("Internal error") + .as_secs(); + + if self + .payable_threshold_gauge + .is_innocent_age(debt_age, payment_thresholds.maturity_threshold_sec) + { + return None; + } + + if self.payable_threshold_gauge.is_innocent_balance( + payable.balance_wei, + gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei), + ) { + return None; + } + + let threshold = self + .payable_threshold_gauge + .calculate_payout_threshold_in_gwei(payment_thresholds, debt_age); + + eprintln!( + "Balance wei: {}\n\ + Threshold: {}", + payable.balance_wei, threshold + ); + if payable.balance_wei > threshold { + Some(threshold) + } else { + None + } + } + } + pub trait PayableThresholdsGauge { fn is_innocent_age(&self, age: u64, limit: u64) -> bool; fn is_innocent_balance(&self, balance: u128, limit: u128) -> bool; @@ -428,7 +483,7 @@ mod tests { PayableThresholdsGaugeReal, }; use crate::accountant::scanners::scanners_utils::receivable_scanner_utils::balance_and_age; - use crate::accountant::{checked_conversion, gwei_to_wei, SentPayables}; + use crate::accountant::{CreditorThresholds, gwei_to_wei, QualifiedPayableAccount, SentPayables}; use crate::blockchain::test_utils::make_tx_hash; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; @@ -588,51 +643,40 @@ mod tests { fn payables_debug_summary_prints_pretty_summary() { init_test_logging(); let now = to_time_t(SystemTime::now()); - let payment_thresholds = PaymentThresholds { - threshold_interval_sec: 2_592_000, - debt_threshold_gwei: 1_000_000_000, - payment_grace_period_sec: 86_400, - maturity_threshold_sec: 86_400, - permanent_debt_allowed_gwei: 10_000_000, - unban_below_gwei: 10_000_000, - }; let qualified_payables_and_threshold_points = vec![ - ( - PayableAccount { + QualifiedPayableAccount { + bare_account: PayableAccount { wallet: make_wallet("wallet0"), - balance_wei: gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei + 2000), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec - + payment_thresholds.threshold_interval_sec, - ), - ), + balance_wei: gwei_to_wei(10_002_000_u64), + last_paid_timestamp: from_time_t(now - 2678400), pending_payable_opt: None, }, - 10_000_000_001_152_000_u128, - ), - ( - PayableAccount { + payment_threshold_intercept_minor: 10_000_000_001_152_000_u128, + creditor_thresholds: CreditorThresholds { + permanent_debt_allowed_minor: 333_333, + }, + }, + QualifiedPayableAccount { + bare_account: PayableAccount { wallet: make_wallet("wallet1"), - balance_wei: gwei_to_wei(payment_thresholds.debt_threshold_gwei - 1), - last_paid_timestamp: from_time_t( - now - checked_conversion::( - payment_thresholds.maturity_threshold_sec + 55, - ), - ), + balance_wei: gwei_to_wei(999_999_999_u64), + last_paid_timestamp: from_time_t(now - 86455), pending_payable_opt: None, }, - 999_978_993_055_555_580, - ), + payment_threshold_intercept_minor: 999_978_993_055_555_580, + creditor_thresholds: CreditorThresholds { + permanent_debt_allowed_minor: 10_000_000, + }, + }, ]; let logger = Logger::new("test"); payables_debug_summary(&qualified_payables_and_threshold_points, &logger); - TestLogHandler::new().exists_log_containing("Paying qualified debts:\n\ - 10,002,000,000,000,000 wei owed for 2678400 sec exceeds threshold: \ + TestLogHandler::new().exists_log_matching("Paying qualified debts:\n\ + 10,002,000,000,000,000 wei owed for 267840\\d sec exceeds threshold: \ 10,000,000,001,152,000 wei; creditor: 0x0000000000000000000000000077616c6c657430\n\ - 999,999,999,000,000,000 wei owed for 86455 sec exceeds threshold: \ + 999,999,999,000,000,000 wei owed for 8645\\d sec exceeds threshold: \ 999,978,993,055,555,580 wei; creditor: 0x0000000000000000000000000077616c6c657431"); } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index c43d6f71b..e9f9a3194 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -2,9 +2,9 @@ #![cfg(test)] -use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::QualifiedPayableAccount; use masq_lib::type_obfuscation::Obfuscated; -pub fn protect_payables_in_test(payables: Vec) -> Obfuscated { +pub fn protect_qualified_payables_in_test(payables: Vec) -> Obfuscated { Obfuscated::obfuscate_vector(payables) } diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index 38c7c56ec..02d5a192a 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -13,20 +13,27 @@ 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::{Adjustment, AnalysisError, PaymentAdjuster}; +use crate::accountant::payment_adjuster::test_utils::exposed_utils::convert_qualified_p_into_analyzed_p; +use crate::accountant::payment_adjuster::{ + 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::{ BlockchainAgentWithContextMessage, QualifiedPayablesMessage, }; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::{ MultistagePayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor, }; -use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; +use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{ + PayableInspector, PayableThresholdsGauge, PayableThresholdsGaugeReal, +}; use crate::accountant::scanners::{ BeginScanError, PayableScanner, PendingPayableScanner, PeriodicalScanScheduler, ReceivableScanner, ScanSchedulers, Scanner, }; use crate::accountant::{ - gwei_to_wei, Accountant, ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, + gwei_to_wei, Accountant, AnalyzedPayableAccount, CreditorThresholds, QualifiedPayableAccount, + ResponseSkeleton, SentPayables, DEFAULT_PENDING_TOO_LONG_SEC, }; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::data_structures::BlockchainTransaction; @@ -42,6 +49,7 @@ use crate::sub_lib::utils::NotifyLaterHandle; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_wallet; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; +use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; use crate::test_utils::unshared_test_utils::make_bc_with_defaults; use actix::{Message, System}; use ethereum_types::H256; @@ -95,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, } } } @@ -208,39 +216,8 @@ fn fill_vacancies_with_given_or_default_daos( make_queue_for_factory } -macro_rules! create_or_update_factory { - ( - $dao_set: expr, //Vec> - $dao_initialization_order_in_regard_to_accountant: expr, //[DestinationMarker;N] - $factory_field_in_builder: ident, //Option - $dao_factory_mock: ident, // XxxDaoFactoryMock - $dao_trait: ident, - $self: expr //mut AccountantBuilder - ) => {{ - let make_queue_uncast = 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 $self.$factory_field_in_builder.take() { - Some(existing_factory) => { - existing_factory.make_results.replace(finished_make_queue); - existing_factory - } - None => { - let mut new_factory = $dao_factory_mock::new(); - new_factory.make_results = RefCell::new(finished_make_queue); - new_factory - } - }; - $self.$factory_field_in_builder = Some(ready_factory); - $self - }}; +pub trait DaoFactoryWithMakeReplace { + fn replace_make_results(&self, results: Vec>); } const PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER: [DestinationMarker; 3] = [ @@ -262,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 } @@ -275,86 +252,94 @@ impl AccountantBuilder { mut self, specially_configured_daos: Vec>, ) -> Self { - create_or_update_factory!( + Self::create_or_update_factory( specially_configured_daos, PENDING_PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - pending_payable_dao_factory, - PendingPayableDaoFactoryMock, - PendingPayableDao, - self - ) + &mut self.pending_payable_dao_factory_opt, + ); + self } pub fn payable_daos( mut self, specially_configured_daos: Vec>, ) -> Self { - create_or_update_factory!( + Self::create_or_update_factory( specially_configured_daos, PAYABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - payable_dao_factory, - PayableDaoFactoryMock, - PayableDao, - self - ) + &mut self.payable_dao_factory_opt, + ); + self } pub fn receivable_daos( mut self, specially_configured_daos: Vec>, ) -> Self { - create_or_update_factory!( + Self::create_or_update_factory( specially_configured_daos, RECEIVABLE_DAOS_ACCOUNTANT_INITIALIZATION_ORDER, - receivable_dao_factory, - ReceivableDaoFactoryMock, - ReceivableDao, - self - ) + &mut self.receivable_dao_factory_opt, + ); + self } - //TODO this method seems to be never used? - pub fn banned_dao(mut self, banned_dao: BannedDaoMock) -> Self { - match self.banned_dao_factory { - None => { - self.banned_dao_factory = Some(BannedDaoFactoryMock::new().make_result(banned_dao)) + fn create_or_update_factory( + dao_set: Vec>, + dao_initialization_order_in_regard_to_accountant: [DestinationMarker; N], + existing_dao_factory_mock_opt: &mut Option, + ) where + DAO: Default, + DAOFactory: DaoFactoryWithMakeReplace + Default, + { + let finished_make_queue: Vec> = fill_vacancies_with_given_or_default_daos( + dao_initialization_order_in_regard_to_accountant, + dao_set, + ); + + let ready_factory = match existing_dao_factory_mock_opt.take() { + Some(existing_factory) => { + existing_factory.replace_make_results(finished_make_queue); + existing_factory } - Some(banned_dao_factory) => { - self.banned_dao_factory = Some(banned_dao_factory.make_result(banned_dao)) + None => { + let new_factory = DAOFactory::default(); + new_factory.replace_make_results(finished_make_queue); + new_factory } - } - self + }; + 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, @@ -366,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; } @@ -376,7 +361,13 @@ impl AccountantBuilder { pub struct PayableDaoFactoryMock { make_params: Arc>>, - make_results: RefCell>>, + make_results: RefCell>>, +} + +impl Default for PayableDaoFactoryMock { + fn default() -> Self { + Self::new() + } } impl PayableDaoFactory for PayableDaoFactoryMock { @@ -391,6 +382,12 @@ impl PayableDaoFactory for PayableDaoFactoryMock { } } +impl DaoFactoryWithMakeReplace for PayableDaoFactoryMock { + fn replace_make_results(&self, results: Vec>) { + self.make_results.replace(results); + } +} + impl PayableDaoFactoryMock { pub fn new() -> Self { Self { @@ -410,9 +407,63 @@ impl PayableDaoFactoryMock { } } +pub struct PendingPayableDaoFactoryMock { + make_params: Arc>>, + 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 { + panic!( + "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." + ) + }; + self.make_params.lock().unwrap().push(()); + self.make_results.borrow_mut().remove(0) + } +} + +impl DaoFactoryWithMakeReplace for PendingPayableDaoFactoryMock { + fn replace_make_results(&self, results: Vec>) { + self.make_results.replace(results); + } +} + +impl PendingPayableDaoFactoryMock { + pub fn new() -> Self { + Self { + make_params: Arc::new(Mutex::new(vec![])), + make_results: RefCell::new(vec![]), + } + } + + pub fn make_params(mut self, params: &Arc>>) -> Self { + self.make_params = params.clone(); + self + } + + pub fn make_result(self, result: PendingPayableDaoMock) -> Self { + self.make_results.borrow_mut().push(Box::new(result)); + self + } +} + pub struct ReceivableDaoFactoryMock { make_params: Arc>>, - make_results: RefCell>>, + make_results: RefCell>>, +} + +impl Default for ReceivableDaoFactoryMock { + fn default() -> Self { + Self::new() + } } impl ReceivableDaoFactory for ReceivableDaoFactoryMock { @@ -427,6 +478,12 @@ impl ReceivableDaoFactory for ReceivableDaoFactoryMock { } } +impl DaoFactoryWithMakeReplace for ReceivableDaoFactoryMock { + fn replace_make_results(&self, results: Vec>) { + self.make_results.replace(results); + } +} + impl ReceivableDaoFactoryMock { pub fn new() -> Self { Self { @@ -1037,46 +1094,11 @@ impl PendingPayableDaoMock { } } -pub struct PendingPayableDaoFactoryMock { - make_params: Arc>>, - make_results: RefCell>>, -} - -impl PendingPayableDaoFactory for PendingPayableDaoFactoryMock { - fn make(&self) -> Box { - if self.make_results.borrow().len() == 0 { - panic!( - "PendingPayableDao Missing. This problem mostly occurs when PendingPayableDao is only supplied for Accountant and not for the Scanner while building Accountant." - ) - }; - self.make_params.lock().unwrap().push(()); - self.make_results.borrow_mut().remove(0) - } -} - -impl PendingPayableDaoFactoryMock { - pub fn new() -> Self { - Self { - make_params: Arc::new(Mutex::new(vec![])), - make_results: RefCell::new(vec![]), - } - } - - pub fn make_params(mut self, params: &Arc>>) -> Self { - self.make_params = params.clone(); - self - } - - pub fn make_result(self, result: PendingPayableDaoMock) -> Self { - self.make_results.borrow_mut().push(Box::new(result)); - self - } -} - pub struct PayableScannerBuilder { payable_dao: PayableDaoMock, pending_payable_dao: PendingPayableDaoMock, payment_thresholds: PaymentThresholds, + payable_inspector: PayableInspector, payment_adjuster: PaymentAdjusterMock, } @@ -1086,6 +1108,9 @@ impl PayableScannerBuilder { payable_dao: PayableDaoMock::new(), pending_payable_dao: PendingPayableDaoMock::new(), payment_thresholds: PaymentThresholds::default(), + payable_inspector: PayableInspector::new(Box::new( + PayableThresholdsGaugeMock::default(), + )), payment_adjuster: PaymentAdjusterMock::default(), } } @@ -1108,6 +1133,14 @@ impl PayableScannerBuilder { self } + pub fn payable_threshold_gauge( + mut self, + payable_threshold_gauge: Box, + ) -> Self { + self.payable_inspector = PayableInspector::new(payable_threshold_gauge); + self + } + pub fn pending_payable_dao( mut self, pending_payable_dao: PendingPayableDaoMock, @@ -1121,6 +1154,7 @@ impl PayableScannerBuilder { Box::new(self.payable_dao), Box::new(self.pending_payable_dao), Rc::new(self.payment_thresholds), + self.payable_inspector, Box::new(self.payment_adjuster), ) } @@ -1254,11 +1288,11 @@ pub fn make_pending_payable_fingerprint() -> PendingPayableFingerprint { } } -pub fn make_payables( +pub fn make_qualified_and_unqualified_payables( now: SystemTime, payment_thresholds: &PaymentThresholds, ) -> ( - Vec, + Vec, Vec, Vec, ) { @@ -1270,7 +1304,7 @@ pub fn make_payables( ), pending_payable_opt: None, }]; - let qualified_payable_accounts = vec![ + let payable_accounts_to_qualify = vec![ PayableAccount { wallet: make_wallet("wallet2"), balance_wei: gwei_to_wei( @@ -1292,9 +1326,11 @@ pub fn make_payables( pending_payable_opt: None, }, ]; + 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(qualified_payable_accounts.clone()); + all_non_pending_payables.extend(payable_accounts_to_qualify); all_non_pending_payables.extend(unqualified_payable_accounts.clone()); ( @@ -1435,71 +1471,58 @@ pub fn trick_rusqlite_with_read_only_conn( #[derive(Default)] pub struct PaymentAdjusterMock { - search_for_indispensable_adjustment_params: - Arc>>, - search_for_indispensable_adjustment_results: - RefCell, AnalysisError>>>, - adjust_payments_params: Arc>>, - adjust_payments_results: RefCell>, + 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, - msg: &BlockchainAgentWithContextMessage, - logger: &Logger, - ) -> Result, AnalysisError> { - self.search_for_indispensable_adjustment_params + qualified_payables: Vec, + agent: &dyn BlockchainAgent, + ) -> AdjustmentAnalysisResult { + self.consider_adjustment_params .lock() .unwrap() - .push((msg.clone(), logger.clone())); - self.search_for_indispensable_adjustment_results - .borrow_mut() - .remove(0) + .push((qualified_payables, agent.arbitrary_id_stamp())); + self.consider_adjustment_results.borrow_mut().remove(0) } fn adjust_payments( &self, setup: PreparedAdjustment, - now: SystemTime, - logger: &Logger, - ) -> OutboundPaymentsInstructions { - self.adjust_payments_params - .lock() - .unwrap() - .push((setup.clone(), now, logger.clone())); + ) -> Result { + self.adjust_payments_params.lock().unwrap().push(setup); self.adjust_payments_results.borrow_mut().remove(0) } } impl PaymentAdjusterMock { - pub fn is_adjustment_required_params( + pub fn consider_adjustment_params( mut self, - params: &Arc>>, + params: &Arc, ArbitraryIdStamp)>>>, ) -> Self { - self.search_for_indispensable_adjustment_params = params.clone(); + self.consider_adjustment_params = params.clone(); self } - pub fn is_adjustment_required_result( - self, - result: Result, AnalysisError>, - ) -> 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 } - pub fn adjust_payments_result(self, result: OutboundPaymentsInstructions) -> Self { + pub fn adjust_payments_result( + self, + result: Result, + ) -> Self { self.adjust_payments_results.borrow_mut().push(result); self } @@ -1514,7 +1537,7 @@ macro_rules! formal_traits_for_payable_mid_scan_msg_handling { &self, _msg: BlockchainAgentWithContextMessage, _logger: &Logger, - ) -> Result, String> { + ) -> Option> { intentionally_blank!() } @@ -1522,7 +1545,11 @@ macro_rules! formal_traits_for_payable_mid_scan_msg_handling { &self, _setup: PreparedAdjustment, _logger: &Logger, - ) -> OutboundPaymentsInstructions { + ) -> Option { + intentionally_blank!() + } + + fn cancel_scan(&mut self, _logger: &Logger) { intentionally_blank!() } } @@ -1699,3 +1726,89 @@ impl ScanSchedulers { } } } + +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), + payment_threshold_intercept, + CreditorThresholds::new(permanent_deb_allowed_minor), + ) +} + +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_qualified_payables( + payables: Vec, + payment_thresholds: &PaymentThresholds, + now: SystemTime, +) -> Vec { + try_to_make_qualified_payables(payables, payment_thresholds, now, true) +} + +pub fn make_analyzed_payables( + payables: Vec, + payment_thresholds: &PaymentThresholds, + now: SystemTime, +) -> Vec { + convert_qualified_p_into_analyzed_p(make_qualified_payables(payables, payment_thresholds, now)) +} + +pub fn try_to_make_qualified_payables( + payables: Vec, + payment_thresholds: &PaymentThresholds, + now: SystemTime, + should_panic: bool, +) -> Vec { + let payable_inspector = PayableInspector::new(Box::new(PayableThresholdsGaugeReal::default())); + payables + .into_iter() + .flat_map(|payable| { + make_single_qualified_payable_opt( + payable, + &payable_inspector, + payment_thresholds, + should_panic, + now, + ) + }) + .collect() +} + +pub fn make_single_qualified_payable_opt( + payable: PayableAccount, + payable_inspector: &PayableInspector, + payment_thresholds: &PaymentThresholds, + should_panic: bool, + now: SystemTime, +) -> Option { + match payable_inspector.payable_exceeded_threshold(&payable, payment_thresholds, now) { + Some(payment_threshold_intercept) => Some(QualifiedPayableAccount::new( + payable, + payment_threshold_intercept, + CreditorThresholds::new(gwei_to_wei(payment_thresholds.permanent_debt_allowed_gwei)), + )), + None => { + if should_panic { + panic!( + "You intend to create qualified payables but their parameters not always make \ + them qualify as in: {:?} where the balance needs to get over {}", + payable, + PayableThresholdsGaugeReal::default().calculate_payout_threshold_in_gwei( + payment_thresholds, + SystemTime::now().duration_since(now).unwrap().as_secs() + ) + ) + } else { + None + } + } + } +} diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index d9f6c630e..07b8ea50d 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -391,7 +391,7 @@ impl BlockchainBridge { Vec>, Option<(BlockchainError, H256)>, ) = (vec![], None); - let (vector_of_results, error_opt) = msg.pending_payable.iter().fold( + let (vector_of_results, error_opt) = msg.pending_payables.iter().fold( init, |(mut ok_receipts, err_opt), current_fingerprint| match err_opt { None => match self @@ -409,7 +409,7 @@ impl BlockchainBridge { ); let pairs = vector_of_results .into_iter() - .zip(msg.pending_payable.into_iter()) + .zip(msg.pending_payables.into_iter()) .collect_vec(); self.pending_payable_confirmation .report_transaction_receipts_sub_opt @@ -519,8 +519,10 @@ mod tests { use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_time_t; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::test_utils::BlockchainAgentMock; - use crate::accountant::scanners::test_utils::protect_payables_in_test; - use crate::accountant::test_utils::make_pending_payable_fingerprint; + use crate::accountant::scanners::test_utils::protect_qualified_payables_in_test; + use crate::accountant::test_utils::{ + make_meaningless_qualified_payable, make_pending_payable_fingerprint, + }; use crate::blockchain::bip32::Bip32EncryptionKeyProvider; use crate::blockchain::blockchain_interface::blockchain_interface_null::BlockchainInterfaceNull; use crate::blockchain::blockchain_interface::data_structures::errors::{ @@ -556,7 +558,7 @@ mod tests { use std::any::TypeId; use std::path::Path; use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime}; + use std::time::SystemTime; use web3::types::{TransactionReceipt, H160, H256}; impl Handler> for BlockchainBridge { @@ -661,25 +663,9 @@ mod tests { let persistent_config_id_stamp = ArbitraryIdStamp::new(); let persistent_configuration = PersistentConfigurationMock::default() .set_arbitrary_id_stamp(persistent_config_id_stamp); - let wallet_1 = make_wallet("booga"); - let wallet_2 = make_wallet("gulp"); let qualified_payables = vec![ - PayableAccount { - wallet: wallet_1.clone(), - balance_wei: 78_654_321_124, - last_paid_timestamp: SystemTime::now() - .checked_sub(Duration::from_secs(1000)) - .unwrap(), - pending_payable_opt: None, - }, - PayableAccount { - wallet: wallet_2.clone(), - balance_wei: 60_457_111_003, - last_paid_timestamp: SystemTime::now() - .checked_sub(Duration::from_secs(500)) - .unwrap(), - pending_payable_opt: None, - }, + make_meaningless_qualified_payable(111), + make_meaningless_qualified_payable(222), ]; let subject = BlockchainBridge::new( Box::new(blockchain_interface), @@ -690,7 +676,7 @@ mod tests { let addr = subject.start(); let subject_subs = BlockchainBridge::make_subs_from(&addr); let peer_actors = peer_actors_builder().accountant(accountant).build(); - let qualified_payables = protect_payables_in_test(qualified_payables.clone()); + let qualified_payables = protect_qualified_payables_in_test(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { protected_qualified_payables: qualified_payables.clone(), response_skeleton_opt: Some(ResponseSkeleton { @@ -727,10 +713,11 @@ mod tests { } #[test] - fn build_of_blockchain_agent_throws_err_out_and_ends_handling_qualified_payables_message() { + fn build_of_blockchain_agent_throws_err_out_and_terminates_handling_qualified_payables_message() + { init_test_logging(); let test_name = - "build_of_blockchain_agent_throws_err_out_and_ends_handling_qualified_payables_message"; + "build_of_blockchain_agent_throws_err_out_and_terminates_handling_qualified_payables_message"; let (accountant, _, accountant_recording_arc) = make_recorder(); let scan_error_recipient: Recipient = accountant .system_stop_conditions(match_every_type_id!(ScanError)) @@ -751,12 +738,9 @@ mod tests { subject.logger = Logger::new(test_name); subject.scan_error_subs_opt = Some(scan_error_recipient); let request = QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![PayableAccount { - wallet: make_wallet("blah"), - balance_wei: 42, - last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, - }]), + protected_qualified_payables: protect_qualified_payables_in_test(vec![ + make_meaningless_qualified_payable(1234), + ]), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11, context_id: 2323, @@ -801,12 +785,9 @@ mod tests { None, ); let request = QualifiedPayablesMessage { - protected_qualified_payables: protect_payables_in_test(vec![PayableAccount { - wallet: make_wallet("blah"), - balance_wei: 4254, - last_paid_timestamp: SystemTime::now(), - pending_payable_opt: None, - }]), + protected_qualified_payables: protect_qualified_payables_in_test(vec![ + make_meaningless_qualified_payable(12345), + ]), response_skeleton_opt: None, }; @@ -1068,7 +1049,7 @@ mod tests { let peer_actors = peer_actors_builder().accountant(accountant).build(); send_bind_message!(subject_subs, peer_actors); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payables: vec![ pending_payable_fingerprint_1.clone(), pending_payable_fingerprint_2.clone(), ], @@ -1222,7 +1203,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_receipt_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![ + pending_payables: vec![ fingerprint_1.clone(), fingerprint_2.clone(), fingerprint_3, @@ -1284,7 +1265,7 @@ mod tests { .pending_payable_confirmation .report_transaction_receipts_sub_opt = Some(recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![], + pending_payables: vec![], response_skeleton_opt: None, }; let system = System::new( @@ -1347,7 +1328,7 @@ mod tests { .report_transaction_receipts_sub_opt = Some(report_transaction_recipient); subject.scan_error_subs_opt = Some(scan_error_recipient); let msg = RequestTransactionReceipts { - pending_payable: vec![fingerprint_1, fingerprint_2], + pending_payables: vec![fingerprint_1, fingerprint_2], response_skeleton_opt: None, }; let system = System::new("test"); @@ -1970,7 +1951,7 @@ pub mod exportable_test_parts { } fn launch_prepared_test_server() -> (TestServer, String) { let port = find_free_port(); - let server_url = format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port); + let server_url = format!("http://{}:{}", Ipv4Addr::LOCALHOST, port); ( TestServer::start( port, diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs index 345853476..f21990b19 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/lower_level_interface_web3.rs @@ -110,11 +110,8 @@ mod tests { port, vec![br#"{"jsonrpc":"2.0","id":0,"result":"0xDEADBEEF"}"#.to_vec()], ); - let (_event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (_event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = make_subject(transport, chain); @@ -170,11 +167,8 @@ mod tests { let test_server = TestServer::start (port, vec![ br#"{"jsonrpc":"2.0","id":0,"result":"0x00000000000000000000000000000000000000000000000000000000DEADBEEF"}"#.to_vec() ]); - let (_event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (_event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = make_subject(transport, chain); @@ -350,14 +344,11 @@ mod tests { F: FnOnce(&LowBlockchainIntWeb3, &Wallet) -> ResultForBalance, { let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ + let test_server = TestServer::start (port, vec![ br#"{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000000000000000000FFFQ"}"#.to_vec() ]); - let (_event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (_event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let wallet = Wallet::from_str("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fc").unwrap(); let subject = make_subject(transport, chain); 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 09d54ad89..580e3d606 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -28,6 +28,7 @@ use serde_json::Value; use std::fmt::Debug; use std::iter::once; use std::rc::Rc; +use lazy_static::lazy_static; use thousands::Separable; use web3::contract::Contract; use web3::transports::{Batch, EventLoopHandle}; @@ -36,8 +37,10 @@ use web3::types::{ H160, H256, U256, }; use web3::{BatchTransport, Error, Web3}; +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}; const CONTRACT_ABI: &str = indoc!( r#"[{ @@ -64,10 +67,13 @@ const TRANSACTION_LITERAL: H256 = H256([ 0x95, 0x2b, 0xa7, 0xf1, 0x63, 0xc4, 0xa1, 0x16, 0x28, 0xf5, 0x5a, 0x4d, 0xf5, 0x23, 0xb3, 0xef, ]); -const TRANSFER_METHOD_ID: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; - 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 TX_FEE_MARGIN_IN_PERCENT: PurePercentage = PurePercentage::try_from(15).expect("Value below 100 should cause no issue"); +} + pub struct BlockchainInterfaceWeb3 where T: 'static + BatchTransport + Debug, @@ -251,7 +257,8 @@ where let consuming_wallet_balances = ConsumingWalletBalances { transaction_fee_balance_in_minor_units: transaction_fee_balance, - masq_token_balance_in_minor_units: masq_token_balance, + service_fee_balance_in_minor_units: u128::try_from(masq_token_balance) + .expect("larger amount than the official supply"), }; let consuming_wallet = consuming_wallet.clone(); @@ -271,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!( @@ -342,7 +349,7 @@ where Rc::clone(&web3_batch), contract, )); - let gas_limit_const_part = Self::web3_gas_limit_const_part(chain); + let gas_limit_const_part = web3_gas_limit_const_part(chain); Self { logger: Logger::new("BlockchainInterface"), @@ -506,8 +513,8 @@ where nonce: U256, gas_price: u64, ) -> Result { - let data = Self::transaction_data(recipient, amount); - let gas_limit = self.compute_gas_limit(data.as_slice()); + let data = transaction_data_web3(recipient, amount); + let gas_limit = compute_gas_limit(self.gas_limit_const_part, data.as_slice()); let gas_price = gwei_to_wei::(gas_price); let transaction_parameters = TransactionParameters { nonce: Some(nonce), @@ -559,30 +566,6 @@ where introduction.chain(body).collect() } - fn transaction_data(recipient: &Wallet, amount: u128) -> [u8; 68] { - let mut data = [0u8; 4 + 32 + 32]; - data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); - data[16..36].copy_from_slice(&recipient.address().0[..]); - U256::try_from(amount) - .expect("shouldn't overflow") - .to_big_endian(&mut data[36..68]); - data - } - - fn compute_gas_limit(&self, data: &[u8]) -> U256 { - ethereum_types::U256::try_from(data.iter().fold(self.gas_limit_const_part, |acc, v| { - acc + if v == &0u8 { 4 } else { 68 } - })) - .expect("Internal error") - } - - fn web3_gas_limit_const_part(chain: Chain) -> u64 { - match chain { - Chain::EthMainnet | Chain::EthRopsten | Chain::Dev => 55_000, - Chain::PolyMainnet | Chain::PolyAmoy => 70_000, - } - } - fn extract_transactions_from_logs(&self, logs: Vec) -> Vec { logs.iter() .filter_map(|log: &Log| match log.block_number { @@ -630,7 +613,7 @@ mod tests { use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, - TRANSFER_METHOD_ID, + TX_FEE_MARGIN_IN_PERCENT, }; use crate::blockchain::blockchain_interface::test_utils::{ test_blockchain_interface_is_connected_and_functioning, LowBlockchainIntMock, @@ -643,7 +626,6 @@ mod tests { all_chains, make_fake_event_loop_handle, make_tx_hash, TestTransport, }; use crate::db_config::persistent_configuration::PersistentConfigError; - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::assert_string_contains; use crate::test_utils::http_test_server::TestServer; @@ -675,7 +657,9 @@ mod tests { use crate::blockchain::blockchain_interface::data_structures::{ BlockchainTransaction, RpcPayablesFailure, }; + use crate::sub_lib::blockchain_interface_web3::web3_gas_limit_const_part; use indoc::indoc; + use masq_lib::percentage::PurePercentage; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::SystemTime; @@ -716,8 +700,11 @@ mod tests { }; assert_eq!(CONTRACT_ABI, contract_abi_expected); assert_eq!(TRANSACTION_LITERAL, transaction_literal_expected); - assert_eq!(TRANSFER_METHOD_ID, [0xa9, 0x05, 0x9c, 0xbb]); assert_eq!(REQUESTS_IN_PARALLEL, 1); + assert_eq!( + *TX_FEE_MARGIN_IN_PERCENT, + PurePercentage::try_from(15).unwrap() + ); } #[test] @@ -795,11 +782,8 @@ mod tests { ] }]"#.to_vec(), ]); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); let end_block_nbr = 1024u64; @@ -857,11 +841,8 @@ mod tests { port, vec![br#"[{"jsonrpc":"2.0","id":2,"result":"0x400"},{"jsonrpc":"2.0","id":3,"result":[]}]"#.to_vec()], ); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, TEST_DEFAULT_CHAIN); let end_block_nbr = 1024u64; @@ -927,14 +908,11 @@ mod tests { fn blockchain_interface_web3_retrieve_transactions_returns_an_error_if_a_response_with_too_few_topics_is_returned( ) { let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ + let test_server = TestServer::start (port, vec![ br#"[{"jsonrpc":"2.0","id":2,"result":"0x400"},{"jsonrpc":"2.0","id":3,"result":[{"address":"0xcd6c588e005032dd882cd43bf53a32129be81302","blockHash":"0x1a24b9169cbaec3f6effa1f600b70c7ab9e8e86db44062b49132a4415d26732a","blockNumber":"0x4be663","data":"0x0000000000000000000000000000000000000000000000056bc75e2d63100000","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],"transactionHash":"0x955cec6ac4f832911ab894ce16aa22c3003f46deff3f7165b32700d2f5ff0681","transactionIndex":"0x0"}]}]"#.to_vec() ]); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); @@ -954,14 +932,11 @@ mod tests { fn blockchain_interface_web3_retrieve_transactions_returns_an_error_if_a_response_with_data_that_is_too_long_is_returned( ) { let port = find_free_port(); - let _test_server = TestServer::start(port, vec![ + let test_server = TestServer::start(port, vec![ br#"[{"jsonrpc":"2.0","id":2,"result":"0x400"},{"jsonrpc":"2.0","id":3,"result":[{"address":"0xcd6c588e005032dd882cd43bf53a32129be81302","blockHash":"0x1a24b9169cbaec3f6effa1f600b70c7ab9e8e86db44062b49132a4415d26732a","blockNumber":"0x4be663","data":"0x0000000000000000000000000000000000000000000000056bc75e2d6310000001","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003f69f9efd4f2592fd70be8c32ecd9dce71c472fc","0x000000000000000000000000adc1853c7859369639eb414b6342b36288fe6092"],"transactionHash":"0x955cec6ac4f832911ab894ce16aa22c3003f46deff3f7165b32700d2f5ff0681","transactionIndex":"0x0"}]}]"#.to_vec() ]); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); @@ -978,15 +953,12 @@ mod tests { fn blockchain_interface_web3_retrieve_transactions_ignores_transaction_logs_that_have_no_block_number( ) { let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ + let test_server = TestServer::start (port, vec![ br#"[{"jsonrpc":"2.0","id":1,"result":"0x400"},{"jsonrpc":"2.0","id":2,"result":[{"address":"0xcd6c588e005032dd882cd43bf53a32129be81302","blockHash":"0x1a24b9169cbaec3f6effa1f600b70c7ab9e8e86db44062b49132a4415d26732a","data":"0x0000000000000000000000000000000000000000000000000010000000000000","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003f69f9efd4f2592fd70be8c32ecd9dce71c472fc","0x000000000000000000000000adc1853c7859369639eb414b6342b36288fe6092"],"transactionHash":"0x955cec6ac4f832911ab894ce16aa22c3003f46deff3f7165b32700d2f5ff0681","transactionIndex":"0x0"}]}]"#.to_vec() ]); init_test_logging(); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let end_block_nbr = 1024u64; let subject = @@ -1015,14 +987,11 @@ mod tests { fn blockchain_interface_non_clandestine_retrieve_transactions_uses_block_number_latest_as_fallback_start_block_plus_one( ) { let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ + let test_server = TestServer::start (port, vec![ br#"[{"jsonrpc":"2.0","id":1,"result":"error"},{"jsonrpc":"2.0","id":2,"result":[{"address":"0xcd6c588e005032dd882cd43bf53a32129be81302","blockHash":"0x1a24b9169cbaec3f6effa1f600b70c7ab9e8e86db44062b49132a4415d26732a","data":"0x0000000000000000000000000000000000000000000000000010000000000000","logIndex":"0x0","removed":false,"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003f69f9efd4f2592fd70be8c32ecd9dce71c472fc","0x000000000000000000000000adc1853c7859369639eb414b6342b36288fe6092"],"transactionHash":"0x955cec6ac4f832911ab894ce16aa22c3003f46deff3f7165b32700d2f5ff0681","transactionIndex":"0x0"}]}]"#.to_vec() ]); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); @@ -1088,20 +1057,18 @@ mod tests { assert_eq!(result.consuming_wallet(), &wallet); assert_eq!(result.pending_transaction_id(), transaction_id); assert_eq!( - result.consuming_wallet_balances(), - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: transaction_fee_balance, - masq_token_balance_in_minor_units: masq_balance - } + result.transaction_fee_balance_minor(), + transaction_fee_balance + ); + assert_eq!(result.service_fee_balance_minor(), masq_balance.as_u128()); + assert_eq!( + result.gas_price_margin(), + PurePercentage::try_from(15).unwrap() ); - assert_eq!(result.agreed_fee_per_computation_unit(), 50); - let expected_fee_estimation = (3 - * (BlockchainInterfaceWeb3::::web3_gas_limit_const_part(chain) - + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) - * 50) as u128; + assert_eq!(result.gas_price(), 50); assert_eq!( - result.estimated_transaction_fee_total(3), - expected_fee_estimation + result.estimated_transaction_fee_per_transaction_minor(), + 3666400000000000 ) } @@ -1613,25 +1580,6 @@ mod tests { assert_eq!(accountant_recording.len(), 1) } - #[test] - fn web3_gas_limit_const_part_returns_reasonable_values() { - type Subject = BlockchainInterfaceWeb3; - assert_eq!( - Subject::web3_gas_limit_const_part(Chain::EthMainnet), - 55_000 - ); - assert_eq!( - Subject::web3_gas_limit_const_part(Chain::EthRopsten), - 55_000 - ); - assert_eq!( - Subject::web3_gas_limit_const_part(Chain::PolyMainnet), - 70_000 - ); - assert_eq!(Subject::web3_gas_limit_const_part(Chain::PolyAmoy), 70_000); - assert_eq!(Subject::web3_gas_limit_const_part(Chain::Dev), 55_000); - } - #[test] fn gas_limit_for_polygon_mainnet_lies_within_limits_for_raw_transaction() { test_gas_limit_is_between_limits(Chain::PolyMainnet); @@ -1647,8 +1595,7 @@ mod tests { let transport = TestTransport::default(); let mut subject = BlockchainInterfaceWeb3::new(transport, make_fake_event_loop_handle(), chain); - let not_under_this_value = - BlockchainInterfaceWeb3::::web3_gas_limit_const_part(chain); + let not_under_this_value = web3_gas_limit_const_part(chain); let not_above_this_value = not_under_this_value + WEB3_MAXIMAL_GAS_LIMIT_MARGIN; let consuming_wallet_secret_raw_bytes = b"my-wallet"; let batch_payable_tools = BatchPayableToolsMock::::default() @@ -2096,15 +2043,12 @@ mod tests { #[test] fn blockchain_interface_web3_can_fetch_transaction_receipt() { let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ + let test_server = TestServer::start (port, vec![ br#"{"jsonrpc":"2.0","id":2,"result":{"transactionHash":"0xa128f9ca1e705cc20a936a24a7fa1df73bad6e0aaf58e8e6ffcc154a7cff6e0e","blockHash":"0x6d0abccae617442c26104c2bc63d1bc05e1e002e555aec4ab62a46e826b18f18","blockNumber":"0xb0328d","contractAddress":null,"cumulativeGasUsed":"0x60ef","effectiveGasPrice":"0x22ecb25c00","from":"0x7424d05b59647119b01ff81e2d3987b6c358bf9c","gasUsed":"0x60ef","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000","status":"0x0","to":"0x384dec25e03f94931767ce4c3556168468ba24c3","transactionIndex":"0x0","type":"0x0"}}"# .to_vec() ]); - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port), - REQUESTS_IN_PARALLEL, - ) - .unwrap(); + let (event_loop_handle, transport) = + Http::with_max_parallel(&test_server.local_url(), REQUESTS_IN_PARALLEL).unwrap(); let chain = TEST_DEFAULT_CHAIN; let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); let tx_hash = @@ -2229,16 +2173,8 @@ 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), ) } - - #[test] - fn hash_the_smart_contract_transfer_function_signature() { - assert_eq!( - "transfer(address,uint256)".keccak256()[0..4], - TRANSFER_METHOD_ID, - ); - } } diff --git a/node/src/node_configurator/unprivileged_parse_args_configuration.rs b/node/src/node_configurator/unprivileged_parse_args_configuration.rs index 4238bd8d5..613712cef 100644 --- a/node/src/node_configurator/unprivileged_parse_args_configuration.rs +++ b/node/src/node_configurator/unprivileged_parse_args_configuration.rs @@ -514,7 +514,7 @@ fn configure_accountant_config( Ok(()) } -fn check_payment_thresholds( +pub fn check_payment_thresholds( payment_thresholds: &PaymentThresholds, ) -> Result<(), ConfiguratorError> { if payment_thresholds.debt_threshold_gwei <= payment_thresholds.permanent_debt_allowed_gwei { diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 058c7c12f..624a268ca 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -794,9 +794,9 @@ impl ProxyServer { accountant_sub .try_send(ReportServicesConsumedMessage { timestamp, - exit, + exit_service: exit, routing_payload_size: pkg.payload.len(), - routing, + routing_services: routing, }) .expect("Accountant is dead"); } @@ -935,9 +935,9 @@ impl ProxyServer { .collect::>(); let report_message = ReportServicesConsumedMessage { timestamp: SystemTime::now(), - exit: exit_service_report, + exit_service: exit_service_report, routing_payload_size: routing_size, - routing: routing_service_reports, + routing_services: routing_service_reports, }; self.subs .as_ref() @@ -2715,14 +2715,14 @@ mod tests { record, &ReportServicesConsumedMessage { timestamp: now, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: exit_earning_wallet, payload_size: exit_payload_size, service_rate: exit_node_rate_pack.exit_service_rate, byte_rate: exit_node_rate_pack.exit_byte_rate }, routing_payload_size: payload_enc_length, - routing: vec![ + routing_services: vec![ RoutingServiceConsumed { earning_wallet: route_1_earning_wallet, service_rate: routing_node_1_rate_pack.routing_service_rate, @@ -3803,14 +3803,14 @@ mod tests { first_report, &ReportServicesConsumedMessage { timestamp: first_report_timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: incoming_route_d_wallet, payload_size: first_exit_size, service_rate: rate_pack_d.exit_service_rate, byte_rate: rate_pack_d.exit_byte_rate }, routing_payload_size: routing_size, - routing: vec![ + routing_services: vec![ RoutingServiceConsumed { earning_wallet: incoming_route_e_wallet, service_rate: rate_pack_e.routing_service_rate, @@ -3832,14 +3832,14 @@ mod tests { second_report, &ReportServicesConsumedMessage { timestamp: second_report_timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: incoming_route_g_wallet, payload_size: second_exit_size, service_rate: rate_pack_g.exit_service_rate, byte_rate: rate_pack_g.exit_byte_rate }, routing_payload_size: routing_size, - routing: vec![ + routing_services: vec![ RoutingServiceConsumed { earning_wallet: incoming_route_h_wallet, service_rate: rate_pack_h.routing_service_rate, @@ -4040,14 +4040,14 @@ mod tests { services_consumed_report, &ReportServicesConsumedMessage { timestamp: returned_timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: incoming_route_d_wallet, payload_size: exit_size, service_rate: rate_pack_d.exit_service_rate, byte_rate: rate_pack_d.exit_byte_rate }, routing_payload_size: routing_size, - routing: vec![RoutingServiceConsumed { + routing_services: vec![RoutingServiceConsumed { earning_wallet: incoming_route_e_wallet, service_rate: rate_pack_e.routing_service_rate, byte_rate: rate_pack_e.routing_byte_rate @@ -4222,14 +4222,14 @@ mod tests { services_consumed_message, &ReportServicesConsumedMessage { timestamp: returned_timestamp, - exit: ExitServiceConsumed { + exit_service: ExitServiceConsumed { earning_wallet: incoming_route_d_wallet, payload_size: 0, service_rate: rate_pack_d.exit_service_rate, byte_rate: rate_pack_d.exit_byte_rate }, routing_payload_size: routing_size, - routing: vec![ + routing_services: vec![ RoutingServiceConsumed { earning_wallet: incoming_route_e_wallet, service_rate: rate_pack_e.routing_service_rate, diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index cf87c0803..9913c79ed 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -62,7 +62,7 @@ impl Default for PaymentThresholds { //this code is used in tests in Accountant impl PaymentThresholds { - pub fn sugg_and_grace(&self, now: i64) -> i64 { + pub fn maturity_and_grace(&self, now: i64) -> i64 { now - checked_conversion::(self.maturity_threshold_sec) - checked_conversion::(self.payment_grace_period_sec) } @@ -141,9 +141,9 @@ pub struct ReportExitServiceProvidedMessage { #[derive(Clone, PartialEq, Eq, Debug, Message)] pub struct ReportServicesConsumedMessage { pub timestamp: SystemTime, - pub exit: ExitServiceConsumed, + pub exit_service: ExitServiceConsumed, pub routing_payload_size: usize, - pub routing: Vec, + pub routing_services: Vec, } #[derive(Clone, PartialEq, Eq, Debug)] @@ -193,6 +193,7 @@ impl MessageIdGenerator for MessageIdGeneratorReal { mod tests { use crate::accountant::test_utils::AccountantBuilder; use crate::accountant::{checked_conversion, Accountant}; + use crate::node_configurator::unprivileged_parse_args_configuration::check_payment_thresholds; use crate::sub_lib::accountant::{ AccountantSubsFactoryReal, MessageIdGenerator, MessageIdGeneratorReal, PaymentThresholds, ScanIntervals, SubsFactory, DEFAULT_EARNING_WALLET, DEFAULT_PAYMENT_THRESHOLDS, @@ -210,7 +211,8 @@ mod tests { impl PaymentThresholds { pub fn sugg_thru_decreasing(&self, now: i64) -> i64 { - self.sugg_and_grace(now) - checked_conversion::(self.threshold_interval_sec) + self.maturity_and_grace(now) + - checked_conversion::(self.threshold_interval_sec) } } @@ -235,6 +237,10 @@ mod tests { }; assert_eq!(*DEFAULT_SCAN_INTERVALS, scan_intervals_expected); assert_eq!(*DEFAULT_PAYMENT_THRESHOLDS, payment_thresholds_expected); + assert_eq!( + check_payment_thresholds(&DEFAULT_PAYMENT_THRESHOLDS), + Ok(()) + ); assert_eq!(*DEFAULT_EARNING_WALLET, default_earning_wallet_expected); assert_eq!( *TEMPORARY_CONSUMING_WALLET, diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index ff006fdaa..526bf557f 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -3,13 +3,17 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::blockchain_agent::BlockchainAgent; use crate::accountant::scanners::mid_scan_msg_handling::payable_scanner::msgs::QualifiedPayablesMessage; -use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; +use crate::accountant::{ + QualifiedPayableAccount, RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder, +}; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; use actix::Message; use actix::Recipient; +use itertools::Either; use masq_lib::blockchains::chains::Chain; use masq_lib::ui_gateway::NodeFromUiMessage; +use masq_lib::utils::convert_collection; use std::fmt; use std::fmt::{Debug, Formatter}; use web3::types::U256; @@ -48,10 +52,15 @@ pub struct OutboundPaymentsInstructions { impl OutboundPaymentsInstructions { pub fn new( - affordable_accounts: Vec, + accounts: Either, Vec>, agent: Box, response_skeleton_opt: Option, ) -> Self { + let affordable_accounts = match accounts { + Either::Left(qualified_accounts) => convert_collection(qualified_accounts), + Either::Right(adjusted_accounts) => adjusted_accounts, + }; + Self { affordable_accounts, agent, @@ -68,15 +77,19 @@ impl SkeletonOptHolder for OutboundPaymentsInstructions { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ConsumingWalletBalances { + // The supply of this currency isn't limited by our database and theoretically can be much + // bigger than of our utility currency pub transaction_fee_balance_in_minor_units: U256, - pub masq_token_balance_in_minor_units: U256, + // This supply must fit in u128 (maybe rather i128) because otherwise our database would not be + // fully capable of handling math over it while not threatened by a fatal overflow + pub service_fee_balance_in_minor_units: u128, } impl ConsumingWalletBalances { - pub fn new(transaction_fee: U256, masq_token: U256) -> Self { + pub fn new(transaction_fee: U256, service_fee: u128) -> Self { Self { transaction_fee_balance_in_minor_units: transaction_fee, - masq_token_balance_in_minor_units: masq_token, + service_fee_balance_in_minor_units: service_fee, } } } diff --git a/node/src/sub_lib/blockchain_interface_web3.rs b/node/src/sub_lib/blockchain_interface_web3.rs new file mode 100644 index 000000000..a514c4575 --- /dev/null +++ b/node/src/sub_lib/blockchain_interface_web3.rs @@ -0,0 +1,62 @@ +// Copyright (c) 2023, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::sub_lib::wallet::Wallet; +use masq_lib::blockchains::chains::Chain; +use web3::types::U256; + +const TRANSFER_METHOD_ID: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; + +pub fn transaction_data_web3(recipient: &Wallet, amount: u128) -> [u8; 68] { + let mut data = [0u8; 4 + 32 + 32]; + data[0..4].copy_from_slice(&TRANSFER_METHOD_ID); + data[16..36].copy_from_slice(&recipient.address().0[..]); + U256::try_from(amount) + .expect("shouldn't overflow") + .to_big_endian(&mut data[36..68]); + data +} + +pub fn compute_gas_limit(gas_limit_const_part: u64, data: &[u8]) -> U256 { + ethereum_types::U256::try_from(data.iter().fold(gas_limit_const_part, |acc, v| { + acc + if v == &0u8 { 4 } else { 68 } + })) + .expect("Internal error") +} + +pub fn web3_gas_limit_const_part(chain: Chain) -> u64 { + match chain { + Chain::EthMainnet | Chain::EthRopsten | Chain::Dev => 55_000, + Chain::PolyMainnet | Chain::PolyAmoy => 70_000, + } +} + +#[cfg(test)] +mod tests { + use crate::sub_lib::blockchain_interface_web3::{ + web3_gas_limit_const_part, TRANSFER_METHOD_ID, + }; + use ethsign_crypto::Keccak256; + use masq_lib::blockchains::chains::Chain; + + #[test] + fn constants_are_correct() { + assert_eq!(TRANSFER_METHOD_ID, [0xa9, 0x05, 0x9c, 0xbb]); + } + + #[test] + fn hash_the_smart_contract_transfer_function_signature() { + assert_eq!( + "transfer(address,uint256)".keccak256()[0..4], + TRANSFER_METHOD_ID, + ); + } + + #[test] + fn web3_gas_limit_const_part_returns_reasonable_values() { + assert_eq!(web3_gas_limit_const_part(Chain::EthMainnet), 55_000); + assert_eq!(web3_gas_limit_const_part(Chain::EthRopsten), 55_000); + assert_eq!(web3_gas_limit_const_part(Chain::PolyMainnet), 70_000); + assert_eq!(web3_gas_limit_const_part(Chain::PolyAmoy), 70_000); + assert_eq!(web3_gas_limit_const_part(Chain::Dev), 55_000); + } +} diff --git a/node/src/sub_lib/mod.rs b/node/src/sub_lib/mod.rs index 51360357b..92c9cfd12 100644 --- a/node/src/sub_lib/mod.rs +++ b/node/src/sub_lib/mod.rs @@ -9,6 +9,7 @@ pub mod accountant; pub mod bidi_hashmap; pub mod binary_traverser; pub mod blockchain_bridge; +pub mod blockchain_interface_web3; pub mod channel_wrappers; pub mod combined_parameters; pub mod configurator; diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index f6de206d7..1b0cd6a25 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -251,6 +251,18 @@ pub struct MessageScheduler { pub delay: Duration, } +// Yes, this refers to code from the test tree, but we play a trick by letting the guts vanish in +// the release version of our code, therefore we can use this outer macro without adverse effects, +// but above all we don't have to add the #[cfg(test)] marks above the invocation itself, but also +// the related 'use' clause. +#[macro_export] +macro_rules! arbitrary_id_stamp_in_trait { + () => { + #[cfg(test)] + $crate::arbitrary_id_stamp_in_trait_internal___!(); + }; +} + #[cfg(test)] mod tests { use super::*; diff --git a/node/src/test_utils/database_utils.rs b/node/src/test_utils/database_utils.rs index 2005166c0..493360302 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -7,12 +7,12 @@ use crate::database::db_initializer::ExternalData; use crate::database::rusqlite_wrappers::ConnectionWrapper; use crate::database::db_migrations::db_migrator::DbMigrator; +use crate::test_utils::test_input_data_standard_dir; use masq_lib::logger::Logger; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::{to_string, NeighborhoodModeLight}; use rusqlite::{Connection, Error}; use std::cell::RefCell; -use std::env::current_dir; use std::fs::{remove_file, File}; use std::io::Read; use std::iter::once; @@ -26,11 +26,7 @@ pub fn bring_db_0_back_to_life_and_return_connection(db_path: &Path) -> Connecti _ => (), }; let conn = Connection::open(&db_path).unwrap(); - let file_path = current_dir() - .unwrap() - .join("src") - .join("test_utils") - .join("database_version_0_sql.txt"); + let file_path = test_input_data_standard_dir().join("database_version_0_sqls.txt"); let mut file = File::open(file_path).unwrap(); let mut buffer = String::new(); file.read_to_string(&mut buffer).unwrap(); diff --git a/node/src/test_utils/http_test_server.rs b/node/src/test_utils/http_test_server.rs index 56e0daddf..54648582c 100644 --- a/node/src/test_utils/http_test_server.rs +++ b/node/src/test_utils/http_test_server.rs @@ -52,6 +52,10 @@ impl TestServer { TestServer { port, rx } } + pub fn local_url(&self) -> String { + format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), self.port) + } + pub fn requests_so_far(&self) -> Vec>> { let mut requests = vec![]; while let Ok(request) = self.rx.try_recv() { diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index edeee2851..d3219d800 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; @@ -362,7 +363,6 @@ pub fn await_messages(expected_message_count: usize, messages_arc_mutex: &Arc } } -//must stay without cfg(test) -- used in another crate pub fn wait_for(interval_ms: Option, limit_ms: Option, mut f: F) where F: FnMut() -> bool, @@ -379,7 +379,6 @@ where .unwrap(); } -//must stay without cfg(test) -- used in another crate pub fn await_value( interval_and_limit_ms: Option<(u64, u64)>, mut f: F, @@ -448,7 +447,6 @@ where set } -//must stay without cfg(test) -- used in another crate pub fn read_until_timeout(stream: &mut dyn Read) -> Vec { let mut response: Vec = vec![]; let mut buf = [0u8; 16384]; @@ -507,7 +505,6 @@ pub fn make_paying_wallet(secret: &[u8]) -> Wallet { ) } -//must stay without cfg(test) -- used in another crate pub fn make_wallet(address: &str) -> Wallet { Wallet::from_str(&dummy_address_to_hex(address)).unwrap() } @@ -518,7 +515,6 @@ pub fn assert_eq_debug(a: T, b: T) { assert_eq!(a_str, b_str); } -// Must stay without cfg(test) -- used in another crate #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct TestRawTransaction { pub nonce: U256, @@ -531,12 +527,18 @@ pub struct TestRawTransaction { pub data: Vec, } -#[macro_export] -macro_rules! arbitrary_id_stamp_in_trait { - () => { - #[cfg(test)] - $crate::arbitrary_id_stamp_in_trait_internal___!(); +pub fn test_input_data_standard_dir() -> PathBuf { + let working_dir = current_dir().unwrap(); + if !working_dir.ends_with("node") { + panic!( + "Project structure with missing \"node\" directory: {:?}.", + working_dir + ); }; + working_dir + .join("src") + .join("test_utils") + .join("test_input_data") } #[cfg(test)] @@ -567,8 +569,6 @@ 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; @@ -968,7 +968,7 @@ pub mod unshared_test_utils { pub mod arbitrary_id_stamp { use super::*; - use crate::arbitrary_id_stamp_in_trait; + use masq_lib::test_utils::utils::MutexIncrementInset; //The issues we are to solve might look as follows: @@ -1018,7 +1018,7 @@ pub mod unshared_test_utils { } // To be added together with other methods in your trait - // DO NOT USE ME DIRECTLY, USE arbitrary_id_stamp_in_trait INSTEAD! + // DO NOT USE ME DIRECTLY, INSTEAD: arbitrary_id_stamp_in_trait! #[macro_export] macro_rules! arbitrary_id_stamp_in_trait_internal___ { () => { diff --git a/node/src/test_utils/database_version_0_sql.txt b/node/src/test_utils/test_input_data/database_version_0_sqls.txt similarity index 100% rename from node/src/test_utils/database_version_0_sql.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/verify_bill_payments_smart_contract b/node/src/test_utils/test_input_data/verify_bill_payments_smart_contract new file mode 100644 index 000000000..91e6e1dd2 --- /dev/null +++ b/node/src/test_utils/test_input_data/verify_bill_payments_smart_contract @@ -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