diff --git a/.gitignore b/.gitignore index cfcb37e57..91a07eaad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. /target/ /results/ **/*.rs.bk diff --git a/automap/Cargo.lock b/automap/Cargo.lock index 0ac7f92e4..aef35fc27 100644 --- a/automap/Cargo.lock +++ b/automap/Cargo.lock @@ -137,6 +137,7 @@ dependencies = [ "port_scanner", "pretty-hex", "rand 0.7.3", + "regex", ] [[package]] diff --git a/automap/Cargo.toml b/automap/Cargo.toml index f29ec687c..091e74656 100644 --- a/automap/Cargo.toml +++ b/automap/Cargo.toml @@ -20,6 +20,10 @@ port_scanner = "0.1.5" pretty-hex = "0.1.0" rand = {version = "0.7.0", features = ["getrandom", "small_rng"]} + +[dev-dependencies] +regex = "1.5.4" + [[bin]] name = "automap" path = "src/main.rs" diff --git a/automap/ci/all.sh b/automap/ci/all.sh index 9357e733e..1ace2bd94 100755 --- a/automap/ci/all.sh +++ b/automap/ci/all.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2019-2021, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" #export RUSTC_WRAPPER="$HOME/.cargo/bin/sccache" diff --git a/automap/ci/build.sh b/automap/ci/build.sh index 28eac72e3..9bcecfaa5 100755 --- a/automap/ci/build.sh +++ b/automap/ci/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2019-2021, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" pushd "$CI_DIR/.." diff --git a/automap/ci/license.sh b/automap/ci/license.sh index 7855b9fd6..3aa9f7e62 100755 --- a/automap/ci/license.sh +++ b/automap/ci/license.sh @@ -1,5 +1,5 @@ #!/bin/bash -e -# Copyright (c) 2019-2021, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" pushd "$CI_DIR/.." diff --git a/automap/ci/lint.sh b/automap/ci/lint.sh index 3a68c8b05..4535cb6e2 100755 --- a/automap/ci/lint.sh +++ b/automap/ci/lint.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2019-2021, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" pushd "$CI_DIR/.." diff --git a/automap/ci/unit_tests.sh b/automap/ci/unit_tests.sh index 9f1ac3528..efc1c7ade 100755 --- a/automap/ci/unit_tests.sh +++ b/automap/ci/unit_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2019-2021, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" export RUST_BACKTRACE=full diff --git a/automap/src/comm_layer/pcp_pmp_common/linux_specific.rs b/automap/src/comm_layer/pcp_pmp_common/linux_specific.rs index f85820be2..0623c192b 100644 --- a/automap/src/comm_layer/pcp_pmp_common/linux_specific.rs +++ b/automap/src/comm_layer/pcp_pmp_common/linux_specific.rs @@ -11,24 +11,38 @@ pub fn linux_find_routers(command: &dyn FindRoutersCommand) -> Result stdout, Err(stderr) => return Err(AutomapError::ProtocolError(stderr)), }; - let addresses = output + let init: Result, AutomapError> = Ok(vec![]); + output .split('\n') - .map(|line| { - line.split(' ') - .filter(|piece| !piece.is_empty()) - .collect::>() + .take_while(|line| line.trim_start().starts_with("default ")) + .fold(init, |acc, line| match acc { + Ok(mut ip_addr_vec) => { + let ip_str: String = line + .chars() + .skip_while(|char| !char.is_numeric()) + .take_while(|char| !char.is_whitespace()) + .collect(); + + match IpAddr::from_str(&ip_str) { + Ok(ip_addr) => { + ip_addr_vec.push(ip_addr); + Ok(ip_addr_vec) + } + Err(e) => Err(AutomapError::FindRouterError(format!( + "Failed to parse an IP from \"ip route\": {:?} Line: {}", + e, line + ))), + } + } + Err(e) => Err(e), }) - .filter(|line_vec| (line_vec.len() >= 4) && (line_vec[3] == "UG")) - .map(|line_vec| IpAddr::from_str(line_vec[1]).expect("Bad syntax from route -n")) - .collect::>(); - Ok(addresses) } pub struct LinuxFindRoutersCommand {} impl FindRoutersCommand for LinuxFindRoutersCommand { fn execute(&self) -> Result { - self.execute_command("route -n") + self.execute_command("ip route") } } @@ -48,19 +62,18 @@ impl LinuxFindRoutersCommand { mod tests { use super::*; use crate::mocks::FindRoutersCommandMock; + use regex::Regex; use std::str::FromStr; #[test] fn find_routers_works_when_there_is_a_router_to_find() { - let route_n_output = "Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 enp4s0 -169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0 -172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 -172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71 -192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0 -"; - let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output)); + let ip_route_output = "\ + default via 192.168.0.1 dev enp4s0 proto dhcp src 192.168.0.100 metric 100\n\ + 169.254.0.0/16 dev enp4s0 scope link metric 1000\n\ + 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown\n\ + 172.18.0.0/16 dev br-85f38f356a58 proto kernel scope link src 172.18.0.1 linkdown\n\ + 192.168.0.0/24 dev enp4s0 proto kernel scope link src 192.168.0.100 metric 100"; + let find_routers_command = FindRoutersCommandMock::new(Ok(&ip_route_output)); let result = linux_find_routers(&find_routers_command).unwrap(); @@ -69,16 +82,14 @@ Destination Gateway Genmask Flags Metric Ref Use Iface #[test] fn find_routers_works_when_there_are_multiple_routers_to_find() { - let route_n_output = "Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 enp4s0 -0.0.0.0 192.168.0.2 0.0.0.0 UG 100 0 0 enp4s0 -169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0 -172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 -172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71 -192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0 -"; - let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output)); + let ip_route_output = "\ + default via 192.168.0.1 dev enp0s8 proto dhcp metric 101\n\ + default via 192.168.0.2 dev enp0s3 proto dhcp metric 102\n\ + 10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 102\n\ + 169.254.0.0/16 dev enp0s3 scope link metric 1000\n\ + 192.168.1.0/24 dev enp0s8 proto kernel scope link src 192.168.1.64 metric 101\n\ + 192.168.1.1 via 10.0.2.15 dev enp0s3"; + let find_routers_command = FindRoutersCommandMock::new(Ok(&ip_route_output)); let result = linux_find_routers(&find_routers_command).unwrap(); @@ -91,16 +102,30 @@ Destination Gateway Genmask Flags Metric Ref Use Iface ) } + #[test] + fn find_routers_supports_ip_address_of_ipv6() { + let route_n_output = "\ + default via 2001:1:2:3:4:5:6:7 dev enX0 proto kernel metric 256 pref medium\n\ + fe80::/64 dev docker0 proto kernel metric 256 pref medium"; + + let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output)); + + let result = linux_find_routers(&find_routers_command); + + assert_eq!( + result, + Ok(vec![IpAddr::from_str("2001:1:2:3:4:5:6:7").unwrap()]) + ) + } + #[test] fn find_routers_works_when_there_is_no_router_to_find() { - let route_n_output = "Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -0.0.0.0 192.168.0.1 0.0.0.0 U 100 0 0 enp4s0 -169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp4s0 -172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 -172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-2c4b4b668d71 -192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 enp4s0 -"; + let route_n_output = "\ + 10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100\n\ + 0.1.0.1 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100\n\ + 168.63.129.16 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100\n\ + 169.254.169.254 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100\n\ + 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown"; let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output)); let result = linux_find_routers(&find_routers_command).unwrap(); @@ -120,55 +145,127 @@ Destination Gateway Genmask Flags Metric Ref Use Iface ) } + #[test] + fn find_routers_returns_error_if_ip_addresses_can_not_be_parsed() { + let route_n_output = "\ + default via 192.168.0.1 dev enp4s0 proto dhcp src 192.168.0.100 metric 100\n\ + default via 192.168.0 dev enp0s3 proto dhcp metric 102\n\ + 169.254.0.0/16 dev enp4s0 scope link metric 1000"; + let find_routers_command = FindRoutersCommandMock::new(Ok(&route_n_output)); + + let result = linux_find_routers(&find_routers_command); + + eprintln!("{:?}", result); + + assert_eq!( + result, + Err(AutomapError::FindRouterError( + "Failed to parse an IP from \"ip route\": AddrParseError(Ip) Line: default via 192.168.0 dev enp0s3 proto dhcp metric 102".to_string() + )) + ) + } + #[test] fn find_routers_command_produces_output_that_looks_right() { let subject = LinuxFindRoutersCommand::new(); let result = subject.execute().unwrap(); - let lines = result.split('\n').collect::>(); - assert_eq!("Kernel IP routing table", lines[0]); - let headings = lines[1] - .split(' ') - .filter(|s| s.len() > 0) - .collect::>(); - assert_eq!( - headings, - vec![ - "Destination", - "Gateway", - "Genmask", - "Flags", - "Metric", - "Ref", - "Use", - "Iface", - ] - ); - for line in &lines[3..] { - if line.len() == 0 { - continue; - } - let columns = line - .split(' ') - .filter(|s| s.len() > 0) - .collect::>(); - for idx in 0..3 { - if IpAddr::from_str(columns[idx]).is_err() { - panic!( - "Column {} should have been an IP address but wasn't: {}", - idx, columns[idx] - ) - } - } - for idx in 4..7 { - if columns[idx].parse::().is_err() { - panic!( - "Column {} should have been numeric but wasn't: {}", - idx, columns[idx] - ) - } - } + let mut lines = result.split('\n').collect::>(); + let len = lines.len(); + if lines[len - 1].is_empty() { + lines.remove(len - 1); } + let reg = ip_route_regex(); + lines.iter().for_each(|line| { + assert!(reg.is_match(line), "Lines: {:?} line: {}", lines, line); + }); + } + + fn ip_route_regex() -> Regex { + let reg_for_ip = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}"; + Regex::new(&format!( + r#"^(default via )?{}(/\d+)?\s(dev|via)(\s.+){{3,}}"#, + reg_for_ip + )) + .unwrap() + } + + #[test] + fn reg_for_ip_route_command_output_good_and_bad_ip() { + let route_n_output = vec![ + ( + "default via 0.1.0.1 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100", + true, + "Example of good IPv4", + ), + ( + "10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100", + true, + "Example of good IPv4", + ), + ( + "168.63.129.16 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100", + true, + "Example of good IPv4", + ), + ( + "169.254.169.254 via 10.1.0.1 dev eth0 proto dhcp src 10.1.0.84 metric 100", + true, + "Example of good IPv4", + ), + ( + "172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown", + true, + "Example of good IPv4", + ), + ( + "10.1.0.0/16 dev eth0 proto kernel scope link src 10.1.0.84 metric 100", + true, + "Example of good IPv4", + ), + ( + "0.1.255.1 dev eth0 proto dhcp", + true, + "Example of good IPv4", + ), + ( + "0.1.0 dev eth0 proto dhcp scope link src 10.1.0.84 metric 100", + false, + "IPv4 address has only three elements", + ), + ( + "0.1.256.1 dev eth0 proto dhcp", + false, + "IPv4 address has an element greater than 255", + ), + ( + "0.1.b.1 dev eth0 proto dhcp", + false, + "IPv4 address contains a letter", + ), + ( + "0.1.0.1/ dev eth0 proto dhcp", + false, + "IPv4 Subnet is missing a netmask", + ), + ( + "2001:0db8:0000:0000:0000:ff00:0042:8329 dev eth0 proto dhcp", + false, + "Regex does not support IPV6", + ), + ]; + + let regex = ip_route_regex(); + + route_n_output.iter().for_each(|line| { + assert_eq!( + regex.is_match(line.0), + line.1, + "{}: Line: {}", + line.2, + line.0 + ); + }); } } diff --git a/automap/src/comm_layer/pmp.rs b/automap/src/comm_layer/pmp.rs index b5833a9df..db6f63adc 100644 --- a/automap/src/comm_layer/pmp.rs +++ b/automap/src/comm_layer/pmp.rs @@ -1442,7 +1442,7 @@ mod tests { next_lifetime: Duration::from_secs(600), remap_interval: Duration::from_secs(0), }; - let mut last_remapped = Instant::now().sub(Duration::from_secs(3600)); + let mut last_remapped = Instant::now().sub(Duration::from_secs(60)); let logger = Logger::new("maybe_remap_handles_remapping_error"); let transactor = PmpTransactor::new(); let mut subject = diff --git a/ci/all.sh b/ci/all.sh index 29492b9f4..91d6b4c8d 100755 --- a/ci/all.sh +++ b/ci/all.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" PARENT_DIR="$1" diff --git a/ci/bashify_workspace.sh b/ci/bashify_workspace.sh index c580d0a41..47292bf8a 100755 --- a/ci/bashify_workspace.sh +++ b/ci/bashify_workspace.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. if [[ "$1" == "" ]]; then WORKSPACE="$HOME" diff --git a/ci/collect_results.sh b/ci/collect_results.sh index 28cce56e9..732a0dcbf 100755 --- a/ci/collect_results.sh +++ b/ci/collect_results.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" function sudo_ask() { case "$OSTYPE" in diff --git a/ci/format.sh b/ci/format.sh index e8cd3ec33..75747e812 100755 --- a/ci/format.sh +++ b/ci/format.sh @@ -1,5 +1,5 @@ #!/bin/bash -xv -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" final_exit_code=0 diff --git a/ci/install_node_toolchain.sh b/ci/install_node_toolchain.sh index 88fd7e8f2..5f1ceda60 100755 --- a/ci/install_node_toolchain.sh +++ b/ci/install_node_toolchain.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" function init_for_linux() { diff --git a/ci/install_release_toolchain.sh b/ci/install_release_toolchain.sh index 65157d784..adbf482ae 100755 --- a/ci/install_release_toolchain.sh +++ b/ci/install_release_toolchain.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" if [[ "$1" == "" ]]; then diff --git a/ci/install_ui_test_toolchain.sh b/ci/install_ui_test_toolchain.sh index 6063455cf..79a217ed0 100755 --- a/ci/install_ui_test_toolchain.sh +++ b/ci/install_ui_test_toolchain.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. function install_linux() { wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - diff --git a/ci/multinode_integration_test.sh b/ci/multinode_integration_test.sh index 04d1b93c6..c4656bb03 100755 --- a/ci/multinode_integration_test.sh +++ b/ci/multinode_integration_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" if [[ "$JENKINS_VERSION" != "" ]]; then diff --git a/ci/prepare_node_build.sh b/ci/prepare_node_build.sh index a52b4ba96..aa7ed8a97 100755 --- a/ci/prepare_node_build.sh +++ b/ci/prepare_node_build.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" "$CI_DIR/format.sh" diff --git a/ci/prepare_node_ui_build.sh b/ci/prepare_node_ui_build.sh index d41746cc8..7c93e0654 100755 --- a/ci/prepare_node_ui_build.sh +++ b/ci/prepare_node_ui_build.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" #PARENT_DIR="$1" diff --git a/ci/prepare_release.sh b/ci/prepare_release.sh index 8853faa2b..9a1bbf3cb 100755 --- a/ci/prepare_release.sh +++ b/ci/prepare_release.sh @@ -1,5 +1,5 @@ #!/bin/bash -ev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. function check_variable() { if [[ "$1" == "" ]]; then diff --git a/ci/publish_results.sh b/ci/publish_results.sh index 55a2f208a..aa4079802 100755 --- a/ci/publish_results.sh +++ b/ci/publish_results.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" STATUS=$1 diff --git a/ci/release.sh b/ci/release.sh index 93c7cbfcf..99298d62e 100755 --- a/ci/release.sh +++ b/ci/release.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" TOOLCHAIN_HOME="$1" NODE_EXECUTABLE="SubstratumNode" diff --git a/ci/sccache.sh b/ci/sccache.sh index d35126df2..65edf40da 100755 --- a/ci/sccache.sh +++ b/ci/sccache.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" #cargo install sccache || echo "sccache already installed" diff --git a/masq/src/commands/financials_command/parsing_and_value_dressing.rs b/masq/src/commands/financials_command/parsing_and_value_dressing.rs index 30c9b9a10..686a0dd28 100644 --- a/masq/src/commands/financials_command/parsing_and_value_dressing.rs +++ b/masq/src/commands/financials_command/parsing_and_value_dressing.rs @@ -2,7 +2,7 @@ pub(in crate::commands::financials_command) mod restricted { use crate::commands::financials_command::data_structures::restricted::UserOriginalTypingOfRanges; - use masq_lib::constants::{MASQ_TOTAL_SUPPLY, WEIS_OF_GWEI}; + use masq_lib::constants::{MASQ_TOTAL_SUPPLY, WEIS_IN_GWEI}; use masq_lib::utils::ExpectValue; use num::CheckedMul; use regex::{Captures, Regex}; @@ -15,8 +15,8 @@ pub(in crate::commands::financials_command) mod restricted { pub fn convert_masq_from_gwei_and_dress_well(balance_gwei: i64) -> String { const MASK_FOR_NON_SIGNIFICANT_DIGITS: i64 = 10_000_000; - let balance_masq_int = (balance_gwei / WEIS_OF_GWEI as i64).abs(); - let balance_masq_frac = (balance_gwei % WEIS_OF_GWEI as i64).abs(); + let balance_masq_int = (balance_gwei / WEIS_IN_GWEI as i64).abs(); + let balance_masq_frac = (balance_gwei % WEIS_IN_GWEI as i64).abs(); let balance_masq_frac_trunc = balance_masq_frac / MASK_FOR_NON_SIGNIFICANT_DIGITS; match ( (balance_masq_int == 0) && (balance_masq_frac_trunc == 0), @@ -310,7 +310,7 @@ mod tests { process_optionally_fractional_number, }; use crate::commands::financials_command::test_utils::transpose_inputs_to_nested_tuples; - use masq_lib::constants::{MASQ_TOTAL_SUPPLY, WEIS_OF_GWEI}; + use masq_lib::constants::{MASQ_TOTAL_SUPPLY, WEIS_IN_GWEI}; use regex::Regex; #[test] @@ -482,7 +482,7 @@ mod tests { #[test] fn i64_interpretation_capabilities_are_good_enough_for_masq_total_supply_in_gwei() { - let _: i64 = (MASQ_TOTAL_SUPPLY * WEIS_OF_GWEI as u64) + let _: i64 = (MASQ_TOTAL_SUPPLY * WEIS_IN_GWEI as u64) .try_into() .unwrap(); } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 2c63818c4..6195a83ce 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -20,7 +20,7 @@ pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really pub const WALLET_ADDRESS_LENGTH: usize = 42; pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; -pub const WEIS_OF_GWEI: i128 = 1_000_000_000; +pub const WEIS_IN_GWEI: i128 = 1_000_000_000; pub const ETH_MAINNET_CONTRACT_CREATION_BLOCK: u64 = 11_170_708; pub const ROPSTEN_TESTNET_CONTRACT_CREATION_BLOCK: u64 = 8_688_171; @@ -102,7 +102,7 @@ mod tests { assert_eq!(DEFAULT_GAS_PRICE, 1); assert_eq!(WALLET_ADDRESS_LENGTH, 42); assert_eq!(MASQ_TOTAL_SUPPLY, 37_500_000); - assert_eq!(WEIS_OF_GWEI, 1_000_000_000); + assert_eq!(WEIS_IN_GWEI, 1_000_000_000); assert_eq!(ETH_MAINNET_CONTRACT_CREATION_BLOCK, 11_170_708); assert_eq!(ROPSTEN_TESTNET_CONTRACT_CREATION_BLOCK, 8_688_171); assert_eq!(MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, 0); diff --git a/masq_lib/src/ui_traffic_converter.rs b/masq_lib/src/ui_traffic_converter.rs index 0d51094e8..8445b4e7e 100644 --- a/masq_lib/src/ui_traffic_converter.rs +++ b/masq_lib/src/ui_traffic_converter.rs @@ -549,7 +549,7 @@ mod tests { #[test] fn new_unmarshaling_handles_badly_typed_json() { - let json = r#"[1, 2, 3, 4]"#; + let json = "[1, 2, 3, 4]"; let result = UiTrafficConverter::new_unmarshal_from_ui(json, 1234); diff --git a/multinode_integration_tests/ci/lint.sh b/multinode_integration_tests/ci/lint.sh index 726499c8f..f8933ddfe 100755 --- a/multinode_integration_tests/ci/lint.sh +++ b/multinode_integration_tests/ci/lint.sh @@ -1,5 +1,5 @@ #!/bin/bash -xev -# Copyright (c) 2017-2019, Substratum LLC (https://substratum.net) and/or its affiliates. All rights reserved. +# Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. CI_DIR="$( cd "$( dirname "$0" )" && pwd )" pushd "$CI_DIR/.." diff --git a/multinode_integration_tests/tests/bookkeeping_test.rs b/multinode_integration_tests/tests/bookkeeping_test.rs index 05b2d6b7e..f931bc757 100644 --- a/multinode_integration_tests/tests/bookkeeping_test.rs +++ b/multinode_integration_tests/tests/bookkeeping_test.rs @@ -41,7 +41,7 @@ fn provided_and_consumed_services_are_recorded_in_databases() { let payables = non_pending_payables(&originating_node); // Waiting until the serving nodes have finished accruing their receivables - thread::sleep(Duration::from_secs(3)); + thread::sleep(Duration::from_secs(7)); // get all receivables from all other nodes let receivable_balances = non_originating_nodes diff --git a/multinode_integration_tests/tests/connection_termination_test.rs b/multinode_integration_tests/tests/connection_termination_test.rs index 5cb6f940c..d07789c44 100644 --- a/multinode_integration_tests/tests/connection_termination_test.rs +++ b/multinode_integration_tests/tests/connection_termination_test.rs @@ -338,7 +338,7 @@ fn create_request_icp( target_hostname: Some(format!("{}", server.local_addr().ip())), target_port: server.local_addr().port(), protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originating_node.main_public_key().clone(), + originator_public_key: originating_node.main_public_key().clone(), }, )), exit_node.main_public_key(), @@ -383,7 +383,7 @@ fn create_meaningless_icp( target_hostname: Some(format!("nowhere.com")), target_port: socket_addr.port(), protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originating_node.main_public_key().clone(), + originator_public_key: originating_node.main_public_key().clone(), }, )), exit_node.main_public_key(), @@ -473,7 +473,7 @@ fn create_client_drop_report( target_hostname: Some(String::from("doesnt.matter.com")), target_port: 80, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originating_node.main_public_key().clone(), + originator_public_key: originating_node.main_public_key().clone(), }, )); diff --git a/multinode_integration_tests/tests/verify_bill_payment.rs b/multinode_integration_tests/tests/verify_bill_payment.rs index b56ca1cfc..d80947187 100644 --- a/multinode_integration_tests/tests/verify_bill_payment.rs +++ b/multinode_integration_tests/tests/verify_bill_payment.rs @@ -2,7 +2,7 @@ use bip39::{Language, Mnemonic, Seed}; use futures::Future; use masq_lib::blockchains::chains::Chain; -use masq_lib::constants::WEIS_OF_GWEI; +use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::utils::{derivation_path, NeighborhoodModeLight}; use multinode_integration_tests_lib::blockchain::BlockchainServer; use multinode_integration_tests_lib::masq_node::{MASQNode, MASQNodeUtils}; @@ -96,7 +96,7 @@ fn verify_bill_payment() { derivation_path(0, 3), ); - let amount = 10 * payment_thresholds.permanent_debt_allowed_gwei as u128 * WEIS_OF_GWEI as u128; + let amount = 10 * payment_thresholds.permanent_debt_allowed_gwei as u128 * WEIS_IN_GWEI as u128; let project_root = MASQNodeUtils::find_project_root(); let (consuming_node_name, consuming_node_index) = cluster.prepare_real_node(&consuming_config); @@ -323,20 +323,26 @@ fn assert_balances( expected_eth_balance: &str, expected_token_balance: &str, ) { - if let (Ok(eth_balance), Ok(token_balance)) = blockchain_interface.get_balances(&wallet) { - assert_eq!( - format!("{}", eth_balance), - String::from(expected_eth_balance), - "EthBalance" - ); - assert_eq!( - token_balance, - web3::types::U256::from_dec_str(expected_token_balance).unwrap(), - "TokenBalance" - ); - } else { - assert!(false, "Failed to retrieve balances {}", wallet); - } + let eth_balance = blockchain_interface + .get_gas_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 + .get_token_balance(&wallet) + .unwrap_or_else(|_| panic!("Failed to retrieve token 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 deploy_smart_contract(wallet: &Wallet, web3: &Web3, chain: Chain) -> Address { diff --git a/node/src/accountant/dao_utils.rs b/node/src/accountant/dao_utils.rs index 75b191841..e45e9fa7d 100644 --- a/node/src/accountant/dao_utils.rs +++ b/node/src/accountant/dao_utils.rs @@ -9,7 +9,7 @@ use crate::database::db_initializer::{ connection_or_panic, DbInitializationConfig, DbInitializerReal, }; use crate::sub_lib::accountant::PaymentThresholds; -use masq_lib::constants::WEIS_OF_GWEI; +use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::messages::{ RangeQuery, TopRecordsConfig, TopRecordsOrdering, UiPayableAccount, UiReceivableAccount, }; @@ -211,7 +211,7 @@ impl CustomQuery { .into_iter() .zip([min_amount, max_amount].into_iter()) .flat_map(|(param_names, gwei_num)| { - let wei_num = i128::from(gwei_num) * WEIS_OF_GWEI; + let wei_num = i128::from(gwei_num) * WEIS_IN_GWEI; let big_int_divided = BigIntDivider::deconstruct(wei_num); Self::balance_constraint_as_integer_pair(param_names, big_int_divided) }) @@ -260,7 +260,7 @@ pub fn remap_payable_accounts(accounts: Vec) -> Vec 0 { gwei } else { @@ -285,7 +285,7 @@ pub fn remap_receivable_accounts(accounts: Vec) -> Vec>, report_accounts_payable_sub_opt: Option>, - retrieve_transactions_sub: Option>, + request_balances_to_pay_payables_sub_opt: Option>, + retrieve_transactions_sub_opt: Option>, request_transaction_receipts_subs_opt: Option>, - report_inbound_payments_sub: Option>, - report_sent_payments_sub: Option>, - ui_message_sub: Option>, + report_inbound_payments_sub_opt: Option>, + report_sent_payables_sub_opt: Option>, + ui_message_sub_opt: Option>, message_id_generator: Box, logger: Logger, } @@ -129,6 +132,13 @@ pub struct SentPayables { pub response_skeleton_opt: Option, } +#[derive(Debug, Message, PartialEq, Eq)] +pub struct ConsumingWalletBalancesAndQualifiedPayables { + pub qualified_payables: Vec, + pub consuming_wallet_balances: ConsumingWalletBalances, + pub response_skeleton_opt: Option, +} + #[derive(Debug, Message, Default, PartialEq, Eq, Clone, Copy)] pub struct ScanForPayables { pub response_skeleton_opt: Option, @@ -192,7 +202,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReceivedPayments, _ctx: &mut Self::Context) -> Self::Result { if let Some(node_to_ui_msg) = self.scanners.receivable.finish_scan(msg, &self.logger) { - self.ui_message_sub + self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) @@ -201,12 +211,32 @@ impl Handler for Accountant { } } +impl Handler for Accountant { + type Result = (); + + fn handle( + &mut self, + msg: ConsumingWalletBalancesAndQualifiedPayables, + _ctx: &mut Self::Context, + ) -> Self::Result { + //TODO GH-672 with PaymentAdjuster hasn't been implemented yet + self.report_accounts_payable_sub_opt + .as_ref() + .expect("BlockchainBridge is unbound") + .try_send(ReportAccountsPayable { + accounts: msg.qualified_payables, + response_skeleton_opt: msg.response_skeleton_opt, + }) + .expect("BlockchainBridge is dead") + } +} + impl Handler for Accountant { type Result = (); fn handle(&mut self, msg: SentPayables, _ctx: &mut Self::Context) -> Self::Result { if let Some(node_to_ui_msg) = self.scanners.payable.finish_scan(msg, &self.logger) { - self.ui_message_sub + self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) @@ -280,7 +310,7 @@ impl Handler for Accountant { }, }; error!(self.logger, "Sending UiScanResponse: {:?}", error_msg); - self.ui_message_sub + self.ui_message_sub_opt .as_ref() .expect("UIGateway not bound") .try_send(error_msg) @@ -352,7 +382,7 @@ impl Handler for Accountant { fn handle(&mut self, msg: ReportTransactionReceipts, _ctx: &mut Self::Context) -> Self::Result { if let Some(node_to_ui_msg) = self.scanners.pending_payable.finish_scan(msg, &self.logger) { - self.ui_message_sub + self.ui_message_sub_opt .as_ref() .expect("UIGateway is not bound") .try_send(node_to_ui_msg) @@ -419,11 +449,12 @@ impl Accountant { scan_timings: ScanTimings::new(scan_intervals), financial_statistics: Rc::clone(&financial_statistics), report_accounts_payable_sub_opt: None, - retrieve_transactions_sub: None, + request_balances_to_pay_payables_sub_opt: None, + report_sent_payables_sub_opt: None, + retrieve_transactions_sub_opt: None, + report_inbound_payments_sub_opt: None, request_transaction_receipts_subs_opt: None, - report_inbound_payments_sub: None, - report_sent_payments_sub: None, - ui_message_sub: None, + ui_message_sub_opt: None, message_id_generator: Box::new(MessageIdGeneratorReal::default()), logger: Logger::new("Accountant"), } @@ -436,6 +467,10 @@ impl Accountant { report_routing_service_provided: recipient!(addr, ReportRoutingServiceProvidedMessage), report_exit_service_provided: recipient!(addr, ReportExitServiceProvidedMessage), report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), + report_consuming_wallet_balances_and_qualified_payables: recipient!( + addr, + ConsumingWalletBalancesAndQualifiedPayables + ), report_inbound_payments: recipient!(addr, ReceivedPayments), pending_payable_fingerprint: recipient!(addr, PendingPayableFingerprint), report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), @@ -528,11 +563,17 @@ impl Accountant { fn handle_bind_message(&mut self, msg: BindMessage) { self.report_accounts_payable_sub_opt = Some(msg.peer_actors.blockchain_bridge.report_accounts_payable); - self.retrieve_transactions_sub = + self.retrieve_transactions_sub_opt = Some(msg.peer_actors.blockchain_bridge.retrieve_transactions); - self.report_inbound_payments_sub = Some(msg.peer_actors.accountant.report_inbound_payments); - self.report_sent_payments_sub = Some(msg.peer_actors.accountant.report_sent_payments); - self.ui_message_sub = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); + self.report_inbound_payments_sub_opt = + Some(msg.peer_actors.accountant.report_inbound_payments); + self.request_balances_to_pay_payables_sub_opt = Some( + msg.peer_actors + .blockchain_bridge + .request_balances_to_pay_payables, + ); + self.report_sent_payables_sub_opt = Some(msg.peer_actors.accountant.report_sent_payments); + self.ui_message_sub_opt = Some(msg.peer_actors.ui_gateway.node_to_ui_message_sub); self.request_transaction_receipts_subs_opt = Some( msg.peer_actors .blockchain_bridge @@ -623,7 +664,7 @@ impl Accountant { fn handle_financials(&self, msg: &UiFinancialsRequest, client_id: u64, context_id: u64) { let body: MessageBody = self.compute_financials(msg, context_id); - self.ui_message_sub + self.ui_message_sub_opt .as_ref() .expect("UiGateway not bound") .try_send(NodeToUiMessage { @@ -759,7 +800,7 @@ impl Accountant { &self.logger, ) { Ok(scan_message) => { - self.report_accounts_payable_sub_opt + self.request_balances_to_pay_payables_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) @@ -806,7 +847,7 @@ impl Accountant { &self.logger, ) { Ok(scan_message) => self - .retrieve_transactions_sub + .retrieve_transactions_sub_opt .as_ref() .expect("BlockchainBridge is unbound") .try_send(scan_message) @@ -904,11 +945,11 @@ pub fn checked_conversion>(num: T) -> S { } pub fn gwei_to_wei + From + From, S>(gwei: S) -> T { - (T::from(gwei)).mul(T::from(WEIS_OF_GWEI as u32)) + (T::from(gwei)).mul(T::from(WEIS_IN_GWEI as u32)) } pub fn wei_to_gwei, S: Display + Copy + Div + From>(wei: S) -> T { - checked_conversion::(wei.div(S::from(WEIS_OF_GWEI as u32))) + checked_conversion::(wei.div(S::from(WEIS_IN_GWEI as u32))) } #[cfg(test)] @@ -975,8 +1016,8 @@ mod tests { ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; use crate::accountant::test_utils::{ - bc_from_earning_wallet, bc_from_wallets, make_payables, BannedDaoFactoryMock, - MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, + bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_payables, + BannedDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock, }; @@ -1264,8 +1305,8 @@ mod tests { system.run(); let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!( - blockchain_bridge_recording.get_record::(0), - &ReportAccountsPayable { + blockchain_bridge_recording.get_record::(0), + &RequestBalancesToPayPayables { accounts: vec![payable_account], response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1309,6 +1350,52 @@ mod tests { ); } + #[test] + fn received_balances_and_qualified_payables_considered_feasible_payments_thus_all_forwarded_to_blockchain_bridge( + ) { + let mut subject = AccountantBuilder::default().build(); + let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); + let report_recipient = blockchain_bridge + .system_stop_conditions(match_every_type_id!(ReportAccountsPayable)) + .start() + .recipient(); + subject.report_accounts_payable_sub_opt = Some(report_recipient); + let subject_addr = subject.start(); + let half_of_u32_max_in_wei = u32::MAX as u64 / (2 * WEIS_IN_GWEI as u64); + let account_1 = make_payable_account(half_of_u32_max_in_wei); + let account_2 = account_1.clone(); + let system = System::new("test"); + let consuming_balances_and_qualified_payments = + ConsumingWalletBalancesAndQualifiedPayables { + qualified_payables: vec![account_1.clone(), account_2.clone()], + consuming_wallet_balances: ConsumingWalletBalances { + gas_currency: U256::from(u32::MAX), + masq_tokens: U256::from(u32::MAX), + }, + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }), + }; + + subject_addr + .try_send(consuming_balances_and_qualified_payments) + .unwrap(); + + system.run(); + let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); + assert_eq!( + blockchain_bridge_recording.get_record::(0), + &ReportAccountsPayable { + accounts: vec![account_1, account_2], + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 1234, + context_id: 4321, + }) + } + ); + } + #[test] fn scan_pending_payables_request() { let mut config = bc_from_earning_wallet(make_wallet("some_wallet_address")); @@ -1621,8 +1708,8 @@ mod tests { } #[test] - fn accountant_sends_report_accounts_payable_to_blockchain_bridge_when_qualified_payable_found() - { + fn accountant_sends_asks_blockchain_bridge_about_consuming_wallet_balances_when_qualified_payable_found( + ) { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let now = SystemTime::now(); let payment_thresholds = PaymentThresholds::default(); @@ -1630,7 +1717,7 @@ mod tests { make_payables(now, &payment_thresholds); let payable_dao = PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let system = System::new("report_accounts_payable forwarded to blockchain_bridge"); + let system = System::new("request for balances forwarded to blockchain_bridge"); let mut subject = AccountantBuilder::default() .bootstrapper_config(bc_from_earning_wallet(make_wallet("some_wallet_address"))) .payable_daos(vec![ForPayableScanner(payable_dao)]) @@ -1650,10 +1737,10 @@ mod tests { system.run(); let blockchain_bridge_recorder = blockchain_bridge_recording_arc.lock().unwrap(); assert_eq!(blockchain_bridge_recorder.len(), 1); - let message = blockchain_bridge_recorder.get_record::(0); + let message = blockchain_bridge_recorder.get_record::(0); assert_eq!( message, - &ReportAccountsPayable { + &RequestBalancesToPayPayables { accounts: qualified_payables, response_skeleton_opt: None, } @@ -1954,7 +2041,7 @@ mod tests { let payable_scanner = ScannerMock::new() .begin_scan_params(&begin_scan_params_arc) .begin_scan_result(Err(BeginScanError::NothingToProcess)) - .begin_scan_result(Ok(ReportAccountsPayable { + .begin_scan_result(Ok(RequestBalancesToPayPayables { accounts: vec![], response_skeleton_opt: None, })) @@ -2157,8 +2244,8 @@ mod tests { ]; let payable_dao = PayableDaoMock::default().non_pending_payables_result(accounts.clone()); let (blockchain_bridge, _, blockchain_bridge_recordings_arc) = make_recorder(); - let blockchain_bridge = - blockchain_bridge.system_stop_conditions(match_every_type_id!(ReportAccountsPayable)); + let blockchain_bridge = blockchain_bridge + .system_stop_conditions(match_every_type_id!(RequestBalancesToPayPayables)); let system = System::new("scan_for_payable_message_triggers_payment_for_balances_over_the_curve"); let peer_actors = peer_actors_builder() @@ -2178,10 +2265,10 @@ mod tests { system.run(); let blockchain_bridge_recordings = blockchain_bridge_recordings_arc.lock().unwrap(); - let message = blockchain_bridge_recordings.get_record::(0); + let message = blockchain_bridge_recordings.get_record::(0); assert_eq!( message, - &ReportAccountsPayable { + &RequestBalancesToPayPayables { accounts, response_skeleton_opt: None, } @@ -2197,11 +2284,11 @@ mod tests { let (blockchain_bridge, _, blockchain_bridge_recording) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge .system_stop_conditions(match_every_type_id!( - ReportAccountsPayable, - ReportAccountsPayable + RequestBalancesToPayPayables, + RequestBalancesToPayPayables )) .start(); - let report_accounts_payable_sub = blockchain_bridge_addr.clone().recipient(); + let request_balances_to_pay_payables_sub = blockchain_bridge_addr.clone().recipient(); let last_paid_timestamp = to_time_t(SystemTime::now()) - DEFAULT_PAYMENT_THRESHOLDS.maturity_threshold_sec as i64 - 1; @@ -2233,7 +2320,8 @@ mod tests { context_id: 444, }), }; - subject.report_accounts_payable_sub_opt = Some(report_accounts_payable_sub); + subject.request_balances_to_pay_payables_sub_opt = + Some(request_balances_to_pay_payables_sub); let addr = subject.start(); addr.try_send(message_before.clone()).unwrap(); @@ -2259,12 +2347,12 @@ mod tests { let recording = blockchain_bridge_recording.lock().unwrap(); let messages_received = recording.len(); assert_eq!(messages_received, 2); - let first_message: &ReportAccountsPayable = recording.get_record(0); + let first_message: &RequestBalancesToPayPayables = recording.get_record(0); assert_eq!( first_message.response_skeleton_opt, message_before.response_skeleton_opt ); - let second_message: &ReportAccountsPayable = recording.get_record(1); + let second_message: &RequestBalancesToPayPayables = recording.get_record(1); assert_eq!( second_message.response_skeleton_opt, message_after.response_skeleton_opt @@ -3036,6 +3124,8 @@ mod tests { let mut transaction_receipt_tx_2_fourth_round = TransactionReceipt::default(); transaction_receipt_tx_2_fourth_round.status = Some(U64::from(1)); // confirmed let blockchain_interface = BlockchainInterfaceMock::default() + .get_gas_balance_result(Ok(U256::from(u128::MAX))) + .get_token_balance_result(Ok(U256::from(u128::MAX))) .get_transaction_count_result(Ok(web3::types::U256::from(1))) .get_transaction_count_result(Ok(web3::types::U256::from(2))) //because we cannot have both, resolution on the high level and also of what's inside blockchain interface, diff --git a/node/src/accountant/receivable_dao.rs b/node/src/accountant/receivable_dao.rs index 473a80850..f8ac78184 100644 --- a/node/src/accountant/receivable_dao.rs +++ b/node/src/accountant/receivable_dao.rs @@ -24,7 +24,7 @@ use crate::sub_lib::wallet::Wallet; use indoc::indoc; use itertools::Either; use itertools::Either::Left; -use masq_lib::constants::WEIS_OF_GWEI; +use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::logger::Logger; use masq_lib::utils::{plus, ExpectValue}; use rusqlite::OptionalExtension; @@ -188,7 +188,7 @@ impl ReceivableDao for ReceivableDaoReal { ); let mut stmt = self.conn.prepare(sql).expect("Couldn't prepare statement"); let (unban_balance_high_b, unban_balance_low_b) = BigIntDivider::deconstruct( - (payment_thresholds.unban_below_gwei as i128) * WEIS_OF_GWEI, + (payment_thresholds.unban_below_gwei as i128) * WEIS_IN_GWEI, ); stmt.query_map( named_params! { diff --git a/node/src/accountant/scanners.rs b/node/src/accountant/scanners.rs index 008006671..7a9d28c07 100644 --- a/node/src/accountant/scanners.rs +++ b/node/src/accountant/scanners.rs @@ -16,13 +16,14 @@ use crate::accountant::{ RequestTransactionReceipts, ResponseSkeleton, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayables, }; -use crate::accountant::{PendingPayableId, PendingTransactionStatus, ReportAccountsPayable}; +use crate::accountant::{PendingPayableId, PendingTransactionStatus}; use crate::banned_dao::BannedDao; use crate::blockchain::blockchain_bridge::{PendingPayableFingerprint, RetrieveTransactions}; use crate::blockchain::blockchain_interface::BlockchainError; use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, ScanIntervals, }; +use crate::sub_lib::blockchain_bridge::RequestBalancesToPayPayables; use crate::sub_lib::utils::{NotifyLaterHandle, NotifyLaterHandleReal}; use crate::sub_lib::wallet::Wallet; use actix::{Context, Message, System}; @@ -43,7 +44,7 @@ use time::OffsetDateTime; use web3::types::TransactionReceipt; pub struct Scanners { - pub payable: Box>, + pub payable: Box>, pub pending_payable: Box>, pub receivable: Box>, } @@ -158,13 +159,13 @@ pub struct PayableScanner { pub payable_threshold_gauge: Box, } -impl Scanner for PayableScanner { +impl Scanner for PayableScanner { fn begin_scan( &mut self, timestamp: SystemTime, response_skeleton_opt: Option, logger: &Logger, - ) -> Result { + ) -> Result { if let Some(timestamp) = self.scan_started_at() { return Err(BeginScanError::ScanAlreadyRunning(timestamp)); } @@ -192,7 +193,7 @@ impl Scanner for PayableScanner { "Chose {} qualified debts to pay", qualified_payable.len() ); - Ok(ReportAccountsPayable { + Ok(RequestBalancesToPayPayables { accounts: qualified_payable, response_skeleton_opt, }) @@ -985,7 +986,7 @@ mod tests { use crate::sub_lib::accountant::{ DaoFactories, FinancialStatistics, PaymentThresholds, DEFAULT_PAYMENT_THRESHOLDS, }; - use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; + use crate::sub_lib::blockchain_bridge::RequestBalancesToPayPayables; use crate::test_utils::make_wallet; use ethereum_types::{BigEndianHash, U64}; use ethsign_crypto::Keccak256; @@ -1110,7 +1111,7 @@ mod tests { assert_eq!(timestamp, Some(now)); assert_eq!( result, - Ok(ReportAccountsPayable { + Ok(RequestBalancesToPayPayables { accounts: qualified_payable_accounts.clone(), response_skeleton_opt: None, }) diff --git a/node/src/accountant/scanners_utils.rs b/node/src/accountant/scanners_utils.rs index 9afeac521..d22455ffa 100644 --- a/node/src/accountant/scanners_utils.rs +++ b/node/src/accountant/scanners_utils.rs @@ -273,7 +273,7 @@ mod tests { use crate::blockchain::blockchain_interface::BlockchainError; use crate::sub_lib::accountant::PaymentThresholds; use crate::test_utils::make_wallet; - use masq_lib::constants::WEIS_OF_GWEI; + use masq_lib::constants::WEIS_IN_GWEI; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use std::time::SystemTime; @@ -448,7 +448,7 @@ mod tests { let middle_point = tested_fn(&payment_thresholds, middle_point_timestamp); let lower_corner_point = tested_fn(&payment_thresholds, lower_corner_timestamp); - let allowed_imprecision = WEIS_OF_GWEI; + let allowed_imprecision = WEIS_IN_GWEI; let ideal_template_higher: i128 = gwei_to_wei(payment_thresholds.debt_threshold_gwei); let ideal_template_middle: i128 = gwei_to_wei( (payment_thresholds.debt_threshold_gwei diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index 4ffc97bef..9c25d7120 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -2,7 +2,8 @@ use crate::accountant::payable_dao::{Payable, PayableAccount}; use crate::accountant::{ - ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, + ConsumingWalletBalancesAndQualifiedPayables, ReceivedPayments, ResponseSkeleton, ScanError, + SentPayables, SkeletonOptHolder, }; use crate::accountant::{ReportTransactionReceipts, RequestTransactionReceipts}; use crate::blockchain::blockchain_interface::{ @@ -15,8 +16,8 @@ use crate::db_config::config_dao::ConfigDaoReal; use crate::db_config::persistent_configuration::{ PersistentConfiguration, PersistentConfigurationReal, }; -use crate::sub_lib::blockchain_bridge::BlockchainBridgeSubs; -use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; +use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, RequestBalancesToPayPayables}; +use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, ReportAccountsPayable}; use crate::sub_lib::peer_actors::BindMessage; use crate::sub_lib::set_consuming_wallet_message::SetConsumingWalletMessage; use crate::sub_lib::utils::handle_ui_crash_request; @@ -46,6 +47,7 @@ pub struct BlockchainBridge { persistent_config: Box, set_consuming_wallet_subs_opt: Option>>, sent_payable_subs_opt: Option>, + balances_and_payables_sub_opt: Option>, received_payments_subs_opt: Option>, scan_error_subs_opt: Option>, crashable: bool, @@ -74,6 +76,11 @@ impl Handler for BlockchainBridge { self.payment_confirmation .report_transaction_receipts_sub_opt = Some(msg.peer_actors.accountant.report_transaction_receipts); + self.balances_and_payables_sub_opt = Some( + msg.peer_actors + .accountant + .report_consuming_wallet_balances_and_qualified_payables, + ); self.sent_payable_subs_opt = Some(msg.peer_actors.accountant.report_sent_payments); self.received_payments_subs_opt = Some(msg.peer_actors.accountant.report_inbound_payments); self.scan_error_subs_opt = Some(msg.peer_actors.accountant.scan_errors); @@ -113,7 +120,7 @@ impl Handler for BlockchainBridge { self.handle_scan( Self::handle_retrieve_transactions, ScanType::Receivables, - &msg, + msg, ) } } @@ -125,11 +132,23 @@ impl Handler for BlockchainBridge { self.handle_scan( Self::handle_request_transaction_receipts, ScanType::PendingPayables, - &msg, + msg, ) } } +impl Handler for BlockchainBridge { + type Result = (); + + fn handle(&mut self, msg: RequestBalancesToPayPayables, _ctx: &mut Self::Context) { + self.handle_scan( + Self::handle_request_balances_to_pay_payables, + ScanType::Payables, + msg, + ); + } +} + impl Handler for BlockchainBridge { type Result = (); @@ -137,7 +156,7 @@ impl Handler for BlockchainBridge { self.handle_scan( Self::handle_report_accounts_payable, ScanType::Payables, - &msg, + msg, ) } } @@ -173,6 +192,7 @@ impl BlockchainBridge { persistent_config, set_consuming_wallet_subs_opt: None, sent_payable_subs_opt: None, + balances_and_payables_sub_opt: None, received_payments_subs_opt: None, scan_error_subs_opt: None, crashable, @@ -227,18 +247,74 @@ impl BlockchainBridge { BlockchainBridgeSubs { bind: recipient!(addr, BindMessage), report_accounts_payable: recipient!(addr, ReportAccountsPayable), + request_balances_to_pay_payables: recipient!(addr, RequestBalancesToPayPayables), retrieve_transactions: recipient!(addr, RetrieveTransactions), ui_sub: recipient!(addr, NodeFromUiMessage), request_transaction_receipts: recipient!(addr, RequestTransactionReceipts), } } + fn handle_request_balances_to_pay_payables( + &mut self, + msg: RequestBalancesToPayPayables, + ) -> Result<(), String> { + let consuming_wallet = match self.consuming_wallet_opt.as_ref() { + Some(wallet) => wallet, + None => { + return Err( + "Cannot inspect available balances for payables while consuming wallet \ + is missing" + .to_string(), + ) + } + }; + //TODO rewrite this into a batch call as soon as GH-629 gets into master + let gas_balance = match self.blockchain_interface.get_gas_balance(consuming_wallet) { + Ok(gas_balance) => gas_balance, + Err(e) => { + return Err(format!( + "Did not find out gas balance of the consuming wallet: {:?}", + e + )) + } + }; + let token_balance = match self + .blockchain_interface + .get_token_balance(consuming_wallet) + { + Ok(token_balance) => token_balance, + Err(e) => { + return Err(format!( + "Did not find out token balance of the consuming wallet: {:?}", + e + )) + } + }; + let consuming_wallet_balances = { + ConsumingWalletBalances { + gas_currency: gas_balance, + masq_tokens: token_balance, + } + }; + self.balances_and_payables_sub_opt + .as_ref() + .expect("Accountant is unbound") + .try_send(ConsumingWalletBalancesAndQualifiedPayables { + qualified_payables: msg.accounts, + consuming_wallet_balances, + response_skeleton_opt: msg.response_skeleton_opt, + }) + .expect("Accountant is dead"); + + Ok(()) + } + fn handle_report_accounts_payable( &mut self, - creditors_msg: &ReportAccountsPayable, + creditors_msg: ReportAccountsPayable, ) -> Result<(), String> { let skeleton = creditors_msg.response_skeleton_opt; - let processed_payments = self.preprocess_payments(creditors_msg); + let processed_payments = self.preprocess_payments(&creditors_msg); processed_payments.map(|payments| { self.sent_payable_subs_opt .as_ref() @@ -269,7 +345,7 @@ impl BlockchainBridge { } } - fn handle_retrieve_transactions(&mut self, msg: &RetrieveTransactions) -> Result<(), String> { + fn handle_retrieve_transactions(&mut self, msg: RetrieveTransactions) -> Result<(), String> { let start_block = match self.persistent_config.start_block() { Ok (sb) => sb, Err (e) => panic! ("Cannot retrieve start block from database; payments to you may not be processed: {:?}", e) @@ -308,7 +384,7 @@ impl BlockchainBridge { fn handle_request_transaction_receipts( &mut self, - msg: &RequestTransactionReceipts, + msg: RequestTransactionReceipts, ) -> Result<(), String> { let short_circuit_result: ( Vec>, @@ -329,7 +405,7 @@ impl BlockchainBridge { let (vector_of_results, error_opt) = short_circuit_result; let pairs = vector_of_results .into_iter() - .zip(msg.pending_payable.iter().cloned()) + .zip(msg.pending_payable.into_iter()) .collect_vec(); self.payment_confirmation .report_transaction_receipts_sub_opt @@ -350,9 +426,9 @@ impl BlockchainBridge { Ok(()) } - fn handle_scan(&mut self, handler: F, scan_type: ScanType, msg: &M) + fn handle_scan(&mut self, handler: F, scan_type: ScanType, msg: M) where - F: FnOnce(&mut BlockchainBridge, &M) -> Result<(), String>, + F: FnOnce(&mut BlockchainBridge, M) -> Result<(), String>, M: SkeletonOptHolder, { let skeleton_opt = msg.skeleton_opt(); @@ -422,6 +498,7 @@ mod tests { use crate::accountant::dao_utils::from_time_t; use crate::accountant::payable_dao::PayableAccount; use crate::accountant::test_utils::make_pending_payable_fingerprint; + use crate::accountant::ConsumingWalletBalancesAndQualifiedPayables; use crate::blockchain::bip32::Bip32ECKeyProvider; use crate::blockchain::blockchain_bridge::Payable; use crate::blockchain::blockchain_interface::{ @@ -434,6 +511,7 @@ mod tests { use crate::db_config::persistent_configuration::PersistentConfigError; use crate::match_every_type_id; use crate::node_test_utils::check_timestamp; + use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::test_utils::persistent_configuration_mock::PersistentConfigurationMock; use crate::test_utils::recorder::{make_recorder, peer_actors_builder}; use crate::test_utils::recorder_stop_conditions::StopCondition; @@ -452,7 +530,7 @@ mod tests { use rustc_hex::FromHex; use std::any::TypeId; use std::sync::{Arc, Mutex}; - use std::time::SystemTime; + use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160, H256, U256}; #[test] @@ -604,6 +682,195 @@ mod tests { assert_eq!(result, Err("No consuming wallet specified".to_string())); } + #[test] + fn handle_request_balances_to_pay_payables_reports_balances_and_payables_back_to_accountant() { + let system = System::new( + "handle_request_balances_to_pay_payables_reports_balances_and_payables_back_to_accountant", + ); + let get_gas_balance_params_arc = Arc::new(Mutex::new(vec![])); + let get_token_balance_params_arc = Arc::new(Mutex::new(vec![])); + let (accountant, _, accountant_recording_arc) = make_recorder(); + let gas_balance = U256::from(4455); + let token_balance = U256::from(112233); + let wallet_balances_found = ConsumingWalletBalances { + gas_currency: gas_balance, + masq_tokens: token_balance, + }; + let blockchain_interface = BlockchainInterfaceMock::default() + .get_gas_balance_params(&get_gas_balance_params_arc) + .get_gas_balance_result(Ok(gas_balance)) + .get_token_balance_params(&get_token_balance_params_arc) + .get_token_balance_result(Ok(token_balance)); + let consuming_wallet = make_paying_wallet(b"somewallet"); + let persistent_configuration = PersistentConfigurationMock::default(); + let qualified_accounts = vec![PayableAccount { + wallet: make_wallet("booga"), + balance_wei: 78_654_321, + last_paid_timestamp: SystemTime::now() + .checked_sub(Duration::from_secs(1000)) + .unwrap(), + pending_payable_opt: None, + }]; + let subject = BlockchainBridge::new( + Box::new(blockchain_interface), + Box::new(persistent_configuration), + false, + Some(consuming_wallet.clone()), + ); + let addr = subject.start(); + let subject_subs = BlockchainBridge::make_subs_from(&addr); + let peer_actors = peer_actors_builder().accountant(accountant).build(); + send_bind_message!(subject_subs, peer_actors); + + addr.try_send(RequestBalancesToPayPayables { + accounts: qualified_accounts.clone(), + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 11122, + context_id: 444, + }), + }) + .unwrap(); + + System::current().stop(); + system.run(); + let get_gas_balance_params = get_gas_balance_params_arc.lock().unwrap(); + assert_eq!(*get_gas_balance_params, vec![consuming_wallet.clone()]); + let get_token_balance_params = get_token_balance_params_arc.lock().unwrap(); + assert_eq!(*get_token_balance_params, vec![consuming_wallet]); + let accountant_received_payment = accountant_recording_arc.lock().unwrap(); + assert_eq!(accountant_received_payment.len(), 1); + let reported_balances_and_qualified_accounts: &ConsumingWalletBalancesAndQualifiedPayables = + accountant_received_payment.get_record(0); + assert_eq!( + reported_balances_and_qualified_accounts, + &ConsumingWalletBalancesAndQualifiedPayables { + qualified_payables: qualified_accounts, + consuming_wallet_balances: wallet_balances_found, + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 11122, + context_id: 444 + }) + } + ); + } + + fn assert_failure_during_balance_inspection( + test_name: &str, + blockchain_interface: BlockchainInterfaceMock, + error_msg: &str, + ) { + init_test_logging(); + let (accountant, _, accountant_recording_arc) = make_recorder(); + let scan_error_recipient: Recipient = accountant + .system_stop_conditions(match_every_type_id!(ScanError)) + .start() + .recipient(); + let persistent_configuration = PersistentConfigurationMock::default(); + let consuming_wallet = make_wallet(test_name); + let mut subject = BlockchainBridge::new( + Box::new(blockchain_interface), + Box::new(persistent_configuration), + false, + Some(consuming_wallet), + ); + subject.logger = Logger::new(test_name); + subject.scan_error_subs_opt = Some(scan_error_recipient); + let request = RequestBalancesToPayPayables { + accounts: vec![PayableAccount { + wallet: make_wallet("blah"), + balance_wei: 42, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }], + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 11, + context_id: 2323, + }), + }; + let subject_addr = subject.start(); + let system = System::new(test_name); + + // Don't eliminate or bypass this message as an important check that + // the Handler employs scan_handle() + subject_addr.try_send(request).unwrap(); + + system.run(); + let recording = accountant_recording_arc.lock().unwrap(); + let message = recording.get_record::(0); + assert_eq!(recording.len(), 1); + assert_eq!( + message, + &ScanError { + scan_type: ScanType::Payables, + response_skeleton_opt: Some(ResponseSkeleton { + client_id: 11, + context_id: 2323 + }), + msg: error_msg.to_string() + } + ); + TestLogHandler::new().exists_log_containing(&format!("WARN: {}: {}", test_name, error_msg)); + } + + #[test] + fn handle_request_balances_to_pay_payables_fails_on_inspection_of_gas_balance() { + let test_name = + "handle_request_balances_to_pay_payables_fails_on_inspection_of_gas_balance"; + let blockchain_interface = BlockchainInterfaceMock::default().get_gas_balance_result(Err( + BlockchainError::QueryFailed("Lazy and yet you're asking for balances?".to_string()), + )); + let error_msg = "Did not find out gas balance of the consuming wallet: \ + QueryFailed(\"Lazy and yet you're asking for balances?\")"; + + assert_failure_during_balance_inspection(test_name, blockchain_interface, error_msg) + } + + #[test] + fn handle_request_balances_to_pay_payables_fails_on_inspection_of_token_balance() { + let test_name = + "handle_request_balances_to_pay_payables_fails_on_inspection_of_token_balance"; + let blockchain_interface = BlockchainInterfaceMock::default() + .get_gas_balance_result(Ok(U256::from(45678))) + .get_token_balance_result(Err(BlockchainError::QueryFailed( + "Go get you a job. This balance must be deserved".to_string(), + ))); + let error_msg = "Did not find out token balance of the consuming wallet: QueryFailed(\ + \"Go get you a job. This balance must be deserved\")"; + + assert_failure_during_balance_inspection(test_name, blockchain_interface, error_msg) + } + + #[test] + fn handle_request_balances_to_pay_payables_fails_at_missing_consuming_wallet() { + let blockchain_interface = BlockchainInterfaceMock::default(); + let persistent_configuration = PersistentConfigurationMock::default(); + let mut subject = BlockchainBridge::new( + Box::new(blockchain_interface), + Box::new(persistent_configuration), + false, + None, + ); + let request = RequestBalancesToPayPayables { + accounts: vec![PayableAccount { + wallet: make_wallet("blah"), + balance_wei: 4254, + last_paid_timestamp: SystemTime::now(), + pending_payable_opt: None, + }], + response_skeleton_opt: None, + }; + + let result = subject.handle_request_balances_to_pay_payables(request); + + assert_eq!( + result, + Err( + "Cannot inspect available balances for payables while consuming wallet is missing" + .to_string() + ) + ) + } + #[test] fn handle_report_accounts_payable_transacts_and_sends_finished_payments_back_to_accountant() { let system = @@ -1021,7 +1288,7 @@ mod tests { "blockchain_bridge_can_return_report_transaction_receipts_with_an_empty_vector", ); - let _ = subject.handle_request_transaction_receipts(&msg); + let _ = subject.handle_request_transaction_receipts(msg); System::current().stop(); system.run(); @@ -1085,7 +1352,7 @@ mod tests { let _ = subject.handle_scan( BlockchainBridge::handle_request_transaction_receipts, ScanType::PendingPayables, - &msg, + msg, ); System::current().stop(); @@ -1269,7 +1536,7 @@ mod tests { response_skeleton_opt: None, }; - let _ = subject.handle_retrieve_transactions(&retrieve_transactions); + let _ = subject.handle_retrieve_transactions(retrieve_transactions); } #[test] @@ -1301,19 +1568,19 @@ mod tests { response_skeleton_opt: None, }; - let _ = subject.handle_retrieve_transactions(&retrieve_transactions); + let _ = subject.handle_retrieve_transactions(retrieve_transactions); } fn success_handler( _bcb: &mut BlockchainBridge, - _msg: &RetrieveTransactions, + _msg: RetrieveTransactions, ) -> Result<(), String> { Ok(()) } fn failure_handler( _bcb: &mut BlockchainBridge, - _msg: &RetrieveTransactions, + _msg: RetrieveTransactions, ) -> Result<(), String> { Err("My tummy hurts".to_string()) } @@ -1340,7 +1607,7 @@ mod tests { subject.handle_scan( success_handler, ScanType::Receivables, - &retrieve_transactions, + retrieve_transactions, ); System::current().stop(); @@ -1369,7 +1636,7 @@ mod tests { subject.handle_scan( failure_handler, ScanType::Receivables, - &retrieve_transactions, + retrieve_transactions, ); System::current().stop(); @@ -1411,7 +1678,7 @@ mod tests { subject.handle_scan( failure_handler, ScanType::Receivables, - &retrieve_transactions, + retrieve_transactions, ); System::current().stop(); diff --git a/node/src/blockchain/blockchain_interface.rs b/node/src/blockchain/blockchain_interface.rs index bef563193..9ad961be9 100644 --- a/node/src/blockchain/blockchain_interface.rs +++ b/node/src/blockchain/blockchain_interface.rs @@ -76,9 +76,10 @@ impl Display for BlockchainError { } pub type BlockchainResult = Result; -pub type Balance = BlockchainResult; -pub type Nonce = BlockchainResult; -pub type Receipt = BlockchainResult>; +pub type ResultForBalance = BlockchainResult; +pub type ResultForBothBalances = BlockchainResult<(web3::types::U256, web3::types::U256)>; +pub type ResultForNonce = BlockchainResult; +pub type ResultForReceipt = BlockchainResult>; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RetrievedBlockchainTransactions { @@ -100,20 +101,13 @@ pub trait BlockchainInterface { inputs: BlockchainTxnInputs, ) -> Result<(H256, SystemTime), BlockchainTransactionError>; - fn get_eth_balance(&self, address: &Wallet) -> Balance; + fn get_gas_balance(&self, address: &Wallet) -> ResultForBalance; - fn get_token_balance(&self, address: &Wallet) -> Balance; + fn get_token_balance(&self, address: &Wallet) -> ResultForBalance; - fn get_balances(&self, address: &Wallet) -> (Balance, Balance) { - ( - self.get_eth_balance(address), - self.get_token_balance(address), - ) - } + fn get_transaction_count(&self, address: &Wallet) -> ResultForNonce; - fn get_transaction_count(&self, address: &Wallet) -> Nonce; - - fn get_transaction_receipt(&self, hash: H256) -> Receipt; + fn get_transaction_receipt(&self, hash: H256) -> ResultForReceipt; fn send_transaction_tools<'a>( &'a self, @@ -166,22 +160,22 @@ impl BlockchainInterface for BlockchainInterfaceClandestine { Err(BlockchainTransactionError::Sending(msg, H256::default())) } - fn get_eth_balance(&self, _address: &Wallet) -> Balance { + fn get_gas_balance(&self, _address: &Wallet) -> ResultForBalance { error!(self.logger, "Can't get eth balance clandestinely yet",); Ok(0.into()) } - fn get_token_balance(&self, _address: &Wallet) -> Balance { + fn get_token_balance(&self, _address: &Wallet) -> ResultForBalance { error!(self.logger, "Can't get token balance clandestinely yet",); Ok(0.into()) } - fn get_transaction_count(&self, _address: &Wallet) -> Nonce { + fn get_transaction_count(&self, _address: &Wallet) -> ResultForNonce { error!(self.logger, "Can't get transaction count clandestinely yet",); Ok(0.into()) } - fn get_transaction_receipt(&self, _hash: H256) -> Receipt { + fn get_transaction_receipt(&self, _hash: H256) -> ResultForReceipt { error!( self.logger, "Can't get transaction receipt clandestinely yet", @@ -333,7 +327,7 @@ where } } - fn get_eth_balance(&self, wallet: &Wallet) -> Balance { + fn get_gas_balance(&self, wallet: &Wallet) -> ResultForBalance { self.web3 .eth() .balance(wallet.address(), None) @@ -341,20 +335,20 @@ where .wait() } - fn get_token_balance(&self, wallet: &Wallet) -> Balance { + fn get_token_balance(&self, wallet: &Wallet) -> ResultForBalance { self.contract .query( "balanceOf", wallet.address(), None, - Options::with(|_| {}), + Options::default(), None, ) .map_err(|e| BlockchainError::QueryFailed(e.to_string())) .wait() } - fn get_transaction_count(&self, wallet: &Wallet) -> Nonce { + fn get_transaction_count(&self, wallet: &Wallet) -> ResultForNonce { self.web3 .eth() .transaction_count(wallet.address(), Some(BlockNumber::Pending)) @@ -362,7 +356,7 @@ where .wait() } - fn get_transaction_receipt(&self, hash: H256) -> Receipt { + fn get_transaction_receipt(&self, hash: H256) -> ResultForReceipt { self.web3 .eth() .transaction_receipt(hash) @@ -580,9 +574,9 @@ mod tests { SendTransactionToolsWrapperMock, TestTransport, }; use crate::sub_lib::wallet::Wallet; + use crate::test_utils::make_paying_wallet; use crate::test_utils::recorder::make_recorder; use crate::test_utils::unshared_test_utils::decode_hex; - use crate::test_utils::{await_value, make_paying_wallet}; use crate::test_utils::{make_wallet, TestRawTransaction}; use actix::{Actor, System}; use crossbeam_channel::{unbounded, Receiver}; @@ -961,7 +955,7 @@ mod tests { ); let result = subject - .get_eth_balance( + .get_gas_balance( &Wallet::from_str("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fc").unwrap(), ) .unwrap(); @@ -986,7 +980,7 @@ mod tests { ); let result = - subject.get_eth_balance(&Wallet::new("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fQ")); + subject.get_gas_balance(&Wallet::new("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fQ")); assert_eq!(result, Err(BlockchainError::InvalidAddress)); } @@ -1012,7 +1006,7 @@ mod tests { TEST_DEFAULT_CHAIN, ); - let result = subject.get_eth_balance( + let result = subject.get_gas_balance( &Wallet::from_str("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fc").unwrap(), ); @@ -1024,6 +1018,16 @@ mod tests { }; } + #[test] + fn blockchain_interface_non_clandestine_returns_error_for_unintelligible_response_to_gas_balance( + ) { + let act = |subject: &BlockchainInterfaceNonClandestine, wallet: &Wallet| { + subject.get_gas_balance(wallet) + }; + + assert_error_during_requesting_balance(act, "invalid hex character"); + } + #[test] fn blockchain_interface_non_clandestine_can_retrieve_token_balance_of_a_wallet() { let port = find_free_port(); @@ -1074,8 +1078,19 @@ mod tests { } #[test] - fn blockchain_interface_non_clandestine_returns_an_error_for_unintelligible_response_when_requesting_token_balance( + fn blockchain_interface_non_clandestine_returns_error_for_unintelligible_response_to_token_balance( ) { + let act = |subject: &BlockchainInterfaceNonClandestine, wallet: &Wallet| { + subject.get_token_balance(wallet) + }; + + assert_error_during_requesting_balance(act, "Invalid hex"); + } + + fn assert_error_during_requesting_balance(act: F, expected_err_msg_fragment: &str) + where + F: FnOnce(&BlockchainInterfaceNonClandestine, &Wallet) -> ResultForBalance, + { let port = find_free_port(); let _test_server = TestServer::start (port, vec![ br#"{"jsonrpc":"2.0","id":0,"result":"0x000000000000000000000000000000000000000000000000000000000000FFFQ"}"#.to_vec() @@ -1092,51 +1107,21 @@ mod tests { TEST_DEFAULT_CHAIN, ); - let result = subject.get_token_balance( + let result = act( + &subject, &Wallet::from_str("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fc").unwrap(), ); - match result { - Err(BlockchainError::QueryFailed(msg)) if msg.contains("Invalid hex") => (), - x => panic!("Expected complaint about hex character, but got {:?}", x), - } - } - - #[test] - fn blockchain_interface_non_clandestine_can_request_both_eth_and_token_balances_happy_path() { - let port = find_free_port(); - let _test_server = TestServer::start (port, vec![ - br#"{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000000000000000000001"}"#.to_vec(), - br#"{"jsonrpc":"2.0","id":0,"result":"0x0000000000000000000000000000000000000000000000000000000000000001"}"#.to_vec(), - ]); - - let (event_loop_handle, transport) = Http::with_max_parallel( - &format!("http://{}:{}", &Ipv4Addr::LOCALHOST.to_string(), port), - REQUESTS_IN_PARALLEL, + let err_msg = match result { + Err(BlockchainError::QueryFailed(msg)) => msg, + x => panic!("Expected BlockchainError::QueryFailed, but got {:?}", x), + }; + assert!( + err_msg.contains(expected_err_msg_fragment), + "Expected this fragment {} in this err msg: {}", + expected_err_msg_fragment, + err_msg ) - .unwrap(); - let subject = BlockchainInterfaceNonClandestine::new( - transport, - event_loop_handle, - TEST_DEFAULT_CHAIN, - ); - - let results: (Balance, Balance) = await_value(None, || { - match subject.get_balances( - &Wallet::from_str("0x3f69f9efd4f2592fd70be8c32ecd9dce71c472fc").unwrap(), - ) { - (Ok(a), Ok(b)) => Ok((Ok(a), Ok(b))), - (Err(a), _) => Err(a), - (_, Err(b)) => Err(b), - } - }) - .unwrap(); - - let eth_balance = results.0.unwrap(); - let token_balance = results.1.unwrap(); - - assert_eq!(eth_balance, U256::from(1),); - assert_eq!(token_balance, U256::from(1)) } #[test] diff --git a/node/src/blockchain/test_utils.rs b/node/src/blockchain/test_utils.rs index 7479908f4..e3e5ba126 100644 --- a/node/src/blockchain/test_utils.rs +++ b/node/src/blockchain/test_utils.rs @@ -4,8 +4,8 @@ use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; use crate::blockchain::blockchain_interface::{ - Balance, BlockchainError, BlockchainInterface, BlockchainResult, BlockchainTransactionError, - BlockchainTxnInputs, Nonce, Receipt, REQUESTS_IN_PARALLEL, + BlockchainError, BlockchainInterface, BlockchainResult, BlockchainTransactionError, + BlockchainTxnInputs, ResultForBalance, ResultForNonce, ResultForReceipt, REQUESTS_IN_PARALLEL, }; use crate::blockchain::tool_wrappers::SendTransactionToolsWrapper; use crate::sub_lib::wallet::Wallet; @@ -58,8 +58,12 @@ pub struct BlockchainInterfaceMock { RefCell>>, send_transaction_parameters: Arc>>, send_transaction_results: RefCell>>, + get_gas_balance_params: Arc>>, + get_gas_balance_results: RefCell>, + get_token_balance_params: Arc>>, + get_token_balance_results: RefCell>, get_transaction_receipt_params: Arc>>, - get_transaction_receipt_results: RefCell>, + get_transaction_receipt_results: RefCell>, send_transaction_tools_results: RefCell>>, contract_address_results: RefCell>, get_transaction_count_parameters: Arc>>, @@ -96,6 +100,26 @@ impl BlockchainInterfaceMock { self } + pub fn get_gas_balance_params(mut self, params: &Arc>>) -> Self { + self.get_gas_balance_params = params.clone(); + self + } + + pub fn get_gas_balance_result(self, result: ResultForBalance) -> Self { + self.get_gas_balance_results.borrow_mut().push(result); + self + } + + pub fn get_token_balance_params(mut self, params: &Arc>>) -> Self { + self.get_token_balance_params = params.clone(); + self + } + + pub fn get_token_balance_result(self, result: ResultForBalance) -> Self { + self.get_token_balance_results.borrow_mut().push(result); + self + } + pub fn contract_address_result(self, address: Address) -> Self { self.contract_address_results.borrow_mut().push(address); self @@ -116,7 +140,7 @@ impl BlockchainInterfaceMock { self } - pub fn get_transaction_receipt_result(self, result: Receipt) -> Self { + pub fn get_transaction_receipt_result(self, result: ResultForReceipt) -> Self { self.get_transaction_receipt_results .borrow_mut() .push(result); @@ -162,15 +186,23 @@ impl BlockchainInterface for BlockchainInterfaceMock { self.send_transaction_results.borrow_mut().remove(0) } - fn get_eth_balance(&self, _address: &Wallet) -> Balance { - unimplemented!() + fn get_gas_balance(&self, address: &Wallet) -> ResultForBalance { + self.get_gas_balance_params + .lock() + .unwrap() + .push(address.clone()); + self.get_gas_balance_results.borrow_mut().remove(0) } - fn get_token_balance(&self, _address: &Wallet) -> Balance { - unimplemented!() + fn get_token_balance(&self, address: &Wallet) -> ResultForBalance { + self.get_token_balance_params + .lock() + .unwrap() + .push(address.clone()); + self.get_token_balance_results.borrow_mut().remove(0) } - fn get_transaction_count(&self, wallet: &Wallet) -> Nonce { + fn get_transaction_count(&self, wallet: &Wallet) -> ResultForNonce { self.get_transaction_count_parameters .lock() .unwrap() @@ -178,7 +210,7 @@ impl BlockchainInterface for BlockchainInterfaceMock { self.get_transaction_count_results.borrow_mut().remove(0) } - fn get_transaction_receipt(&self, hash: H256) -> Receipt { + fn get_transaction_receipt(&self, hash: H256) -> ResultForReceipt { self.get_transaction_receipt_params .lock() .unwrap() diff --git a/node/src/daemon/setup_reporter.rs b/node/src/daemon/setup_reporter.rs index c1ee05c65..c0f5968d5 100644 --- a/node/src/daemon/setup_reporter.rs +++ b/node/src/daemon/setup_reporter.rs @@ -45,7 +45,10 @@ use std::str::FromStr; const CONSOLE_DIAGNOSTICS: bool = false; -const ERR_SENSITIVE_BLANKED_OUT_VALUE_PAIRS: &[(&str, &str)] = &[("chain", "data-directory")]; +const ARG_PAIRS_SENSITIVE_TO_SETUP_ERRS: &[ErrorSensitiveArgPair] = &[ErrorSensitiveArgPair { + blanked_arg: "chain", + linked_arg: "data-directory", +}]; pub type SetupCluster = HashMap; @@ -86,8 +89,11 @@ impl SetupReporter for SetupReporterReal { blanked_out_former_values.insert(v.name.clone(), former_value); }; }); - let err_conflicts_for_blanked_out = - Self::get_err_conflicts_for_blanked_out(&blanked_out_former_values, &existing_setup); + let prevention_to_err_induced_setup_impairments = + Self::prevent_err_induced_setup_impairments( + &blanked_out_former_values, + &existing_setup, + ); let mut incoming_setup = incoming_setup .into_iter() .filter(|v| v.value.is_some()) @@ -168,7 +174,7 @@ impl SetupReporter for SetupReporterReal { let setup = Self::combine_clusters(vec![ &final_setup, &blanked_out_former_values, - &err_conflicts_for_blanked_out, + &prevention_to_err_induced_setup_impairments, ]); Err((setup, error_so_far)) } @@ -226,23 +232,25 @@ impl SetupReporterReal { } } - fn get_err_conflicts_for_blanked_out( - blanked_out_former_values: &SetupCluster, + fn prevent_err_induced_setup_impairments( + blanked_out_former_setup: &SetupCluster, existing_setup: &SetupCluster, ) -> SetupCluster { - ERR_SENSITIVE_BLANKED_OUT_VALUE_PAIRS.iter().fold( - HashMap::new(), - |mut acc, (blanked_out_arg, err_persistent_linked_arg)| { - if blanked_out_former_values.contains_key(&blanked_out_arg.to_string()) { - if let Some(former_value) = - existing_setup.get(&err_persistent_linked_arg.to_string()) + // this function arose as an unconvincing patch for a corner case situation where blanking out + // one parameter and a following hit of an (unrelated) error makes another parameter get out of sync with the restored + // blanked out one; this should remember the initial state and restore both params the way they use to be + ARG_PAIRS_SENSITIVE_TO_SETUP_ERRS + .iter() + .fold(HashMap::new(), |mut acc, pair| { + if blanked_out_former_setup.contains_key(&pair.blanked_arg.to_string()) { + if let Some(existing_linked_value) = + existing_setup.get(&pair.linked_arg.to_string()) { - acc.insert(err_persistent_linked_arg.to_string(), former_value.clone()); + acc.insert(pair.linked_arg.to_string(), existing_linked_value.clone()); } }; acc - }, - ) + }) } fn calculate_fundamentals( @@ -488,6 +496,11 @@ impl SetupReporterReal { } } +struct ErrorSensitiveArgPair<'arg_names> { + blanked_arg: &'arg_names str, + linked_arg: &'arg_names str, +} + trait ValueRetriever { fn value_name(&self) -> &'static str; diff --git a/node/src/database/db_initializer.rs b/node/src/database/db_initializer.rs index d84abc803..9c2170789 100644 --- a/node/src/database/db_initializer.rs +++ b/node/src/database/db_initializer.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::database::connection_wrapper::{ConnectionWrapper, ConnectionWrapperReal}; -use crate::database::db_migrations::{DbMigrator, DbMigratorReal}; + +use crate::database::db_migrations::db_migrator::{DbMigrator, DbMigratorReal}; use crate::db_config::secure_config_layer::EXAMPLE_ENCRYPTED; use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; @@ -1375,8 +1376,8 @@ mod tests { ) ); let mut migrate_database_params = migrate_database_params_arc.lock().unwrap(); - let (mismatched_schema, target_version, _) = migrate_database_params.remove(0); - assert_eq!(mismatched_schema, 0); + let (obsolete_schema, target_version, _) = migrate_database_params.remove(0); + assert_eq!(obsolete_schema, 0); assert_eq!(target_version, 5); TestLogHandler::new().exists_log_containing( "WARN: DbInitializer: Database is incompatible and its updating is necessary", diff --git a/node/src/database/db_migrations.rs b/node/src/database/db_migrations.rs deleted file mode 100644 index 52c97a660..000000000 --- a/node/src/database/db_migrations.rs +++ /dev/null @@ -1,2328 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::big_int_processing::big_int_divider::BigIntDivider; -use crate::accountant::dao_utils::VigilantRusqliteFlatten; -use crate::accountant::gwei_to_wei; -use crate::blockchain::bip39::Bip39; -use crate::database::connection_wrapper::ConnectionWrapper; -use crate::database::db_initializer::{ExternalData, CURRENT_SCHEMA_VERSION}; -use crate::db_config::db_encryption_layer::DbEncryptionLayer; -use crate::db_config::typed_config_layer::decode_bytes; -use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; -use crate::sub_lib::cryptde::PlainData; -use crate::sub_lib::neighborhood::{RatePack, DEFAULT_RATE_PACK}; -use itertools::Itertools; -use masq_lib::logger::Logger; -use masq_lib::utils::ExpectValue; -use rusqlite::{params_from_iter, Error, Row, ToSql, Transaction}; -use std::fmt::{Debug, Display, Formatter}; -use tiny_hderive::bip32::ExtendedPrivKey; - -pub trait DbMigrator { - fn migrate_database( - &self, - mismatched_schema: usize, - target_version: usize, - conn: Box, - ) -> Result<(), String>; -} - -pub struct DbMigratorReal { - external: ExternalData, - logger: Logger, -} - -impl DbMigrator for DbMigratorReal { - fn migrate_database( - &self, - mismatched_schema: usize, - target_version: usize, - mut conn: Box, - ) -> Result<(), String> { - let migrator_config = DBMigratorInnerConfiguration::new(); - let migration_utils = match DBMigrationUtilitiesReal::new(&mut *conn, migrator_config) { - Err(e) => return Err(e.to_string()), - Ok(utils) => utils, - }; - self.initiate_migrations( - mismatched_schema, - target_version, - Box::new(migration_utils), - Self::list_of_migrations(), - ) - } -} - -trait DatabaseMigration { - fn migrate<'a>( - &self, - mig_declaration_utilities: Box, - ) -> rusqlite::Result<()>; - fn old_version(&self) -> usize; -} - -trait MigDeclarationUtilities { - fn db_password(&self) -> Option; - fn transaction(&self) -> &Transaction; - fn execute_upon_transaction<'a>( - &self, - sql_statements: &[&'a dyn StatementObject], - ) -> rusqlite::Result<()>; - fn external_parameters(&self) -> &ExternalData; - fn logger(&self) -> &Logger; -} - -trait DBMigrationUtilities { - fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()>; - - fn commit(&mut self) -> Result<(), String>; - - fn make_mig_declaration_utils<'a>( - &'a self, - external: &'a ExternalData, - logger: &'a Logger, - ) -> Box; - - fn too_high_schema_panics(&self, mismatched_schema: usize); -} - -struct DBMigrationUtilitiesReal<'a> { - root_transaction: Option>, - db_migrator_configuration: DBMigratorInnerConfiguration, -} - -impl<'a> DBMigrationUtilitiesReal<'a> { - fn new<'b: 'a>( - conn: &'b mut dyn ConnectionWrapper, - db_migrator_configuration: DBMigratorInnerConfiguration, - ) -> rusqlite::Result { - Ok(Self { - root_transaction: Some(conn.transaction()?), - db_migrator_configuration, - }) - } - - fn root_transaction_ref(&self) -> &Transaction<'a> { - self.root_transaction.as_ref().expectv("root transaction") - } -} - -impl<'a> DBMigrationUtilities for DBMigrationUtilitiesReal<'a> { - fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()> { - DbMigratorReal::update_schema_version( - self.db_migrator_configuration - .db_configuration_table - .as_str(), - self.root_transaction_ref(), - update_to, - ) - } - - fn commit(&mut self) -> Result<(), String> { - self.root_transaction - .take() - .expectv("owned root transaction") - .commit() - .map_err(|e| e.to_string()) - } - - fn make_mig_declaration_utils<'b>( - &'b self, - external: &'b ExternalData, - logger: &'b Logger, - ) -> Box { - Box::new(MigDeclarationUtilitiesReal::new( - self.root_transaction_ref(), - external, - logger, - )) - } - - fn too_high_schema_panics(&self, mismatched_schema: usize) { - if mismatched_schema > self.db_migrator_configuration.current_schema_version { - panic!( - "Database claims to be more advanced ({}) than the version {} which is the latest \ - version this Node knows about.", - mismatched_schema, CURRENT_SCHEMA_VERSION - ) - } - } -} - -struct MigDeclarationUtilitiesReal<'a> { - root_transaction_ref: &'a Transaction<'a>, - external: &'a ExternalData, - logger: &'a Logger, -} - -impl<'a> MigDeclarationUtilitiesReal<'a> { - fn new( - root_transaction_ref: &'a Transaction<'a>, - external: &'a ExternalData, - logger: &'a Logger, - ) -> Self { - Self { - root_transaction_ref, - external, - logger, - } - } -} - -impl MigDeclarationUtilities for MigDeclarationUtilitiesReal<'_> { - fn db_password(&self) -> Option { - self.external.db_password_opt.clone() - } - - fn transaction(&self) -> &Transaction { - self.root_transaction_ref - } - - fn execute_upon_transaction<'a>( - &self, - sql_statements: &[&dyn StatementObject], - ) -> rusqlite::Result<()> { - let transaction = self.root_transaction_ref; - sql_statements.iter().fold(Ok(()), |so_far, stm| { - if so_far.is_ok() { - match stm.execute(transaction) { - Ok(_) => Ok(()), - Err(e) if e == Error::ExecuteReturnedResults => Ok(()), - Err(e) => Err(e), - } - } else { - so_far - } - }) - } - - fn external_parameters(&self) -> &ExternalData { - self.external - } - - fn logger(&self) -> &Logger { - self.logger - } -} - -trait StatementObject: Display { - fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()>; -} - -impl StatementObject for &str { - fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { - transaction.execute(self, []).map(|_| ()) - } -} - -impl StatementObject for String { - fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { - self.as_str().execute(transaction) - } -} - -struct StatementWithRusqliteParams { - sql_stm: String, - params: Vec>, -} - -impl StatementObject for StatementWithRusqliteParams { - fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { - transaction - .execute(&self.sql_stm, params_from_iter(self.params.iter())) - .map(|_| ()) - } -} - -impl Display for StatementWithRusqliteParams { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.sql_stm) - } -} - -struct DBMigratorInnerConfiguration { - db_configuration_table: String, - current_schema_version: usize, -} - -impl DBMigratorInnerConfiguration { - fn new() -> Self { - DBMigratorInnerConfiguration { - db_configuration_table: "config".to_string(), - current_schema_version: CURRENT_SCHEMA_VERSION, - } - } -} - -#[allow(non_camel_case_types)] -struct Migrate_0_to_1; - -impl DatabaseMigration for Migrate_0_to_1 { - fn migrate<'a>( - &self, - declaration_utils: Box, - ) -> rusqlite::Result<()> { - declaration_utils.execute_upon_transaction(&[ - &"INSERT INTO config (name, value, encrypted) VALUES ('mapping_protocol', null, 0)", - ]) - } - - fn old_version(&self) -> usize { - 0 - } -} - -#[allow(non_camel_case_types)] -struct Migrate_1_to_2; - -impl DatabaseMigration for Migrate_1_to_2 { - fn migrate<'a>( - &self, - declaration_utils: Box, - ) -> rusqlite::Result<()> { - let statement = format!( - "INSERT INTO config (name, value, encrypted) VALUES ('chain_name', '{}', 0)", - declaration_utils - .external_parameters() - .chain - .rec() - .literal_identifier - ); - declaration_utils.execute_upon_transaction(&[&statement]) - } - - fn old_version(&self) -> usize { - 1 - } -} - -#[allow(non_camel_case_types)] -struct Migrate_2_to_3; - -impl DatabaseMigration for Migrate_2_to_3 { - fn migrate<'a>( - &self, - declaration_utils: Box, - ) -> rusqlite::Result<()> { - let statement_1 = - "INSERT INTO config (name, value, encrypted) VALUES ('blockchain_service_url', null, 0)"; - let statement_2 = format!( - "INSERT INTO config (name, value, encrypted) VALUES ('neighborhood_mode', '{}', 0)", - declaration_utils.external_parameters().neighborhood_mode - ); - declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2]) - } - - fn old_version(&self) -> usize { - 2 - } -} - -#[allow(non_camel_case_types)] -struct Migrate_3_to_4; - -impl DatabaseMigration for Migrate_3_to_4 { - fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { - let transaction = utils.transaction(); - let mut stmt = transaction - .prepare("select name, value from config where name in ('example_encrypted', 'seed', 'consuming_wallet_derivation_path') order by name") - .expect("Internal error"); - - let rows = stmt - .query_map([], |row| { - let name = row.get::(0).expect("Internal error"); - let value_opt = row.get::>(1).expect("Internal error"); - Ok((name, value_opt)) - }) - .expect("Database is corrupt") - .map(|r| r.unwrap()) - .collect::)>>(); - if rows.iter().map(|r| r.0.as_str()).collect_vec() - != vec![ - "consuming_wallet_derivation_path", - "example_encrypted", - "seed", - ] - { - panic!("Database is corrupt"); - } - let consuming_path_opt = rows[0].1.clone(); - let example_encrypted = rows[1].1.clone(); - let seed_encrypted = rows[2].1.clone(); - let private_key_encoded = match (consuming_path_opt, example_encrypted, seed_encrypted) { - (Some(consuming_path), Some(example_encrypted), Some(seed_encrypted)) => { - let password_opt = utils.db_password(); - if !DbEncryptionLayer::password_matches(&password_opt, &Some(example_encrypted)) { - panic!("Bad password"); - } - let seed_encoded = - DbEncryptionLayer::decrypt_value(&Some(seed_encrypted), &password_opt, "seed") - .expect("Internal error") - .expect("Internal error"); - let seed_data = decode_bytes(Some(seed_encoded)) - .expect("Internal error") - .expect("Internal error"); - let extended_private_key = - ExtendedPrivKey::derive(seed_data.as_ref(), consuming_path.as_str()) - .expect("Internal error"); - let private_key_data = PlainData::new(&extended_private_key.secret()); - Some( - Bip39::encrypt_bytes( - &private_key_data.as_slice(), - password_opt.as_ref().expect("Test-drive me!"), - ) - .expect("Internal error: encryption failed"), - ) - } - _ => None, - }; - let private_key_column = if let Some(private_key) = private_key_encoded { - format!("'{}'", private_key) - } else { - "null".to_string() - }; - utils.execute_upon_transaction(&[ - &format! ("insert into config (name, value, encrypted) values ('consuming_wallet_private_key', {}, 1)", - private_key_column), - &"delete from config where name in ('seed', 'consuming_wallet_derivation_path', 'consuming_wallet_public_key')", - ]) - } - - fn old_version(&self) -> usize { - 3 - } -} - -#[allow(non_camel_case_types)] -struct Migrate_4_to_5; - -impl DatabaseMigration for Migrate_4_to_5 { - fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { - let mut select_statement = utils - .transaction() - .prepare("select pending_payment_transaction from payable where pending_payment_transaction is not null")?; - let unresolved_pending_transactions: Vec = select_statement - .query_map([], |row| { - Ok(row - .get::(0) - .expect("select statement was badly prepared")) - })? - .vigilant_flatten() - .collect(); - if !unresolved_pending_transactions.is_empty() { - warning!(utils.logger(), - "Migration from 4 to 5: database belonging to the chain '{}'; \ - we discovered possibly abandoned transactions that are said yet to be pending, these are: '{}'; continuing", - utils.external_parameters().chain.rec().literal_identifier,unresolved_pending_transactions.join("', '") ) - } else { - debug!( - utils.logger(), - "Migration from 4 to 5: no previous pending transactions found; continuing" - ) - }; - let statement_1 = "alter table payable drop column pending_payment_transaction"; - let statement_2 = "alter table payable add pending_payable_rowid integer null"; - let statement_3 = "create table pending_payable (\ - rowid integer primary key, \ - transaction_hash text not null, \ - amount integer not null, \ - payable_timestamp integer not null, \ - attempt integer not null, \ - process_error text null\ - )"; - let statement_4 = - "create unique index pending_payable_hash_idx ON pending_payable (transaction_hash)"; - let statement_5 = "drop index idx_receivable_wallet_address"; - let statement_6 = "drop index idx_banned_wallet_address"; - let statement_7 = "drop index idx_payable_wallet_address"; - let statement_8 = "alter table config rename to _config_old"; - let statement_9 = "create table config (\ - name text primary key,\ - value text,\ - encrypted integer not null\ - )"; - let statement_10 = "insert into config (name, value, encrypted) select name, value, encrypted from _config_old"; - let statement_11 = "drop table _config_old"; - utils.execute_upon_transaction(&[ - &statement_1, - &statement_2, - &statement_3, - &statement_4, - &statement_5, - &statement_6, - &statement_7, - &statement_8, - &statement_9, - &statement_10, - &statement_11, - ]) - } - - fn old_version(&self) -> usize { - 4 - } -} - -#[allow(non_camel_case_types)] -struct Migrate_5_to_6; - -impl DatabaseMigration for Migrate_5_to_6 { - fn migrate<'a>( - &self, - declaration_utils: Box, - ) -> rusqlite::Result<()> { - let statement_1 = Self::make_initialization_statement( - "payment_thresholds", - &DEFAULT_PAYMENT_THRESHOLDS.to_string(), - ); - let statement_2 = - Self::make_initialization_statement("rate_pack", &DEFAULT_RATE_PACK.to_string()); - let statement_3 = Self::make_initialization_statement( - "scan_intervals", - &DEFAULT_SCAN_INTERVALS.to_string(), - ); - declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2, &statement_3]) - } - - fn old_version(&self) -> usize { - 5 - } -} - -impl Migrate_5_to_6 { - fn make_initialization_statement(name: &str, value: &str) -> String { - format!( - "INSERT INTO config (name, value, encrypted) VALUES ('{}', '{}', 0)", - name, value - ) - } -} - -#[allow(non_camel_case_types)] -struct Migrate_6_to_7; - -#[allow(non_camel_case_types)] -struct Migrate_6_to_7_carrier<'a> { - utils: &'a (dyn MigDeclarationUtilities + 'a), - statements: Vec>, -} - -impl DatabaseMigration for Migrate_6_to_7 { - fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { - let mut migration_carrier = Migrate_6_to_7_carrier::new(utils.as_ref()); - migration_carrier.retype_table( - "payable", - "balance", - "wallet_address text primary key, - balance_high_b integer not null, - balance_low_b integer not null, - last_paid_timestamp integer not null, - pending_payable_rowid integer null", - )?; - migration_carrier.retype_table( - "receivable", - "balance", - "wallet_address text primary key, - balance_high_b integer not null, - balance_low_b integer not null, - last_received_timestamp integer not null", - )?; - migration_carrier.retype_table( - "pending_payable", - "amount", - "rowid integer primary key, - transaction_hash text not null, - amount_high_b integer not null, - amount_low_b integer not null, - payable_timestamp integer not null, - attempt integer not null, - process_error text null", - )?; - - migration_carrier.update_rate_pack(); - - migration_carrier.utils.execute_upon_transaction( - &migration_carrier - .statements - .iter() - .map(|boxed| boxed.as_ref()) - .collect_vec(), - ) - } - - fn old_version(&self) -> usize { - 6 - } -} - -impl<'a> Migrate_6_to_7_carrier<'a> { - fn new(utils: &'a (dyn MigDeclarationUtilities + 'a)) -> Self { - Self { - utils, - statements: vec![], - } - } - - fn retype_table( - &mut self, - table: &str, - old_param_name_of_future_big_int: &str, - create_new_table_stm: &str, - ) -> rusqlite::Result<()> { - self.utils.execute_upon_transaction(&[ - &format!("alter table {table} rename to _{table}_old"), - &format!( - "create table compensatory_{table} (old_rowid integer, high_bytes integer null, low_bytes integer null)" - ), - &format!("create table {table} ({create_new_table_stm}) strict"), - ])?; - let param_names = Self::extract_param_names(create_new_table_stm); - self.maybe_compose_insert_stm_with_auxiliary_table_to_handle_new_big_int_data( - table, - old_param_name_of_future_big_int, - param_names, - ); - self.statements - .push(Box::new(format!("drop table _{table}_old"))); - Ok(()) - } - - fn maybe_compose_insert_stm_with_auxiliary_table_to_handle_new_big_int_data( - &mut self, - table: &str, - big_int_param_old_name: &str, - param_names: Vec, - ) { - let big_int_params_new_names = param_names - .iter() - .filter(|segment| segment.contains(big_int_param_old_name)) - .map(|name| name.to_owned()) - .collect::>(); - let (easy_params, normal_params_prepared_for_inner_join) = - Self::prepare_unchanged_params(param_names, &big_int_params_new_names); - let future_big_int_values_including_old_rowids = self - .utils - .transaction() - .prepare(&format!( - "select rowid, {big_int_param_old_name} from _{table}_old", - )) - .expect("rusqlite internal error") - .query_map([], |row: &Row| { - let old_rowid = row.get(0).expect("rowid fetching error"); - let balance = row.get(1).expect("old param fetching error"); - Ok((old_rowid, balance)) - }) - .expect("map failed") - .vigilant_flatten() - .collect::>(); - if !future_big_int_values_including_old_rowids.is_empty() { - self.fill_compensatory_table(future_big_int_values_including_old_rowids, table); - let new_big_int_params = big_int_params_new_names.join(", "); - let final_insert_statement = format!( - "insert into {table} ({easy_params}, {new_big_int_params}) select {normal_params_prepared_for_inner_join}, \ - R.high_bytes, R.low_bytes from _{table}_old L inner join compensatory_{table} R where L.rowid = R.old_rowid", - ); - self.statements.push(Box::new(final_insert_statement)) - } else { - debug!( - self.utils.logger(), - "Migration from 6 to 7: no data to migrate in {}", table - ) - }; - } - - fn prepare_unchanged_params( - param_names_for_select_stm: Vec, - big_int_params_names: &[String], - ) -> (String, String) { - let easy_params_vec = param_names_for_select_stm - .into_iter() - .filter(|name| !big_int_params_names.contains(name)) - .collect_vec(); - let easy_params = easy_params_vec.iter().join(", "); - let easy_params_preformatted_for_inner_join = easy_params_vec - .into_iter() - .map(|word| format!("L.{}", word.trim())) - .join(", "); - (easy_params, easy_params_preformatted_for_inner_join) - } - - fn fill_compensatory_table(&mut self, all_big_int_values_found: Vec<(i64, i64)>, table: &str) { - let sql_stm = format!( - "insert into compensatory_{} (old_rowid, high_bytes, low_bytes) values {}", - table, - (0..all_big_int_values_found.len()) - .map(|_| "(?, ?, ?)") - .collect::() - ); - let params = all_big_int_values_found - .into_iter() - .flat_map(|(old_rowid, i64_balance)| { - let (high, low) = BigIntDivider::deconstruct(gwei_to_wei(i64_balance)); - vec![ - Box::new(old_rowid) as Box, - Box::new(high), - Box::new(low), - ] - }) - .collect::>>(); - let statement = StatementWithRusqliteParams { sql_stm, params }; - self.statements.push(Box::new(statement)); - } - - fn extract_param_names(table_creation_lines: &str) -> Vec { - table_creation_lines - .split(',') - .map(|line| { - let line = line.trim_start(); - line.chars() - .take_while(|char| !char.is_whitespace()) - .collect::() - }) - .collect() - } - - fn update_rate_pack(&mut self) { - let transaction = self.utils.transaction(); - let mut stm = transaction - .prepare("select value from config where name = 'rate_pack'") - .expect("stm preparation failed"); - let old_rate_pack = stm - .query_row([], |row| row.get::(0)) - .expect("row query failed"); - let old_rate_pack_as_native = - RatePack::try_from(old_rate_pack.as_str()).unwrap_or_else(|_| { - panic!( - "rate pack conversion failed with value: {}; database corrupt!", - old_rate_pack - ) - }); - let new_rate_pack = RatePack { - routing_byte_rate: gwei_to_wei(old_rate_pack_as_native.routing_byte_rate), - routing_service_rate: gwei_to_wei(old_rate_pack_as_native.routing_service_rate), - exit_byte_rate: gwei_to_wei(old_rate_pack_as_native.exit_byte_rate), - exit_service_rate: gwei_to_wei(old_rate_pack_as_native.exit_service_rate), - }; - let serialized_rate_pack = new_rate_pack.to_string(); - let params: Vec> = vec![Box::new(serialized_rate_pack)]; - - self.statements.push(Box::new(StatementWithRusqliteParams { - sql_stm: "update config set value = ? where name = 'rate_pack'".to_string(), - params, - })) - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// - -impl DbMigratorReal { - pub fn new(external: ExternalData) -> Self { - Self { - external, - logger: Logger::new("DbMigrator"), - } - } - - const fn list_of_migrations<'a>() -> &'a [&'a dyn DatabaseMigration] { - &[ - &Migrate_0_to_1, - &Migrate_1_to_2, - &Migrate_2_to_3, - &Migrate_3_to_4, - &Migrate_4_to_5, - &Migrate_5_to_6, - &Migrate_6_to_7, - ] - } - - fn initiate_migrations<'a>( - &self, - mismatched_schema: usize, - target_version: usize, - mut migration_utilities: Box, - list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)], - ) -> Result<(), String> { - let migrations_to_process = Self::select_migrations_to_process( - mismatched_schema, - list_of_migrations, - target_version, - &*migration_utilities, - ); - for record in migrations_to_process { - let present_db_version = record.old_version(); - if let Err(e) = self.migrate_semi_automated(record, &*migration_utilities, &self.logger) - { - return self.dispatch_bad_news(present_db_version, e); - } - self.log_success(present_db_version) - } - migration_utilities.commit() - } - - fn migrate_semi_automated<'a>( - &self, - record: &dyn DatabaseMigration, - migration_utilities: &'a (dyn DBMigrationUtilities + 'a), - logger: &'a Logger, - ) -> rusqlite::Result<()> { - info!( - &self.logger, - "Migrating from version {} to version {}", - record.old_version(), - record.old_version() + 1 - ); - record.migrate(migration_utilities.make_mig_declaration_utils(&self.external, logger))?; - let migrate_to = record.old_version() + 1; - migration_utilities.update_schema_version(migrate_to) - } - - fn update_schema_version( - name_of_given_table: &str, - transaction: &Transaction, - update_to: usize, - ) -> rusqlite::Result<()> { - transaction.execute( - &format!( - "UPDATE {} SET value = {} WHERE name = 'schema_version'", - name_of_given_table, update_to - ), - [], - )?; - Ok(()) - } - - fn select_migrations_to_process<'a>( - mismatched_schema: usize, - list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)], - target_version: usize, - mig_utils: &dyn DBMigrationUtilities, - ) -> Vec<&'a (dyn DatabaseMigration + 'a)> { - mig_utils.too_high_schema_panics(mismatched_schema); - list_of_migrations - .iter() - .skip_while(|entry| entry.old_version() != mismatched_schema) - .take_while(|entry| entry.old_version() < target_version) - .map(Self::deref) - .collect::>() - } - - fn deref<'a, T: ?Sized>(value: &'a &T) -> &'a T { - *value - } - - fn dispatch_bad_news( - &self, - current_version: usize, - error: rusqlite::Error, - ) -> Result<(), String> { - let error_message = format!( - "Migrating database from version {} to {} failed: {:?}", - current_version, - current_version + 1, - error - ); - error!(self.logger, "{}", &error_message); - Err(error_message) - } - - fn log_success(&self, previous_version: usize) { - info!( - self.logger, - "Database successfully migrated from version {} to {}", - previous_version, - previous_version + 1 - ) - } -} - -#[derive(Debug)] -struct InterimMigrationPlaceholder(usize); - -impl DatabaseMigration for InterimMigrationPlaceholder { - fn migrate<'a>( - &self, - _mig_declaration_utilities: Box, - ) -> rusqlite::Result<()> { - Ok(()) - } - - fn old_version(&self) -> usize { - self.0 - 1 - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::dao_utils::{from_time_t, to_time_t}; - use crate::blockchain::bip39::Bip39; - use crate::database::connection_wrapper::{ConnectionWrapper, ConnectionWrapperReal}; - use crate::database::db_initializer::test_utils::ConnectionWrapperMock; - use crate::database::db_initializer::{ - DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, - CURRENT_SCHEMA_VERSION, DATABASE_FILE, - }; - use crate::database::db_migrations::{ - DBMigrationUtilities, DBMigrationUtilitiesReal, DatabaseMigration, DbMigrator, - MigDeclarationUtilities, Migrate_0_to_1, StatementObject, StatementWithRusqliteParams, - }; - use crate::database::db_migrations::{DBMigratorInnerConfiguration, DbMigratorReal}; - use crate::db_config::db_encryption_layer::DbEncryptionLayer; - use crate::db_config::persistent_configuration::{ - PersistentConfiguration, PersistentConfigurationReal, - }; - use crate::db_config::typed_config_layer::encode_bytes; - use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; - use crate::sub_lib::cryptde::PlainData; - use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; - use crate::sub_lib::wallet::Wallet; - use crate::test_utils::database_utils::{ - assert_create_table_stm_contains_all_parts, - assert_index_stm_is_coupled_with_right_parameter, assert_no_index_exists_for_table, - assert_table_does_not_exist, bring_db_0_back_to_life_and_return_connection, - make_external_data, - }; - use crate::test_utils::database_utils::{assert_table_created_as_strict, retrieve_config_row}; - use crate::test_utils::make_wallet; - use bip39::{Language, Mnemonic, MnemonicType, Seed}; - use ethereum_types::BigEndianHash; - use itertools::Itertools; - use masq_lib::constants::DEFAULT_CHAIN; - use masq_lib::logger::Logger; - use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; - use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, TEST_DEFAULT_CHAIN}; - use masq_lib::utils::{derivation_path, NeighborhoodModeLight}; - use rand::Rng; - use rusqlite::types::Value::Null; - use rusqlite::{Connection, Error, OptionalExtension, Row, ToSql, Transaction}; - use std::cell::RefCell; - use std::collections::HashMap; - use std::fmt::Debug; - use std::fs::create_dir_all; - use std::iter::once; - use std::panic::{catch_unwind, AssertUnwindSafe}; - use std::str::FromStr; - use std::sync::{Arc, Mutex}; - use std::time::SystemTime; - use tiny_hderive::bip32::ExtendedPrivKey; - use web3::types::{H256, U256}; - - #[derive(Default)] - struct DBMigrationUtilitiesMock { - too_high_found_schema_will_panic_params: Arc>>, - make_mig_declaration_utils_params: Arc>>, - make_mig_declaration_utils_results: RefCell>>, - update_schema_version_params: Arc>>, - update_schema_version_results: RefCell>>, - commit_results: RefCell>>, - } - - impl DBMigrationUtilitiesMock { - pub fn update_schema_version_params(mut self, params: &Arc>>) -> Self { - self.update_schema_version_params = params.clone(); - self - } - - pub fn update_schema_version_result(self, result: rusqlite::Result<()>) -> Self { - self.update_schema_version_results.borrow_mut().push(result); - self - } - - pub fn commit_result(self, result: Result<(), String>) -> Self { - self.commit_results.borrow_mut().push(result); - self - } - - pub fn make_mig_declaration_utils_params( - mut self, - params: &Arc>>, - ) -> Self { - self.make_mig_declaration_utils_params = params.clone(); - self - } - - pub fn make_mig_declaration_utils_result( - self, - result: Box, - ) -> Self { - self.make_mig_declaration_utils_results - .borrow_mut() - .push(result); - self - } - } - - impl DBMigrationUtilities for DBMigrationUtilitiesMock { - fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()> { - self.update_schema_version_params - .lock() - .unwrap() - .push(update_to); - self.update_schema_version_results.borrow_mut().remove(0) - } - - fn commit(&mut self) -> Result<(), String> { - self.commit_results.borrow_mut().remove(0) - } - - fn make_mig_declaration_utils<'a>( - &'a self, - external: &'a ExternalData, - _logger: &'a Logger, - ) -> Box { - self.make_mig_declaration_utils_params - .lock() - .unwrap() - .push(external.clone()); - self.make_mig_declaration_utils_results - .borrow_mut() - .remove(0) - } - - fn too_high_schema_panics(&self, mismatched_schema: usize) { - self.too_high_found_schema_will_panic_params - .lock() - .unwrap() - .push(mismatched_schema); - } - } - - #[derive(Default)] - struct DBMigrateDeclarationUtilitiesMock { - db_password_results: RefCell>>, - execute_upon_transaction_params: Arc>>>, - execute_upon_transaction_results: RefCell>>, - } - - impl DBMigrateDeclarationUtilitiesMock { - #[allow(dead_code)] - pub fn db_password_result(self, result: Option) -> Self { - self.db_password_results.borrow_mut().push(result); - self - } - - pub fn execute_upon_transaction_params( - mut self, - params: &Arc>>>, - ) -> Self { - self.execute_upon_transaction_params = params.clone(); - self - } - - pub fn execute_upon_transaction_result(self, result: rusqlite::Result<()>) -> Self { - self.execute_upon_transaction_results - .borrow_mut() - .push(result); - self - } - } - - impl MigDeclarationUtilities for DBMigrateDeclarationUtilitiesMock { - fn db_password(&self) -> Option { - self.db_password_results.borrow_mut().remove(0) - } - - fn transaction(&self) -> &Transaction { - unimplemented!("Not needed so far") - } - - fn execute_upon_transaction<'a>( - &self, - sql_statements: &[&'a dyn StatementObject], - ) -> rusqlite::Result<()> { - self.execute_upon_transaction_params.lock().unwrap().push( - sql_statements - .iter() - .map(|stm_obj| stm_obj.to_string()) - .collect::>(), - ); - self.execute_upon_transaction_results.borrow_mut().remove(0) - } - - fn external_parameters(&self) -> &ExternalData { - unimplemented!("Not needed so far") - } - - fn logger(&self) -> &Logger { - unimplemented!("Not needed so far") - } - } - - #[test] - fn statement_with_rusqlite_params_can_display_its_stm() { - let subject = StatementWithRusqliteParams { - sql_stm: "insert into table2 (column) values (?)".to_string(), - params: vec![Box::new(12345)], - }; - - let stm = subject.to_string(); - - assert_eq!(stm, "insert into table2 (column) values (?)".to_string()) - } - - const _REMINDER_FROM_COMPILATION_TIME: () = check_schema_version_continuity(); - - #[allow(dead_code)] - const fn check_schema_version_continuity() { - if DbMigratorReal::list_of_migrations().len() != CURRENT_SCHEMA_VERSION { - panic!( - "It appears you need to increment the current schema version to have DbMigrator \ - work correctly if any new migration added" - ) - }; - } - - #[test] - fn migrate_database_handles_an_error_from_creating_the_root_transaction() { - let subject = DbMigratorReal::new(make_external_data()); - let mismatched_schema = 0; - let target_version = 5; //irrelevant - let connection = ConnectionWrapperMock::default() - .transaction_result(Err(Error::SqliteSingleThreadedMode)); //hard to find a real-like error for this - - let result = - subject.migrate_database(mismatched_schema, target_version, Box::new(connection)); - - assert_eq!( - result, - Err("SQLite was compiled or configured for single-threaded use only".to_string()) - ) - } - - #[test] - fn initiate_migrations_panics_if_the_schema_is_of_higher_number_than_the_latest_official() { - let last_version = CURRENT_SCHEMA_VERSION; - let too_advanced = last_version + 1; - let connection = Connection::open_in_memory().unwrap(); - let mut conn_wrapper = ConnectionWrapperReal::new(connection); - let mig_config = DBMigratorInnerConfiguration::new(); - let migration_utilities = - DBMigrationUtilitiesReal::new(&mut conn_wrapper, mig_config).unwrap(); - let subject = DbMigratorReal::new(make_external_data()); - - let captured_panic = catch_unwind(AssertUnwindSafe(|| { - subject.initiate_migrations( - too_advanced, - CURRENT_SCHEMA_VERSION, - Box::new(migration_utilities), - DbMigratorReal::list_of_migrations(), - ) - })) - .unwrap_err(); - - let panic_message = captured_panic.downcast_ref::().unwrap(); - assert_eq!( - *panic_message, - format!( - "Database claims to be more advanced ({}) than the version {} which \ - is the latest version this Node knows about.", - too_advanced, CURRENT_SCHEMA_VERSION - ) - ) - } - - #[derive(Default, Debug)] - struct DBMigrationRecordMock { - old_version_result: RefCell, - migrate_params: Arc>>, - migrate_result: RefCell>>, - } - - impl DBMigrationRecordMock { - fn old_version_result(self, result: usize) -> Self { - self.old_version_result.replace(result); - self - } - - fn migrate_result(self, result: rusqlite::Result<()>) -> Self { - self.migrate_result.borrow_mut().push(result); - self - } - - fn migrate_params(mut self, params: &Arc>>) -> Self { - self.migrate_params = params.clone(); - self - } - - fn set_up_necessary_stuff_for_mocked_migration_record( - self, - result_o_v: usize, - result_m: rusqlite::Result<()>, - params_m: &Arc>>, - ) -> Self { - self.old_version_result(result_o_v) - .migrate_result(result_m) - .migrate_params(params_m) - } - } - - impl DatabaseMigration for DBMigrationRecordMock { - fn migrate<'a>( - &self, - _migration_utilities: Box, - ) -> rusqlite::Result<()> { - self.migrate_params.lock().unwrap().push(()); - self.migrate_result.borrow_mut().remove(0) - } - - fn old_version(&self) -> usize { - *self.old_version_result.borrow() - } - } - - #[test] - #[should_panic(expected = "The list of database migrations is not ordered properly")] - fn list_validation_check_works_for_badly_ordered_migrations_when_inside() { - let fake_one = DBMigrationRecordMock::default().old_version_result(6); - let fake_two = DBMigrationRecordMock::default().old_version_result(2); - let list: &[&dyn DatabaseMigration] = &[&Migrate_0_to_1, &fake_one, &fake_two]; - - let _ = list_validation_check(list); - } - - #[test] - #[should_panic(expected = "The list of database migrations is not ordered properly")] - fn list_validation_check_works_for_badly_ordered_migrations_when_at_the_end() { - let fake_one = DBMigrationRecordMock::default().old_version_result(1); - let fake_two = DBMigrationRecordMock::default().old_version_result(3); - let list: &[&dyn DatabaseMigration] = &[&Migrate_0_to_1, &fake_one, &fake_two]; - - let _ = list_validation_check(list); - } - - fn list_validation_check<'a>(list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)]) { - let begins_at_version = list_of_migrations[0].old_version(); - let iterator = list_of_migrations.iter(); - let ending_sentinel = &DBMigrationRecordMock::default() - .old_version_result(begins_at_version + iterator.len()) - as &dyn DatabaseMigration; - let iterator_shifted = list_of_migrations - .iter() - .skip(1) - .chain(once(&ending_sentinel)); - iterator.zip(iterator_shifted).for_each(|(first, second)| { - assert!( - two_numbers_are_sequential(first.old_version(), second.old_version()), - "The list of database migrations is not ordered properly" - ) - }); - } - - fn two_numbers_are_sequential(first: usize, second: usize) -> bool { - (first + 1) == second - } - - #[test] - fn list_of_migrations_is_correctly_ordered() { - let _ = list_validation_check(DbMigratorReal::list_of_migrations()); - //success if no panicking - } - - #[test] - fn list_of_migrations_ends_on_the_current_version() { - let last_entry = DbMigratorReal::list_of_migrations().into_iter().last(); - - let result = last_entry.unwrap().old_version(); - - assert!(two_numbers_are_sequential(result, CURRENT_SCHEMA_VERSION)) - } - - #[test] - fn migrate_semi_automated_returns_an_error_from_update_schema_version() { - let update_schema_version_params_arc = Arc::new(Mutex::new(vec![])); - let mut migration_record = DBMigrationRecordMock::default() - .old_version_result(4) - .migrate_result(Ok(())); - let migration_utilities = DBMigrationUtilitiesMock::default() - .make_mig_declaration_utils_result(Box::new( - DBMigrateDeclarationUtilitiesMock::default(), - )) - .update_schema_version_result(Err(Error::InvalidQuery)) - .update_schema_version_params(&update_schema_version_params_arc); - let subject = DbMigratorReal::new(make_external_data()); - - let result = subject.migrate_semi_automated( - &mut migration_record, - &migration_utilities, - &Logger::new("test logger"), - ); - - assert_eq!(result, Err(Error::InvalidQuery)); - let update_schema_version_params = update_schema_version_params_arc.lock().unwrap(); - assert_eq!(*update_schema_version_params, vec![5]) //doesn't mean the state really changed, this is just an image of the supplied params - } - - #[test] - fn initiate_migrations_returns_an_error_from_migrate() { - init_test_logging(); - let list = &[&DBMigrationRecordMock::default() - .old_version_result(0) - .migrate_result(Err(Error::InvalidColumnIndex(5))) - as &dyn DatabaseMigration]; - let migrate_declaration_utils = DBMigrateDeclarationUtilitiesMock::default(); - let migration_utils = DBMigrationUtilitiesMock::default() - .make_mig_declaration_utils_result(Box::new(migrate_declaration_utils)); - let mismatched_schema = 0; - let target_version = 5; //not relevant - let subject = DbMigratorReal::new(make_external_data()); - - let result = subject.initiate_migrations( - mismatched_schema, - target_version, - Box::new(migration_utils), - list, - ); - - assert_eq!( - result, - Err( - r#"Migrating database from version 0 to 1 failed: InvalidColumnIndex(5)"# - .to_string() - ) - ); - TestLogHandler::new().exists_log_containing( - r#"ERROR: DbMigrator: Migrating database from version 0 to 1 failed: InvalidColumnIndex(5)"#, - ); - } - - #[test] - fn db_password_works() { - let dir_path = ensure_node_home_directory_exists("db_migrations", "db_password_works"); - let db_path = dir_path.join("test_database.db"); - let mut connection_wrapper = - ConnectionWrapperReal::new(Connection::open(&db_path).unwrap()); - let utils = DBMigrationUtilitiesReal::new( - &mut connection_wrapper, - DBMigratorInnerConfiguration { - db_configuration_table: "irrelevant".to_string(), - current_schema_version: 0, - }, - ) - .unwrap(); - let mut external_parameters = make_external_data(); - external_parameters.db_password_opt = Some("booga".to_string()); - let logger = Logger::new("test_logger"); - let subject = utils.make_mig_declaration_utils(&external_parameters, &logger); - - let result = subject.db_password(); - - assert_eq!(result, Some("booga".to_string())); - } - - #[test] - fn transaction_works() { - let dir_path = ensure_node_home_directory_exists("db_migrations", "transaction_works"); - let db_path = dir_path.join("test_database.db"); - let mut connection_wrapper = - ConnectionWrapperReal::new(Connection::open(&db_path).unwrap()); - let utils = DBMigrationUtilitiesReal::new( - &mut connection_wrapper, - DBMigratorInnerConfiguration { - db_configuration_table: "irrelevant".to_string(), - current_schema_version: 0, - }, - ) - .unwrap(); - let external_parameters = make_external_data(); - let logger = Logger::new("test_logger"); - let subject = utils.make_mig_declaration_utils(&external_parameters, &logger); - - let result = subject.transaction(); - - result - .execute("CREATE TABLE IF NOT EXISTS test (column TEXT)", []) - .unwrap(); - // no panic? Test passes! - } - - #[test] - fn execute_upon_transaction_returns_the_first_error_encountered_and_the_transaction_is_canceled( - ) { - let dir_path = ensure_node_home_directory_exists("db_migrations","execute_upon_transaction_returns_the_first_error_encountered_and_the_transaction_is_canceled"); - let db_path = dir_path.join("test_database.db"); - let connection = Connection::open(&db_path).unwrap(); - connection - .execute( - "CREATE TABLE test ( - name TEXT, - count integer - )", - [], - ) - .unwrap(); - let correct_statement_1 = "INSERT INTO test (name,count) VALUES ('mushrooms',270)"; - let erroneous_statement_1 = - "INSERT INTO botanic_garden (name, count) VALUES (sunflowers, 100)"; - let erroneous_statement_2 = "INSERT INTO milky_way (star) VALUES (just_discovered)"; - let set_of_sql_statements: &[&dyn StatementObject] = &[ - &correct_statement_1, - &erroneous_statement_1, - &erroneous_statement_2, - ]; - let mut connection_wrapper = ConnectionWrapperReal::new(connection); - let config = DBMigratorInnerConfiguration::new(); - let external_parameters = make_external_data(); - let subject = DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap(); - - let result = subject - .make_mig_declaration_utils(&external_parameters, &Logger::new("test logger")) - .execute_upon_transaction(set_of_sql_statements); - - assert_eq!( - result.unwrap_err().to_string(), - "no such table: botanic_garden" - ); - let connection = Connection::open(&db_path).unwrap(); - //when an error occurs, the underlying transaction gets rolled back, and we cannot see any changes to the database - let assertion: Option<(String, String)> = connection - .query_row("SELECT count FROM test WHERE name='mushrooms'", [], |row| { - Ok((row.get(0).unwrap(), row.get(1).unwrap())) - }) - .optional() - .unwrap(); - assert!(assertion.is_none()) //means no result for this query - } - - #[test] - fn execute_upon_transaction_handles_also_statements_that_return_something() { - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "execute_upon_transaction_handles_also_statements_that_return_something", - ); - let db_path = dir_path.join("test_database.db"); - let connection = Connection::open(&db_path).unwrap(); - connection - .execute( - "CREATE TABLE botanic_garden ( - name TEXT, - count integer - )", - [], - ) - .unwrap(); - let statement_1 = "INSERT INTO botanic_garden (name,count) VALUES ('sun_flowers', 100)"; - let statement_2 = "ALTER TABLE botanic_garden RENAME TO just_garden"; //this statement returns an overview of the new table on its execution - let statement_3 = "COMMIT"; - let set_of_sql_statements: &[&dyn StatementObject] = - &[&statement_1, &statement_2, &statement_3]; - let mut connection_wrapper = ConnectionWrapperReal::new(connection); - let config = DBMigratorInnerConfiguration::new(); - let external_parameters = make_external_data(); - let subject = DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap(); - - let result = subject - .make_mig_declaration_utils(&external_parameters, &Logger::new("test logger")) - .execute_upon_transaction(set_of_sql_statements); - - assert_eq!(result, Ok(())); - let connection = Connection::open(&db_path).unwrap(); - let assertion: Option<(String, i64)> = connection - .query_row("SELECT name, count FROM just_garden", [], |row| { - Ok((row.get(0).unwrap(), row.get(1).unwrap())) - }) - .optional() - .unwrap(); - assert!(assertion.is_some()) //means there is a table named 'just_garden' now - } - - #[test] - fn execute_upon_transaction_handles_also_error_from_stm_with_params() { - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "execute_upon_transaction_handles_also_error_from_stm_with_params", - ); - let db_path = dir_path.join("test_database.db"); - let conn = Connection::open(&db_path).unwrap(); - conn.execute( - "CREATE TABLE botanic_garden ( - name TEXT, - count integer - )", - [], - ) - .unwrap(); - let statement_1_simple = - "INSERT INTO botanic_garden (name,count) VALUES ('sun_flowers', 100)"; - let statement_2_good = StatementWithRusqliteParams { - sql_stm: "select * from botanic_garden".to_string(), - params: { - let params: Vec> = vec![]; - params - }, - }; - let statement_3_bad = StatementWithRusqliteParams { - sql_stm: "select name, count from foo".to_string(), - params: vec![Box::new("another_whatever")], - }; - //we expect not to get down to this statement, the error from statement_3 immediately terminates the circuit - let statement_4_demonstrative = StatementWithRusqliteParams { - sql_stm: "select name, count from bar".to_string(), - params: vec![Box::new("also_whatever")], - }; - let set_of_sql_statements: &[&dyn StatementObject] = &[ - &statement_1_simple, - &statement_2_good, - &statement_3_bad, - &statement_4_demonstrative, - ]; - let mut conn_wrapper = ConnectionWrapperReal::new(conn); - let config = DBMigratorInnerConfiguration::new(); - let external_params = make_external_data(); - let subject = DBMigrationUtilitiesReal::new(&mut conn_wrapper, config).unwrap(); - - let result = subject - .make_mig_declaration_utils(&external_params, &Logger::new("test logger")) - .execute_upon_transaction(set_of_sql_statements); - - match result { - Err(Error::SqliteFailure(_, err_msg_opt)) => { - assert_eq!(err_msg_opt, Some("no such table: foo".to_string())) - } - x => panic!("we expected SqliteFailure(..) but got: {:?}", x), - } - let assert_conn = Connection::open(&db_path).unwrap(); - let assertion: Option<(String, i64)> = assert_conn - .query_row("SELECT * FROM botanic_garden", [], |row| { - Ok((row.get(0).unwrap(), row.get(1).unwrap())) - }) - .optional() - .unwrap(); - assert_eq!(assertion, None) - //the table remained empty because an error causes the whole transaction to abort - } - - fn make_success_mig_record( - old_version: usize, - empty_params_arc: &Arc>>, - ) -> Box { - Box::new( - DBMigrationRecordMock::default().set_up_necessary_stuff_for_mocked_migration_record( - old_version, - Ok(()), - empty_params_arc, - ), - ) - } - - #[test] - fn initiate_migrations_skips_records_already_included_in_the_current_database_and_migrates_only_the_others( - ) { - let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let third_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let fourth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let fifth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let mig_record_1 = make_success_mig_record(0, &first_record_migration_p_arc); - let mig_record_2 = make_success_mig_record(1, &second_record_migration_p_arc); - let mig_record_3 = make_success_mig_record(2, &third_record_migration_p_arc); - let mig_record_4 = make_success_mig_record(3, &fourth_record_migration_p_arc); - let mig_record_5 = make_success_mig_record(4, &fifth_record_migration_p_arc); - let list_of_migrations: &[&dyn DatabaseMigration] = &[ - mig_record_1.as_ref(), - mig_record_2.as_ref(), - mig_record_3.as_ref(), - mig_record_4.as_ref(), - mig_record_5.as_ref(), - ]; - let connection = Connection::open_in_memory().unwrap(); - connection - .execute( - "CREATE TABLE test ( - name TEXT, - value TEXT - )", - [], - ) - .unwrap(); - connection - .execute( - "INSERT INTO test (name, value) VALUES ('schema_version', '2')", - [], - ) - .unwrap(); - let mut connection_wrapper = ConnectionWrapperReal::new(connection); - let config = DBMigratorInnerConfiguration { - db_configuration_table: "test".to_string(), - current_schema_version: 5, - }; - let subject = DbMigratorReal::new(make_external_data()); - let mismatched_schema = 2; - let target_version = 5; - - let result = subject.initiate_migrations( - mismatched_schema, - target_version, - Box::new(DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap()), - list_of_migrations, - ); - - assert_eq!(result, Ok(())); - let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); - assert_eq!(*first_record_migration_params, vec![]); - let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); - assert_eq!(*second_record_migration_params, vec![]); - let third_record_migration_params = third_record_migration_p_arc.lock().unwrap(); - assert_eq!(*third_record_migration_params, vec![()]); - let fourth_record_migration_params = fourth_record_migration_p_arc.lock().unwrap(); - assert_eq!(*fourth_record_migration_params, vec![()]); - let fifth_record_migration_params = fifth_record_migration_p_arc.lock().unwrap(); - assert_eq!(*fifth_record_migration_params, vec![()]); - let assertion: (String, String) = connection_wrapper - .transaction() - .unwrap() - .query_row( - "SELECT name, value FROM test WHERE name='schema_version'", - [], - |row| Ok((row.get(0).unwrap(), row.get(1).unwrap())), - ) - .unwrap(); - assert_eq!(assertion.1, "5") - } - - #[test] - fn initiate_migrations_terminates_at_the_specified_version() { - let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let third_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let fourth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let fifth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let mig_record_1 = make_success_mig_record(0, &first_record_migration_p_arc); - let mig_record_2 = make_success_mig_record(1, &second_record_migration_p_arc); - let mig_record_3 = make_success_mig_record(2, &third_record_migration_p_arc); - let mig_record_4 = make_success_mig_record(3, &fourth_record_migration_p_arc); - let mig_record_5 = make_success_mig_record(4, &fifth_record_migration_p_arc); - let list_of_migrations: &[&dyn DatabaseMigration] = &[ - mig_record_1.as_ref(), - mig_record_2.as_ref(), - mig_record_3.as_ref(), - mig_record_4.as_ref(), - mig_record_5.as_ref(), - ]; - let connection = Connection::open_in_memory().unwrap(); - connection - .execute( - "CREATE TABLE test ( - name TEXT, - value TEXT - )", - [], - ) - .unwrap(); - let mut connection_wrapper = ConnectionWrapperReal::new(connection); - let config = DBMigratorInnerConfiguration { - db_configuration_table: "test".to_string(), - current_schema_version: 5, - }; - let subject = DbMigratorReal::new(make_external_data()); - let mismatched_schema = 0; - let target_version = 3; - - let result = subject.initiate_migrations( - mismatched_schema, - target_version, - Box::new(DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap()), - list_of_migrations, - ); - - assert_eq!(result, Ok(())); - let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); - assert_eq!(*first_record_migration_params, vec![()]); - let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); - assert_eq!(*second_record_migration_params, vec![()]); - let third_record_migration_params = third_record_migration_p_arc.lock().unwrap(); - assert_eq!(*third_record_migration_params, vec![()]); - let fourth_record_migration_params = fourth_record_migration_p_arc.lock().unwrap(); - assert_eq!(*fourth_record_migration_params, vec![]); - let fifth_record_migration_params = fifth_record_migration_p_arc.lock().unwrap(); - assert_eq!(*fifth_record_migration_params, vec![]); - } - - #[test] - fn db_migration_happy_path() { - init_test_logging(); - let execute_upon_transaction_params_arc = Arc::new(Mutex::new(vec![])); - let update_schema_version_params_arc = Arc::new(Mutex::new(vec![])); - let make_mig_declaration_params_arc = Arc::new(Mutex::new(vec![])); - let outdated_schema = 0; - let list = &[&Migrate_0_to_1 as &dyn DatabaseMigration]; - let db_migrate_declaration_utilities = DBMigrateDeclarationUtilitiesMock::default() - .execute_upon_transaction_params(&execute_upon_transaction_params_arc) - .execute_upon_transaction_result(Ok(())); - let migration_utils = DBMigrationUtilitiesMock::default() - .make_mig_declaration_utils_params(&make_mig_declaration_params_arc) - .make_mig_declaration_utils_result(Box::new(db_migrate_declaration_utilities)) - .update_schema_version_params(&update_schema_version_params_arc) - .update_schema_version_result(Ok(())) - .commit_result(Ok(())); - let target_version = 5; //not relevant - let subject = DbMigratorReal::new(make_external_data()); - - let result = subject.initiate_migrations( - outdated_schema, - target_version, - Box::new(migration_utils), - list, - ); - - assert!(result.is_ok()); - let execute_upon_transaction_params = execute_upon_transaction_params_arc.lock().unwrap(); - assert_eq!( - *execute_upon_transaction_params.get(0).unwrap(), - vec![ - "INSERT INTO config (name, value, encrypted) VALUES ('mapping_protocol', null, 0)" - .to_string() - ], - ); - let update_schema_version_params = update_schema_version_params_arc.lock().unwrap(); - assert_eq!(update_schema_version_params[0], 1); - TestLogHandler::new().exists_log_containing( - "INFO: DbMigrator: Database successfully migrated from version 0 to 1", - ); - let make_mig_declaration_utils_params = make_mig_declaration_params_arc.lock().unwrap(); - assert_eq!( - *make_mig_declaration_utils_params, - vec![ExternalData { - chain: TEST_DEFAULT_CHAIN, - neighborhood_mode: NeighborhoodModeLight::Standard, - db_password_opt: None, - }] - ) - } - - #[test] - fn final_commit_of_the_root_transaction_sad_path() { - let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); - let list_of_migrations: &[&dyn DatabaseMigration] = &[ - &DBMigrationRecordMock::default().set_up_necessary_stuff_for_mocked_migration_record( - 0, - Ok(()), - &first_record_migration_p_arc, - ), - &DBMigrationRecordMock::default().set_up_necessary_stuff_for_mocked_migration_record( - 1, - Ok(()), - &second_record_migration_p_arc, - ), - ]; - let migration_utils = DBMigrationUtilitiesMock::default() - .make_mig_declaration_utils_result(Box::new( - DBMigrateDeclarationUtilitiesMock::default(), - )) - .make_mig_declaration_utils_result(Box::new( - DBMigrateDeclarationUtilitiesMock::default(), - )) - .update_schema_version_result(Ok(())) - .update_schema_version_result(Ok(())) - .commit_result(Err("Committing transaction failed".to_string())); - let subject = DbMigratorReal::new(make_external_data()); - - let result = - subject.initiate_migrations(0, 2, Box::new(migration_utils), list_of_migrations); - - assert_eq!(result, Err(String::from("Committing transaction failed"))); - let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); - assert_eq!(*first_record_migration_params, vec![()]); - let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); - assert_eq!(*second_record_migration_params, vec![()]); - } - - #[test] - fn migration_from_0_to_1_is_properly_set() { - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_0_to_1_is_properly_set", - ); - create_dir_all(&dir_path).unwrap(); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - - let result = subject.initialize_to_version( - &dir_path, - 1, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - let connection = result.unwrap(); - let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "mapping_protocol"); - let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); - assert_eq!(mp_value, None); - assert_eq!(mp_encrypted, false); - assert_eq!(cs_value, Some("1".to_string())); - assert_eq!(cs_encrypted, false) - } - - #[test] - fn migration_from_1_to_2_is_properly_set() { - init_test_logging(); - let start_at = 1; - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_1_to_2_is_properly_set", - ); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - { - subject - .initialize_to_version( - &dir_path, - start_at, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - } - - let result = subject.initialize_to_version( - &dir_path, - start_at + 1, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - let connection = result.unwrap(); - let (chn_value, chn_encrypted) = retrieve_config_row(connection.as_ref(), "chain_name"); - let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); - assert_eq!(chn_value, Some("eth-ropsten".to_string())); - assert_eq!(chn_encrypted, false); - assert_eq!(cs_value, Some("2".to_string())); - assert_eq!(cs_encrypted, false); - TestLogHandler::new().exists_log_containing( - "DbMigrator: Database successfully migrated from version 1 to 2", - ); - } - - #[test] - fn migration_from_2_to_3_is_properly_set() { - let start_at = 2; - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_2_to_3_is_properly_set", - ); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - { - subject - .initialize_to_version( - &dir_path, - start_at, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - } - - let result = subject.initialize_to_version( - &dir_path, - start_at + 1, - DbInitializationConfig::create_or_migrate(ExternalData::new( - DEFAULT_CHAIN, - NeighborhoodModeLight::ConsumeOnly, - None, - )), - ); - - let connection = result.unwrap(); - let (bchs_value, bchs_encrypted) = - retrieve_config_row(connection.as_ref(), "blockchain_service_url"); - assert_eq!(bchs_value, None); - assert_eq!(bchs_encrypted, false); - let (nm_value, nm_encrypted) = - retrieve_config_row(connection.as_ref(), "neighborhood_mode"); - assert_eq!(nm_value, Some("consume-only".to_string())); - assert_eq!(nm_encrypted, false); - let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); - assert_eq!(cs_value, Some("3".to_string())); - assert_eq!(cs_encrypted, false); - } - - #[test] - fn migration_from_3_to_4_with_wallets() { - let data_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_3_to_4_with_wallets", - ); - let db_path = data_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let password_opt = &Some("password".to_string()); - let subject = DbInitializerReal::default(); - let mut external_data = make_external_data(); - external_data.db_password_opt = password_opt.as_ref().cloned(); - let init_config = DbInitializationConfig::create_or_migrate(external_data); - let original_private_key = { - let schema3_conn = subject - .initialize_to_version(&data_path, 3, init_config.clone()) - .unwrap(); - let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); - let seed = Seed::new(&mnemonic, "booga"); - let consuming_path = derivation_path(0, 150); - let original_private_key = - ExtendedPrivKey::derive(seed.as_bytes(), consuming_path.as_str()) - .unwrap() - .secret(); - let seed_plain = PlainData::new(seed.as_bytes()); - let seed_encoded = encode_bytes(Some(seed_plain)).unwrap().unwrap(); - let seed_encrypted = - DbEncryptionLayer::encrypt_value(&Some(seed_encoded), password_opt, "seed") - .unwrap() - .unwrap(); - let mut example_data = [0u8; 32]; - rand::thread_rng().fill(&mut example_data); - let example_encrypted = - Bip39::encrypt_bytes(&example_data, password_opt.as_ref().unwrap()) - .expect("Encryption failed"); - let updates = vec![ - ("consuming_wallet_derivation_path", consuming_path, false), - ("consuming_wallet_public_key", "booga".to_string(), false), - ("example_encrypted", example_encrypted, true), - ("seed", seed_encrypted, true), - ]; - updates.into_iter().for_each(|(name, value, flag)| { - let mut stmt = schema3_conn - .prepare("update config set value = ?, encrypted = ? where name = ?") - .expect(&format!( - "Couldn't prepare statement to set {} to {}", - name, value - )); - let params: &[&dyn ToSql] = - &[&value, &(if flag { 1 } else { 0 }), &name.to_string()]; - let count = stmt.execute(params).unwrap(); - if count != 1 { - panic!( - "Updating {} to '{}' should have affected 1 row, but affected {}", - name, value, count - ); - } - }); - original_private_key.to_vec() - }; - - let migrated_private_key = { - let mut schema4_conn = subject - .initialize_to_version(&data_path, 4, init_config) - .unwrap(); - { - let mut stmt = schema4_conn.prepare("select count(*) from config where name in ('consuming_wallet_derivation_path', 'consuming_wallet_public_key', 'seed')").unwrap(); - let cruft = stmt - .query_row([], |row| Ok(row.get::(0))) - .unwrap() - .unwrap(); - assert_eq!(cruft, 0); - } - let (private_key_encrypted, encrypted) = - retrieve_config_row(schema4_conn.as_mut(), "consuming_wallet_private_key"); - assert_eq!(encrypted, true); - let private_key = Bip39::decrypt_bytes( - &private_key_encrypted.unwrap(), - password_opt.as_ref().unwrap(), - ) - .unwrap(); - private_key.as_slice().to_vec() - }; - - assert_eq!(migrated_private_key, original_private_key); - } - - #[test] - fn migration_from_3_to_4_without_password() { - let data_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_3_to_4_without_password", - ); - let db_path = data_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let password_opt = &Some("password".to_string()); - let subject = DbInitializerReal::default(); - let mut external_data = make_external_data(); - external_data.db_password_opt = password_opt.as_ref().cloned(); - let init_config = DbInitializationConfig::create_or_migrate(external_data); - { - subject - .initialize_to_version(&data_path, 3, init_config.clone()) - .unwrap(); - }; - - let mut schema4_conn = subject - .initialize_to_version(&data_path, 4, init_config) - .unwrap(); - - { - let mut stmt = schema4_conn.prepare("select count(*) from config where name in ('consuming_wallet_derivation_path', 'consuming_wallet_public_key', 'seed')").unwrap(); - let cruft = stmt - .query_row([], |row| Ok(row.get::(0))) - .unwrap() - .unwrap(); - assert_eq!(cruft, 0); - } - let (private_key_encrypted, encrypted) = - retrieve_config_row(schema4_conn.as_mut(), "consuming_wallet_private_key"); - assert_eq!(private_key_encrypted, None); - assert_eq!(encrypted, true); - } - - #[test] - fn migration_from_4_to_5_without_pending_transactions() { - init_test_logging(); - let start_at = 4; - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_4_to_5_without_pending_transactions", - ); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - let conn = subject - .initialize_to_version( - &dir_path, - start_at, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - let wallet_1 = make_wallet("scotland_yard"); - prepare_old_fashioned_account_with_pending_transaction_opt( - conn.as_ref(), - None, - &wallet_1, - 113344, - from_time_t(250_000_000), - ); - let config_table_before = fetch_all_from_config_table(conn.as_ref()); - - let _ = subject - .initialize_to_version( - &dir_path, - start_at + 1, - DbInitializationConfig::create_or_migrate(ExternalData::new( - TEST_DEFAULT_CHAIN, - NeighborhoodModeLight::ConsumeOnly, - Some("password".to_string()), - )), - ) - .unwrap(); - - let config_table_after = fetch_all_from_config_table(conn.as_ref()); - assert_eq!(config_table_before, config_table_after); - assert_on_schema_5_was_adopted(conn.as_ref()); - TestLogHandler::new().exists_log_containing("DEBUG: DbMigrator: Migration from 4 to 5: no previous pending transactions found; continuing"); - } - - fn prepare_old_fashioned_account_with_pending_transaction_opt( - conn: &dyn ConnectionWrapper, - transaction_hash_opt: Option, - wallet: &Wallet, - amount: i64, - timestamp: SystemTime, - ) { - let hash_str = transaction_hash_opt - .map(|hash| format!("{:?}", hash)) - .unwrap_or(String::new()); - let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); - let params: &[&dyn ToSql] = &[ - &wallet, - &amount, - &to_time_t(timestamp), - if !hash_str.is_empty() { - &hash_str - } else { - &Null - }, - ]; - let row_count = stm.execute(params).unwrap(); - assert_eq!(row_count, 1); - } - - #[test] - fn migration_from_4_to_5_with_pending_transactions() { - init_test_logging(); - let start_at = 4; - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_4_to_5_with_pending_transactions", - ); - let db_path = dir_path.join(DATABASE_FILE); - let conn = bring_db_0_back_to_life_and_return_connection(&db_path); - let conn = ConnectionWrapperReal::new(conn); - let wallet_1 = make_wallet("james_bond"); - let transaction_hash_1 = H256::from_uint(&U256::from(45454545)); - let wallet_2 = make_wallet("robinson_crusoe"); - let transaction_hash_2 = H256::from_uint(&U256::from(999888)); - let subject = DbInitializerReal::default(); - { - let _ = subject - .initialize_to_version( - &dir_path, - start_at, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - } - prepare_old_fashioned_account_with_pending_transaction_opt( - &conn, - Some(transaction_hash_1), - &wallet_1, - 555555, - SystemTime::now(), - ); - prepare_old_fashioned_account_with_pending_transaction_opt( - &conn, - Some(transaction_hash_2), - &wallet_2, - 1111111, - from_time_t(200_000_000), - ); - let config_table_before = fetch_all_from_config_table(&conn); - - let conn_schema5 = subject - .initialize_to_version( - &dir_path, - start_at + 1, - DbInitializationConfig::create_or_migrate(ExternalData::new( - TEST_DEFAULT_CHAIN, - NeighborhoodModeLight::ConsumeOnly, - Some("password".to_string()), - )), - ) - .unwrap(); - - let config_table_after = fetch_all_from_config_table(&conn); - assert_eq!(config_table_before, config_table_after); - assert_on_schema_5_was_adopted(conn_schema5.as_ref()); - TestLogHandler::new().exists_log_containing("WARN: DbMigrator: Migration from 4 to 5: database belonging to the chain 'eth-ropsten'; \ - we discovered possibly abandoned transactions that are said yet to be pending, these are: \ - '0x0000000000000000000000000000000000000000000000000000000002b594d1', \ - '0x00000000000000000000000000000000000000000000000000000000000f41d0'; continuing"); - } - - fn assert_on_schema_5_was_adopted(conn_schema5: &dyn ConnectionWrapper) { - let expected_key_words: &[&[&str]] = &[ - &["rowid", "integer", "primary", "key"], - &["transaction_hash", "text", "not", "null"], - &["amount", "integer", "not", "null"], - &["payable_timestamp", "integer", "not", "null"], - &["attempt", "integer", "not", "null"], - &["process_error", "text", "null"], - ]; - assert_create_table_stm_contains_all_parts( - conn_schema5, - "pending_payable", - expected_key_words, - ); - let expected_key_words: &[&[&str]] = &[&["transaction_hash"]]; - assert_index_stm_is_coupled_with_right_parameter( - conn_schema5, - "pending_payable_hash_idx", - expected_key_words, - ); - let expected_key_words: &[&[&str]] = &[ - &["wallet_address", "text", "primary", "key"], - &["balance", "integer", "not", "null"], - &["last_paid_timestamp", "integer", "not", "null"], - &["pending_payable_rowid", "integer", "null"], - ]; - assert_create_table_stm_contains_all_parts(conn_schema5, "payable", expected_key_words); - let expected_key_words: &[&[&str]] = &[ - &["name", "text", "primary", "key"], - &["value", "text"], - &["encrypted", "integer", "not", "null"], - ]; - assert_create_table_stm_contains_all_parts(conn_schema5, "config", expected_key_words); - assert_no_index_exists_for_table(conn_schema5, "config"); - assert_no_index_exists_for_table(conn_schema5, "payable"); - assert_no_index_exists_for_table(conn_schema5, "receivable"); - assert_no_index_exists_for_table(conn_schema5, "banned"); - assert_table_does_not_exist(conn_schema5, "_config_old") - } - - fn fetch_all_from_config_table( - conn: &dyn ConnectionWrapper, - ) -> Vec<(String, (Option, bool))> { - let mut stmt = conn - .prepare("select name, value, encrypted from config") - .unwrap(); - let mut hash_map_of_values = stmt - .query_map([], |row| { - Ok(( - row.get::(0).unwrap(), - ( - row.get::>(1).unwrap(), - row.get::(2) - .map(|encrypted: i64| encrypted > 0) - .unwrap(), - ), - )) - }) - .unwrap() - .flatten() - .collect::, bool)>>(); - hash_map_of_values.remove("schema_version").unwrap(); - let mut vec_of_values = hash_map_of_values.into_iter().collect_vec(); - vec_of_values.sort_by_key(|(name, _)| name.clone()); - vec_of_values - } - - #[test] - fn migration_from_5_to_6_works() { - let dir_path = - ensure_node_home_directory_exists("db_migrations", "migration_from_5_to_6_works"); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - { - subject - .initialize_to_version( - &dir_path, - 5, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - } - - let result = subject.initialize_to_version( - &dir_path, - 6, - DbInitializationConfig::create_or_migrate(make_external_data()), - ); - - let connection = result.unwrap(); - let (payment_thresholds, encrypted) = - retrieve_config_row(connection.as_ref(), "payment_thresholds"); - assert_eq!( - payment_thresholds, - Some(DEFAULT_PAYMENT_THRESHOLDS.to_string()) - ); - assert_eq!(encrypted, false); - let (rate_pack, encrypted) = retrieve_config_row(connection.as_ref(), "rate_pack"); - assert_eq!(rate_pack, Some(DEFAULT_RATE_PACK.to_string())); - assert_eq!(encrypted, false); - let (scan_intervals, encrypted) = - retrieve_config_row(connection.as_ref(), "scan_intervals"); - assert_eq!(scan_intervals, Some(DEFAULT_SCAN_INTERVALS.to_string())); - assert_eq!(encrypted, false); - } - - #[test] - fn migration_from_6_to_7_works() { - let dir_path = - ensure_node_home_directory_exists("db_migrations", "migration_from_6_to_7_works"); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - let pre_db_conn = subject - .initialize_to_version( - &dir_path, - 6, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - insert_value(&*pre_db_conn,"insert into payable (wallet_address, balance, last_paid_timestamp, pending_payable_rowid) \ - values (\"0xD7d1b2cF58f6500c7CB22fCA42B8512d06813a03\", 56784545484899, 11111, null)"); - insert_value( - &*pre_db_conn, - "insert into receivable (wallet_address, balance, last_received_timestamp) \ - values (\"0xD2d1b2eF58f6500c7ae22fCA42B8512d06813a03\",-56784,22222)", - ); - insert_value(&*pre_db_conn,"insert into pending_payable (rowid, transaction_hash, amount, payable_timestamp,attempt, process_error) \ - values (5, \"0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f222a4bc8cd032f768fc5219838\" ,9123 ,33333 ,1 ,null)"); - let mut persistent_config = PersistentConfigurationReal::from(pre_db_conn); - let old_rate_pack_in_gwei = "44|50|20|32".to_string(); - persistent_config - .set_rate_pack(old_rate_pack_in_gwei.clone()) - .unwrap(); - - let conn = subject - .initialize_to_version( - &dir_path, - 7, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - - assert_table_created_as_strict(&*conn, "payable"); - assert_table_created_as_strict(&*conn, "receivable"); - let select_sql = "select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid from payable"; - query_rows_helper(&*conn, select_sql, |row| { - assert_eq!( - row.get::(0).unwrap(), - Wallet::from_str("0xD7d1b2cF58f6500c7CB22fCA42B8512d06813a03").unwrap() - ); - assert_eq!(row.get::(1).unwrap(), 6156); - assert_eq!(row.get::(2).unwrap(), 5467226021000125952); - assert_eq!(row.get::(3).unwrap(), 11111); - assert_eq!(row.get::>(4).unwrap(), None); - Ok(()) - }); - let select_sql = "select wallet_address, balance_high_b, balance_low_b, last_received_timestamp from receivable"; - query_rows_helper(&*conn, select_sql, |row| { - assert_eq!( - row.get::(0).unwrap(), - Wallet::from_str("0xD2d1b2eF58f6500c7ae22fCA42B8512d06813a03").unwrap() - ); - assert_eq!(row.get::(1).unwrap(), -1); - assert_eq!(row.get::(2).unwrap(), 9223315252854775808); - assert_eq!(row.get::(3).unwrap(), 22222); - Ok(()) - }); - let select_sql = "select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable"; - query_rows_helper(&*conn, select_sql, |row| { - assert_eq!(row.get::(0).unwrap(), 5); - assert_eq!( - row.get::(1).unwrap(), - "0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f222a4bc8cd032f768fc5219838".to_string() - ); - assert_eq!(row.get::(2).unwrap(), 0); - assert_eq!(row.get::(3).unwrap(), 9123000000000); - assert_eq!(row.get::(4).unwrap(), 33333); - assert_eq!(row.get::(5).unwrap(), 1); - assert_eq!(row.get::>(6).unwrap(), None); - Ok(()) - }); - let (rate_pack, encrypted) = retrieve_config_row(&*conn, "rate_pack"); - assert_eq!( - rate_pack, - Some("44000000000|50000000000|20000000000|32000000000".to_string()) - ); - assert_eq!(encrypted, false); - } - - #[test] - fn migration_from_6_to_7_without_any_data() { - init_test_logging(); - let dir_path = ensure_node_home_directory_exists( - "db_migrations", - "migration_from_6_to_7_without_any_data", - ); - let db_path = dir_path.join(DATABASE_FILE); - let _ = bring_db_0_back_to_life_and_return_connection(&db_path); - let subject = DbInitializerReal::default(); - let conn = subject - .initialize_to_version( - &dir_path, - 6, - DbInitializationConfig::create_or_migrate(make_external_data()), - ) - .unwrap(); - let mut subject = DbMigratorReal::new(make_external_data()); - subject.logger = Logger::new("migration_from_6_to_7_without_any_data"); - - subject.migrate_database(6, 7, conn).unwrap(); - - let test_log_handler = TestLogHandler::new(); - ["payable", "receivable", "pending_payable"] - .iter() - .for_each(|table_name| { - test_log_handler.exists_log_containing(&format!("DEBUG: migration_from_6_to_7_without_any_data: Migration from 6 to 7: no data to migrate in {table_name}")); - }) - } - - fn insert_value(conn: &dyn ConnectionWrapper, insert_stm: &str) { - let mut statement = conn.prepare(insert_stm).unwrap(); - statement.execute([]).unwrap(); - } - - fn query_rows_helper( - conn: &dyn ConnectionWrapper, - sql: &str, - expected_typed_values: fn(&Row) -> rusqlite::Result<()>, - ) { - let mut statement = conn.prepare(sql).unwrap(); - statement.query_row([], expected_typed_values).unwrap(); - } -} diff --git a/node/src/database/db_migrations/db_migrator.rs b/node/src/database/db_migrations/db_migrator.rs new file mode 100644 index 000000000..cc43aec78 --- /dev/null +++ b/node/src/database/db_migrations/db_migrator.rs @@ -0,0 +1,721 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::connection_wrapper::ConnectionWrapper; +use crate::database::db_initializer::ExternalData; +use crate::database::db_migrations::migrations::migration_0_to_1::Migrate_0_to_1; +use crate::database::db_migrations::migrations::migration_1_to_2::Migrate_1_to_2; +use crate::database::db_migrations::migrations::migration_2_to_3::Migrate_2_to_3; +use crate::database::db_migrations::migrations::migration_3_to_4::Migrate_3_to_4; +use crate::database::db_migrations::migrations::migration_4_to_5::Migrate_4_to_5; +use crate::database::db_migrations::migrations::migration_5_to_6::Migrate_5_to_6; +use crate::database::db_migrations::migrations::migration_6_to_7::Migrate_6_to_7; +use crate::database::db_migrations::migrator_utils::{ + DBMigDeclarator, DBMigrationUtilities, DBMigrationUtilitiesReal, DBMigratorInnerConfiguration, +}; +use masq_lib::logger::Logger; +use rusqlite::Transaction; + +pub trait DbMigrator { + fn migrate_database( + &self, + obsolete_schema: usize, + target_version: usize, + conn: Box, + ) -> Result<(), String>; +} + +pub struct DbMigratorReal { + external: ExternalData, + logger: Logger, +} + +impl DbMigrator for DbMigratorReal { + fn migrate_database( + &self, + obsolete_schema: usize, + target_version: usize, + mut conn: Box, + ) -> Result<(), String> { + let migrator_config = DBMigratorInnerConfiguration::new(); + let migration_utils = match DBMigrationUtilitiesReal::new(&mut *conn, migrator_config) { + Err(e) => return Err(e.to_string()), + Ok(utils) => utils, + }; + self.initiate_migrations( + obsolete_schema, + target_version, + Box::new(migration_utils), + Self::list_of_migrations(), + ) + } +} + +pub trait DatabaseMigration { + fn migrate<'a>( + &self, + mig_declaration_utilities: Box, + ) -> rusqlite::Result<()>; + fn old_version(&self) -> usize; +} + +impl DbMigratorReal { + pub fn new(external: ExternalData) -> Self { + Self { + external, + logger: Logger::new("DbMigrator"), + } + } + + const fn list_of_migrations<'a>() -> &'a [&'a dyn DatabaseMigration] { + &[ + &Migrate_0_to_1, + &Migrate_1_to_2, + &Migrate_2_to_3, + &Migrate_3_to_4, + &Migrate_4_to_5, + &Migrate_5_to_6, + &Migrate_6_to_7, + ] + } + + fn initiate_migrations<'a>( + &self, + obsolete_schema: usize, + target_version: usize, + mut migration_utilities: Box, + list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)], + ) -> Result<(), String> { + let migrations_to_process = Self::select_migrations_to_process( + obsolete_schema, + list_of_migrations, + target_version, + &*migration_utilities, + ); + for record in migrations_to_process { + let present_db_version = record.old_version(); + if let Err(e) = self.migrate_semi_automated(record, &*migration_utilities, &self.logger) + { + return self.dispatch_bad_news(present_db_version, e); + } + self.log_success(present_db_version) + } + migration_utilities.commit() + } + + fn migrate_semi_automated<'a>( + &self, + record: &dyn DatabaseMigration, + migration_utilities: &'a (dyn DBMigrationUtilities + 'a), + logger: &'a Logger, + ) -> rusqlite::Result<()> { + info!( + &self.logger, + "Migrating from version {} to version {}", + record.old_version(), + record.old_version() + 1 + ); + record.migrate(migration_utilities.make_mig_declarator(&self.external, logger))?; + let migrate_to = record.old_version() + 1; + migration_utilities.update_schema_version(migrate_to) + } + + pub fn update_schema_version( + name_of_given_table: &str, + transaction: &Transaction, + update_to: usize, + ) -> rusqlite::Result<()> { + transaction.execute( + &format!( + "UPDATE {} SET value = {} WHERE name = 'schema_version'", + name_of_given_table, update_to + ), + [], + )?; + Ok(()) + } + + fn select_migrations_to_process<'a>( + obsolete_schema: usize, + list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)], + target_version: usize, + mig_utils: &dyn DBMigrationUtilities, + ) -> Vec<&'a (dyn DatabaseMigration + 'a)> { + mig_utils.too_high_schema_panics(obsolete_schema); + list_of_migrations + .iter() + .skip_while(|entry| entry.old_version() != obsolete_schema) + .take_while(|entry| entry.old_version() < target_version) + .map(Self::deref) + .collect::>() + } + + fn deref<'a, T: ?Sized>(value: &'a &T) -> &'a T { + *value + } + + fn dispatch_bad_news( + &self, + current_version: usize, + error: rusqlite::Error, + ) -> Result<(), String> { + let error_message = format!( + "Migrating database from version {} to {} failed: {:?}", + current_version, + current_version + 1, + error + ); + error!(self.logger, "{}", &error_message); + Err(error_message) + } + + fn log_success(&self, previous_version: usize) { + info!( + self.logger, + "Database successfully migrated from version {} to {}", + previous_version, + previous_version + 1 + ) + } +} + +#[cfg(test)] +mod tests { + use crate::database::connection_wrapper::{ConnectionWrapper, ConnectionWrapperReal}; + use crate::database::db_initializer::test_utils::ConnectionWrapperMock; + use crate::database::db_initializer::{ExternalData, CURRENT_SCHEMA_VERSION}; + use crate::database::db_migrations::db_migrator::{ + DatabaseMigration, DbMigrator, DbMigratorReal, + }; + use crate::database::db_migrations::migrations::migration_0_to_1::Migrate_0_to_1; + use crate::database::db_migrations::migrator_utils::{ + DBMigDeclarator, DBMigrationUtilities, DBMigrationUtilitiesReal, + DBMigratorInnerConfiguration, + }; + use crate::database::db_migrations::test_utils::DBMigDeclaratorMock; + use crate::test_utils::database_utils::make_external_data; + use masq_lib::logger::Logger; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; + use masq_lib::utils::NeighborhoodModeLight; + use rusqlite::{Connection, Error}; + use std::cell::RefCell; + use std::fmt::Debug; + use std::iter::once; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use std::sync::{Arc, Mutex}; + + #[derive(Default)] + struct DBMigrationUtilitiesMock { + too_high_schema_panics_params: Arc>>, + make_mig_declarator_params: Arc>>, + make_mig_declarator_results: RefCell>>, + update_schema_version_params: Arc>>, + update_schema_version_results: RefCell>>, + commit_results: RefCell>>, + } + + impl DBMigrationUtilitiesMock { + pub fn update_schema_version_params(mut self, params: &Arc>>) -> Self { + self.update_schema_version_params = params.clone(); + self + } + + pub fn update_schema_version_result(self, result: rusqlite::Result<()>) -> Self { + self.update_schema_version_results.borrow_mut().push(result); + self + } + + pub fn commit_result(self, result: Result<(), String>) -> Self { + self.commit_results.borrow_mut().push(result); + self + } + + pub fn make_mig_declarator_params( + mut self, + params: &Arc>>, + ) -> Self { + self.make_mig_declarator_params = params.clone(); + self + } + + pub fn make_mig_declarator_result(self, result: Box) -> Self { + self.make_mig_declarator_results.borrow_mut().push(result); + self + } + } + + impl DBMigrationUtilities for DBMigrationUtilitiesMock { + fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()> { + self.update_schema_version_params + .lock() + .unwrap() + .push(update_to); + self.update_schema_version_results.borrow_mut().remove(0) + } + + fn commit(&mut self) -> Result<(), String> { + self.commit_results.borrow_mut().remove(0) + } + + fn make_mig_declarator<'a>( + &'a self, + external: &'a ExternalData, + _logger: &'a Logger, + ) -> Box { + self.make_mig_declarator_params + .lock() + .unwrap() + .push(external.clone()); + self.make_mig_declarator_results.borrow_mut().remove(0) + } + + fn too_high_schema_panics(&self, obsolete_schema: usize) { + self.too_high_schema_panics_params + .lock() + .unwrap() + .push(obsolete_schema); + } + } + + #[derive(Default, Debug)] + struct DatabaseMigrationMock { + old_version_result: RefCell, + migrate_params: Arc>>, + migrate_results: RefCell>>, + } + + impl DatabaseMigrationMock { + fn old_version_result(self, result: usize) -> Self { + self.old_version_result.replace(result); + self + } + + fn migrate_result(self, result: rusqlite::Result<()>) -> Self { + self.migrate_results.borrow_mut().push(result); + self + } + + fn migrate_params(mut self, params: &Arc>>) -> Self { + self.migrate_params = params.clone(); + self + } + + fn set_up_necessary_stuff_for_mocked_migration_record( + self, + result_o_v: usize, + result_m: rusqlite::Result<()>, + params_m: &Arc>>, + ) -> Self { + self.old_version_result(result_o_v) + .migrate_result(result_m) + .migrate_params(params_m) + } + } + + impl DatabaseMigration for DatabaseMigrationMock { + fn migrate<'a>( + &self, + _migration_utilities: Box, + ) -> rusqlite::Result<()> { + self.migrate_params.lock().unwrap().push(()); + self.migrate_results.borrow_mut().remove(0) + } + + fn old_version(&self) -> usize { + *self.old_version_result.borrow() + } + } + + const _TEST_FROM_COMPILATION_TIME: () = + if DbMigratorReal::list_of_migrations().len() != CURRENT_SCHEMA_VERSION { + panic!( + "It appears you need to increment the current schema version to have DbMigrator \ + work correctly if any new migration added" + ) + }; + + #[test] + fn migrate_database_handles_an_error_from_creating_the_root_transaction() { + let subject = DbMigratorReal::new(make_external_data()); + let obsolete_schema = 0; + let target_version = 5; //irrelevant + let connection = ConnectionWrapperMock::default() + .transaction_result(Err(Error::SqliteSingleThreadedMode)); //hard to find a real-like error for this + + let result = + subject.migrate_database(obsolete_schema, target_version, Box::new(connection)); + + assert_eq!( + result, + Err("SQLite was compiled or configured for single-threaded use only".to_string()) + ) + } + + #[test] + fn initiate_migrations_panics_if_the_schema_is_of_higher_number_than_the_latest_official() { + let last_version = CURRENT_SCHEMA_VERSION; + let too_advanced = last_version + 1; + let connection = Connection::open_in_memory().unwrap(); + let mut conn_wrapper = ConnectionWrapperReal::new(connection); + let mig_config = DBMigratorInnerConfiguration::new(); + let migration_utilities = + DBMigrationUtilitiesReal::new(&mut conn_wrapper, mig_config).unwrap(); + let subject = DbMigratorReal::new(make_external_data()); + + let captured_panic = catch_unwind(AssertUnwindSafe(|| { + subject.initiate_migrations( + too_advanced, + CURRENT_SCHEMA_VERSION, + Box::new(migration_utilities), + DbMigratorReal::list_of_migrations(), + ) + })) + .unwrap_err(); + + let panic_message = captured_panic.downcast_ref::().unwrap(); + assert_eq!( + *panic_message, + format!( + "Database claims to be more advanced ({}) than the version {} which \ + is the latest version this Node knows about.", + too_advanced, CURRENT_SCHEMA_VERSION + ) + ) + } + + #[test] + #[should_panic(expected = "The list of database migrations is not ordered properly")] + fn list_validation_check_works_for_badly_ordered_migrations_when_inside() { + let fake_one = DatabaseMigrationMock::default().old_version_result(6); + let fake_two = DatabaseMigrationMock::default().old_version_result(2); + let list: &[&dyn DatabaseMigration] = &[&Migrate_0_to_1, &fake_one, &fake_two]; + + let _ = list_validation_check(list); + } + + #[test] + #[should_panic(expected = "The list of database migrations is not ordered properly")] + fn list_validation_check_works_for_badly_ordered_migrations_when_at_the_end() { + let fake_one = DatabaseMigrationMock::default().old_version_result(1); + let fake_two = DatabaseMigrationMock::default().old_version_result(3); + let list: &[&dyn DatabaseMigration] = &[&Migrate_0_to_1, &fake_one, &fake_two]; + + let _ = list_validation_check(list); + } + + fn list_validation_check<'a>(list_of_migrations: &'a [&'a (dyn DatabaseMigration + 'a)]) { + let begins_at_version = list_of_migrations[0].old_version(); + let iterator = list_of_migrations.iter(); + let ending_sentinel = &DatabaseMigrationMock::default() + .old_version_result(begins_at_version + iterator.len()) + as &dyn DatabaseMigration; + let iterator_shifted = list_of_migrations + .iter() + .skip(1) + .chain(once(&ending_sentinel)); + iterator.zip(iterator_shifted).for_each(|(first, second)| { + assert!( + two_numbers_are_sequential(first.old_version(), second.old_version()), + "The list of database migrations is not ordered properly" + ) + }); + } + + fn two_numbers_are_sequential(first: usize, second: usize) -> bool { + (first + 1) == second + } + + #[test] + fn list_of_migrations_is_correctly_ordered() { + let _ = list_validation_check(DbMigratorReal::list_of_migrations()); + //success if no panicking + } + + #[test] + fn list_of_migrations_ends_on_the_current_version() { + let last_entry = DbMigratorReal::list_of_migrations().into_iter().last(); + + let result = last_entry.unwrap().old_version(); + + assert!(two_numbers_are_sequential(result, CURRENT_SCHEMA_VERSION)) + } + + #[test] + fn migrate_semi_automated_returns_an_error_from_update_schema_version() { + let update_schema_version_params_arc = Arc::new(Mutex::new(vec![])); + let mut migration_record = DatabaseMigrationMock::default() + .old_version_result(4) + .migrate_result(Ok(())); + let migration_utilities = DBMigrationUtilitiesMock::default() + .make_mig_declarator_result(Box::new(DBMigDeclaratorMock::default())) + .update_schema_version_params(&update_schema_version_params_arc) + .update_schema_version_result(Err(Error::InvalidQuery)); + let subject = DbMigratorReal::new(make_external_data()); + + let result = subject.migrate_semi_automated( + &mut migration_record, + &migration_utilities, + &Logger::new("test logger"), + ); + + assert_eq!(result, Err(Error::InvalidQuery)); + let update_schema_version_params = update_schema_version_params_arc.lock().unwrap(); + assert_eq!(*update_schema_version_params, vec![5]) //doesn't mean the state really changed, this is just an image of the supplied params + } + + #[test] + fn initiate_migrations_returns_an_error_from_migrate() { + init_test_logging(); + let list = &[&DatabaseMigrationMock::default() + .old_version_result(0) + .migrate_result(Err(Error::InvalidColumnIndex(5))) + as &dyn DatabaseMigration]; + let mig_declarator = DBMigDeclaratorMock::default(); + let migration_utils = DBMigrationUtilitiesMock::default() + .make_mig_declarator_result(Box::new(mig_declarator)); + let obsolete_schema = 0; + let target_version = 5; //not relevant + let subject = DbMigratorReal::new(make_external_data()); + + let result = subject.initiate_migrations( + obsolete_schema, + target_version, + Box::new(migration_utils), + list, + ); + + assert_eq!( + result, + Err("Migrating database from version 0 to 1 failed: InvalidColumnIndex(5)".to_string()) + ); + TestLogHandler::new().exists_log_containing( + "ERROR: DbMigrator: Migrating database from version 0 to 1 failed: InvalidColumnIndex(5)", + ); + } + + fn make_success_mig_record( + old_version: usize, + empty_params_arc: &Arc>>, + ) -> Box { + Box::new( + DatabaseMigrationMock::default().set_up_necessary_stuff_for_mocked_migration_record( + old_version, + Ok(()), + empty_params_arc, + ), + ) + } + + #[test] + fn initiate_migrations_skips_records_already_included_in_the_current_database_and_migrates_only_the_others( + ) { + let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let third_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let fourth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let fifth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let mig_record_1 = make_success_mig_record(0, &first_record_migration_p_arc); + let mig_record_2 = make_success_mig_record(1, &second_record_migration_p_arc); + let mig_record_3 = make_success_mig_record(2, &third_record_migration_p_arc); + let mig_record_4 = make_success_mig_record(3, &fourth_record_migration_p_arc); + let mig_record_5 = make_success_mig_record(4, &fifth_record_migration_p_arc); + let list_of_migrations: &[&dyn DatabaseMigration] = &[ + mig_record_1.as_ref(), + mig_record_2.as_ref(), + mig_record_3.as_ref(), + mig_record_4.as_ref(), + mig_record_5.as_ref(), + ]; + let connection = Connection::open_in_memory().unwrap(); + connection + .execute( + "CREATE TABLE test ( + name TEXT, + value TEXT + )", + [], + ) + .unwrap(); + connection + .execute( + "INSERT INTO test (name, value) VALUES ('schema_version', '2')", + [], + ) + .unwrap(); + let mut connection_wrapper = ConnectionWrapperReal::new(connection); + let config = DBMigratorInnerConfiguration { + db_configuration_table: "test".to_string(), + current_schema_version: 5, + }; + let subject = DbMigratorReal::new(make_external_data()); + let obsolete_schema = 2; + let target_version = 5; + + let result = subject.initiate_migrations( + obsolete_schema, + target_version, + Box::new(DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap()), + list_of_migrations, + ); + + assert_eq!(result, Ok(())); + let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); + assert_eq!(*first_record_migration_params, vec![]); + let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); + assert_eq!(*second_record_migration_params, vec![]); + let third_record_migration_params = third_record_migration_p_arc.lock().unwrap(); + assert_eq!(*third_record_migration_params, vec![()]); + let fourth_record_migration_params = fourth_record_migration_p_arc.lock().unwrap(); + assert_eq!(*fourth_record_migration_params, vec![()]); + let fifth_record_migration_params = fifth_record_migration_p_arc.lock().unwrap(); + assert_eq!(*fifth_record_migration_params, vec![()]); + let assertion: (String, String) = connection_wrapper + .transaction() + .unwrap() + .query_row( + "SELECT name, value FROM test WHERE name='schema_version'", + [], + |row| Ok((row.get(0).unwrap(), row.get(1).unwrap())), + ) + .unwrap(); + assert_eq!(assertion.1, "5") + } + + #[test] + fn initiate_migrations_terminates_at_the_specified_version() { + let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let third_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let fourth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let fifth_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let mig_record_1 = make_success_mig_record(0, &first_record_migration_p_arc); + let mig_record_2 = make_success_mig_record(1, &second_record_migration_p_arc); + let mig_record_3 = make_success_mig_record(2, &third_record_migration_p_arc); + let mig_record_4 = make_success_mig_record(3, &fourth_record_migration_p_arc); + let mig_record_5 = make_success_mig_record(4, &fifth_record_migration_p_arc); + let list_of_migrations: &[&dyn DatabaseMigration] = &[ + mig_record_1.as_ref(), + mig_record_2.as_ref(), + mig_record_3.as_ref(), + mig_record_4.as_ref(), + mig_record_5.as_ref(), + ]; + let connection = Connection::open_in_memory().unwrap(); + connection + .execute("CREATE TABLE test (name TEXT, value TEXT)", []) + .unwrap(); + let mut connection_wrapper = ConnectionWrapperReal::new(connection); + let config = DBMigratorInnerConfiguration { + db_configuration_table: "test".to_string(), + current_schema_version: 5, + }; + let subject = DbMigratorReal::new(make_external_data()); + let obsolete_schema = 0; + let target_version = 3; + + let result = subject.initiate_migrations( + obsolete_schema, + target_version, + Box::new(DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap()), + list_of_migrations, + ); + + assert_eq!(result, Ok(())); + let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); + assert_eq!(*first_record_migration_params, vec![()]); + let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); + assert_eq!(*second_record_migration_params, vec![()]); + let third_record_migration_params = third_record_migration_p_arc.lock().unwrap(); + assert_eq!(*third_record_migration_params, vec![()]); + let fourth_record_migration_params = fourth_record_migration_p_arc.lock().unwrap(); + assert_eq!(*fourth_record_migration_params, vec![]); + let fifth_record_migration_params = fifth_record_migration_p_arc.lock().unwrap(); + assert_eq!(*fifth_record_migration_params, vec![]); + } + + #[test] + fn db_migration_happy_path() { + init_test_logging(); + let execute_upon_transaction_params_arc = Arc::new(Mutex::new(vec![])); + let update_schema_version_params_arc = Arc::new(Mutex::new(vec![])); + let make_mig_declarator_params_arc = Arc::new(Mutex::new(vec![])); + let outdated_schema = 0; + let list = &[&Migrate_0_to_1 as &dyn DatabaseMigration]; + let mig_declarator = DBMigDeclaratorMock::default() + .execute_upon_transaction_params(&execute_upon_transaction_params_arc) + .execute_upon_transaction_result(Ok(())); + let migration_utils = DBMigrationUtilitiesMock::default() + .make_mig_declarator_params(&make_mig_declarator_params_arc) + .make_mig_declarator_result(Box::new(mig_declarator)) + .update_schema_version_params(&update_schema_version_params_arc) + .update_schema_version_result(Ok(())) + .commit_result(Ok(())); + let target_version = 5; //not relevant + let subject = DbMigratorReal::new(make_external_data()); + + let result = subject.initiate_migrations( + outdated_schema, + target_version, + Box::new(migration_utils), + list, + ); + + assert!(result.is_ok()); + let execute_upon_transaction_params = execute_upon_transaction_params_arc.lock().unwrap(); + assert_eq!( + *execute_upon_transaction_params.get(0).unwrap(), + vec![ + "INSERT INTO config (name, value, encrypted) VALUES ('mapping_protocol', null, 0)" + .to_string() + ], + ); + let update_schema_version_params = update_schema_version_params_arc.lock().unwrap(); + assert_eq!(update_schema_version_params[0], 1); + TestLogHandler::new().exists_log_containing( + "INFO: DbMigrator: Database successfully migrated from version 0 to 1", + ); + let make_mig_declarator_params = make_mig_declarator_params_arc.lock().unwrap(); + assert_eq!( + *make_mig_declarator_params, + vec![ExternalData { + chain: TEST_DEFAULT_CHAIN, + neighborhood_mode: NeighborhoodModeLight::Standard, + db_password_opt: None, + }] + ) + } + + #[test] + fn final_commit_of_the_root_transaction_sad_path() { + let first_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let second_record_migration_p_arc = Arc::new(Mutex::new(vec![])); + let list_of_migrations: &[&dyn DatabaseMigration] = &[ + &DatabaseMigrationMock::default().set_up_necessary_stuff_for_mocked_migration_record( + 0, + Ok(()), + &first_record_migration_p_arc, + ), + &DatabaseMigrationMock::default().set_up_necessary_stuff_for_mocked_migration_record( + 1, + Ok(()), + &second_record_migration_p_arc, + ), + ]; + let migration_utils = DBMigrationUtilitiesMock::default() + .make_mig_declarator_result(Box::new(DBMigDeclaratorMock::default())) + .make_mig_declarator_result(Box::new(DBMigDeclaratorMock::default())) + .update_schema_version_result(Ok(())) + .update_schema_version_result(Ok(())) + .commit_result(Err("Committing transaction failed".to_string())); + let subject = DbMigratorReal::new(make_external_data()); + + let result = + subject.initiate_migrations(0, 2, Box::new(migration_utils), list_of_migrations); + + assert_eq!(result, Err(String::from("Committing transaction failed"))); + let first_record_migration_params = first_record_migration_p_arc.lock().unwrap(); + assert_eq!(*first_record_migration_params, vec![()]); + let second_record_migration_params = second_record_migration_p_arc.lock().unwrap(); + assert_eq!(*second_record_migration_params, vec![()]); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_0_to_1.rs b/node/src/database/db_migrations/migrations/migration_0_to_1.rs new file mode 100644 index 000000000..c94709ac6 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_0_to_1.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_0_to_1; + +impl DatabaseMigration for Migrate_0_to_1 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + declaration_utils.execute_upon_transaction(&[ + &"INSERT INTO config (name, value, encrypted) VALUES ('mapping_protocol', null, 0)", + ]) + } + + fn old_version(&self) -> usize { + 0 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::test_utils::database_utils::{ + bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, + }; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use std::fs::create_dir_all; + + #[test] + fn migration_from_0_to_1_is_properly_set() { + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_0_to_1_is_properly_set", + ); + create_dir_all(&dir_path).unwrap(); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + + let result = subject.initialize_to_version( + &dir_path, + 1, + DbInitializationConfig::create_or_migrate(make_external_data()), + ); + let connection = result.unwrap(); + let (mp_value, mp_encrypted) = retrieve_config_row(connection.as_ref(), "mapping_protocol"); + let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); + assert_eq!(mp_value, None); + assert_eq!(mp_encrypted, false); + assert_eq!(cs_value, Some("1".to_string())); + assert_eq!(cs_encrypted, false) + } +} diff --git a/node/src/database/db_migrations/migrations/migration_1_to_2.rs b/node/src/database/db_migrations/migrations/migration_1_to_2.rs new file mode 100644 index 000000000..d92bcaf68 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_1_to_2.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_1_to_2; + +impl DatabaseMigration for Migrate_1_to_2 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let statement = format!( + "INSERT INTO config (name, value, encrypted) VALUES ('chain_name', '{}', 0)", + declaration_utils + .external_parameters() + .chain + .rec() + .literal_identifier + ); + declaration_utils.execute_upon_transaction(&[&statement]) + } + + fn old_version(&self) -> usize { + 1 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::test_utils::database_utils::{ + bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, + }; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + + #[test] + fn migration_from_1_to_2_is_properly_set() { + init_test_logging(); + let start_at = 1; + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_1_to_2_is_properly_set", + ); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + { + subject + .initialize_to_version( + &dir_path, + start_at, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + } + + let result = subject.initialize_to_version( + &dir_path, + start_at + 1, + DbInitializationConfig::create_or_migrate(make_external_data()), + ); + + let connection = result.unwrap(); + let (chn_value, chn_encrypted) = retrieve_config_row(connection.as_ref(), "chain_name"); + let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); + assert_eq!(chn_value, Some("eth-ropsten".to_string())); + assert_eq!(chn_encrypted, false); + assert_eq!(cs_value, Some("2".to_string())); + assert_eq!(cs_encrypted, false); + TestLogHandler::new().exists_log_containing( + "DbMigrator: Database successfully migrated from version 1 to 2", + ); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_2_to_3.rs b/node/src/database/db_migrations/migrations/migration_2_to_3.rs new file mode 100644 index 000000000..4ae798b26 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_2_to_3.rs @@ -0,0 +1,83 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_2_to_3; + +impl DatabaseMigration for Migrate_2_to_3 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let statement_1 = + "INSERT INTO config (name, value, encrypted) VALUES ('blockchain_service_url', null, 0)"; + let statement_2 = format!( + "INSERT INTO config (name, value, encrypted) VALUES ('neighborhood_mode', '{}', 0)", + declaration_utils.external_parameters().neighborhood_mode + ); + declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2]) + } + + fn old_version(&self) -> usize { + 2 + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, DATABASE_FILE, + }; + use crate::test_utils::database_utils::{ + bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, + }; + use masq_lib::constants::DEFAULT_CHAIN; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use masq_lib::utils::NeighborhoodModeLight; + + #[test] + fn migration_from_2_to_3_is_properly_set() { + let start_at = 2; + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_2_to_3_is_properly_set", + ); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + { + subject + .initialize_to_version( + &dir_path, + start_at, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + } + + let result = subject.initialize_to_version( + &dir_path, + start_at + 1, + DbInitializationConfig::create_or_migrate(ExternalData::new( + DEFAULT_CHAIN, + NeighborhoodModeLight::ConsumeOnly, + None, + )), + ); + + let connection = result.unwrap(); + let (bchs_value, bchs_encrypted) = + retrieve_config_row(connection.as_ref(), "blockchain_service_url"); + assert_eq!(bchs_value, None); + assert_eq!(bchs_encrypted, false); + let (nm_value, nm_encrypted) = + retrieve_config_row(connection.as_ref(), "neighborhood_mode"); + assert_eq!(nm_value, Some("consume-only".to_string())); + assert_eq!(nm_encrypted, false); + let (cs_value, cs_encrypted) = retrieve_config_row(connection.as_ref(), "schema_version"); + assert_eq!(cs_value, Some("3".to_string())); + assert_eq!(cs_encrypted, false); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_3_to_4.rs b/node/src/database/db_migrations/migrations/migration_3_to_4.rs new file mode 100644 index 000000000..2853bb94d --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_3_to_4.rs @@ -0,0 +1,349 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::blockchain::bip39::Bip39; +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; +use crate::db_config::db_encryption_layer::DbEncryptionLayer; +use crate::db_config::typed_config_layer::decode_bytes; +use crate::sub_lib::cryptde::PlainData; +use itertools::Itertools; +use tiny_hderive::bip32::ExtendedPrivKey; + +#[allow(non_camel_case_types)] +pub struct Migrate_3_to_4; + +impl Migrate_3_to_4 { + fn maybe_exchange_seed_for_private_key( + consuming_path_opt: Option, + example_encrypted_opt: Option, + seed_encrypted_opt: Option, + utils: &dyn DBMigDeclarator, + ) -> Option { + match ( + consuming_path_opt, + example_encrypted_opt, + seed_encrypted_opt, + ) { + (Some(consuming_path), Some(example_encrypted), Some(seed_encrypted)) => { + let password_opt = utils.db_password(); + if !DbEncryptionLayer::password_matches(&password_opt, &Some(example_encrypted)) { + panic!("Migrating Database from 3 to 4: bad password"); + } + let seed_encoded = + DbEncryptionLayer::decrypt_value(&Some(seed_encrypted), &password_opt, "seed") + .expect("Internal error") + .expect("Internal error"); + let seed_data = decode_bytes(Some(seed_encoded)) + .expect("Internal error") + .expect("Internal error"); + let extended_private_key = + ExtendedPrivKey::derive(seed_data.as_ref(), consuming_path.as_str()) + .expect("Internal error"); + let private_key_data = PlainData::new(&extended_private_key.secret()); + Some( + Bip39::encrypt_bytes( + &private_key_data.as_slice(), + password_opt.as_ref().expect("Password somehow disappeared"), + ) + .expect("Internal error: encryption failed"), + ) + } + (None, None, None) => None, + (None, Some(_), None) => None, + (consuming_path_opt, example_encrypted_opt, seed_encrypted_opt) => panic!( + "these three options {:?}, {:?}, {:?} leave the database in an inconsistent state", + consuming_path_opt, example_encrypted_opt, seed_encrypted_opt + ), + } + } +} + +impl DatabaseMigration for Migrate_3_to_4 { + fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { + let transaction = utils.transaction(); + let mut stmt = transaction + .prepare("select name, value from config where name in ('example_encrypted', 'seed', 'consuming_wallet_derivation_path') order by name") + .expect("Internal error"); + + let rows = stmt + .query_map([], |row| { + let name = row.get::(0).expect("Internal error"); + let value_opt = row.get::>(1).expect("Internal error"); + Ok((name, value_opt)) + }) + .expect("Database is corrupt") + .map(|r| r.unwrap()) + .collect::)>>(); + if rows.iter().map(|r| r.0.as_str()).collect_vec() + != vec![ + "consuming_wallet_derivation_path", + "example_encrypted", + "seed", + ] + { + panic!("Database is corrupt"); + } + let consuming_path_opt = rows[0].1.clone(); + let example_encrypted_opt = rows[1].1.clone(); + let seed_encrypted_opt = rows[2].1.clone(); + let private_key_encoded_opt = Self::maybe_exchange_seed_for_private_key( + consuming_path_opt, + example_encrypted_opt, + seed_encrypted_opt, + utils.as_ref(), + ); + let private_key_column = if let Some(private_key) = private_key_encoded_opt { + format!("'{}'", private_key) + } else { + "null".to_string() + }; + utils.execute_upon_transaction(&[ + &format! ("insert into config (name, value, encrypted) values ('consuming_wallet_private_key', {}, 1)", + private_key_column), + &"delete from config where name in ('seed', 'consuming_wallet_derivation_path', 'consuming_wallet_public_key')", + ]) + } + + fn old_version(&self) -> usize { + 3 + } +} + +#[cfg(test)] +mod tests { + use crate::blockchain::bip39::Bip39; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::db_migrations::migrations::migration_3_to_4::Migrate_3_to_4; + use crate::database::db_migrations::test_utils::DBMigDeclaratorMock; + use crate::db_config::db_encryption_layer::DbEncryptionLayer; + use crate::db_config::typed_config_layer::encode_bytes; + use crate::sub_lib::cryptde::PlainData; + use crate::test_utils::database_utils::{ + bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, + }; + use bip39::{Language, Mnemonic, MnemonicType, Seed}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use masq_lib::utils::derivation_path; + use rand::Rng; + use rusqlite::ToSql; + use std::panic::{catch_unwind, AssertUnwindSafe}; + use tiny_hderive::bip32::ExtendedPrivKey; + + #[test] + fn migration_from_3_to_4_with_wallets() { + let data_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_3_to_4_with_wallets", + ); + let db_path = data_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let password_opt = &Some("password".to_string()); + let subject = DbInitializerReal::default(); + let mut external_data = make_external_data(); + external_data.db_password_opt = password_opt.as_ref().cloned(); + let init_config = DbInitializationConfig::create_or_migrate(external_data); + let original_private_key = { + let schema3_conn = subject + .initialize_to_version(&data_path, 3, init_config.clone()) + .unwrap(); + let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); + let seed = Seed::new(&mnemonic, "booga"); + let consuming_path = derivation_path(0, 150); + let original_private_key = + ExtendedPrivKey::derive(seed.as_bytes(), consuming_path.as_str()) + .unwrap() + .secret(); + let seed_plain = PlainData::new(seed.as_bytes()); + let seed_encoded = encode_bytes(Some(seed_plain)).unwrap().unwrap(); + let seed_encrypted = + DbEncryptionLayer::encrypt_value(&Some(seed_encoded), password_opt, "seed") + .unwrap() + .unwrap(); + let mut example_data = [0u8; 32]; + rand::thread_rng().fill(&mut example_data); + let example_encrypted = + Bip39::encrypt_bytes(&example_data, password_opt.as_ref().unwrap()) + .expect("Encryption failed"); + let updates = vec![ + ("consuming_wallet_derivation_path", consuming_path, false), + ("consuming_wallet_public_key", "booga".to_string(), false), + ("example_encrypted", example_encrypted, true), + ("seed", seed_encrypted, true), + ]; + updates.into_iter().for_each(|(name, value, flag)| { + let mut stmt = schema3_conn + .prepare("update config set value = ?, encrypted = ? where name = ?") + .expect(&format!( + "Couldn't prepare statement to set {} to {}", + name, value + )); + let params: &[&dyn ToSql] = + &[&value, &(if flag { 1 } else { 0 }), &name.to_string()]; + let count = stmt.execute(params).unwrap(); + if count != 1 { + panic!( + "Updating {} to '{}' should have affected 1 row, but affected {}", + name, value, count + ); + } + }); + original_private_key.to_vec() + }; + + let migrated_private_key = { + let mut schema4_conn = subject + .initialize_to_version(&data_path, 4, init_config) + .unwrap(); + { + let mut stmt = schema4_conn.prepare("select count(*) from config where name in ('consuming_wallet_derivation_path', 'consuming_wallet_public_key', 'seed')").unwrap(); + let cruft = stmt + .query_row([], |row| Ok(row.get::(0))) + .unwrap() + .unwrap(); + assert_eq!(cruft, 0); + } + let (private_key_encrypted, encrypted) = + retrieve_config_row(schema4_conn.as_mut(), "consuming_wallet_private_key"); + assert_eq!(encrypted, true); + let private_key = Bip39::decrypt_bytes( + &private_key_encrypted.unwrap(), + password_opt.as_ref().unwrap(), + ) + .unwrap(); + private_key.as_slice().to_vec() + }; + + assert_eq!(migrated_private_key, original_private_key); + } + + #[test] + fn migration_from_3_to_4_without_secrets() { + let data_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_3_to_4_without_secrets", + ); + let db_path = data_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let password_opt = &Some("password".to_string()); + let subject = DbInitializerReal::default(); + let mut external_data = make_external_data(); + external_data.db_password_opt = password_opt.as_ref().cloned(); + let init_config = DbInitializationConfig::create_or_migrate(external_data); + { + subject + .initialize_to_version(&data_path, 3, init_config.clone()) + .unwrap(); + }; + + let mut schema4_conn = subject + .initialize_to_version(&data_path, 4, init_config) + .unwrap(); + + { + let mut stmt = schema4_conn.prepare("select count(*) from config where name in ('consuming_wallet_derivation_path', 'consuming_wallet_public_key', 'seed')").unwrap(); + let cruft = stmt + .query_row([], |row| Ok(row.get::(0))) + .unwrap() + .unwrap(); + assert_eq!(cruft, 0); + } + let (private_key_encrypted, encrypted) = + retrieve_config_row(schema4_conn.as_mut(), "consuming_wallet_private_key"); + assert_eq!(private_key_encrypted, None); + assert_eq!(encrypted, true); + } + + #[test] + #[should_panic(expected = "Migrating Database from 3 to 4: bad password")] + fn migration_from_3_to_4_bad_password() { + let example_encrypted = Bip39::encrypt_bytes(&b"BBBB", "GoodPassword").unwrap(); + let mig_declarator = + DBMigDeclaratorMock::default().db_password_result(Some("BadPassword".to_string())); + let consuming_path_opt = Some("AAAAA".to_string()); + let example_encrypted_opt = Some(example_encrypted); + let seed_encrypted_opt = Some("CCCCC".to_string()); + + Migrate_3_to_4::maybe_exchange_seed_for_private_key( + consuming_path_opt, + example_encrypted_opt, + seed_encrypted_opt, + &mig_declarator, + ); + } + + #[test] + fn database_with_password_but_without_secrets_yet_still_accepted() { + let mig_declarator = DBMigDeclaratorMock::default(); + let example_encrypted_opt = Some("random garbage".to_string()); + + let result = Migrate_3_to_4::maybe_exchange_seed_for_private_key( + None, + example_encrypted_opt, + None, + &mig_declarator, + ); + + assert_eq!(result, None); + } + + fn catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + consuming_path_opt: Option<&str>, + example_encrypted_opt: Option<&str>, + seed_encrypted_opt: Option<&str>, + ) -> String { + let mig_declarator = &DBMigDeclaratorMock::default(); + let consuming_path_opt = consuming_path_opt.map(|str| str.to_string()); + let example_encrypted_opt = example_encrypted_opt.map(|str| str.to_string()); + let seed_encrypted_opt = seed_encrypted_opt.map(|str| str.to_string()); + let panic = catch_unwind(AssertUnwindSafe(|| { + Migrate_3_to_4::maybe_exchange_seed_for_private_key( + consuming_path_opt, + example_encrypted_opt, + seed_encrypted_opt, + mig_declarator, + ) + })) + .unwrap_err(); + panic.downcast_ref::().unwrap().to_owned() + } + + #[test] + fn migration_panics_if_the_database_is_corrupt() { + let panic = catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + Some("consuming_path"), + Some("example_encrypted"), + None, + ); + assert_eq!(panic, "these three options Some(\"consuming_path\"), Some(\"example_encrypted\"), None leave the database in an inconsistent state"); + + let panic = catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + Some("consuming_path"), + None, + Some("seed_encrypted"), + ); + assert_eq!(panic, "these three options Some(\"consuming_path\"), None, Some(\"seed_encrypted\") leave the database in an inconsistent state"); + + let panic = catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + None, + Some("example_encrypted"), + Some("seed_encrypted"), + ); + assert_eq!(panic, "these three options None, Some(\"example_encrypted\"), Some(\"seed_encrypted\") leave the database in an inconsistent state"); + + let panic = catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + None, + None, + Some("seed_encrypted"), + ); + assert_eq!(panic, "these three options None, None, Some(\"seed_encrypted\") leave the database in an inconsistent state"); + + let panic = catch_panic_for_maybe_exchange_seed_for_private_key_with_corrupt_database( + Some("consuming_path"), + None, + None, + ); + assert_eq!(panic, "these three options Some(\"consuming_path\"), None, None leave the database in an inconsistent state"); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_4_to_5.rs b/node/src/database/db_migrations/migrations/migration_4_to_5.rs new file mode 100644 index 000000000..31e449d82 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_4_to_5.rs @@ -0,0 +1,302 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::dao_utils::VigilantRusqliteFlatten; +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; + +#[allow(non_camel_case_types)] +pub struct Migrate_4_to_5; + +impl DatabaseMigration for Migrate_4_to_5 { + fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { + let mut select_statement = utils + .transaction() + .prepare("select pending_payment_transaction from payable where pending_payment_transaction is not null")?; + let unresolved_pending_transactions: Vec = select_statement + .query_map([], |row| { + Ok(row + .get::(0) + .expect("select statement was badly prepared")) + })? + .vigilant_flatten() + .collect(); + if !unresolved_pending_transactions.is_empty() { + warning!(utils.logger(), + "Migration from 4 to 5: database belonging to the chain '{}'; \ + we discovered possibly abandoned transactions that are said yet to be pending, these are: '{}'; continuing", + utils.external_parameters().chain.rec().literal_identifier,unresolved_pending_transactions.join("', '") ) + } else { + debug!( + utils.logger(), + "Migration from 4 to 5: no previous pending transactions found; continuing" + ) + }; + let statement_1 = "alter table payable drop column pending_payment_transaction"; + let statement_2 = "alter table payable add pending_payable_rowid integer null"; + let statement_3 = "create table pending_payable (\ + rowid integer primary key, \ + transaction_hash text not null, \ + amount integer not null, \ + payable_timestamp integer not null, \ + attempt integer not null, \ + process_error text null\ + )"; + let statement_4 = + "create unique index pending_payable_hash_idx ON pending_payable (transaction_hash)"; + let statement_5 = "drop index idx_receivable_wallet_address"; + let statement_6 = "drop index idx_banned_wallet_address"; + let statement_7 = "drop index idx_payable_wallet_address"; + let statement_8 = "alter table config rename to _config_old"; + let statement_9 = "create table config (\ + name text primary key,\ + value text,\ + encrypted integer not null\ + )"; + let statement_10 = "insert into config (name, value, encrypted) select name, value, encrypted from _config_old"; + let statement_11 = "drop table _config_old"; + utils.execute_upon_transaction(&[ + &statement_1, + &statement_2, + &statement_3, + &statement_4, + &statement_5, + &statement_6, + &statement_7, + &statement_8, + &statement_9, + &statement_10, + &statement_11, + ]) + } + + fn old_version(&self) -> usize { + 4 + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::dao_utils::{from_time_t, to_time_t}; + use crate::database::connection_wrapper::{ConnectionWrapper, ConnectionWrapperReal}; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, ExternalData, DATABASE_FILE, + }; + use crate::sub_lib::wallet::Wallet; + use crate::test_utils::database_utils::{ + assert_create_table_stm_contains_all_parts, + assert_index_stm_is_coupled_with_right_parameter, assert_no_index_exists_for_table, + assert_table_does_not_exist, bring_db_0_back_to_life_and_return_connection, + make_external_data, + }; + use crate::test_utils::make_wallet; + use ethereum_types::BigEndianHash; + use itertools::Itertools; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::{ensure_node_home_directory_exists, TEST_DEFAULT_CHAIN}; + use masq_lib::utils::NeighborhoodModeLight; + use rusqlite::types::Value::Null; + use rusqlite::ToSql; + use std::collections::HashMap; + use std::time::SystemTime; + use web3::types::{H256, U256}; + + #[test] + fn migration_from_4_to_5_without_pending_transactions() { + init_test_logging(); + let start_at = 4; + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_4_to_5_without_pending_transactions", + ); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + let conn = subject + .initialize_to_version( + &dir_path, + start_at, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + let wallet_1 = make_wallet("scotland_yard"); + prepare_old_fashioned_account_with_pending_transaction_opt( + conn.as_ref(), + None, + &wallet_1, + 113344, + from_time_t(250_000_000), + ); + let config_table_before = fetch_all_from_config_table(conn.as_ref()); + + let _ = subject + .initialize_to_version( + &dir_path, + start_at + 1, + DbInitializationConfig::create_or_migrate(ExternalData::new( + TEST_DEFAULT_CHAIN, + NeighborhoodModeLight::ConsumeOnly, + Some("password".to_string()), + )), + ) + .unwrap(); + + let config_table_after = fetch_all_from_config_table(conn.as_ref()); + assert_eq!(config_table_before, config_table_after); + assert_on_schema_5_was_adopted(conn.as_ref()); + TestLogHandler::new().exists_log_containing("DEBUG: DbMigrator: Migration from 4 to 5: no previous pending transactions found; continuing"); + } + + fn prepare_old_fashioned_account_with_pending_transaction_opt( + conn: &dyn ConnectionWrapper, + transaction_hash_opt: Option, + wallet: &Wallet, + amount: i64, + timestamp: SystemTime, + ) { + let hash_str = transaction_hash_opt + .map(|hash| format!("{:?}", hash)) + .unwrap_or(String::new()); + let mut stm = conn.prepare("insert into payable (wallet_address, balance, last_paid_timestamp, pending_payment_transaction) values (?,?,?,?)").unwrap(); + let params: &[&dyn ToSql] = &[ + &wallet, + &amount, + &to_time_t(timestamp), + if !hash_str.is_empty() { + &hash_str + } else { + &Null + }, + ]; + let row_count = stm.execute(params).unwrap(); + assert_eq!(row_count, 1); + } + + #[test] + fn migration_from_4_to_5_with_pending_transactions() { + init_test_logging(); + let start_at = 4; + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_4_to_5_with_pending_transactions", + ); + let db_path = dir_path.join(DATABASE_FILE); + let conn = bring_db_0_back_to_life_and_return_connection(&db_path); + let conn = ConnectionWrapperReal::new(conn); + let wallet_1 = make_wallet("james_bond"); + let transaction_hash_1 = H256::from_uint(&U256::from(45454545)); + let wallet_2 = make_wallet("robinson_crusoe"); + let transaction_hash_2 = H256::from_uint(&U256::from(999888)); + let subject = DbInitializerReal::default(); + { + let _ = subject + .initialize_to_version( + &dir_path, + start_at, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + } + prepare_old_fashioned_account_with_pending_transaction_opt( + &conn, + Some(transaction_hash_1), + &wallet_1, + 555555, + SystemTime::now(), + ); + prepare_old_fashioned_account_with_pending_transaction_opt( + &conn, + Some(transaction_hash_2), + &wallet_2, + 1111111, + from_time_t(200_000_000), + ); + let config_table_before = fetch_all_from_config_table(&conn); + + let conn_schema5 = subject + .initialize_to_version( + &dir_path, + start_at + 1, + DbInitializationConfig::create_or_migrate(ExternalData::new( + TEST_DEFAULT_CHAIN, + NeighborhoodModeLight::ConsumeOnly, + Some("password".to_string()), + )), + ) + .unwrap(); + + let config_table_after = fetch_all_from_config_table(&conn); + assert_eq!(config_table_before, config_table_after); + assert_on_schema_5_was_adopted(conn_schema5.as_ref()); + TestLogHandler::new().exists_log_containing("WARN: DbMigrator: Migration from 4 to 5: database belonging to the chain 'eth-ropsten'; \ + we discovered possibly abandoned transactions that are said yet to be pending, these are: \ + '0x0000000000000000000000000000000000000000000000000000000002b594d1', \ + '0x00000000000000000000000000000000000000000000000000000000000f41d0'; continuing"); + } + + fn assert_on_schema_5_was_adopted(conn_schema5: &dyn ConnectionWrapper) { + let expected_key_words: &[&[&str]] = &[ + &["rowid", "integer", "primary", "key"], + &["transaction_hash", "text", "not", "null"], + &["amount", "integer", "not", "null"], + &["payable_timestamp", "integer", "not", "null"], + &["attempt", "integer", "not", "null"], + &["process_error", "text", "null"], + ]; + assert_create_table_stm_contains_all_parts( + conn_schema5, + "pending_payable", + expected_key_words, + ); + let expected_key_words: &[&[&str]] = &[&["transaction_hash"]]; + assert_index_stm_is_coupled_with_right_parameter( + conn_schema5, + "pending_payable_hash_idx", + expected_key_words, + ); + let expected_key_words: &[&[&str]] = &[ + &["wallet_address", "text", "primary", "key"], + &["balance", "integer", "not", "null"], + &["last_paid_timestamp", "integer", "not", "null"], + &["pending_payable_rowid", "integer", "null"], + ]; + assert_create_table_stm_contains_all_parts(conn_schema5, "payable", expected_key_words); + let expected_key_words: &[&[&str]] = &[ + &["name", "text", "primary", "key"], + &["value", "text"], + &["encrypted", "integer", "not", "null"], + ]; + assert_create_table_stm_contains_all_parts(conn_schema5, "config", expected_key_words); + assert_no_index_exists_for_table(conn_schema5, "config"); + assert_no_index_exists_for_table(conn_schema5, "payable"); + assert_no_index_exists_for_table(conn_schema5, "receivable"); + assert_no_index_exists_for_table(conn_schema5, "banned"); + assert_table_does_not_exist(conn_schema5, "_config_old") + } + + fn fetch_all_from_config_table( + conn: &dyn ConnectionWrapper, + ) -> Vec<(String, (Option, bool))> { + let mut stmt = conn + .prepare("select name, value, encrypted from config") + .unwrap(); + let mut hash_map_of_values = stmt + .query_map([], |row| { + Ok(( + row.get::(0).unwrap(), + ( + row.get::>(1).unwrap(), + row.get::(2) + .map(|encrypted: i64| encrypted > 0) + .unwrap(), + ), + )) + }) + .unwrap() + .flatten() + .collect::, bool)>>(); + hash_map_of_values.remove("schema_version").unwrap(); + let mut vec_of_values = hash_map_of_values.into_iter().collect_vec(); + vec_of_values.sort_by_key(|(name, _)| name.clone()); + vec_of_values + } +} diff --git a/node/src/database/db_migrations/migrations/migration_5_to_6.rs b/node/src/database/db_migrations/migrations/migration_5_to_6.rs new file mode 100644 index 000000000..a5f902cb9 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_5_to_6.rs @@ -0,0 +1,94 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::DBMigDeclarator; +use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; +use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + +#[allow(non_camel_case_types)] +pub struct Migrate_5_to_6; + +impl DatabaseMigration for Migrate_5_to_6 { + fn migrate<'a>( + &self, + declaration_utils: Box, + ) -> rusqlite::Result<()> { + let statement_1 = Self::make_initialization_statement( + "payment_thresholds", + &DEFAULT_PAYMENT_THRESHOLDS.to_string(), + ); + let statement_2 = + Self::make_initialization_statement("rate_pack", &DEFAULT_RATE_PACK.to_string()); + let statement_3 = Self::make_initialization_statement( + "scan_intervals", + &DEFAULT_SCAN_INTERVALS.to_string(), + ); + declaration_utils.execute_upon_transaction(&[&statement_1, &statement_2, &statement_3]) + } + + fn old_version(&self) -> usize { + 5 + } +} + +impl Migrate_5_to_6 { + fn make_initialization_statement(name: &str, value: &str) -> String { + format!( + "INSERT INTO config (name, value, encrypted) VALUES ('{}', '{}', 0)", + name, value + ) + } +} + +#[cfg(test)] +mod tests { + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::sub_lib::accountant::{DEFAULT_PAYMENT_THRESHOLDS, DEFAULT_SCAN_INTERVALS}; + use crate::sub_lib::neighborhood::DEFAULT_RATE_PACK; + use crate::test_utils::database_utils::{ + bring_db_0_back_to_life_and_return_connection, make_external_data, retrieve_config_row, + }; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + + #[test] + fn migration_from_5_to_6_works() { + let dir_path = + ensure_node_home_directory_exists("db_migrations", "migration_from_5_to_6_works"); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + { + subject + .initialize_to_version( + &dir_path, + 5, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + } + + let result = subject.initialize_to_version( + &dir_path, + 6, + DbInitializationConfig::create_or_migrate(make_external_data()), + ); + + let connection = result.unwrap(); + let (payment_thresholds, encrypted) = + retrieve_config_row(connection.as_ref(), "payment_thresholds"); + assert_eq!( + payment_thresholds, + Some(DEFAULT_PAYMENT_THRESHOLDS.to_string()) + ); + assert_eq!(encrypted, false); + let (rate_pack, encrypted) = retrieve_config_row(connection.as_ref(), "rate_pack"); + assert_eq!(rate_pack, Some(DEFAULT_RATE_PACK.to_string())); + assert_eq!(encrypted, false); + let (scan_intervals, encrypted) = + retrieve_config_row(connection.as_ref(), "scan_intervals"); + assert_eq!(scan_intervals, Some(DEFAULT_SCAN_INTERVALS.to_string())); + assert_eq!(encrypted, false); + } +} diff --git a/node/src/database/db_migrations/migrations/migration_6_to_7.rs b/node/src/database/db_migrations/migrations/migration_6_to_7.rs new file mode 100644 index 000000000..36bb973a0 --- /dev/null +++ b/node/src/database/db_migrations/migrations/migration_6_to_7.rs @@ -0,0 +1,377 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::big_int_processing::big_int_divider::BigIntDivider; +use crate::accountant::dao_utils::VigilantRusqliteFlatten; +use crate::accountant::gwei_to_wei; +use crate::database::db_migrations::db_migrator::DatabaseMigration; +use crate::database::db_migrations::migrator_utils::{ + DBMigDeclarator, StatementObject, StatementWithRusqliteParams, +}; +use crate::sub_lib::neighborhood::RatePack; +use itertools::Itertools; +use rusqlite::{Row, ToSql}; + +#[allow(non_camel_case_types)] +pub struct Migrate_6_to_7; + +impl DatabaseMigration for Migrate_6_to_7 { + fn migrate<'a>(&self, utils: Box) -> rusqlite::Result<()> { + let mut migration_carrier = Migrate_6_to_7_carrier::new(utils.as_ref()); + migration_carrier.retype_table( + "payable", + "balance", + "wallet_address text primary key, + balance_high_b integer not null, + balance_low_b integer not null, + last_paid_timestamp integer not null, + pending_payable_rowid integer null", + )?; + migration_carrier.retype_table( + "receivable", + "balance", + "wallet_address text primary key, + balance_high_b integer not null, + balance_low_b integer not null, + last_received_timestamp integer not null", + )?; + migration_carrier.retype_table( + "pending_payable", + "amount", + "rowid integer primary key, + transaction_hash text not null, + amount_high_b integer not null, + amount_low_b integer not null, + payable_timestamp integer not null, + attempt integer not null, + process_error text null", + )?; + + migration_carrier.update_rate_pack(); + + migration_carrier.utils.execute_upon_transaction( + &migration_carrier + .statements + .iter() + .map(|boxed| boxed.as_ref()) + .collect_vec(), + ) + } + + fn old_version(&self) -> usize { + 6 + } +} + +#[allow(non_camel_case_types)] +struct Migrate_6_to_7_carrier<'a> { + utils: &'a (dyn DBMigDeclarator + 'a), + statements: Vec>, +} + +impl<'a> Migrate_6_to_7_carrier<'a> { + fn new(utils: &'a (dyn DBMigDeclarator + 'a)) -> Self { + Self { + utils, + statements: vec![], + } + } + + fn retype_table( + &mut self, + table: &str, + old_param_name_of_future_big_int: &str, + create_new_table_stm: &str, + ) -> rusqlite::Result<()> { + self.utils.execute_upon_transaction(&[ + &format!("alter table {table} rename to _{table}_old"), + &format!( + "create table compensatory_{table} (old_rowid integer, high_bytes integer null, low_bytes integer null)" + ), + &format!("create table {table} ({create_new_table_stm}) strict"), + ])?; + let param_names = Self::extract_param_names(create_new_table_stm); + self.maybe_compose_insert_stm_with_auxiliary_table_to_handle_new_big_int_data( + table, + old_param_name_of_future_big_int, + param_names, + ); + self.statements + .push(Box::new(format!("drop table _{table}_old"))); + Ok(()) + } + + fn maybe_compose_insert_stm_with_auxiliary_table_to_handle_new_big_int_data( + &mut self, + table: &str, + big_int_param_old_name: &str, + param_names: Vec, + ) { + let big_int_params_new_names = param_names + .iter() + .filter(|segment| segment.contains(big_int_param_old_name)) + .map(|name| name.to_owned()) + .collect::>(); + let (easy_params, normal_params_prepared_for_inner_join) = + Self::prepare_unchanged_params(param_names, &big_int_params_new_names); + let future_big_int_values_including_old_rowids = self + .utils + .transaction() + .prepare(&format!( + "select rowid, {big_int_param_old_name} from _{table}_old", + )) + .expect("rusqlite internal error") + .query_map([], |row: &Row| { + let old_rowid = row.get(0).expect("rowid fetching error"); + let balance = row.get(1).expect("old param fetching error"); + Ok((old_rowid, balance)) + }) + .expect("map failed") + .vigilant_flatten() + .collect::>(); + if !future_big_int_values_including_old_rowids.is_empty() { + self.fill_compensatory_table(future_big_int_values_including_old_rowids, table); + let new_big_int_params = big_int_params_new_names.join(", "); + let final_insert_statement = format!( + "insert into {table} ({easy_params}, {new_big_int_params}) select {normal_params_prepared_for_inner_join}, \ + R.high_bytes, R.low_bytes from _{table}_old L inner join compensatory_{table} R where L.rowid = R.old_rowid", + ); + self.statements.push(Box::new(final_insert_statement)) + } else { + debug!( + self.utils.logger(), + "Migration from 6 to 7: no data to migrate in {}", table + ) + }; + } + + fn prepare_unchanged_params( + param_names_for_select_stm: Vec, + big_int_params_names: &[String], + ) -> (String, String) { + let easy_params_vec = param_names_for_select_stm + .into_iter() + .filter(|name| !big_int_params_names.contains(name)) + .collect_vec(); + let easy_params = easy_params_vec.iter().join(", "); + let easy_params_preformatted_for_inner_join = easy_params_vec + .into_iter() + .map(|word| format!("L.{}", word.trim())) + .join(", "); + (easy_params, easy_params_preformatted_for_inner_join) + } + + fn fill_compensatory_table(&mut self, all_big_int_values_found: Vec<(i64, i64)>, table: &str) { + let sql_stm = format!( + "insert into compensatory_{} (old_rowid, high_bytes, low_bytes) values {}", + table, + (0..all_big_int_values_found.len()) + .map(|_| "(?, ?, ?)") + .collect::() + ); + let params = all_big_int_values_found + .into_iter() + .flat_map(|(old_rowid, i64_balance)| { + let (high, low) = BigIntDivider::deconstruct(gwei_to_wei(i64_balance)); + vec![ + Box::new(old_rowid) as Box, + Box::new(high), + Box::new(low), + ] + }) + .collect::>>(); + let statement = StatementWithRusqliteParams { sql_stm, params }; + self.statements.push(Box::new(statement)); + } + + fn extract_param_names(table_creation_lines: &str) -> Vec { + table_creation_lines + .split(',') + .map(|line| { + let line = line.trim_start(); + line.chars() + .take_while(|char| !char.is_whitespace()) + .collect::() + }) + .collect() + } + + fn update_rate_pack(&mut self) { + let transaction = self.utils.transaction(); + let mut stm = transaction + .prepare("select value from config where name = 'rate_pack'") + .expect("stm preparation failed"); + let old_rate_pack = stm + .query_row([], |row| row.get::(0)) + .expect("row query failed"); + let old_rate_pack_as_native = + RatePack::try_from(old_rate_pack.as_str()).unwrap_or_else(|_| { + panic!( + "rate pack conversion failed with value: {}; database corrupt!", + old_rate_pack + ) + }); + let new_rate_pack = RatePack { + routing_byte_rate: gwei_to_wei(old_rate_pack_as_native.routing_byte_rate), + routing_service_rate: gwei_to_wei(old_rate_pack_as_native.routing_service_rate), + exit_byte_rate: gwei_to_wei(old_rate_pack_as_native.exit_byte_rate), + exit_service_rate: gwei_to_wei(old_rate_pack_as_native.exit_service_rate), + }; + let serialized_rate_pack = new_rate_pack.to_string(); + let params: Vec> = vec![Box::new(serialized_rate_pack)]; + + self.statements.push(Box::new(StatementWithRusqliteParams { + sql_stm: "update config set value = ? where name = 'rate_pack'".to_string(), + params, + })) + } +} + +#[cfg(test)] +mod tests { + use crate::database::connection_wrapper::ConnectionWrapper; + use crate::database::db_initializer::{ + DbInitializationConfig, DbInitializer, DbInitializerReal, DATABASE_FILE, + }; + use crate::database::db_migrations::db_migrator::{DbMigrator, DbMigratorReal}; + use crate::db_config::persistent_configuration::{ + PersistentConfiguration, PersistentConfigurationReal, + }; + use crate::sub_lib::wallet::Wallet; + use crate::test_utils::database_utils::{ + assert_table_created_as_strict, bring_db_0_back_to_life_and_return_connection, + make_external_data, retrieve_config_row, + }; + use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::Row; + use std::str::FromStr; + + #[test] + fn migration_from_6_to_7_works() { + let dir_path = + ensure_node_home_directory_exists("db_migrations", "migration_from_6_to_7_works"); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + let pre_db_conn = subject + .initialize_to_version( + &dir_path, + 6, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + insert_value(&*pre_db_conn,"insert into payable (wallet_address, balance, last_paid_timestamp, pending_payable_rowid) \ + values (\"0xD7d1b2cF58f6500c7CB22fCA42B8512d06813a03\", 56784545484899, 11111, null)"); + insert_value( + &*pre_db_conn, + "insert into receivable (wallet_address, balance, last_received_timestamp) \ + values (\"0xD2d1b2eF58f6500c7ae22fCA42B8512d06813a03\",-56784,22222)", + ); + insert_value(&*pre_db_conn,"insert into pending_payable (rowid, transaction_hash, amount, payable_timestamp,attempt, process_error) \ + values (5, \"0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f222a4bc8cd032f768fc5219838\" ,9123 ,33333 ,1 ,null)"); + let mut persistent_config = PersistentConfigurationReal::from(pre_db_conn); + let old_rate_pack_in_gwei = "44|50|20|32".to_string(); + persistent_config + .set_rate_pack(old_rate_pack_in_gwei.clone()) + .unwrap(); + + let conn = subject + .initialize_to_version( + &dir_path, + 7, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + + assert_table_created_as_strict(&*conn, "payable"); + assert_table_created_as_strict(&*conn, "receivable"); + let select_sql = "select wallet_address, balance_high_b, balance_low_b, last_paid_timestamp, pending_payable_rowid from payable"; + query_rows_helper(&*conn, select_sql, |row| { + assert_eq!( + row.get::(0).unwrap(), + Wallet::from_str("0xD7d1b2cF58f6500c7CB22fCA42B8512d06813a03").unwrap() + ); + assert_eq!(row.get::(1).unwrap(), 6156); + assert_eq!(row.get::(2).unwrap(), 5467226021000125952); + assert_eq!(row.get::(3).unwrap(), 11111); + assert_eq!(row.get::>(4).unwrap(), None); + Ok(()) + }); + let select_sql = "select wallet_address, balance_high_b, balance_low_b, last_received_timestamp from receivable"; + query_rows_helper(&*conn, select_sql, |row| { + assert_eq!( + row.get::(0).unwrap(), + Wallet::from_str("0xD2d1b2eF58f6500c7ae22fCA42B8512d06813a03").unwrap() + ); + assert_eq!(row.get::(1).unwrap(), -1); + assert_eq!(row.get::(2).unwrap(), 9223315252854775808); + assert_eq!(row.get::(3).unwrap(), 22222); + Ok(()) + }); + let select_sql = "select rowid, transaction_hash, amount_high_b, amount_low_b, payable_timestamp, attempt, process_error from pending_payable"; + query_rows_helper(&*conn, select_sql, |row| { + assert_eq!(row.get::(0).unwrap(), 5); + assert_eq!( + row.get::(1).unwrap(), + "0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f222a4bc8cd032f768fc5219838".to_string() + ); + assert_eq!(row.get::(2).unwrap(), 0); + assert_eq!(row.get::(3).unwrap(), 9123000000000); + assert_eq!(row.get::(4).unwrap(), 33333); + assert_eq!(row.get::(5).unwrap(), 1); + assert_eq!(row.get::>(6).unwrap(), None); + Ok(()) + }); + let (rate_pack, encrypted) = retrieve_config_row(&*conn, "rate_pack"); + assert_eq!( + rate_pack, + Some("44000000000|50000000000|20000000000|32000000000".to_string()) + ); + assert_eq!(encrypted, false); + } + + #[test] + fn migration_from_6_to_7_without_any_data() { + init_test_logging(); + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "migration_from_6_to_7_without_any_data", + ); + let db_path = dir_path.join(DATABASE_FILE); + let _ = bring_db_0_back_to_life_and_return_connection(&db_path); + let subject = DbInitializerReal::default(); + let conn = subject + .initialize_to_version( + &dir_path, + 6, + DbInitializationConfig::create_or_migrate(make_external_data()), + ) + .unwrap(); + let subject = DbMigratorReal::new(make_external_data()); + + subject.migrate_database(6, 7, conn).unwrap(); + + let test_log_handler = TestLogHandler::new(); + ["payable", "receivable", "pending_payable"] + .iter() + .for_each(|table_name| { + test_log_handler.exists_log_containing(&format!( + "DEBUG: DbMigrator: Migration from 6 to 7: no data to migrate in {table_name}" + )); + }) + } + + fn insert_value(conn: &dyn ConnectionWrapper, insert_stm: &str) { + let mut statement = conn.prepare(insert_stm).unwrap(); + statement.execute([]).unwrap(); + } + + fn query_rows_helper( + conn: &dyn ConnectionWrapper, + sql: &str, + expected_typed_values: fn(&Row) -> rusqlite::Result<()>, + ) { + let mut statement = conn.prepare(sql).unwrap(); + statement.query_row([], expected_typed_values).unwrap(); + } +} diff --git a/node/src/database/db_migrations/migrations/mod.rs b/node/src/database/db_migrations/migrations/mod.rs new file mode 100644 index 000000000..69df36eab --- /dev/null +++ b/node/src/database/db_migrations/migrations/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod migration_0_to_1; +pub mod migration_1_to_2; +pub mod migration_2_to_3; +pub mod migration_3_to_4; +pub mod migration_4_to_5; +pub mod migration_5_to_6; +pub mod migration_6_to_7; diff --git a/node/src/database/db_migrations/migrator_utils.rs b/node/src/database/db_migrations/migrator_utils.rs new file mode 100644 index 000000000..d553d959a --- /dev/null +++ b/node/src/database/db_migrations/migrator_utils.rs @@ -0,0 +1,447 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::database::connection_wrapper::ConnectionWrapper; +use crate::database::db_initializer::{ExternalData, CURRENT_SCHEMA_VERSION}; +use crate::database::db_migrations::db_migrator::{DatabaseMigration, DbMigratorReal}; +use masq_lib::logger::Logger; +use masq_lib::utils::ExpectValue; +use rusqlite::{params_from_iter, Error, ToSql, Transaction}; +use std::fmt::{Display, Formatter}; + +pub trait DBMigDeclarator { + fn db_password(&self) -> Option; + fn transaction(&self) -> &Transaction; + fn execute_upon_transaction<'a>( + &self, + sql_statements: &[&'a dyn StatementObject], + ) -> rusqlite::Result<()>; + fn external_parameters(&self) -> &ExternalData; + fn logger(&self) -> &Logger; +} + +pub trait DBMigrationUtilities { + fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()>; + + fn commit(&mut self) -> Result<(), String>; + + fn make_mig_declarator<'a>( + &'a self, + external: &'a ExternalData, + logger: &'a Logger, + ) -> Box; + + fn too_high_schema_panics(&self, obsolete_schema: usize); +} + +pub struct DBMigrationUtilitiesReal<'a> { + root_transaction: Option>, + db_migrator_configuration: DBMigratorInnerConfiguration, +} + +impl<'a> DBMigrationUtilitiesReal<'a> { + pub fn new<'b: 'a>( + conn: &'b mut dyn ConnectionWrapper, + db_migrator_configuration: DBMigratorInnerConfiguration, + ) -> rusqlite::Result { + Ok(Self { + root_transaction: Some(conn.transaction()?), + db_migrator_configuration, + }) + } + + fn root_transaction_ref(&self) -> &Transaction<'a> { + self.root_transaction.as_ref().expectv("root transaction") + } +} + +impl<'a> DBMigrationUtilities for DBMigrationUtilitiesReal<'a> { + fn update_schema_version(&self, update_to: usize) -> rusqlite::Result<()> { + DbMigratorReal::update_schema_version( + self.db_migrator_configuration + .db_configuration_table + .as_str(), + self.root_transaction_ref(), + update_to, + ) + } + + fn commit(&mut self) -> Result<(), String> { + self.root_transaction + .take() + .expectv("owned root transaction") + .commit() + .map_err(|e| e.to_string()) + } + + fn make_mig_declarator<'b>( + &'b self, + external: &'b ExternalData, + logger: &'b Logger, + ) -> Box { + Box::new(DBMigDeclaratorReal::new( + self.root_transaction_ref(), + external, + logger, + )) + } + + fn too_high_schema_panics(&self, obsolete_schema: usize) { + if obsolete_schema > self.db_migrator_configuration.current_schema_version { + panic!( + "Database claims to be more advanced ({}) than the version {} which is the latest \ + version this Node knows about.", + obsolete_schema, CURRENT_SCHEMA_VERSION + ) + } + } +} + +struct DBMigDeclaratorReal<'a> { + root_transaction_ref: &'a Transaction<'a>, + external: &'a ExternalData, + logger: &'a Logger, +} + +impl<'a> DBMigDeclaratorReal<'a> { + fn new( + root_transaction_ref: &'a Transaction<'a>, + external: &'a ExternalData, + logger: &'a Logger, + ) -> Self { + Self { + root_transaction_ref, + external, + logger, + } + } +} + +impl DBMigDeclarator for DBMigDeclaratorReal<'_> { + fn db_password(&self) -> Option { + self.external.db_password_opt.clone() + } + + fn transaction(&self) -> &Transaction { + self.root_transaction_ref + } + + fn execute_upon_transaction<'a>( + &self, + sql_statements: &[&dyn StatementObject], + ) -> rusqlite::Result<()> { + let transaction = self.root_transaction_ref; + sql_statements.iter().fold(Ok(()), |so_far, stm| { + if so_far.is_ok() { + match stm.execute(transaction) { + Ok(_) => Ok(()), + Err(e) if e == Error::ExecuteReturnedResults => + panic!("Statements returning values should be avoided with execute_upon_transaction, caused by: {}",stm), + Err(e) => Err(e), + } + } else { + so_far + } + }) + } + + fn external_parameters(&self) -> &ExternalData { + self.external + } + + fn logger(&self) -> &Logger { + self.logger + } +} + +pub trait StatementObject: Display { + fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()>; +} + +impl StatementObject for &str { + fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { + transaction.execute(self, []).map(|_| ()) + } +} + +impl StatementObject for String { + fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { + self.as_str().execute(transaction) + } +} + +pub struct StatementWithRusqliteParams { + pub sql_stm: String, + pub params: Vec>, +} + +impl StatementObject for StatementWithRusqliteParams { + fn execute(&self, transaction: &Transaction) -> rusqlite::Result<()> { + transaction + .execute(&self.sql_stm, params_from_iter(self.params.iter())) + .map(|_| ()) + } +} + +impl Display for StatementWithRusqliteParams { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.sql_stm) + } +} + +pub struct DBMigratorInnerConfiguration { + pub db_configuration_table: String, + pub current_schema_version: usize, +} + +impl DBMigratorInnerConfiguration { + pub fn new() -> Self { + DBMigratorInnerConfiguration { + db_configuration_table: "config".to_string(), + current_schema_version: CURRENT_SCHEMA_VERSION, + } + } +} + +impl Default for DBMigratorInnerConfiguration { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +struct InterimMigrationPlaceholder(usize); + +impl DatabaseMigration for InterimMigrationPlaceholder { + fn migrate<'a>( + &self, + _mig_declaration_utilities: Box, + ) -> rusqlite::Result<()> { + Ok(()) + } + + fn old_version(&self) -> usize { + self.0 - 1 + } +} + +#[cfg(test)] +mod tests { + use crate::database::connection_wrapper::ConnectionWrapperReal; + use crate::database::db_migrations::migrator_utils::{ + DBMigrationUtilities, DBMigrationUtilitiesReal, DBMigratorInnerConfiguration, + StatementObject, StatementWithRusqliteParams, + }; + use crate::test_utils::database_utils::make_external_data; + use masq_lib::logger::Logger; + use masq_lib::test_utils::utils::ensure_node_home_directory_exists; + use rusqlite::{Connection, Error, OptionalExtension, ToSql}; + + #[test] + fn statement_with_rusqlite_params_can_display_its_stm() { + let subject = StatementWithRusqliteParams { + sql_stm: "insert into table2 (column) values (?)".to_string(), + params: vec![Box::new(12345)], + }; + + let stm = subject.to_string(); + + assert_eq!(stm, "insert into table2 (column) values (?)".to_string()) + } + + #[test] + fn db_password_works() { + let dir_path = ensure_node_home_directory_exists("db_migrations", "db_password_works"); + let db_path = dir_path.join("test_database.db"); + let mut connection_wrapper = + ConnectionWrapperReal::new(Connection::open(&db_path).unwrap()); + let utils = DBMigrationUtilitiesReal::new( + &mut connection_wrapper, + DBMigratorInnerConfiguration { + db_configuration_table: "irrelevant".to_string(), + current_schema_version: 0, + }, + ) + .unwrap(); + let mut external_parameters = make_external_data(); + external_parameters.db_password_opt = Some("booga".to_string()); + let logger = Logger::new("test_logger"); + let subject = utils.make_mig_declarator(&external_parameters, &logger); + + let result = subject.db_password(); + + assert_eq!(result, Some("booga".to_string())); + } + + #[test] + fn transaction_works() { + let dir_path = ensure_node_home_directory_exists("db_migrations", "transaction_works"); + let db_path = dir_path.join("test_database.db"); + let mut connection_wrapper = + ConnectionWrapperReal::new(Connection::open(&db_path).unwrap()); + let utils = DBMigrationUtilitiesReal::new( + &mut connection_wrapper, + DBMigratorInnerConfiguration { + db_configuration_table: "irrelevant".to_string(), + current_schema_version: 0, + }, + ) + .unwrap(); + let external_parameters = make_external_data(); + let logger = Logger::new("test_logger"); + let subject = utils.make_mig_declarator(&external_parameters, &logger); + + let result = subject.transaction(); + + result + .execute("CREATE TABLE IF NOT EXISTS test (column TEXT)", []) + .unwrap(); + // no panic? Test passes! + } + + #[test] + fn execute_upon_transaction_returns_the_first_error_encountered_and_the_transaction_is_canceled( + ) { + let dir_path = ensure_node_home_directory_exists("db_migrations","execute_upon_transaction_returns_the_first_error_encountered_and_the_transaction_is_canceled"); + let db_path = dir_path.join("test_database.db"); + let connection = Connection::open(&db_path).unwrap(); + connection + .execute( + "CREATE TABLE test ( + name TEXT, + count integer + )", + [], + ) + .unwrap(); + let correct_statement_1 = "INSERT INTO test (name,count) VALUES ('mushrooms',270)"; + let erroneous_statement_1 = + "INSERT INTO botanic_garden (name, count) VALUES (sunflowers, 100)"; + let erroneous_statement_2 = "INSERT INTO milky_way (star) VALUES (just_discovered)"; + let set_of_sql_statements: &[&dyn StatementObject] = &[ + &correct_statement_1, + &erroneous_statement_1, + &erroneous_statement_2, + ]; + let mut connection_wrapper = ConnectionWrapperReal::new(connection); + let config = DBMigratorInnerConfiguration::new(); + let external_parameters = make_external_data(); + let subject = DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap(); + + let result = subject + .make_mig_declarator(&external_parameters, &Logger::new("test logger")) + .execute_upon_transaction(set_of_sql_statements); + + assert_eq!( + result.unwrap_err().to_string(), + "no such table: botanic_garden" + ); + let connection = Connection::open(&db_path).unwrap(); + //when an error occurs, the underlying transaction gets rolled back, and we cannot see any changes to the database + let assertion: Option<(String, String)> = connection + .query_row("SELECT count FROM test WHERE name='mushrooms'", [], |row| { + Ok((row.get(0).unwrap(), row.get(1).unwrap())) + }) + .optional() + .unwrap(); + assert!(assertion.is_none()) //means no result for this query + } + + #[test] + #[should_panic( + expected = "Statements returning values should be avoided with execute_upon_transaction, caused by: SELECT * FROM botanic_garden" + )] + fn execute_upon_transaction_panics_because_statement_returns() { + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "execute_upon_transaction_panics_because_statement_returns", + ); + let db_path = dir_path.join("test_database.db"); + let connection = Connection::open(&db_path).unwrap(); + connection + .execute( + "CREATE TABLE botanic_garden ( + name TEXT, + count integer + )", + [], + ) + .unwrap(); + let statement_1 = "INSERT INTO botanic_garden (name,count) VALUES ('sun_flowers', 100)"; + let statement_2 = "SELECT * FROM botanic_garden"; //this statement returns data + let set_of_sql_statements: &[&dyn StatementObject] = &[&statement_1, &statement_2]; + let mut connection_wrapper = ConnectionWrapperReal::new(connection); + let config = DBMigratorInnerConfiguration::new(); + let external_parameters = make_external_data(); + let subject = DBMigrationUtilitiesReal::new(&mut connection_wrapper, config).unwrap(); + + let _ = subject + .make_mig_declarator(&external_parameters, &Logger::new("test logger")) + .execute_upon_transaction(set_of_sql_statements); + } + + #[test] + fn execute_upon_transaction_handles_also_error_from_stm_with_params() { + let dir_path = ensure_node_home_directory_exists( + "db_migrations", + "execute_upon_transaction_handles_also_error_from_stm_with_params", + ); + let db_path = dir_path.join("test_database.db"); + let conn = Connection::open(&db_path).unwrap(); + conn.execute( + "CREATE TABLE botanic_garden ( + name TEXT, + count integer + )", + [], + ) + .unwrap(); + let statement_1_simple = + "INSERT INTO botanic_garden (name,count) VALUES ('sun_flowers', 100)"; + let statement_2_good = StatementWithRusqliteParams { + sql_stm: "update botanic_garden set count = 111 where name = 'sun_flowers'".to_string(), + params: { + let params: Vec> = vec![]; + params + }, + }; + let statement_3_bad = StatementWithRusqliteParams { + sql_stm: "select name, count from foo".to_string(), + params: vec![Box::new("another_whatever")], + }; + //we expect not to get down to this statement, the error from statement_3 immediately terminates the circuit + let statement_4_demonstrative = StatementWithRusqliteParams { + sql_stm: "select name, count from bar".to_string(), + params: vec![Box::new("also_whatever")], + }; + let set_of_sql_statements: &[&dyn StatementObject] = &[ + &statement_1_simple, + &statement_2_good, + &statement_3_bad, + &statement_4_demonstrative, + ]; + let mut conn_wrapper = ConnectionWrapperReal::new(conn); + let config = DBMigratorInnerConfiguration::new(); + let external_params = make_external_data(); + let subject = DBMigrationUtilitiesReal::new(&mut conn_wrapper, config).unwrap(); + + let result = subject + .make_mig_declarator(&external_params, &Logger::new("test logger")) + .execute_upon_transaction(set_of_sql_statements); + + match result { + Err(Error::SqliteFailure(_, err_msg_opt)) => { + assert_eq!(err_msg_opt, Some("no such table: foo".to_string())) + } + x => panic!("we expected SqliteFailure(..) but got: {:?}", x), + } + let assert_conn = Connection::open(&db_path).unwrap(); + let assertion: Option<(String, i64)> = assert_conn + .query_row("SELECT * FROM botanic_garden", [], |row| { + Ok((row.get(0).unwrap(), row.get(1).unwrap())) + }) + .optional() + .unwrap(); + assert_eq!(assertion, None) + //the table remained empty because an error causes the whole transaction to abort + } +} diff --git a/node/src/database/db_migrations/mod.rs b/node/src/database/db_migrations/mod.rs new file mode 100644 index 000000000..166ef4752 --- /dev/null +++ b/node/src/database/db_migrations/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +pub mod db_migrator; +pub mod migrations; +pub mod migrator_utils; +pub mod test_utils; diff --git a/node/src/database/db_migrations/test_utils.rs b/node/src/database/db_migrations/test_utils.rs new file mode 100644 index 000000000..d85fbc41b --- /dev/null +++ b/node/src/database/db_migrations/test_utils.rs @@ -0,0 +1,69 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +#![cfg(test)] + +use crate::database::db_initializer::ExternalData; +use crate::database::db_migrations::migrator_utils::{DBMigDeclarator, StatementObject}; +use masq_lib::logger::Logger; +use rusqlite::Transaction; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; + +#[derive(Default)] +pub struct DBMigDeclaratorMock { + db_password_results: RefCell>>, + execute_upon_transaction_params: Arc>>>, + execute_upon_transaction_results: RefCell>>, +} + +impl DBMigDeclaratorMock { + pub fn db_password_result(self, result: Option) -> Self { + self.db_password_results.borrow_mut().push(result); + self + } + + pub fn execute_upon_transaction_params( + mut self, + params: &Arc>>>, + ) -> Self { + self.execute_upon_transaction_params = params.clone(); + self + } + + pub fn execute_upon_transaction_result(self, result: rusqlite::Result<()>) -> Self { + self.execute_upon_transaction_results + .borrow_mut() + .push(result); + self + } +} + +impl DBMigDeclarator for DBMigDeclaratorMock { + fn db_password(&self) -> Option { + self.db_password_results.borrow_mut().remove(0) + } + + fn transaction(&self) -> &Transaction { + unimplemented!("Not needed so far") + } + + fn execute_upon_transaction<'a>( + &self, + sql_statements: &[&'a dyn StatementObject], + ) -> rusqlite::Result<()> { + self.execute_upon_transaction_params.lock().unwrap().push( + sql_statements + .iter() + .map(|stm_obj| stm_obj.to_string()) + .collect::>(), + ); + self.execute_upon_transaction_results.borrow_mut().remove(0) + } + + fn external_parameters(&self) -> &ExternalData { + unimplemented!("Not needed so far") + } + + fn logger(&self) -> &Logger { + unimplemented!("Not needed so far") + } +} diff --git a/node/src/hopper/routing_service.rs b/node/src/hopper/routing_service.rs index 81f14263a..dae972f87 100644 --- a/node/src/hopper/routing_service.rs +++ b/node/src/hopper/routing_service.rs @@ -1911,7 +1911,7 @@ mod tests { target_hostname: Some("hostname".to_string()), target_port: 1234, protocol: ProxyProtocol::TLS, - originator_alias_public_key: PublicKey::new(b"1234"), + originator_public_key: PublicKey::new(b"1234"), }, )), ) diff --git a/node/src/node_test_utils.rs b/node/src/node_test_utils.rs index 293dbb464..b82c663a3 100644 --- a/node/src/node_test_utils.rs +++ b/node/src/node_test_utils.rs @@ -15,6 +15,7 @@ use crate::sub_lib::framer::FramedChunk; use crate::sub_lib::framer::Framer; use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; +use crate::sub_lib::utils::MessageScheduler; use crate::test_utils::recorder::Recorder; use actix::Actor; use actix::Addr; @@ -307,6 +308,10 @@ pub fn make_stream_handler_pool_subs_from_recorder(addr: &Addr) -> Str bind: recipient!(addr, PoolBindMessage), node_query_response: recipient!(addr, DispatcherNodeQueryResponse), node_from_ui_sub: recipient!(addr, NodeFromUiMessage), + scheduled_node_query_response_sub: recipient!( + addr, + MessageScheduler + ), } } diff --git a/node/src/proxy_client/mod.rs b/node/src/proxy_client/mod.rs index fd17d4e72..20f287b2d 100644 --- a/node/src/proxy_client/mod.rs +++ b/node/src/proxy_client/mod.rs @@ -113,7 +113,7 @@ impl Handler> for ProxyClient { let return_route = msg.remaining_route; let latest_stream_context = StreamContext { return_route, - payload_destination_key: payload.originator_alias_public_key.clone(), + payload_destination_key: payload.originator_public_key.clone(), paying_wallet: paying_wallet.clone(), }; debug!( @@ -610,7 +610,7 @@ mod tests { target_hostname: Some(String::from("target.hostname.com")), target_port: 1234, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"originator_public_key"[..]), + originator_public_key: PublicKey::new(&b"originator_public_key"[..]), }; let cryptde = main_cryptde(); let package = ExpiredCoresPackage::new( @@ -753,7 +753,7 @@ mod tests { target_hostname: None, target_port: 0, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"originator"[..]), + originator_public_key: PublicKey::new(&b"originator"[..]), }; let key1 = make_meaningless_public_key(); let key2 = make_meaningless_public_key(); @@ -813,7 +813,7 @@ mod tests { target_hostname: None, target_port: 0, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"originator"[..]), + originator_public_key: PublicKey::new(&b"originator"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -870,7 +870,7 @@ mod tests { target_hostname: None, target_port: 0, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let zero_hop_remaining_route = Route::one_way( RouteSegment::new( @@ -1223,7 +1223,7 @@ mod tests { target_hostname: None, target_port: 0, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originator_public_key.clone(), + originator_public_key: originator_public_key.clone(), }; let before = SystemTime::now(); diff --git a/node/src/proxy_client/stream_establisher.rs b/node/src/proxy_client/stream_establisher.rs index 364e12c1e..e798e9847 100644 --- a/node/src/proxy_client/stream_establisher.rs +++ b/node/src/proxy_client/stream_establisher.rs @@ -189,7 +189,7 @@ mod tests { target_hostname: Some("blah".to_string()), target_port: 0, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: subject.cryptde.public_key().clone(), + originator_public_key: subject.cryptde.public_key().clone(), }, read_stream, SocketAddr::from_str("1.2.3.4:5678").unwrap(), diff --git a/node/src/proxy_client/stream_handler_pool.rs b/node/src/proxy_client/stream_handler_pool.rs index 2334e29fc..20d7d6c9f 100644 --- a/node/src/proxy_client/stream_handler_pool.rs +++ b/node/src/proxy_client/stream_handler_pool.rs @@ -602,7 +602,7 @@ mod tests { target_hostname: Some("www.example.com".to_string()), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }; StreamHandlerPoolReal::process_package(payload, None, Arc::new(Mutex::new(inner))); @@ -635,7 +635,7 @@ mod tests { target_hostname: None, target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let write_parameters = Arc::new(Mutex::new(vec![])); let tx_to_write = Box::new( @@ -697,7 +697,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originator_key, + originator_public_key: originator_key, }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -766,7 +766,7 @@ mod tests { target_hostname: Some(String::from("3.4.5.6:80")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -875,7 +875,7 @@ mod tests { target_hostname: Some(String::from("3.4.5.6")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -982,7 +982,7 @@ mod tests { target_hostname: None, target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originator_key, + originator_public_key: originator_key, }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -1051,7 +1051,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -1164,7 +1164,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originator_key, + originator_public_key: originator_key, }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -1254,7 +1254,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let package = ExpiredCoresPackage::new( @@ -1367,7 +1367,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: originator_key, + originator_public_key: originator_key, }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -1429,7 +1429,7 @@ mod tests { target_hostname: Some(String::from("that.try")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"men's souls"[..]), + originator_public_key: PublicKey::new(&b"men's souls"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), @@ -1497,7 +1497,7 @@ mod tests { target_hostname: None, target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&b"booga"[..]), + originator_public_key: PublicKey::new(&b"booga"[..]), }; let package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), diff --git a/node/src/proxy_server/client_request_payload_factory.rs b/node/src/proxy_server/client_request_payload_factory.rs index 29d83f137..1f8e45689 100644 --- a/node/src/proxy_server/client_request_payload_factory.rs +++ b/node/src/proxy_server/client_request_payload_factory.rs @@ -60,7 +60,7 @@ impl ClientRequestPayloadFactory for ClientRequestPayloadFactoryReal { target_hostname, target_port, protocol: protocol_pack.proxy_protocol(), - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }) } } @@ -113,7 +113,7 @@ mod tests { target_hostname: Some(String::from("borkoed.com")), target_port: 2345, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }) ); } @@ -148,7 +148,7 @@ mod tests { target_hostname: Some(String::from("borkoed.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }) ); } @@ -202,7 +202,7 @@ mod tests { target_hostname: Some(String::from("server.com")), target_port: 443, protocol: ProxyProtocol::TLS, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }) ); } @@ -250,7 +250,7 @@ mod tests { target_hostname: None, target_port: 443, protocol: ProxyProtocol::TLS, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }) ); } diff --git a/node/src/proxy_server/mod.rs b/node/src/proxy_server/mod.rs index 3ca2841ef..2e0dd4701 100644 --- a/node/src/proxy_server/mod.rs +++ b/node/src/proxy_server/mod.rs @@ -1239,7 +1239,7 @@ mod tests { target_hostname: Some(String::from("nowhere.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -1355,7 +1355,7 @@ mod tests { target_hostname: Some(String::from("realdomain.nu")), target_port: 443, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -1835,7 +1835,7 @@ mod tests { target_hostname: Some("nowhere.com".to_string()), target_port: 80, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), } )), main_cryptde.public_key() @@ -1916,7 +1916,7 @@ mod tests { target_hostname: None, target_port: 443, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), } ),), main_cryptde.public_key() @@ -1967,7 +1967,7 @@ mod tests { target_hostname: Some(String::from("nowhere.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -2048,7 +2048,7 @@ mod tests { target_hostname: Some(String::from("nowhere.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -2166,7 +2166,7 @@ mod tests { target_hostname: Some(String::from("nowhere.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -2315,7 +2315,7 @@ mod tests { target_hostname: Some(String::from("nowhere.com")), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -2430,7 +2430,7 @@ mod tests { target_hostname: Some("nowhere.com".to_string()), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(b"originator_public_key"), + originator_public_key: PublicKey::new(b"originator_public_key"), }; let logger = Logger::new("test"); let local_tth_args = TTHLocalArgs { @@ -2516,7 +2516,7 @@ mod tests { target_hostname: Some("nowhere.com".to_string()), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(b"originator_public_key"), + originator_public_key: PublicKey::new(b"originator_public_key"), }; let logger = Logger::new("test"); let local_tth_args = TTHLocalArgs { @@ -2771,7 +2771,7 @@ mod tests { target_hostname: None, target_port: 0, protocol: ProxyProtocol::TLS, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), }; let logger = Logger::new("ProxyServer"); let source_addr = SocketAddr::from_str("1.2.3.4:5678").unwrap(); @@ -2969,7 +2969,7 @@ mod tests { target_hostname: Some(String::from("server.com")), target_port: TLS_PORT, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -3055,7 +3055,7 @@ mod tests { target_hostname: None, target_port: TLS_PORT, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -3139,7 +3139,7 @@ mod tests { target_hostname: None, target_port: TLS_PORT, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde.public_key().clone(), + originator_public_key: alias_cryptde.public_key().clone(), }; let expected_pkg = IncipientCoresPackage::new( main_cryptde, @@ -4579,7 +4579,7 @@ mod tests { target_hostname: Some(String::from("tunneled.com")), target_port: 443, protocol: ProxyProtocol::TLS, - originator_alias_public_key: alias_cryptde().public_key().clone(), + originator_public_key: alias_cryptde().public_key().clone(), } ), other => panic!("Wrong payload type: {:?}", other), @@ -4696,7 +4696,7 @@ mod tests { target_hostname: None, target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: alias_cryptde().public_key().clone(), + originator_public_key: alias_cryptde().public_key().clone(), } ), other => panic!("Wrong payload type: {:?}", other), diff --git a/node/src/stream_handler_pool.rs b/node/src/stream_handler_pool.rs index 46c600e73..a2bac3507 100644 --- a/node/src/stream_handler_pool.rs +++ b/node/src/stream_handler_pool.rs @@ -30,12 +30,12 @@ use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::tokio_wrappers::ReadHalfWrapper; use crate::sub_lib::tokio_wrappers::WriteHalfWrapper; -use crate::sub_lib::utils::{handle_ui_crash_request, NODE_MAILBOX_CAPACITY}; -use actix::Actor; +use crate::sub_lib::utils::{handle_ui_crash_request, MessageScheduler, NODE_MAILBOX_CAPACITY}; use actix::Addr; use actix::Context; use actix::Handler; use actix::Recipient; +use actix::{Actor, AsyncContext}; use masq_lib::logger::Logger; use masq_lib::ui_gateway::NodeFromUiMessage; use masq_lib::utils::localhost; @@ -43,7 +43,6 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::io; use std::net::SocketAddr; -use std::thread; use std::time::Duration; use tokio::prelude::Future; @@ -62,6 +61,7 @@ pub struct StreamHandlerPoolSubs { pub bind: Recipient, pub node_query_response: Recipient, pub node_from_ui_sub: Recipient, + pub scheduled_node_query_response_sub: Recipient>, } impl Clone for StreamHandlerPoolSubs { @@ -73,6 +73,7 @@ impl Clone for StreamHandlerPoolSubs { bind: self.bind.clone(), node_query_response: self.node_query_response.clone(), node_from_ui_sub: self.node_from_ui_sub.clone(), + scheduled_node_query_response_sub: self.scheduled_node_query_response_sub.clone(), } } } @@ -155,6 +156,18 @@ impl Handler for StreamHandlerPool { } } +// TODO: GH-686 - This handler can be implemented using a Procedural Macro +impl Handler> for StreamHandlerPool +where + StreamHandlerPool: Handler, +{ + type Result = (); + + fn handle(&mut self, msg: MessageScheduler, ctx: &mut Self::Context) -> Self::Result { + ctx.notify_later(msg.scheduled_msg, msg.delay); + } +} + impl Handler for StreamHandlerPool { type Result = (); @@ -205,6 +218,10 @@ impl StreamHandlerPool { bind: recipient!(pool_addr, PoolBindMessage), node_query_response: recipient!(pool_addr, DispatcherNodeQueryResponse), node_from_ui_sub: recipient!(pool_addr, NodeFromUiMessage), + scheduled_node_query_response_sub: recipient!( + pool_addr, + MessageScheduler + ), } } @@ -533,18 +550,19 @@ impl StreamHandlerPool { peer_addr, msg.context.data.len() ); - let recipient = self + let scheduled_node_query_response_sub = self .self_subs_opt .as_ref() - .expect("StreamHandlerPool is unbound.") - .node_query_response + .expect("StreamHandlerPool is unbound") + .scheduled_node_query_response_sub .clone(); - // TODO FIXME revisit once SC-358/GH-96 is done (idea: use notify_later() to delay messages) - thread::spawn(move || { - // to avoid getting into too-tight a resubmit loop, add a delay; in a separate thread, to avoid delaying other traffic - thread::sleep(Duration::from_millis(100)); - recipient.try_send(msg).expect("StreamHandlerPool is dead"); - }); + + scheduled_node_query_response_sub + .try_send(MessageScheduler { + scheduled_msg: msg, + delay: Duration::from_millis(100), + }) + .expect("StreamHandlerPool is dead"); } fn open_new_stream_and_recycle_message( diff --git a/node/src/sub_lib/accountant.rs b/node/src/sub_lib/accountant.rs index f89b6dea9..2cd1d2a07 100644 --- a/node/src/sub_lib/accountant.rs +++ b/node/src/sub_lib/accountant.rs @@ -3,8 +3,8 @@ use crate::accountant::payable_dao::PayableDaoFactory; use crate::accountant::pending_payable_dao::PendingPayableDaoFactory; use crate::accountant::receivable_dao::ReceivableDaoFactory; use crate::accountant::{ - checked_conversion, Accountant, ReceivedPayments, ReportTransactionReceipts, ScanError, - SentPayables, + checked_conversion, Accountant, ConsumingWalletBalancesAndQualifiedPayables, ReceivedPayments, + ReportTransactionReceipts, ScanError, SentPayables, }; use crate::banned_dao::BannedDaoFactory; use crate::blockchain::blockchain_bridge::PendingPayableFingerprint; @@ -94,6 +94,8 @@ pub struct AccountantSubs { pub report_routing_service_provided: Recipient, pub report_exit_service_provided: Recipient, pub report_services_consumed: Recipient, + pub report_consuming_wallet_balances_and_qualified_payables: + Recipient, pub report_inbound_payments: Recipient, pub pending_payable_fingerprint: Recipient, pub report_transaction_receipts: Recipient, diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 04d3e792d..7a5e679de 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -10,6 +10,7 @@ use masq_lib::blockchains::chains::Chain; use masq_lib::ui_gateway::NodeFromUiMessage; use std::fmt; use std::fmt::{Debug, Formatter}; +use web3::types::U256; #[derive(Clone, PartialEq, Eq, Debug, Default)] pub struct BlockchainBridgeConfig { @@ -22,6 +23,7 @@ pub struct BlockchainBridgeConfig { pub struct BlockchainBridgeSubs { pub bind: Recipient, pub report_accounts_payable: Recipient, + pub request_balances_to_pay_payables: Recipient, pub retrieve_transactions: Recipient, pub ui_sub: Recipient, pub request_transaction_receipts: Recipient, @@ -34,27 +36,33 @@ impl Debug for BlockchainBridgeSubs { } #[derive(Clone, PartialEq, Eq, Debug, Message)] -pub struct ReportAccountsPayable { +pub struct RequestBalancesToPayPayables { pub accounts: Vec, pub response_skeleton_opt: Option, } -impl SkeletonOptHolder for ReportAccountsPayable { +impl SkeletonOptHolder for RequestBalancesToPayPayables { fn skeleton_opt(&self) -> Option { self.response_skeleton_opt } } #[derive(Clone, PartialEq, Eq, Debug, Message)] -pub struct SetDbPasswordMsg { - pub client_id: u64, - pub password: String, +pub struct ReportAccountsPayable { + pub accounts: Vec, + pub response_skeleton_opt: Option, } -#[derive(Clone, PartialEq, Eq, Debug, Message)] -pub struct SetGasPriceMsg { - pub client_id: u64, - pub gas_price: String, +impl SkeletonOptHolder for ReportAccountsPayable { + fn skeleton_opt(&self) -> Option { + self.response_skeleton_opt + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConsumingWalletBalances { + pub gas_currency: U256, + pub masq_tokens: U256, } #[cfg(test)] diff --git a/node/src/sub_lib/migrations/client_request_payload.rs b/node/src/sub_lib/migrations/client_request_payload.rs index 83c9d7b88..555755a28 100644 --- a/node/src/sub_lib/migrations/client_request_payload.rs +++ b/node/src/sub_lib/migrations/client_request_payload.rs @@ -107,7 +107,7 @@ impl TryFrom<&Value> for ClientRequestPayload_0v1 { target_hostname: target_hostname_opt.expect("target_hostname disappeared"), target_port: target_port_opt.expect("target_port disappeared"), protocol: protocol_opt.expect("protocol disappeared"), - originator_alias_public_key: originator_public_key_opt + originator_public_key: originator_public_key_opt .expect("originator_public_key disappeared"), }) } @@ -150,7 +150,7 @@ mod tests { target_hostname: Some("target.hostname.com".to_string()), target_port: 1234, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: PublicKey::new(&[2, 3, 4, 5]), + originator_public_key: PublicKey::new(&[2, 3, 4, 5]), }; let future_crp = ExampleFutureCRP { stream_key: expected_crp.stream_key.clone(), @@ -158,7 +158,7 @@ mod tests { target_hostname: expected_crp.target_hostname.clone(), target_port: expected_crp.target_port.clone(), protocol: expected_crp.protocol.clone(), - originator_public_key: expected_crp.originator_alias_public_key.clone(), + originator_public_key: expected_crp.originator_public_key.clone(), another_field: "These are the times that try men's souls".to_string(), yet_another_field: 1234567890, }; diff --git a/node/src/sub_lib/proxy_server.rs b/node/src/sub_lib/proxy_server.rs index 23b193cd6..8c69e6878 100644 --- a/node/src/sub_lib/proxy_server.rs +++ b/node/src/sub_lib/proxy_server.rs @@ -37,7 +37,7 @@ pub struct ClientRequestPayload_0v1 { pub target_hostname: Option, pub target_port: u16, pub protocol: ProxyProtocol, - pub originator_alias_public_key: PublicKey, + pub originator_public_key: PublicKey, } impl From for MessageType { diff --git a/node/src/sub_lib/utils.rs b/node/src/sub_lib/utils.rs index 74758bd0b..9b9480821 100644 --- a/node/src/sub_lib/utils.rs +++ b/node/src/sub_lib/utils.rs @@ -237,6 +237,12 @@ where implement_as_any!(); } +#[derive(Message, Clone, PartialEq, Eq)] +pub struct MessageScheduler { + pub scheduled_msg: M, + pub delay: Duration, +} + #[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 43594962c..4080f393c 100644 --- a/node/src/test_utils/database_utils.rs +++ b/node/src/test_utils/database_utils.rs @@ -5,7 +5,8 @@ use crate::accountant::dao_utils::VigilantRusqliteFlatten; use crate::database::connection_wrapper::ConnectionWrapper; use crate::database::db_initializer::ExternalData; -use crate::database::db_migrations::DbMigrator; + +use crate::database::db_migrations::db_migrator::DbMigrator; use masq_lib::logger::Logger; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::NeighborhoodModeLight; diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index 62f4dd702..24cb68ca9 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -290,7 +290,7 @@ pub fn make_request_payload(bytes: usize, cryptde: &dyn CryptDE) -> ClientReques target_hostname: Some("example.com".to_string()), target_port: HTTP_PORT, protocol: ProxyProtocol::HTTP, - originator_alias_public_key: cryptde.public_key().clone(), + originator_public_key: cryptde.public_key().clone(), } } diff --git a/node/src/test_utils/recorder.rs b/node/src/test_utils/recorder.rs index a3935d27a..29bed80ac 100644 --- a/node/src/test_utils/recorder.rs +++ b/node/src/test_utils/recorder.rs @@ -1,6 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. #![cfg(test)] -use crate::accountant::ReportTransactionReceipts; +use crate::accountant::{ConsumingWalletBalancesAndQualifiedPayables, ReportTransactionReceipts}; use crate::accountant::{ ReceivedPayments, RequestTransactionReceipts, ScanError, ScanForPayables, ScanForPendingPayables, ScanForReceivables, SentPayables, @@ -15,7 +15,8 @@ use crate::sub_lib::accountant::AccountantSubs; use crate::sub_lib::accountant::ReportExitServiceProvidedMessage; use crate::sub_lib::accountant::ReportRoutingServiceProvidedMessage; use crate::sub_lib::accountant::ReportServicesConsumedMessage; -use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, ReportAccountsPayable}; +use crate::sub_lib::blockchain_bridge::ReportAccountsPayable; +use crate::sub_lib::blockchain_bridge::{BlockchainBridgeSubs, RequestBalancesToPayPayables}; use crate::sub_lib::configurator::{ConfiguratorSubs, NewPasswordMessage}; use crate::sub_lib::dispatcher::InboundClientData; use crate::sub_lib::dispatcher::{DispatcherSubs, StreamShutdownMsg}; @@ -43,6 +44,7 @@ use crate::sub_lib::set_consuming_wallet_message::SetConsumingWalletMessage; use crate::sub_lib::stream_handler_pool::DispatcherNodeQueryResponse; use crate::sub_lib::stream_handler_pool::TransmitDataMsg; use crate::sub_lib::ui_gateway::UiGatewaySubs; +use crate::sub_lib::utils::MessageScheduler; use crate::test_utils::recorder_stop_conditions::StopConditions; use crate::test_utils::to_millis; use crate::test_utils::unshared_test_utils::system_killer_actor::SystemKillerActor; @@ -124,8 +126,10 @@ recorder_message_handler!(ReportServicesConsumedMessage); recorder_message_handler!(ReportExitServiceProvidedMessage); recorder_message_handler!(ReportRoutingServiceProvidedMessage); recorder_message_handler!(ScanError); +recorder_message_handler!(ConsumingWalletBalancesAndQualifiedPayables); recorder_message_handler!(SentPayables); recorder_message_handler!(SetConsumingWalletMessage); +recorder_message_handler!(RequestBalancesToPayPayables); recorder_message_handler!(StartMessage); recorder_message_handler!(StreamShutdownMsg); recorder_message_handler!(TransmitDataMsg); @@ -139,6 +143,17 @@ recorder_message_handler!(ScanForPayables); recorder_message_handler!(ConnectionProgressMessage); recorder_message_handler!(ScanForPendingPayables); +impl Handler> for Recorder +where + M: Message + PartialEq + Send + 'static, +{ + type Result = (); + + fn handle(&mut self, msg: MessageScheduler, _ctx: &mut Self::Context) { + self.handle_msg(msg) + } +} + impl Handler for Recorder { type Result = MessageResult; @@ -408,6 +423,10 @@ pub fn make_accountant_subs_from_recorder(addr: &Addr) -> AccountantSu report_routing_service_provided: recipient!(addr, ReportRoutingServiceProvidedMessage), report_exit_service_provided: recipient!(addr, ReportExitServiceProvidedMessage), report_services_consumed: recipient!(addr, ReportServicesConsumedMessage), + report_consuming_wallet_balances_and_qualified_payables: recipient!( + addr, + ConsumingWalletBalancesAndQualifiedPayables + ), report_inbound_payments: recipient!(addr, ReceivedPayments), pending_payable_fingerprint: recipient!(addr, PendingPayableFingerprint), report_transaction_receipts: recipient!(addr, ReportTransactionReceipts), @@ -429,6 +448,7 @@ pub fn make_blockchain_bridge_subs_from(addr: &Addr) -> BlockchainBrid BlockchainBridgeSubs { bind: recipient!(addr, BindMessage), report_accounts_payable: recipient!(addr, ReportAccountsPayable), + request_balances_to_pay_payables: recipient!(addr, RequestBalancesToPayPayables), retrieve_transactions: recipient!(addr, RetrieveTransactions), ui_sub: recipient!(addr, NodeFromUiMessage), request_transaction_receipts: recipient!(addr, RequestTransactionReceipts),