diff --git a/masq_lib/src/blockchains/blockchain_records.rs b/masq_lib/src/blockchains/blockchain_records.rs index cc1198afa..00ac1bb66 100644 --- a/masq_lib/src/blockchains/blockchain_records.rs +++ b/masq_lib/src/blockchains/blockchain_records.rs @@ -2,63 +2,75 @@ use crate::blockchains::chains::Chain; use crate::constants::{ - BASE_MAINNET_CONTRACT_CREATION_BLOCK, BASE_MAINNET_FULL_IDENTIFIER, - BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, - ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, + BASE_GAS_PRICE_CEILING_WEI, BASE_MAINNET_CHAIN_ID, BASE_MAINNET_CONTRACT_CREATION_BLOCK, + BASE_MAINNET_FULL_IDENTIFIER, BASE_SEPOLIA_CHAIN_ID, BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, + BASE_SEPOLIA_FULL_IDENTIFIER, DEV_CHAIN_FULL_IDENTIFIER, DEV_CHAIN_ID, + DEV_GAS_PRICE_CEILING_WEI, ETH_GAS_PRICE_CEILING_WEI, ETH_MAINNET_CHAIN_ID, + ETH_MAINNET_CONTRACT_CREATION_BLOCK, ETH_MAINNET_FULL_IDENTIFIER, ETH_ROPSTEN_CHAIN_ID, ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, ETH_ROPSTEN_FULL_IDENTIFIER, - MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_CONTRACT_CREATION_BLOCK, - POLYGON_AMOY_FULL_IDENTIFIER, POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, - POLYGON_MAINNET_FULL_IDENTIFIER, + MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_CHAIN_ID, + POLYGON_AMOY_CONTRACT_CREATION_BLOCK, POLYGON_AMOY_FULL_IDENTIFIER, + POLYGON_GAS_PRICE_CEILING_WEI, POLYGON_MAINNET_CHAIN_ID, + POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, POLYGON_MAINNET_FULL_IDENTIFIER, }; use ethereum_types::{Address, H160}; +// TODO these should probably be a static (it's a shame that we construct the data every time anew +// when we ask for the chain specs), and dynamic initialization should be allowed as well pub const CHAINS: [BlockchainRecord; 7] = [ BlockchainRecord { self_id: Chain::PolyMainnet, - num_chain_id: 137, + num_chain_id: POLYGON_MAINNET_CHAIN_ID, literal_identifier: POLYGON_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::EthMainnet, - num_chain_id: 1, + num_chain_id: ETH_MAINNET_CHAIN_ID, literal_identifier: ETH_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::BaseMainnet, - num_chain_id: 8453, + num_chain_id: BASE_MAINNET_CHAIN_ID, literal_identifier: BASE_MAINNET_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::BaseSepolia, - num_chain_id: 84532, + num_chain_id: BASE_SEPOLIA_CHAIN_ID, literal_identifier: BASE_SEPOLIA_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: BASE_GAS_PRICE_CEILING_WEI, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::PolyAmoy, - num_chain_id: 80002, + num_chain_id: POLYGON_AMOY_CHAIN_ID, literal_identifier: POLYGON_AMOY_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: POLYGON_GAS_PRICE_CEILING_WEI, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::EthRopsten, - num_chain_id: 3, + num_chain_id: ETH_ROPSTEN_CHAIN_ID, literal_identifier: ETH_ROPSTEN_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: ETH_GAS_PRICE_CEILING_WEI, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, }, BlockchainRecord { self_id: Chain::Dev, - num_chain_id: 2, + num_chain_id: DEV_CHAIN_ID, literal_identifier: DEV_CHAIN_FULL_IDENTIFIER, + gas_price_safe_ceiling_minor: DEV_GAS_PRICE_CEILING_WEI, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, }, @@ -69,6 +81,7 @@ pub struct BlockchainRecord { pub self_id: Chain, pub num_chain_id: u64, pub literal_identifier: &'static str, + pub gas_price_safe_ceiling_minor: u128, pub contract: Address, pub contract_creation_block: u64, } @@ -115,7 +128,7 @@ const POLYGON_MAINNET_CONTRACT_ADDRESS: Address = H160([ mod tests { use super::*; use crate::blockchains::chains::chain_from_chain_identifier_opt; - use crate::constants::BASE_MAINNET_CONTRACT_CREATION_BLOCK; + use crate::constants::{BASE_MAINNET_CONTRACT_CREATION_BLOCK, WEIS_IN_GWEI}; use std::collections::HashSet; use std::iter::FromIterator; @@ -195,6 +208,7 @@ mod tests { num_chain_id: 1, self_id: examined_chain, literal_identifier: "eth-mainnet", + gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, contract: ETH_MAINNET_CONTRACT_ADDRESS, contract_creation_block: ETH_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -211,6 +225,7 @@ mod tests { num_chain_id: 3, self_id: examined_chain, literal_identifier: "eth-ropsten", + gas_price_safe_ceiling_minor: 100 * WEIS_IN_GWEI as u128, contract: ETH_ROPSTEN_TESTNET_CONTRACT_ADDRESS, contract_creation_block: ETH_ROPSTEN_CONTRACT_CREATION_BLOCK, } @@ -227,6 +242,7 @@ mod tests { num_chain_id: 137, self_id: examined_chain, literal_identifier: "polygon-mainnet", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: POLYGON_MAINNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -243,6 +259,7 @@ mod tests { num_chain_id: 80002, self_id: examined_chain, literal_identifier: "polygon-amoy", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: POLYGON_AMOY_TESTNET_CONTRACT_ADDRESS, contract_creation_block: POLYGON_AMOY_CONTRACT_CREATION_BLOCK, } @@ -259,6 +276,7 @@ mod tests { num_chain_id: 8453, self_id: examined_chain, literal_identifier: "base-mainnet", + gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, contract: BASE_MAINNET_CONTRACT_ADDRESS, contract_creation_block: BASE_MAINNET_CONTRACT_CREATION_BLOCK, } @@ -275,6 +293,7 @@ mod tests { num_chain_id: 84532, self_id: examined_chain, literal_identifier: "base-sepolia", + gas_price_safe_ceiling_minor: 50 * WEIS_IN_GWEI as u128, contract: BASE_SEPOLIA_TESTNET_CONTRACT_ADDRESS, contract_creation_block: BASE_SEPOLIA_CONTRACT_CREATION_BLOCK, } @@ -291,6 +310,7 @@ mod tests { num_chain_id: 2, self_id: examined_chain, literal_identifier: "dev", + gas_price_safe_ceiling_minor: 200 * WEIS_IN_GWEI as u128, contract: MULTINODE_TESTNET_CONTRACT_ADDRESS, contract_creation_block: MULTINODE_TESTNET_CONTRACT_CREATION_BLOCK, } diff --git a/masq_lib/src/blockchains/chains.rs b/masq_lib/src/blockchains/chains.rs index b7733b842..b7061d899 100644 --- a/masq_lib/src/blockchains/chains.rs +++ b/masq_lib/src/blockchains/chains.rs @@ -141,6 +141,7 @@ mod tests { num_chain_id: 0, self_id: Chain::PolyMainnet, literal_identifier: "", + gas_price_safe_ceiling_minor: 0, contract: Default::default(), contract_creation_block: 0, } diff --git a/masq_lib/src/constants.rs b/masq_lib/src/constants.rs index 6beea5748..67338f5a3 100644 --- a/masq_lib/src/constants.rs +++ b/masq_lib/src/constants.rs @@ -19,6 +19,7 @@ pub const CURRENT_LOGFILE_NAME: &str = "MASQNode_rCURRENT.log"; pub const MASQ_PROMPT: &str = "masq> "; pub const DEFAULT_GAS_PRICE: u64 = 1; //TODO ?? Really +pub const DEFAULT_GAS_PRICE_MARGIN: u64 = 30; pub const WALLET_ADDRESS_LENGTH: usize = 42; pub const MASQ_TOTAL_SUPPLY: u64 = 37_500_000; @@ -94,6 +95,13 @@ pub const CENTRAL_DELIMITER: char = '@'; pub const CHAIN_IDENTIFIER_DELIMITER: char = ':'; //chains +pub const POLYGON_MAINNET_CHAIN_ID: u64 = 137; +pub const POLYGON_AMOY_CHAIN_ID: u64 = 80002; +pub const BASE_MAINNET_CHAIN_ID: u64 = 8453; +pub const BASE_SEPOLIA_CHAIN_ID: u64 = 84532; +pub const ETH_MAINNET_CHAIN_ID: u64 = 1; +pub const ETH_ROPSTEN_CHAIN_ID: u64 = 3; +pub const DEV_CHAIN_ID: u64 = 2; const POLYGON_FAMILY: &str = "polygon"; const ETH_FAMILY: &str = "eth"; const BASE_FAMILY: &str = "base"; @@ -106,6 +114,10 @@ pub const ETH_ROPSTEN_FULL_IDENTIFIER: &str = concatcp!(ETH_FAMILY, LINK, "ropst pub const BASE_MAINNET_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, MAINNET); pub const BASE_SEPOLIA_FULL_IDENTIFIER: &str = concatcp!(BASE_FAMILY, LINK, "sepolia"); pub const DEV_CHAIN_FULL_IDENTIFIER: &str = "dev"; +pub const POLYGON_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; +pub const ETH_GAS_PRICE_CEILING_WEI: u128 = 100_000_000_000; +pub const BASE_GAS_PRICE_CEILING_WEI: u128 = 50_000_000_000; +pub const DEV_GAS_PRICE_CEILING_WEI: u128 = 200_000_000_000; #[cfg(test)] mod tests { @@ -124,6 +136,7 @@ mod tests { assert_eq!(CURRENT_LOGFILE_NAME, "MASQNode_rCURRENT.log"); assert_eq!(MASQ_PROMPT, "masq> "); assert_eq!(DEFAULT_GAS_PRICE, 1); + assert_eq!(DEFAULT_GAS_PRICE_MARGIN, 30); assert_eq!(WALLET_ADDRESS_LENGTH, 42); assert_eq!(MASQ_TOTAL_SUPPLY, 37_500_000); assert_eq!(WEIS_IN_GWEI, 1_000_000_000); @@ -169,6 +182,13 @@ mod tests { assert_eq!(VALUE_EXCEEDS_ALLOWED_LIMIT, ACCOUNTANT_PREFIX | 3); assert_eq!(CENTRAL_DELIMITER, '@'); assert_eq!(CHAIN_IDENTIFIER_DELIMITER, ':'); + assert_eq!(POLYGON_MAINNET_CHAIN_ID, 137); + assert_eq!(POLYGON_AMOY_CHAIN_ID, 80002); + assert_eq!(BASE_MAINNET_CHAIN_ID, 8453); + assert_eq!(BASE_SEPOLIA_CHAIN_ID, 84532); + assert_eq!(ETH_MAINNET_CHAIN_ID, 1); + assert_eq!(ETH_ROPSTEN_CHAIN_ID, 3); + assert_eq!(DEV_CHAIN_ID, 2); assert_eq!(POLYGON_FAMILY, "polygon"); assert_eq!(ETH_FAMILY, "eth"); assert_eq!(BASE_FAMILY, "base"); @@ -180,6 +200,10 @@ mod tests { assert_eq!(ETH_ROPSTEN_FULL_IDENTIFIER, "eth-ropsten"); assert_eq!(BASE_SEPOLIA_FULL_IDENTIFIER, "base-sepolia"); assert_eq!(DEV_CHAIN_FULL_IDENTIFIER, "dev"); + assert_eq!(POLYGON_GAS_PRICE_CEILING_WEI, 200_000_000_000); + assert_eq!(ETH_GAS_PRICE_CEILING_WEI, 100_000_000_000); + assert_eq!(BASE_GAS_PRICE_CEILING_WEI, 50_000_000_000); + assert_eq!(DEV_GAS_PRICE_CEILING_WEI, 200_000_000_000); assert_eq!( CLIENT_REQUEST_PAYLOAD_CURRENT_VERSION, DataVersion { major: 0, minor: 1 } diff --git a/masq_lib/src/test_utils/mock_blockchain_client_server.rs b/masq_lib/src/test_utils/mock_blockchain_client_server.rs index 424a4433d..80df649be 100644 --- a/masq_lib/src/test_utils/mock_blockchain_client_server.rs +++ b/masq_lib/src/test_utils/mock_blockchain_client_server.rs @@ -220,7 +220,7 @@ impl MockBlockchainClientServer { Err(e) if e.kind() == ErrorKind::TimedOut => (), Err(e) => panic!("MBCS accept() failed: {:?}", e), }; - thread::sleep(Duration::from_millis(100)); + thread::sleep(Duration::from_millis(50)); }; drop(listener); conn.set_nonblocking(true).unwrap(); diff --git a/node/src/accountant/mod.rs b/node/src/accountant/mod.rs index 24dbdcc68..7237110a8 100644 --- a/node/src/accountant/mod.rs +++ b/node/src/accountant/mod.rs @@ -1231,7 +1231,7 @@ mod tests { use crate::accountant::test_utils::DaoWithDestination::{ ForAccountantBody, ForPayableScanner, ForPendingPayableScanner, ForReceivableScanner, }; - use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_qualified_and_unqualified_payables, make_pending_payable_fingerprint, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; + use crate::accountant::test_utils::{bc_from_earning_wallet, bc_from_wallets, make_payable_account, make_pending_payable_fingerprint, make_qualified_and_unqualified_payables, make_unpriced_qualified_payables_for_retry_mode, make_priced_qualified_payables, BannedDaoFactoryMock, ConfigDaoFactoryMock, MessageIdGeneratorMock, PayableDaoFactoryMock, PayableDaoMock, PayableScannerBuilder, PaymentAdjusterMock, PendingPayableDaoFactoryMock, PendingPayableDaoMock, ReceivableDaoFactoryMock, ReceivableDaoMock}; use crate::accountant::test_utils::{AccountantBuilder, BannedDaoMock}; use crate::accountant::Accountant; use crate::blockchain::blockchain_interface::blockchain_interface_web3::HashAndAmount; @@ -1286,6 +1286,7 @@ mod tests { use std::sync::Mutex; use std::time::{Duration, UNIX_EPOCH}; use std::vec; + use crate::accountant::scanners::payable_scanner_extension::msgs::UnpricedQualifiedPayables; use crate::accountant::scanners::scan_schedulers::{NewPayableScanDynIntervalComputer, NewPayableScanDynIntervalComputerReal}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; @@ -1541,7 +1542,7 @@ mod tests { assert_eq!( blockchain_bridge_recording.get_record::(0), &QualifiedPayablesMessage { - qualified_payables: vec![payable_account], + qualified_payables: UnpricedQualifiedPayables::from(vec![payable_account]), consuming_wallet, response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1631,9 +1632,12 @@ mod tests { let system = System::new("test"); let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp); - let accounts = vec![account_1, account_2]; + let qualified_payables = make_priced_qualified_payables(vec![ + (account_1, 1_000_000_001), + (account_2, 1_000_000_002), + ]); let msg = BlockchainAgentWithContextMessage { - qualified_payables: accounts.clone(), + qualified_payables: qualified_payables.clone(), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -1649,7 +1653,7 @@ mod tests { is_adjustment_required_params.remove(0); assert_eq!( blockchain_agent_with_context_msg_actual.qualified_payables, - accounts.clone() + qualified_payables.clone() ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -1668,7 +1672,10 @@ mod tests { let blockchain_bridge_recording = blockchain_bridge_recording_arc.lock().unwrap(); let payments_instructions = blockchain_bridge_recording.get_record::(0); - assert_eq!(payments_instructions.affordable_accounts, accounts); + assert_eq!( + payments_instructions.affordable_accounts, + qualified_payables + ); assert_eq!( payments_instructions.response_skeleton_opt, Some(ResponseSkeleton { @@ -1682,8 +1689,8 @@ mod tests { ); assert_eq!(blockchain_bridge_recording.len(), 1); assert_using_the_same_logger(&logger_clone, test_name, None) - // adjust_payments() did not need a prepared result, which means it wasn't reached - // because otherwise this test would've panicked + // The adjust_payments() function doesn't require prepared results, indicating it shouldn't + // have been reached during the test, or it would have caused a panic. } fn assert_using_the_same_logger( @@ -1732,8 +1739,10 @@ mod tests { let agent_id_stamp_first_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_first_phase); - let initial_unadjusted_accounts = - vec![unadjusted_account_1.clone(), unadjusted_account_2.clone()]; + let initial_unadjusted_accounts = make_priced_qualified_payables(vec![ + (unadjusted_account_1.clone(), 111_222_333), + (unadjusted_account_2.clone(), 222_333_444), + ]); let msg = BlockchainAgentWithContextMessage { qualified_payables: initial_unadjusted_accounts.clone(), agent: Box::new(agent), @@ -1744,7 +1753,10 @@ mod tests { let agent_id_stamp_second_phase = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default().set_arbitrary_id_stamp(agent_id_stamp_second_phase); - let affordable_accounts = vec![adjusted_account_1.clone(), adjusted_account_2.clone()]; + let affordable_accounts = make_priced_qualified_payables(vec![ + (adjusted_account_1.clone(), 111_222_333), + (adjusted_account_2.clone(), 222_333_444), + ]); let payments_instructions = OutboundPaymentsInstructions { affordable_accounts: affordable_accounts.clone(), agent: Box::new(agent), @@ -1791,7 +1803,7 @@ mod tests { ); assert!( before <= captured_now && captured_now <= after, - "captured timestamp should have been between {:?} and {:?} but was {:?}", + "timestamp should be between {:?} and {:?} but was {:?}", before, after, captured_now @@ -2223,7 +2235,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - qualified_payables, + qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), consuming_wallet, response_skeleton_opt: None, } @@ -2299,7 +2311,10 @@ mod tests { let consuming_wallet = make_wallet("abc"); subject.consuming_wallet_opt = Some(consuming_wallet.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: vec![make_payable_account(789)], + qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![ + (make_payable_account(789), 111_222_333), + (make_payable_account(888), 222_333_444), + ]), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -2706,9 +2721,13 @@ mod tests { let payable_scanner = ScannerMock::new() .scan_started_at_result(None) .scan_started_at_result(None) + // These values belong to the RetryPayableScanner .start_scan_params(&scan_params.payable_start_scan) .start_scan_result(Ok(QualifiedPayablesMessage { - qualified_payables: vec![make_payable_account(123)], + qualified_payables: make_unpriced_qualified_payables_for_retry_mode(vec![( + make_payable_account(123), + 555_666_777, + )]), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, })) @@ -3463,10 +3482,13 @@ mod tests { let (blockchain_bridge, _, blockchain_bridge_recording_arc) = make_recorder(); let blockchain_bridge_addr = blockchain_bridge.start(); let payable_account = make_payable_account(123); - let qualified_payable = vec![payable_account.clone()]; + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(vec![payable_account.clone()]); + let priced_qualified_payables = + make_priced_qualified_payables(vec![(payable_account, 123_456_789)]); let consuming_wallet = make_paying_wallet(b"consuming"); let counter_msg_1 = BlockchainAgentWithContextMessage { - qualified_payables: qualified_payable.clone(), + qualified_payables: priced_qualified_payables.clone(), agent: Box::new(BlockchainAgentMock::default()), response_skeleton_opt: None, }; @@ -3498,7 +3520,7 @@ mod tests { response_skeleton_opt: None, }; let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: qualified_payable.clone(), + qualified_payables: unpriced_qualified_payables, consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: None, }; @@ -3569,7 +3591,7 @@ mod tests { blockchain_bridge_recording.get_record::(1); assert_eq!( actual_outbound_payment_instructions_msg.affordable_accounts, - vec![payable_account] + priced_qualified_payables ); let actual_requested_receipts_1 = blockchain_bridge_recording.get_record::(2); @@ -3849,7 +3871,7 @@ mod tests { }); let now = to_unix_timestamp(SystemTime::now()); let qualified_payables = vec![ - // slightly above the minimum balance, to the right of the curve (time intersection) + // Slightly above the minimum balance, to the right of the curve (time intersection) PayableAccount { wallet: make_wallet("wallet0"), balance_wei: gwei_to_wei( @@ -3864,7 +3886,7 @@ mod tests { ), pending_payable_opt: None, }, - // slightly above the curve (balance intersection), to the right of minimum time + // Slightly above the curve (balance intersection), to the right of minimum time PayableAccount { wallet: make_wallet("wallet1"), balance_wei: gwei_to_wei(DEFAULT_PAYMENT_THRESHOLDS.debt_threshold_gwei + 1), @@ -3905,7 +3927,7 @@ mod tests { assert_eq!( message, &QualifiedPayablesMessage { - qualified_payables, + qualified_payables: UnpricedQualifiedPayables::from(qualified_payables), consuming_wallet, response_skeleton_opt: None, } diff --git a/node/src/accountant/payment_adjuster.rs b/node/src/accountant/payment_adjuster.rs index 74c88690b..5062fc1ab 100644 --- a/node/src/accountant/payment_adjuster.rs +++ b/node/src/accountant/payment_adjuster.rs @@ -73,19 +73,18 @@ mod tests { use crate::accountant::payment_adjuster::{PaymentAdjuster, PaymentAdjusterReal}; use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::make_payable_account; + use crate::accountant::test_utils::{make_payable_account, make_priced_qualified_payables}; use masq_lib::logger::Logger; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; #[test] fn search_for_indispensable_adjustment_always_returns_none() { init_test_logging(); - let test_name = "is_adjustment_required_always_returns_none"; - let mut payable = make_payable_account(111); - payable.balance_wei = 100_000_000; + let test_name = "search_for_indispensable_adjustment_always_returns_none"; + let payable = make_payable_account(123); let agent = BlockchainAgentMock::default(); let setup_msg = BlockchainAgentWithContextMessage { - qualified_payables: vec![payable], + qualified_payables: make_priced_qualified_payables(vec![(payable, 111_111_111)]), agent: Box::new(agent), response_skeleton_opt: None, }; diff --git a/node/src/accountant/scanners/mod.rs b/node/src/accountant/scanners/mod.rs index 349ffe3df..cbd844bc4 100644 --- a/node/src/accountant/scanners/mod.rs +++ b/node/src/accountant/scanners/mod.rs @@ -45,7 +45,7 @@ use time::OffsetDateTime; use variant_count::VariantCount; use web3::types::H256; use crate::accountant::scanners::payable_scanner_extension::{MultistageDualPayableScanner, PreparedAdjustment, SolvencySensitivePaymentInstructor}; -use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError; use crate::db_config::persistent_configuration::{PersistentConfiguration, PersistentConfigurationReal}; @@ -499,7 +499,7 @@ impl StartableScanner for PayableS "Chose {} qualified debts to pay", qualified_payables.len() ); - + let qualified_payables = UnpricedQualifiedPayables::from(qualified_payables); let outgoing_msg = QualifiedPayablesMessage::new( qualified_payables, consuming_wallet.clone(), @@ -1395,7 +1395,7 @@ mod tests { PendingPayable, PendingPayableDaoError, TransactionHashes, }; use crate::accountant::db_access_objects::utils::{from_unix_timestamp, to_unix_timestamp}; - use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; + use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayablesMessage, UnpricedQualifiedPayables}; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::{OperationOutcome, PayableScanResult, PendingPayableMetadata}; use crate::accountant::scanners::scanners_utils::pending_payable_scanner_utils::{handle_none_status, handle_status_with_failure, PendingPayableScanReport, PendingPayableScanResult}; use crate::accountant::scanners::{Scanner, StartScanError, StartableScanner, PayableScanner, PendingPayableScanner, ReceivableScanner, ScannerCommon, Scanners, MTError}; @@ -1641,10 +1641,17 @@ mod tests { let timestamp = subject.payable.scan_started_at(); assert_eq!(timestamp, Some(now)); + let qualified_payables_count = qualified_payable_accounts.len(); + let expected_unpriced_qualified_payables = UnpricedQualifiedPayables { + payables: qualified_payable_accounts + .into_iter() + .map(|payable| QualifiedPayablesBeforeGasPriceSelection::new(payable, None)) + .collect::>(), + }; assert_eq!( result, Ok(QualifiedPayablesMessage { - qualified_payables: qualified_payable_accounts.clone(), + qualified_payables: expected_unpriced_qualified_payables, consuming_wallet, response_skeleton_opt: None, }) @@ -1653,7 +1660,7 @@ mod tests { &format!("INFO: {test_name}: Scanning for new payables"), &format!( "INFO: {test_name}: Chose {} qualified debts to pay", - qualified_payable_accounts.len() + qualified_payables_count ), ]) } @@ -1730,44 +1737,48 @@ mod tests { #[test] fn retry_payable_scanner_can_initiate_a_scan() { - init_test_logging(); - let test_name = "retry_payable_scanner_can_initiate_a_scan"; - let consuming_wallet = make_paying_wallet(b"consuming wallet"); - let now = SystemTime::now(); - let (qualified_payable_accounts, _, all_non_pending_payables) = - make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); - let payable_dao = - PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); - let mut subject = make_dull_subject(); - let payable_scanner = PayableScannerBuilder::new() - .payable_dao(payable_dao) - .build(); - subject.payable = Box::new(payable_scanner); - - let result = subject.start_retry_payable_scan_guarded( - &consuming_wallet, - now, - None, - &Logger::new(test_name), - ); - - let timestamp = subject.payable.scan_started_at(); - assert_eq!(timestamp, Some(now)); - assert_eq!( - result, - Ok(QualifiedPayablesMessage { - qualified_payables: qualified_payable_accounts.clone(), - consuming_wallet, - response_skeleton_opt: None, - }) - ); - TestLogHandler::new().assert_logs_match_in_order(vec![ - &format!("INFO: {test_name}: Scanning for retry-required payables"), - &format!( - "INFO: {test_name}: Chose {} qualified debts to pay", - qualified_payable_accounts.len() - ), - ]) + todo!("this must be set up under GH-605"); + // TODO make sure the QualifiedPayableRawPack will express the difference from + // the NewPayable scanner: The QualifiedPayablesBeforeGasPriceSelection needs to carry + // `Some()` instead of None + // init_test_logging(); + // let test_name = "retry_payable_scanner_can_initiate_a_scan"; + // let consuming_wallet = make_paying_wallet(b"consuming wallet"); + // let now = SystemTime::now(); + // let (qualified_payable_accounts, _, all_non_pending_payables) = + // make_qualified_and_unqualified_payables(now, &PaymentThresholds::default()); + // let payable_dao = + // PayableDaoMock::new().non_pending_payables_result(all_non_pending_payables); + // let mut subject = make_dull_subject(); + // let payable_scanner = PayableScannerBuilder::new() + // .payable_dao(payable_dao) + // .build(); + // subject.payable = Box::new(payable_scanner); + // + // let result = subject.start_retry_payable_scan_guarded( + // &consuming_wallet, + // now, + // None, + // &Logger::new(test_name), + // ); + // + // let timestamp = subject.payable.scan_started_at(); + // assert_eq!(timestamp, Some(now)); + // assert_eq!( + // result, + // Ok(QualifiedPayablesMessage { + // qualified_payables: todo!(""), + // consuming_wallet, + // response_skeleton_opt: None, + // }) + // ); + // TestLogHandler::new().assert_logs_match_in_order(vec![ + // &format!("INFO: {test_name}: Scanning for retry-required payables"), + // &format!( + // "INFO: {test_name}: Chose {} qualified debts to pay", + // qualified_payable_accounts.len() + // ), + // ]) } #[test] diff --git a/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs deleted file mode 100644 index 5f9811204..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/agent_null.rs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; -use crate::sub_lib::wallet::Wallet; -use ethereum_types::U256; -use masq_lib::blockchains::chains::Chain; -use masq_lib::logger::Logger; -use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - -#[derive(Clone)] -pub struct BlockchainAgentNull { - wallet: Wallet, - logger: Logger, -} - -impl BlockchainAgent for BlockchainAgentNull { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - self.log_function_call("estimated_transaction_fee_total()"); - 0 - } - - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - self.log_function_call("consuming_wallet_balances()"); - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero(), - } - } - - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.log_function_call("agreed_fee_per_computation_unit()"); - 0 - } - - fn consuming_wallet(&self) -> &Wallet { - self.log_function_call("consuming_wallet()"); - &self.wallet - } - - fn get_chain(&self) -> Chain { - self.log_function_call("get_chain()"); - TEST_DEFAULT_CHAIN - } - - #[cfg(test)] - fn dup(&self) -> Box { - intentionally_blank!() - } - - #[cfg(test)] - as_any_ref_in_trait_impl!(); -} - -impl BlockchainAgentNull { - pub fn new() -> Self { - Self { - wallet: Wallet::null(), - logger: Logger::new("BlockchainAgentNull"), - } - } - - fn log_function_call(&self, function_call: &str) { - error!( - self.logger, - "calling null version of {function_call} for BlockchainAgentNull will be without effect", - ); - } -} - -impl Default for BlockchainAgentNull { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::scanners::payable_scanner_extension::agent_null::BlockchainAgentNull; - use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; - use crate::sub_lib::wallet::Wallet; - - 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 web3::types::U256; - - fn blockchain_agent_null_constructor_works(constructor: C) - where - C: Fn() -> BlockchainAgentNull, - { - init_test_logging(); - - let result = constructor(); - - assert_eq!(result.wallet, Wallet::null()); - warning!(result.logger, "blockchain_agent_null_constructor_works"); - TestLogHandler::default().exists_log_containing( - "WARN: BlockchainAgentNull: \ - blockchain_agent_null_constructor_works", - ); - } - - #[test] - fn blockchain_agent_null_constructor_works_for_new() { - blockchain_agent_null_constructor_works(BlockchainAgentNull::new) - } - - #[test] - fn blockchain_agent_null_constructor_works_for_default() { - blockchain_agent_null_constructor_works(BlockchainAgentNull::default) - } - - fn assert_error_log(test_name: &str, expected_operation: &str) { - TestLogHandler::default().exists_log_containing(&format!( - "ERROR: {test_name}: calling \ - null version of {expected_operation}() for BlockchainAgentNull \ - will be without effect" - )); - } - - #[test] - fn null_agent_estimated_transaction_fee_total() { - init_test_logging(); - let test_name = "null_agent_estimated_transaction_fee_total"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.estimated_transaction_fee_total(4); - - assert_eq!(result, 0); - assert_error_log(test_name, "estimated_transaction_fee_total"); - } - - #[test] - fn null_agent_consuming_wallet_balances() { - init_test_logging(); - let test_name = "null_agent_consuming_wallet_balances"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.consuming_wallet_balances(); - - assert_eq!( - result, - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::zero(), - masq_token_balance_in_minor_units: U256::zero() - } - ); - assert_error_log(test_name, "consuming_wallet_balances") - } - - #[test] - fn null_agent_agreed_fee_per_computation_unit() { - init_test_logging(); - let test_name = "null_agent_agreed_fee_per_computation_unit"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.agreed_fee_per_computation_unit(); - - assert_eq!(result, 0); - assert_error_log(test_name, "agreed_fee_per_computation_unit") - } - - #[test] - fn null_agent_consuming_wallet() { - init_test_logging(); - let test_name = "null_agent_consuming_wallet"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.consuming_wallet(); - - assert_eq!(result, &Wallet::null()); - assert_error_log(test_name, "consuming_wallet") - } - - #[test] - fn null_agent_get_chain() { - init_test_logging(); - let test_name = "null_agent_get_chain"; - let mut subject = BlockchainAgentNull::new(); - subject.logger = Logger::new(test_name); - - let result = subject.get_chain(); - - assert_eq!(result, TEST_DEFAULT_CHAIN); - assert_error_log(test_name, "get_chain") - } -} diff --git a/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs b/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs deleted file mode 100644 index 8acf40ef8..000000000 --- a/node/src/accountant/scanners/payable_scanner_extension/agent_web3.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. - -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; -use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; -use crate::sub_lib::wallet::Wallet; -use masq_lib::blockchains::chains::Chain; - -#[derive(Debug, Clone)] -pub struct BlockchainAgentWeb3 { - gas_price_wei: u128, - gas_limit_const_part: u128, - maximum_added_gas_margin: u128, - consuming_wallet: Wallet, - consuming_wallet_balances: ConsumingWalletBalances, - chain: Chain, -} - -impl BlockchainAgent for BlockchainAgentWeb3 { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128 { - let gas_price = self.gas_price_wei; - let max_gas_limit = self.maximum_added_gas_margin + self.gas_limit_const_part; - number_of_transactions as u128 * gas_price * max_gas_limit - } - - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { - self.consuming_wallet_balances - } - - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.gas_price_wei - } - - fn consuming_wallet(&self) -> &Wallet { - &self.consuming_wallet - } - - fn get_chain(&self) -> Chain { - self.chain - } -} - -// 64 * (64 - 12) ... std transaction has data of 64 bytes and 12 bytes are never used with us; -// each non-zero byte costs 64 units of gas -pub const WEB3_MAXIMAL_GAS_LIMIT_MARGIN: u128 = 3328; - -impl BlockchainAgentWeb3 { - pub fn new( - gas_price_wei: u128, - gas_limit_const_part: u128, - consuming_wallet: Wallet, - consuming_wallet_balances: ConsumingWalletBalances, - chain: Chain, - ) -> Self { - Self { - gas_price_wei, - gas_limit_const_part, - consuming_wallet, - maximum_added_gas_margin: WEB3_MAXIMAL_GAS_LIMIT_MARGIN, - consuming_wallet_balances, - chain, - } - } -} - -#[cfg(test)] -mod tests { - use crate::accountant::scanners::payable_scanner_extension::agent_web3::{ - BlockchainAgentWeb3, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, - }; - use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; - use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; - use crate::test_utils::make_wallet; - use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; - use web3::types::U256; - - #[test] - fn constants_are_correct() { - assert_eq!(WEB3_MAXIMAL_GAS_LIMIT_MARGIN, 3_328) - } - - #[test] - fn blockchain_agent_can_return_non_computed_input_values() { - let gas_price_gwei = 123; - let gas_limit_const_part = 44_000; - let consuming_wallet = make_wallet("abcde"); - let consuming_wallet_balances = ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: U256::from(456_789), - masq_token_balance_in_minor_units: U256::from(123_000_000), - }; - - let subject = BlockchainAgentWeb3::new( - gas_price_gwei, - gas_limit_const_part, - consuming_wallet.clone(), - consuming_wallet_balances, - TEST_DEFAULT_CHAIN, - ); - - assert_eq!(subject.agreed_fee_per_computation_unit(), gas_price_gwei); - assert_eq!(subject.consuming_wallet(), &consuming_wallet); - assert_eq!( - subject.consuming_wallet_balances(), - consuming_wallet_balances - ); - assert_eq!(subject.get_chain(), TEST_DEFAULT_CHAIN); - } - - #[test] - fn estimated_transaction_fee_works() { - let consuming_wallet = make_wallet("efg"); - let consuming_wallet_balances = ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: Default::default(), - masq_token_balance_in_minor_units: Default::default(), - }; - let agent = BlockchainAgentWeb3::new( - 444, - 77_777, - consuming_wallet, - consuming_wallet_balances, - TEST_DEFAULT_CHAIN, - ); - - let result = agent.estimated_transaction_fee_total(3); - - assert_eq!(agent.gas_limit_const_part, 77_777); - assert_eq!( - agent.maximum_added_gas_margin, - WEB3_MAXIMAL_GAS_LIMIT_MARGIN - ); - assert_eq!( - result, - (3 * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) as u128 * 444 - ); - } -} diff --git a/node/src/accountant/scanners/payable_scanner_extension/mod.rs b/node/src/accountant/scanners/payable_scanner_extension/mod.rs index 649bc820f..1d1e8cb0b 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/mod.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/mod.rs @@ -1,8 +1,5 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -pub mod agent_null; -pub mod agent_web3; -pub mod blockchain_agent; pub mod msgs; pub mod test_utils; diff --git a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs index 599f17390..1e9dbe59d 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/msgs.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/msgs.rs @@ -1,22 +1,85 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::accountant::{ResponseSkeleton, SkeletonOptHolder}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::sub_lib::wallet::Wallet; use actix::Message; use std::fmt::Debug; #[derive(Debug, Message, PartialEq, Eq, Clone)] pub struct QualifiedPayablesMessage { - pub qualified_payables: Vec, + pub qualified_payables: UnpricedQualifiedPayables, pub consuming_wallet: Wallet, pub response_skeleton_opt: Option, } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct UnpricedQualifiedPayables { + pub payables: Vec, +} + +impl From> for UnpricedQualifiedPayables { + fn from(qualified_payable: Vec) -> Self { + UnpricedQualifiedPayables { + payables: qualified_payable + .into_iter() + .map(|payable| QualifiedPayablesBeforeGasPriceSelection::new(payable, None)) + .collect(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct QualifiedPayablesBeforeGasPriceSelection { + pub payable: PayableAccount, + pub previous_attempt_gas_price_minor_opt: Option, +} + +impl QualifiedPayablesBeforeGasPriceSelection { + pub fn new( + payable: PayableAccount, + previous_attempt_gas_price_minor_opt: Option, + ) -> Self { + Self { + payable, + previous_attempt_gas_price_minor_opt, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PricedQualifiedPayables { + pub payables: Vec, +} + +impl Into> for PricedQualifiedPayables { + fn into(self) -> Vec { + self.payables + .into_iter() + .map(|qualified_payable| qualified_payable.payable) + .collect() + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct QualifiedPayableWithGasPrice { + pub payable: PayableAccount, + pub gas_price_minor: u128, +} + +impl QualifiedPayableWithGasPrice { + pub fn new(payable: PayableAccount, gas_price_minor: u128) -> Self { + Self { + payable, + gas_price_minor, + } + } +} + impl QualifiedPayablesMessage { pub(in crate::accountant) fn new( - qualified_payables: Vec, + qualified_payables: UnpricedQualifiedPayables, consuming_wallet: Wallet, response_skeleton_opt: Option, ) -> Self { @@ -36,14 +99,14 @@ impl SkeletonOptHolder for QualifiedPayablesMessage { #[derive(Message)] pub struct BlockchainAgentWithContextMessage { - pub qualified_payables: Vec, + pub qualified_payables: PricedQualifiedPayables, pub agent: Box, pub response_skeleton_opt: Option, } impl BlockchainAgentWithContextMessage { pub fn new( - qualified_payables: Vec, + qualified_payables: PricedQualifiedPayables, agent: Box, response_skeleton_opt: Option, ) -> Self { diff --git a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs index def16f20e..b8e83b78d 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs +++ b/node/src/accountant/scanners/payable_scanner_extension/test_utils.rs @@ -2,7 +2,10 @@ #![cfg(test)] -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, UnpricedQualifiedPayables, +}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::unshared_test_utils::arbitrary_id_stamp::ArbitraryIdStamp; @@ -12,7 +15,7 @@ use std::cell::RefCell; pub struct BlockchainAgentMock { consuming_wallet_balances_results: RefCell>, - agreed_fee_per_computation_unit_results: RefCell>, + gas_price_results: RefCell>, consuming_wallet_result_opt: Option, arbitrary_id_stamp_opt: Option, get_chain_result_opt: Option, @@ -22,7 +25,7 @@ impl Default for BlockchainAgentMock { fn default() -> Self { BlockchainAgentMock { consuming_wallet_balances_results: RefCell::new(vec![]), - agreed_fee_per_computation_unit_results: RefCell::new(vec![]), + gas_price_results: RefCell::new(vec![]), consuming_wallet_result_opt: None, arbitrary_id_stamp_opt: None, get_chain_result_opt: None, @@ -31,18 +34,22 @@ impl Default for BlockchainAgentMock { } impl BlockchainAgent for BlockchainAgentMock { - fn estimated_transaction_fee_total(&self, _number_of_transactions: usize) -> u128 { - todo!("to be implemented by GH-711") + fn price_qualified_payables( + &self, + _qualified_payables: UnpricedQualifiedPayables, + ) -> PricedQualifiedPayables { + unimplemented!("not needed yet") } - fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + fn estimate_transaction_fee_total( + &self, + _qualified_payables: &PricedQualifiedPayables, + ) -> u128 { todo!("to be implemented by GH-711") } - fn agreed_fee_per_computation_unit(&self) -> u128 { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .remove(0) + fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + todo!("to be implemented by GH-711") } fn consuming_wallet(&self) -> &Wallet { @@ -68,10 +75,8 @@ impl BlockchainAgentMock { self } - pub fn agreed_fee_per_computation_unit_result(self, result: u128) -> Self { - self.agreed_fee_per_computation_unit_results - .borrow_mut() - .push(result); + pub fn gas_price_result(self, result: u128) -> Self { + self.gas_price_results.borrow_mut().push(result); self } diff --git a/node/src/accountant/scanners/test_utils.rs b/node/src/accountant/scanners/test_utils.rs index 26aa15dc3..2445ff565 100644 --- a/node/src/accountant/scanners/test_utils.rs +++ b/node/src/accountant/scanners/test_utils.rs @@ -23,7 +23,7 @@ use crate::accountant::{ SentPayables, }; use crate::blockchain::blockchain_bridge::RetrieveTransactions; -use crate::sub_lib::blockchain_bridge::OutboundPaymentsInstructions; +use crate::sub_lib::blockchain_bridge::{ConsumingWalletBalances, OutboundPaymentsInstructions}; use crate::sub_lib::wallet::Wallet; use actix::{Message, System}; use itertools::Either; @@ -488,3 +488,7 @@ impl RescheduleScanOnErrorResolverMock { self } } + +pub fn make_zeroed_consuming_wallet_balances() -> ConsumingWalletBalances { + ConsumingWalletBalances::new(0.into(), 0.into()) +} diff --git a/node/src/accountant/test_utils.rs b/node/src/accountant/test_utils.rs index e0e5a6cdd..a186ff016 100644 --- a/node/src/accountant/test_utils.rs +++ b/node/src/accountant/test_utils.rs @@ -16,7 +16,10 @@ use crate::accountant::db_access_objects::utils::{ from_unix_timestamp, to_unix_timestamp, CustomQuery, }; use crate::accountant::payment_adjuster::{Adjustment, AnalysisError, PaymentAdjuster}; -use crate::accountant::scanners::payable_scanner_extension::msgs::BlockchainAgentWithContextMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + BlockchainAgentWithContextMessage, PricedQualifiedPayables, QualifiedPayableWithGasPrice, + QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, +}; use crate::accountant::scanners::payable_scanner_extension::PreparedAdjustment; use crate::accountant::scanners::scanners_utils::payable_scanner_utils::PayableThresholdsGauge; use crate::accountant::scanners::{PayableScanner, PendingPayableScanner, ReceivableScanner}; @@ -1497,3 +1500,33 @@ impl PaymentAdjusterMock { self } } + +pub fn make_priced_qualified_payables( + inputs: Vec<(PayableAccount, u128)>, +) -> PricedQualifiedPayables { + PricedQualifiedPayables { + payables: inputs + .into_iter() + .map(|(payable, gas_price_minor)| QualifiedPayableWithGasPrice { + payable, + gas_price_minor, + }) + .collect(), + } +} + +pub fn make_unpriced_qualified_payables_for_retry_mode( + inputs: Vec<(PayableAccount, u128)>, +) -> UnpricedQualifiedPayables { + UnpricedQualifiedPayables { + payables: inputs + .into_iter() + .map(|(payable, previous_attempt_gas_price_minor)| { + QualifiedPayablesBeforeGasPriceSelection { + payable, + previous_attempt_gas_price_minor_opt: Some(previous_attempt_gas_price_minor), + } + }) + .collect(), + } +} diff --git a/node/src/blockchain/blockchain_agent/agent_web3.rs b/node/src/blockchain/blockchain_agent/agent_web3.rs new file mode 100644 index 000000000..8899a0743 --- /dev/null +++ b/node/src/blockchain/blockchain_agent/agent_web3.rs @@ -0,0 +1,817 @@ +// Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +use crate::accountant::comma_joined_stringifiable; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, +}; +use crate::blockchain::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; +use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; +use crate::sub_lib::wallet::Wallet; +use itertools::{Either, Itertools}; +use masq_lib::blockchains::chains::Chain; +use masq_lib::logger::Logger; +use masq_lib::utils::ExpectValue; +use thousands::Separable; +use web3::types::Address; + +#[derive(Debug, Clone)] +pub struct BlockchainAgentWeb3 { + logger: Logger, + latest_gas_price_wei: u128, + gas_limit_const_part: u128, + consuming_wallet: Wallet, + consuming_wallet_balances: ConsumingWalletBalances, + chain: Chain, +} + +impl BlockchainAgent for BlockchainAgentWeb3 { + fn price_qualified_payables( + &self, + qualified_payables: UnpricedQualifiedPayables, + ) -> PricedQualifiedPayables { + let warning_data_collector_opt = + self.set_up_warning_data_collector_opt(&qualified_payables); + + let init: ( + Vec, + Option, + ) = (vec![], warning_data_collector_opt); + let (priced_qualified_payables, warning_data_collector_opt) = + qualified_payables.payables.into_iter().fold( + init, + |(mut priced_payables, mut warning_data_collector_opt), unpriced_payable| { + let selected_gas_price_wei = + match unpriced_payable.previous_attempt_gas_price_minor_opt { + None => self.latest_gas_price_wei, + Some(previous_price) if self.latest_gas_price_wei < previous_price => { + previous_price + } + Some(_) => self.latest_gas_price_wei, + }; + + let gas_price_increased_by_margin_wei = + increase_gas_price_by_margin(selected_gas_price_wei); + + let price_ceiling_wei = self.chain.rec().gas_price_safe_ceiling_minor; + let checked_gas_price_wei = + if gas_price_increased_by_margin_wei > price_ceiling_wei { + warning_data_collector_opt.as_mut().map(|collector| { + match collector.data.as_mut() { + Either::Left(new_payable_data) => { + new_payable_data + .addresses + .push(unpriced_payable.payable.wallet.address()); + new_payable_data.gas_price_above_limit_wei = + gas_price_increased_by_margin_wei + } + Either::Right(retry_payable_data) => retry_payable_data + .addresses_and_gas_price_value_above_limit_wei + .push(( + unpriced_payable.payable.wallet.address(), + gas_price_increased_by_margin_wei, + )), + } + }); + price_ceiling_wei + } else { + gas_price_increased_by_margin_wei + }; + + priced_payables.push(QualifiedPayableWithGasPrice::new( + unpriced_payable.payable, + checked_gas_price_wei, + )); + + (priced_payables, warning_data_collector_opt) + }, + ); + + warning_data_collector_opt + .map(|collector| collector.log_warning_if_some_reason(&self.logger, self.chain)); + + PricedQualifiedPayables { + payables: priced_qualified_payables, + } + } + + fn estimate_transaction_fee_total(&self, qualified_payables: &PricedQualifiedPayables) -> u128 { + let prices_sum: u128 = qualified_payables + .payables + .iter() + .map(|priced_payable| priced_payable.gas_price_minor) + .sum(); + (self.gas_limit_const_part + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) * prices_sum + } + + fn consuming_wallet_balances(&self) -> ConsumingWalletBalances { + self.consuming_wallet_balances + } + + fn consuming_wallet(&self) -> &Wallet { + &self.consuming_wallet + } + + fn get_chain(&self) -> Chain { + self.chain + } +} + +struct GasPriceAboveLimitWarningReporter { + data: Either, +} + +impl GasPriceAboveLimitWarningReporter { + fn log_warning_if_some_reason(self, logger: &Logger, chain: Chain) { + let ceiling_value_wei = chain.rec().gas_price_safe_ceiling_minor; + match self.data { + Either::Left(new_payable_data) => { + if !new_payable_data.addresses.is_empty() { + warning!( + logger, + "{}", + Self::new_payables_warning_msg(new_payable_data, ceiling_value_wei) + ) + } + } + Either::Right(retry_payable_data) => { + if !retry_payable_data + .addresses_and_gas_price_value_above_limit_wei + .is_empty() + { + warning!( + logger, + "{}", + Self::retry_payable_warning_msg(retry_payable_data, ceiling_value_wei) + ) + } + } + } + } + + fn new_payables_warning_msg( + new_payable_warning_data: NewPayableWarningData, + ceiling_value_wei: u128, + ) -> String { + let accounts = comma_joined_stringifiable(&new_payable_warning_data.addresses, |address| { + format!("{:?}", address) + }); + format!( + "Calculated gas price {} wei for txs to {} is over the spend limit {} wei.", + new_payable_warning_data + .gas_price_above_limit_wei + .separate_with_commas(), + accounts, + ceiling_value_wei.separate_with_commas() + ) + } + + fn retry_payable_warning_msg( + retry_payable_warning_data: RetryPayableWarningData, + ceiling_value_wei: u128, + ) -> String { + let accounts = retry_payable_warning_data + .addresses_and_gas_price_value_above_limit_wei + .into_iter() + .map(|(address, calculated_price_wei)| { + format!( + "{} wei for tx to {:?}", + calculated_price_wei.separate_with_commas(), + address + ) + }) + .join(", "); + format!( + "Calculated gas price {} surplussed the spend limit {} wei.", + accounts, + ceiling_value_wei.separate_with_commas() + ) + } +} + +#[derive(Default)] +struct NewPayableWarningData { + addresses: Vec
, + gas_price_above_limit_wei: u128, +} + +#[derive(Default)] +struct RetryPayableWarningData { + addresses_and_gas_price_value_above_limit_wei: Vec<(Address, u128)>, +} + +// 64 * (64 - 12) ... std transaction has data of 64 bytes and 12 bytes are never used with us; +// each non-zero byte costs 64 units of gas +pub const WEB3_MAXIMAL_GAS_LIMIT_MARGIN: u128 = 3328; + +impl BlockchainAgentWeb3 { + pub fn new( + latest_gas_price_wei: u128, + gas_limit_const_part: u128, + consuming_wallet: Wallet, + consuming_wallet_balances: ConsumingWalletBalances, + chain: Chain, + ) -> BlockchainAgentWeb3 { + Self { + logger: Logger::new("BlockchainAgentWeb3"), + latest_gas_price_wei, + gas_limit_const_part, + consuming_wallet, + consuming_wallet_balances, + chain, + } + } + + fn set_up_warning_data_collector_opt( + &self, + qualified_payables: &UnpricedQualifiedPayables, + ) -> Option { + self.logger.warning_enabled().then(|| { + let is_retry = Self::is_retry(qualified_payables); + GasPriceAboveLimitWarningReporter { + data: if !is_retry { + Either::Left(NewPayableWarningData::default()) + } else { + Either::Right(RetryPayableWarningData::default()) + }, + } + }) + } + + fn is_retry(qualified_payables: &UnpricedQualifiedPayables) -> bool { + qualified_payables + .payables + .first() + .expectv("payable") + .previous_attempt_gas_price_minor_opt + .is_some() + } +} + +#[cfg(test)] +mod tests { + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayableWithGasPrice, + QualifiedPayablesBeforeGasPriceSelection, UnpricedQualifiedPayables, + }; + use crate::accountant::scanners::test_utils::make_zeroed_consuming_wallet_balances; + use crate::accountant::test_utils::{ + make_payable_account, make_unpriced_qualified_payables_for_retry_mode, + }; + use crate::blockchain::blockchain_agent::agent_web3::{ + BlockchainAgentWeb3, GasPriceAboveLimitWarningReporter, NewPayableWarningData, + RetryPayableWarningData, WEB3_MAXIMAL_GAS_LIMIT_MARGIN, + }; + use crate::blockchain::blockchain_agent::BlockchainAgent; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; + use crate::test_utils::make_wallet; + use itertools::Itertools; + use masq_lib::blockchains::chains::Chain; + use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; + 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 thousands::Separable; + + #[test] + fn constants_are_correct() { + assert_eq!(WEB3_MAXIMAL_GAS_LIMIT_MARGIN, 3_328) + } + + #[test] + fn returns_correct_priced_qualified_payables_for_new_payable_scan() { + init_test_logging(); + let test_name = "returns_correct_priced_qualified_payables_for_new_payable_scan"; + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let address_1 = account_1.wallet.address(); + let address_2 = account_2.wallet.address(); + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let rpc_gas_price_wei = 555_666_777; + let chain = TEST_DEFAULT_CHAIN; + let mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + let gas_price_with_margin_wei = increase_gas_price_by_margin(rpc_gas_price_wei); + let expected_result = PricedQualifiedPayables { + payables: vec![ + QualifiedPayableWithGasPrice::new(account_1, gas_price_with_margin_wei), + QualifiedPayableWithGasPrice::new(account_2, gas_price_with_margin_wei), + ], + }; + assert_eq!(priced_qualified_payables, expected_result); + let msg_that_should_not_occur = { + let mut new_payable_data = NewPayableWarningData::default(); + new_payable_data.addresses = vec![address_1, address_2]; + + GasPriceAboveLimitWarningReporter::new_payables_warning_msg( + new_payable_data, + chain.rec().gas_price_safe_ceiling_minor, + ) + }; + TestLogHandler::new() + .exists_no_log_containing(&format!("WARN: {test_name}: {msg_that_should_not_occur}")); + } + + #[test] + fn returns_correct_priced_qualified_payables_for_retry_payable_scan() { + init_test_logging(); + let test_name = "returns_correct_priced_qualified_payables_for_retry_payable_scan"; + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let rpc_gas_price_wei = 444_555_666; + let chain = TEST_DEFAULT_CHAIN; + let unpriced_qualified_payables = { + let payables = vec![ + rpc_gas_price_wei - 1, + rpc_gas_price_wei, + rpc_gas_price_wei + 1, + rpc_gas_price_wei - 123_456, + rpc_gas_price_wei + 456_789, + ] + .into_iter() + .enumerate() + .map(|(idx, previous_attempt_gas_price_wei)| { + let account = make_payable_account((idx as u64 + 1) * 3_000); + QualifiedPayablesBeforeGasPriceSelection::new( + account, + Some(previous_attempt_gas_price_wei), + ) + }) + .collect_vec(); + UnpricedQualifiedPayables { payables } + }; + let accounts_from_1_to_5 = unpriced_qualified_payables + .payables + .iter() + .map(|unpriced_payable| unpriced_payable.payable.clone()) + .collect_vec(); + let mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + let expected_result = { + let price_wei_for_accounts_from_1_to_5 = vec![ + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 1), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 456_789), + ]; + if price_wei_for_accounts_from_1_to_5.len() != accounts_from_1_to_5.len() { + panic!("Corrupted test") + } + PricedQualifiedPayables { + payables: accounts_from_1_to_5 + .into_iter() + .zip(price_wei_for_accounts_from_1_to_5.into_iter()) + .map(|(account, previous_attempt_price_wei)| { + QualifiedPayableWithGasPrice::new(account, previous_attempt_price_wei) + }) + .collect_vec(), + } + }; + assert_eq!(priced_qualified_payables, expected_result); + let msg_that_should_not_occur = { + let mut retry_payable_data = RetryPayableWarningData::default(); + retry_payable_data.addresses_and_gas_price_value_above_limit_wei = expected_result + .payables + .into_iter() + .map(|payable_with_gas_price| { + ( + payable_with_gas_price.payable.wallet.address(), + payable_with_gas_price.gas_price_minor, + ) + }) + .collect(); + GasPriceAboveLimitWarningReporter::retry_payable_warning_msg( + retry_payable_data, + chain.rec().gas_price_safe_ceiling_minor, + ) + }; + TestLogHandler::new() + .exists_no_log_containing(&format!("WARN: {test_name}: {}", msg_that_should_not_occur)); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_a_border_value() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_a_border_value"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is + // applied. + // Adding just 1 didn't work, therefore 2 + let rpc_gas_price_wei = + ((ceiling_gas_price_wei * 100) / (DEFAULT_GAS_PRICE_MARGIN as u128 + 100)) + 2; + let check_value_wei = increase_gas_price_by_margin(rpc_gas_price_wei); + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + rpc_gas_price_wei, + 50_000_000_001, + ); + + assert!( + check_value_wei > ceiling_gas_price_wei, + "should be {} > {} but isn't", + check_value_wei, + ceiling_gas_price_wei + ); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_a_bit_bigger_even_with_no_margin() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_a_bit_bigger_even_with_no_margin"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + ceiling_gas_price_wei + 1, + 65_000_000_001, + ); + } + + #[test] + fn new_payables_gas_price_ceiling_test_if_latest_price_is_just_gigantic() { + let test_name = "new_payables_gas_price_ceiling_test_if_latest_price_is_just_gigantic"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + + test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name, + chain, + 10 * ceiling_gas_price_wei, + 650_000_000_000, + ); + } + + fn test_gas_price_must_not_break_through_ceiling_value_in_the_new_payable_mode( + test_name: &str, + chain: Chain, + rpc_gas_price_wei: u128, + expected_calculated_surplus_value_wei: u128, + ) { + init_test_logging(); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let qualified_payables = + UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + let expected_result = PricedQualifiedPayables { + payables: vec![ + QualifiedPayableWithGasPrice::new(account_1.clone(), ceiling_gas_price_wei), + QualifiedPayableWithGasPrice::new(account_2.clone(), ceiling_gas_price_wei), + ], + }; + assert_eq!(priced_qualified_payables, expected_result); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Calculated gas price {} wei for txs to {}, {} is over the spend \ + limit {} wei.", + expected_calculated_surplus_value_wei.separate_with_commas(), + account_1.wallet, + account_2.wallet, + chain + .rec() + .gas_price_safe_ceiling_minor + .separate_with_commas() + )); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_border_value_if_the_latest_fetch_being_bigger() { + let test_name = "retry_payables_gas_price_ceiling_test_of_border_value_if_the_latest_fetch_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is + // applied. + // Adding just 1 didn't work, therefore 2 + let rpc_gas_price_wei = + (ceiling_gas_price_wei * 100) / (DEFAULT_GAS_PRICE_MARGIN as u128 + 100) + 2; + let check_value_wei = increase_gas_price_by_margin(rpc_gas_price_wei); + let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ + (account_1.clone(), rpc_gas_price_wei - 1), + (account_2.clone(), rpc_gas_price_wei - 2), + ]); + let expected_surpluses_wallet_and_wei_as_text = "\ + 50,000,000,001 wei for tx to 0x00000000000000000000000077616c6c65743132, 50,000,000,001 \ + wei for tx to 0x00000000000000000000000077616c6c65743334"; + + test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name, + chain, + rpc_gas_price_wei, + unpriced_qualified_payables, + expected_surpluses_wallet_and_wei_as_text, + ); + + assert!( + check_value_wei > ceiling_gas_price_wei, + "should be {} > {} but isn't", + check_value_wei, + ceiling_gas_price_wei + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_border_value_if_the_previous_attempt_being_bigger() + { + let test_name = "retry_payables_gas_price_ceiling_test_of_border_value_if_the_previous_attempt_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + // This should be the value that would surplus the ceiling just slightly if the margin is applied + let border_gas_price_wei = + (ceiling_gas_price_wei * 100) / (DEFAULT_GAS_PRICE_MARGIN as u128 + 100) + 2; + let rpc_gas_price_wei = border_gas_price_wei - 1; + let check_value_wei = increase_gas_price_by_margin(border_gas_price_wei); + let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ + (account_1.clone(), border_gas_price_wei), + (account_2.clone(), border_gas_price_wei), + ]); + let expected_surpluses_wallet_and_wei_as_text = "50,000,000,001 wei for tx to \ + 0x00000000000000000000000077616c6c65743132, 50,000,000,001 wei for tx to \ + 0x00000000000000000000000077616c6c65743334"; + + test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name, + chain, + rpc_gas_price_wei, + unpriced_qualified_payables, + expected_surpluses_wallet_and_wei_as_text, + ); + assert!(check_value_wei > ceiling_gas_price_wei); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_big_value_if_the_latest_fetch_being_bigger() { + let test_name = + "retry_payables_gas_price_ceiling_test_of_big_value_if_the_latest_fetch_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let fetched_gas_price_wei = ceiling_gas_price_wei - 1; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ + (account_1.clone(), fetched_gas_price_wei - 2), + (account_2.clone(), fetched_gas_price_wei - 3), + ]); + let expected_surpluses_wallet_and_wei_as_text = "64,999,999,998 wei for tx to \ + 0x00000000000000000000000077616c6c65743132, 64,999,999,998 wei for tx to \ + 0x00000000000000000000000077616c6c65743334"; + + test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name, + chain, + fetched_gas_price_wei, + unpriced_qualified_payables, + expected_surpluses_wallet_and_wei_as_text, + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_big_value_if_the_previous_attempt_being_bigger() { + let test_name = "retry_payables_gas_price_ceiling_test_of_big_value_if_the_previous_attempt_being_bigger"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ + (account_1.clone(), ceiling_gas_price_wei - 1), + (account_2.clone(), ceiling_gas_price_wei - 2), + ]); + let expected_surpluses_wallet_and_wei_as_text = "64,999,999,998 wei for tx to \ + 0x00000000000000000000000077616c6c65743132, 64,999,999,997 wei for tx to \ + 0x00000000000000000000000077616c6c65743334"; + + test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name, + chain, + ceiling_gas_price_wei - 3, + unpriced_qualified_payables, + expected_surpluses_wallet_and_wei_as_text, + ); + } + + #[test] + fn retry_payables_gas_price_ceiling_test_of_giant_value_for_the_latest_fetch() { + let test_name = "retry_payables_gas_price_ceiling_test_of_giant_value_for_the_latest_fetch"; + let chain = TEST_DEFAULT_CHAIN; + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let fetched_gas_price_wei = 10 * ceiling_gas_price_wei; + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + // The values can never go above the ceiling, therefore, we can assume only values even or + // smaller than that in the previous attempts + let unpriced_qualified_payables = make_unpriced_qualified_payables_for_retry_mode(vec![ + (account_1.clone(), ceiling_gas_price_wei), + (account_2.clone(), ceiling_gas_price_wei), + ]); + let expected_surpluses_wallet_and_wei_as_text = + "650,000,000,000 wei for tx to 0x00000000000000000000\ + 000077616c6c65743132, 650,000,000,000 wei for tx to 0x00000000000000000000000077616c6c65743334"; + + test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name, + chain, + fetched_gas_price_wei, + unpriced_qualified_payables, + expected_surpluses_wallet_and_wei_as_text, + ); + } + + fn test_gas_price_must_not_break_through_ceiling_value_in_the_retry_payable_mode( + test_name: &str, + chain: Chain, + rpc_gas_price_wei: u128, + qualified_payables: UnpricedQualifiedPayables, + expected_surpluses_wallet_and_wei_as_text: &str, + ) { + init_test_logging(); + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let ceiling_gas_price_wei = chain.rec().gas_price_safe_ceiling_minor; + let expected_priced_payables = PricedQualifiedPayables { + payables: qualified_payables + .payables + .clone() + .into_iter() + .map(|payable| { + QualifiedPayableWithGasPrice::new(payable.payable, ceiling_gas_price_wei) + }) + .collect(), + }; + let mut subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + subject.logger = Logger::new(test_name); + + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + assert_eq!(priced_qualified_payables, expected_priced_payables); + TestLogHandler::new().exists_log_containing(&format!( + "WARN: {test_name}: Calculated gas price {expected_surpluses_wallet_and_wei_as_text} \ + surplussed the spend limit {} wei.", + ceiling_gas_price_wei.separate_with_commas() + )); + } + + #[test] + fn returns_correct_non_computed_values() { + let gas_limit_const_part = 44_000; + let consuming_wallet = make_wallet("abcde"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + + let subject = BlockchainAgentWeb3::new( + 222_333_444, + gas_limit_const_part, + consuming_wallet.clone(), + consuming_wallet_balances, + TEST_DEFAULT_CHAIN, + ); + + assert_eq!(subject.consuming_wallet(), &consuming_wallet); + assert_eq!( + subject.consuming_wallet_balances(), + consuming_wallet_balances + ); + assert_eq!(subject.get_chain(), TEST_DEFAULT_CHAIN); + } + + #[test] + fn estimate_transaction_fee_total_works_for_new_payable() { + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let chain = TEST_DEFAULT_CHAIN; + let qualified_payables = UnpricedQualifiedPayables::from(vec![account_1, account_2]); + let subject = BlockchainAgentWeb3::new( + 444_555_666, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + let priced_qualified_payables = subject.price_qualified_payables(qualified_payables); + + let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); + + assert_eq!( + result, + (2 * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) + * increase_gas_price_by_margin(444_555_666) + ); + } + + #[test] + fn estimate_transaction_fee_total_works_for_retry_payable() { + let consuming_wallet = make_wallet("efg"); + let consuming_wallet_balances = make_zeroed_consuming_wallet_balances(); + let rpc_gas_price_wei = 444_555_666; + let chain = TEST_DEFAULT_CHAIN; + let unpriced_qualified_payables = { + let payables = vec![ + rpc_gas_price_wei - 1, + rpc_gas_price_wei, + rpc_gas_price_wei + 1, + rpc_gas_price_wei - 123_456, + rpc_gas_price_wei + 456_789, + ] + .into_iter() + .enumerate() + .map(|(idx, previous_attempt_gas_price_wei)| { + let account = make_payable_account((idx as u64 + 1) * 3_000); + QualifiedPayablesBeforeGasPriceSelection::new( + account, + Some(previous_attempt_gas_price_wei), + ) + }) + .collect_vec(); + UnpricedQualifiedPayables { payables } + }; + let subject = BlockchainAgentWeb3::new( + rpc_gas_price_wei, + 77_777, + consuming_wallet, + consuming_wallet_balances, + chain, + ); + let priced_qualified_payables = + subject.price_qualified_payables(unpriced_qualified_payables); + + let result = subject.estimate_transaction_fee_total(&priced_qualified_payables); + + let gas_prices_for_accounts_from_1_to_5 = vec![ + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 1), + increase_gas_price_by_margin(rpc_gas_price_wei), + increase_gas_price_by_margin(rpc_gas_price_wei + 456_789), + ]; + let expected_result = gas_prices_for_accounts_from_1_to_5 + .into_iter() + .sum::() + * (77_777 + WEB3_MAXIMAL_GAS_LIMIT_MARGIN); + assert_eq!(result, expected_result) + } + + #[test] + fn blockchain_agent_web3_logs_with_right_name() { + let test_name = "blockchain_agent_web3_logs_with_right_name"; + let subject = BlockchainAgentWeb3::new( + 0, + 0, + make_wallet("abcde"), + make_zeroed_consuming_wallet_balances(), + TEST_DEFAULT_CHAIN, + ); + + info!(subject.logger, "{}", test_name); + + TestLogHandler::new() + .exists_log_containing(&format!("INFO: BlockchainAgentWeb3: {}", test_name)); + } +} diff --git a/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs b/node/src/blockchain/blockchain_agent/mod.rs similarity index 74% rename from node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs rename to node/src/blockchain/blockchain_agent/mod.rs index 2f2af4015..fb8030a09 100644 --- a/node/src/accountant/scanners/payable_scanner_extension/blockchain_agent.rs +++ b/node/src/blockchain/blockchain_agent/mod.rs @@ -1,10 +1,14 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. +pub mod agent_web3; + +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, UnpricedQualifiedPayables, +}; use crate::arbitrary_id_stamp_in_trait; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use masq_lib::blockchains::chains::Chain; - // Table of chains by // // a) adoption of the fee market (variations on "gas price") @@ -22,11 +26,13 @@ use masq_lib::blockchains::chains::Chain; //* defaulted limit pub trait BlockchainAgent: Send { - fn estimated_transaction_fee_total(&self, number_of_transactions: usize) -> u128; + fn price_qualified_payables( + &self, + qualified_payables: UnpricedQualifiedPayables, + ) -> PricedQualifiedPayables; + fn estimate_transaction_fee_total(&self, qualified_payables: &PricedQualifiedPayables) -> u128; fn consuming_wallet_balances(&self) -> ConsumingWalletBalances; - fn agreed_fee_per_computation_unit(&self) -> u128; fn consuming_wallet(&self) -> &Wallet; - fn get_chain(&self) -> Chain; #[cfg(test)] diff --git a/node/src/blockchain/blockchain_bridge.rs b/node/src/blockchain/blockchain_bridge.rs index b93d8770c..e427ab934 100644 --- a/node/src/blockchain/blockchain_bridge.rs +++ b/node/src/blockchain/blockchain_bridge.rs @@ -1,8 +1,6 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::scanners::payable_scanner_extension::msgs::{ - BlockchainAgentWithContextMessage, QualifiedPayablesMessage, -}; +use crate::accountant::scanners::payable_scanner_extension::msgs::{BlockchainAgentWithContextMessage, QualifiedPayablesMessage, PricedQualifiedPayables}; use crate::accountant::{ ReceivedPayments, ResponseSkeleton, ScanError, SentPayables, SkeletonOptHolder, @@ -44,9 +42,9 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use ethabi::Hash; use web3::types::H256; +use masq_lib::constants::DEFAULT_GAS_PRICE_MARGIN; use masq_lib::messages::ScanType; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionReceiptResult, TxStatus}; pub const CRASH_KEY: &str = "BLOCKCHAINBRIDGE"; @@ -263,11 +261,13 @@ impl BlockchainBridge { let accountant_recipient = self.payable_payments_setup_subs_opt.clone(); Box::new( self.blockchain_interface - .build_blockchain_agent(incoming_message.consuming_wallet) + .introduce_blockchain_agent(incoming_message.consuming_wallet) .map_err(|e| format!("Blockchain agent build error: {:?}", e)) .and_then(move |agent| { + let priced_qualified_payables = + agent.price_qualified_payables(incoming_message.qualified_payables); let outgoing_message = BlockchainAgentWithContextMessage::new( - incoming_message.qualified_payables, + priced_qualified_payables, agent, incoming_message.response_skeleton_opt, ); @@ -485,7 +485,7 @@ impl BlockchainBridge { fn process_payments( &self, agent: Box, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { let new_fingerprints_recipient = self.new_fingerprints_recipient(); @@ -535,6 +535,10 @@ struct PendingTxInfo { when_sent: SystemTime, } +pub fn increase_gas_price_by_margin(gas_price: u128) -> u128 { + (gas_price * (100 + DEFAULT_GAS_PRICE_MARGIN as u128)) / 100 +} + pub struct BlockchainBridgeSubsFactoryReal {} impl SubsFactory for BlockchainBridgeSubsFactoryReal { @@ -549,10 +553,8 @@ mod tests { use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; use crate::accountant::db_access_objects::utils::from_unix_timestamp; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::scanners::payable_scanner_extension::test_utils::BlockchainAgentMock; - use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint}; - use crate::blockchain::blockchain_interface::blockchain_interface_web3::BlockchainInterfaceWeb3; + use crate::accountant::test_utils::{make_payable_account, make_pending_payable_fingerprint, make_priced_qualified_payables}; use crate::blockchain::blockchain_interface::data_structures::errors::PayableTransactionError::TransactionID; use crate::blockchain::blockchain_interface::data_structures::errors::{ BlockchainAgentBuildError, PayableTransactionError, @@ -595,6 +597,7 @@ mod tests { use std::time::{Duration, SystemTime}; use web3::types::{TransactionReceipt, H160}; use masq_lib::constants::DEFAULT_MAX_BLOCK_COUNT; + use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, QualifiedPayableWithGasPrice}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt}; impl Handler> for BlockchainBridge { @@ -676,15 +679,14 @@ mod tests { } #[test] - fn qualified_payables_msg_is_handled_and_new_msg_with_an_added_blockchain_agent_returns_to_accountant( - ) { + fn handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant() { let system = System::new( - "qualified_payables_msg_is_handled_and_new_msg_with_an_added_blockchain_agent_returns_to_accountant", - ); + "handles_qualified_payables_msg_in_new_payables_mode_and_sends_response_back_to_accountant"); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) - .ok_response("0x230000000".to_string(), 1) // 9395240960 - .ok_response("0x23".to_string(), 1) + // Fetching a recommended gas price + .ok_response("0x230000000".to_string(), 1) + .ok_response("0xAAAA".to_string(), 1) .ok_response( "0x000000000000000000000000000000000000000000000000000000000000FFFF".to_string(), 0, @@ -721,8 +723,10 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(qualified_payables.clone()); let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: qualified_payables.clone(), + qualified_payables: unpriced_qualified_payables.clone(), consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -737,42 +741,33 @@ mod tests { System::current().stop(); system.run(); - let accountant_received_payment = accountant_recording_arc.lock().unwrap(); let blockchain_agent_with_context_msg_actual: &BlockchainAgentWithContextMessage = accountant_received_payment.get_record(0); + let expected_priced_qualified_payables = PricedQualifiedPayables { + payables: qualified_payables + .into_iter() + .map(|payable| QualifiedPayableWithGasPrice { + payable, + gas_price_minor: increase_gas_price_by_margin(0x230000000), + }) + .collect(), + }; assert_eq!( blockchain_agent_with_context_msg_actual.qualified_payables, - qualified_payables - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .consuming_wallet(), - &consuming_wallet + expected_priced_qualified_payables ); + let actual_agent = blockchain_agent_with_context_msg_actual.agent.as_ref(); + assert_eq!(actual_agent.consuming_wallet(), &consuming_wallet); assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .agreed_fee_per_computation_unit(), - 0x230000000 - ); - assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .consuming_wallet_balances(), - ConsumingWalletBalances::new( - 35.into(), - 0x000000000000000000000000000000000000000000000000000000000000FFFF.into() - ) + actual_agent.consuming_wallet_balances(), + ConsumingWalletBalances::new(0xAAAA.into(), 0xFFFF.into()) ); - let gas_limit_const_part = - BlockchainInterfaceWeb3::web3_gas_limit_const_part(Chain::PolyMainnet); assert_eq!( - blockchain_agent_with_context_msg_actual - .agent - .estimated_transaction_fee_total(1), - (1 * 0x230000000 * (gas_limit_const_part + WEB3_MAXIMAL_GAS_LIMIT_MARGIN)) + actual_agent.estimate_transaction_fee_total( + &actual_agent.price_qualified_payables(unpriced_qualified_payables) + ), + 1_791_228_995_698_688 ); assert_eq!( blockchain_agent_with_context_msg_actual.response_skeleton_opt, @@ -785,9 +780,10 @@ mod tests { } #[test] - fn qualified_payables_msg_is_handled_but_fails_on_build_blockchain_agent() { - let system = - System::new("qualified_payables_msg_is_handled_but_fails_on_build_blockchain_agent"); + fn qualified_payables_msg_is_handled_but_fails_on_introduce_blockchain_agent() { + let system = System::new( + "qualified_payables_msg_is_handled_but_fails_on_introduce_blockchain_agent", + ); let port = find_free_port(); // build blockchain agent fails by not providing the third response. let _blockchain_client_server = MBCSBuilder::new(port) @@ -804,8 +800,9 @@ mod tests { false, ); subject.payable_payments_setup_subs_opt = Some(accountant_recipient); + let qualified_payables = UnpricedQualifiedPayables::from(vec![make_payable_account(123)]); let qualified_payables_msg = QualifiedPayablesMessage { - qualified_payables: vec![make_payable_account(123)], + qualified_payables, consuming_wallet: consuming_wallet.clone(), response_skeleton_opt: Some(ResponseSkeleton { client_id: 11122, @@ -820,7 +817,6 @@ mod tests { System::current().stop(); system.run(); - let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); let service_fee_balance_error = BlockchainAgentBuildError::ServiceFeeBalance( @@ -839,10 +835,10 @@ mod tests { } #[test] - fn handle_outbound_payments_instructions_sees_payments_happen_and_sends_payment_results_back_to_accountant( + fn handle_outbound_payments_instructions_sees_payment_happen_and_sends_payment_results_back_to_accountant( ) { let system = System::new( - "handle_outbound_payments_instructions_sees_payments_happen_and_sends_payment_results_back_to_accountant", + "handle_outbound_payments_instructions_sees_payment_happen_and_sends_payment_results_back_to_accountant", ); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) @@ -868,16 +864,15 @@ mod tests { let subject_subs = BlockchainBridge::make_subs_from(&addr); let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); - let accounts = vec![PayableAccount { + let account = PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, - }]; + }; let agent_id_stamp = ArbitraryIdStamp::new(); let agent = BlockchainAgentMock::default() .set_arbitrary_id_stamp(agent_id_stamp) - .agreed_fee_per_computation_unit_result(123) .consuming_wallet_result(consuming_wallet) .get_chain_result(Chain::PolyMainnet); @@ -885,7 +880,10 @@ mod tests { let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: accounts.clone(), + affordable_accounts: make_priced_qualified_payables(vec![( + account.clone(), + 111_222_333, + )]), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -905,9 +903,9 @@ mod tests { sent_payables_msg, &SentPayables { payment_procedure_result: Ok(vec![Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), + recipient_wallet: account.wallet, hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap() })]), @@ -923,10 +921,10 @@ mod tests { pending_payable_fingerprint_seeds_msg.hashes_and_balances, vec![HashAndAmount { hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap(), - amount: accounts[0].balance_wei + amount: account.balance_wei }] ); assert_eq!(accountant_recording.len(), 2); @@ -958,22 +956,25 @@ mod tests { let subject_subs = BlockchainBridge::make_subs_from(&addr); let mut peer_actors = peer_actors_builder().build(); peer_actors.accountant = make_accountant_subs_from_recorder(&accountant_addr); - let accounts = vec![PayableAccount { + let account = PayableAccount { wallet: wallet_account, balance_wei: 111_420_204, last_paid_timestamp: from_unix_timestamp(150_000_000), pending_payable_opt: None, - }]; + }; let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let agent = BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(123) + .gas_price_result(123) .get_chain_result(Chain::PolyMainnet); send_bind_message!(subject_subs, peer_actors); let _ = addr .try_send(OutboundPaymentsInstructions { - affordable_accounts: accounts.clone(), + affordable_accounts: make_priced_qualified_payables(vec![( + account.clone(), + 111_222_333, + )]), agent: Box::new(agent), response_skeleton_opt: Some(ResponseSkeleton { client_id: 1234, @@ -999,10 +1000,10 @@ mod tests { pending_payable_fingerprint_seeds_msg.hashes_and_balances, vec![HashAndAmount { hash: H256::from_str( - "36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) .unwrap(), - amount: accounts[0].balance_wei + amount: account.balance_wei }] ); assert_eq!( @@ -1014,7 +1015,7 @@ mod tests { context_id: 4321 }), msg: format!( - "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x36e9d7cdd657181317dd461192d537d9944c57a51ee950607de5a618b00e57a1" + "ReportAccountsPayable: Sending phase: \"Transport error: Error(IncompleteMessage)\". Signed and hashed transactions: 0x81d20df32920161727cd20e375e53c2f9df40fd80256a236fb39e444c999fb6c" ) } ); @@ -1036,13 +1037,17 @@ mod tests { let consuming_wallet = make_paying_wallet(b"consuming_wallet"); let accounts_1 = make_payable_account(1); let accounts_2 = make_payable_account(2); - let accounts = vec![accounts_1.clone(), accounts_2.clone()]; + let affordable_qualified_payables = make_priced_qualified_payables(vec![ + (accounts_1.clone(), 777_777_777), + (accounts_2.clone(), 999_999_999), + ]); let system = System::new(test_name); let agent = BlockchainAgentMock::default() .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(1) + .gas_price_result(1) .get_chain_result(Chain::PolyMainnet); - let msg = OutboundPaymentsInstructions::new(accounts, Box::new(agent), None); + let msg = + OutboundPaymentsInstructions::new(affordable_qualified_payables, Box::new(agent), None); let persistent_config = PersistentConfigurationMock::new(); let mut subject = BlockchainBridge::new( Box::new(blockchain_interface_web3), @@ -1066,7 +1071,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_1.wallet, hash: H256::from_str( - "cc73f3d5fe9fc3dac28b510ddeb157b0f8030b201e809014967396cdf365488a" + "c0756e8da662cee896ed979456c77931668b7f8456b9f978fc3305671f8f82ad" ) .unwrap() }) @@ -1076,7 +1081,7 @@ mod tests { Correct(PendingPayable { recipient_wallet: accounts_2.wallet, hash: H256::from_str( - "891d9ffa838aedc0bb2f6f7e9737128ce98bb33d07b4c8aa5645871e20d6cd13" + "9ba19f88ce43297d700b1f57ed8bc6274d01a5c366b78dd05167f9874c867ba0" ) .unwrap() }) @@ -1098,8 +1103,12 @@ mod tests { let agent = BlockchainAgentMock::default() .get_chain_result(TEST_DEFAULT_CHAIN) .consuming_wallet_result(consuming_wallet) - .agreed_fee_per_computation_unit_result(123); - let msg = OutboundPaymentsInstructions::new(vec![], Box::new(agent), None); + .gas_price_result(123); + let msg = OutboundPaymentsInstructions::new( + make_priced_qualified_payables(vec![(make_payable_account(111), 111_000_000)]), + Box::new(agent), + None, + ); let persistent_config = configure_default_persistent_config(ZERO); let mut subject = BlockchainBridge::new( Box::new(blockchain_interface_web3), @@ -2224,6 +2233,12 @@ mod tests { assert_on_initialization_with_panic_on_migration(&data_dir, &act); } + + #[test] + fn increase_gas_price_by_margin_works() { + assert_eq!(increase_gas_price_by_margin(1_000_000_000), 1_300_000_000); + assert_eq!(increase_gas_price_by_margin(9_000_000_000), 11_700_000_000); + } } #[cfg(test)] diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs index 852b02a4e..81c7fe62d 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/mod.rs @@ -4,7 +4,6 @@ pub mod lower_level_interface_web3; mod utils; use std::cmp::PartialEq; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{BlockchainTransaction, ProcessedPayableFallible}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -21,11 +20,18 @@ use actix::Recipient; use ethereum_types::U64; use web3::transports::{EventLoopHandle, Http}; use web3::types::{Address, Log, H256, U256, FilterBuilder, TransactionReceipt, BlockNumber}; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::msgs::{UnpricedQualifiedPayables, PricedQualifiedPayables}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{LowBlockchainIntWeb3, TransactionReceiptResult, TxReceipt, TxStatus}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::utils::{create_blockchain_agent_web3, send_payables_within_batch, BlockchainAgentFutureResult}; +// TODO We should probably begin to attach these constants to the interfaces more tightly, so that +// we aren't baffled by which interface they belong with. I suggest to declare them inside +// their inherent impl blocks. They will then need to be preceded by the class name +// of the respective interface if you want to use them. This could be a distinction we desire, +// despite the increased wordiness. + const CONTRACT_ABI: &str = indoc!( r#"[{ "constant":true, @@ -156,7 +162,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { ) } - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>> { @@ -193,8 +199,8 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { masq_token_balance, }; Ok(create_blockchain_agent_web3( - gas_limit_const_part, blockchain_agent_future_result, + gas_limit_const_part, consuming_wallet, chain, )) @@ -246,7 +252,7 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { logger: Logger, agent: Box, fingerprints_recipient: Recipient, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>> { let consuming_wallet = agent.consuming_wallet().clone(); @@ -254,7 +260,6 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { let get_transaction_id = self .lower_interface() .get_transaction_id(consuming_wallet.address()); - let gas_price_wei = agent.agreed_fee_per_computation_unit(); let chain = agent.get_chain(); Box::new( @@ -266,7 +271,6 @@ impl BlockchainInterface for BlockchainInterfaceWeb3 { chain, &web3_batch, consuming_wallet, - gas_price_wei, pending_nonce, fingerprints_recipient, affordable_accounts, @@ -430,7 +434,6 @@ impl BlockchainInterfaceWeb3 { #[cfg(test)] mod tests { use super::*; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, CONTRACT_ABI, REQUESTS_IN_PARALLEL, TRANSACTION_LITERAL, TRANSFER_METHOD_ID, @@ -441,9 +444,7 @@ mod tests { BlockchainAgentBuildError, BlockchainError, BlockchainInterface, RetrievedBlockchainTransactions, }; - use crate::blockchain::test_utils::{ - all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder, - }; + use crate::blockchain::test_utils::{all_chains, make_blockchain_interface_web3, ReceiptResponseBuilder}; use crate::sub_lib::blockchain_bridge::ConsumingWalletBalances; use crate::sub_lib::wallet::Wallet; use crate::test_utils::make_paying_wallet; @@ -459,6 +460,9 @@ mod tests { use std::str::FromStr; use web3::transports::Http; use web3::types::{H256, U256}; + use crate::accountant::scanners::payable_scanner_extension::msgs::{QualifiedPayablesBeforeGasPriceSelection, QualifiedPayableWithGasPrice}; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock, TxReceipt, TxStatus}; #[test] @@ -831,11 +835,96 @@ mod tests { } #[test] - fn blockchain_interface_web3_can_build_blockchain_agent() { + fn blockchain_interface_web3_can_introduce_blockchain_agent_in_the_new_payables_mode() { + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let gas_price_wei_from_rpc_hex = "0x3B9ACA00"; // 1000000000 + let gas_price_wei_from_rpc_u128_wei = + u128::from_str_radix(&gas_price_wei_from_rpc_hex[2..], 16).unwrap(); + let gas_price_wei_from_rpc_u128_wei_with_margin = + increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); + let expected_priced_qualified_payables = PricedQualifiedPayables { + payables: vec![ + QualifiedPayableWithGasPrice::new( + account_1, + gas_price_wei_from_rpc_u128_wei_with_margin, + ), + QualifiedPayableWithGasPrice::new( + account_2, + gas_price_wei_from_rpc_u128_wei_with_margin, + ), + ], + }; + let expected_estimated_transaction_fee_total = 190_652_800_000_000; + + test_blockchain_interface_web3_can_introduce_blockchain_agent( + unpriced_qualified_payables, + gas_price_wei_from_rpc_hex, + expected_priced_qualified_payables, + expected_estimated_transaction_fee_total, + ); + } + + #[test] + fn blockchain_interface_web3_can_introduce_blockchain_agent_in_the_retry_payables_mode() { + let gas_price_wei_from_rpc_hex = "0x3B9ACA00"; // 1000000000 + let gas_price_wei_from_rpc_u128_wei = + u128::from_str_radix(&gas_price_wei_from_rpc_hex[2..], 16).unwrap(); + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let account_3 = make_payable_account(56); + let unpriced_qualified_payables = UnpricedQualifiedPayables { + payables: vec![ + QualifiedPayablesBeforeGasPriceSelection::new( + account_1.clone(), + Some(gas_price_wei_from_rpc_u128_wei - 1), + ), + QualifiedPayablesBeforeGasPriceSelection::new( + account_2.clone(), + Some(gas_price_wei_from_rpc_u128_wei), + ), + QualifiedPayablesBeforeGasPriceSelection::new( + account_3.clone(), + Some(gas_price_wei_from_rpc_u128_wei + 1), + ), + ], + }; + + let expected_priced_qualified_payables = { + let gas_price_account_1 = increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); + let gas_price_account_2 = increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei); + let gas_price_account_3 = + increase_gas_price_by_margin(gas_price_wei_from_rpc_u128_wei + 1); + PricedQualifiedPayables { + payables: vec![ + QualifiedPayableWithGasPrice::new(account_1, gas_price_account_1), + QualifiedPayableWithGasPrice::new(account_2, gas_price_account_2), + QualifiedPayableWithGasPrice::new(account_3, gas_price_account_3), + ], + } + }; + let expected_estimated_transaction_fee_total = 285_979_200_073_328; + + test_blockchain_interface_web3_can_introduce_blockchain_agent( + unpriced_qualified_payables, + gas_price_wei_from_rpc_hex, + expected_priced_qualified_payables, + expected_estimated_transaction_fee_total, + ); + } + + fn test_blockchain_interface_web3_can_introduce_blockchain_agent( + unpriced_qualified_payables: UnpricedQualifiedPayables, + gas_price_wei_from_rpc_hex: &str, + expected_priced_qualified_payables: PricedQualifiedPayables, + expected_estimated_transaction_fee_total: u128, + ) { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) // gas_price - .ok_response("0x3B9ACA00".to_string(), 0) // 1000000000 + .ok_response(gas_price_wei_from_rpc_hex.to_string(), 0) // transaction_fee_balance .ok_response("0xFFF0".to_string(), 0) // 65520 // masq_balance @@ -844,18 +933,16 @@ mod tests { 0, ) .start(); - let chain = Chain::PolyMainnet; let wallet = make_wallet("abc"); let subject = make_blockchain_interface_web3(port); let result = subject - .build_blockchain_agent(wallet.clone()) + .introduce_blockchain_agent(wallet.clone()) .wait() .unwrap(); let expected_transaction_fee_balance = U256::from(65_520); let expected_masq_balance = U256::from(65_535); - let expected_gas_price_wei = 1_000_000_000; assert_eq!(result.consuming_wallet(), &wallet); assert_eq!( result.consuming_wallet_balances(), @@ -864,17 +951,15 @@ mod tests { masq_token_balance_in_minor_units: expected_masq_balance } ); + let priced_qualified_payables = + result.price_qualified_payables(unpriced_qualified_payables); assert_eq!( - result.agreed_fee_per_computation_unit(), - expected_gas_price_wei + priced_qualified_payables, + expected_priced_qualified_payables ); - let expected_fee_estimation = (3 - * (BlockchainInterfaceWeb3::web3_gas_limit_const_part(chain) - + WEB3_MAXIMAL_GAS_LIMIT_MARGIN) - * expected_gas_price_wei) as u128; assert_eq!( - result.estimated_transaction_fee_total(3), - expected_fee_estimation + result.estimate_transaction_fee_total(&priced_qualified_payables), + expected_estimated_transaction_fee_total ) } @@ -886,7 +971,9 @@ mod tests { { let wallet = make_wallet("bcd"); let subject = make_blockchain_interface_web3(port); - let result = subject.build_blockchain_agent(wallet.clone()).wait(); + + let result = subject.introduce_blockchain_agent(wallet.clone()).wait(); + let err = match result { Err(e) => e, _ => panic!("we expected Err() but got Ok()"), @@ -899,15 +986,16 @@ mod tests { fn build_of_the_blockchain_agent_fails_on_fetching_gas_price() { let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port).start(); - let wallet = make_wallet("abc"); - let subject = make_blockchain_interface_web3(port); - - let err = subject.build_blockchain_agent(wallet).wait().err().unwrap(); + let expected_err_factory = |_wallet: &Wallet| { + BlockchainAgentBuildError::GasPrice(QueryFailed( + "Transport error: Error(IncompleteMessage)".to_string(), + )) + }; - let expected_err = BlockchainAgentBuildError::GasPrice(QueryFailed( - "Transport error: Error(IncompleteMessage)".to_string(), - )); - assert_eq!(err, expected_err) + build_of_the_blockchain_agent_fails_on_blockchain_interface_error( + port, + expected_err_factory, + ); } #[test] diff --git a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs index 7172987f4..00489febc 100644 --- a/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs +++ b/node/src/blockchain/blockchain_interface/blockchain_interface_web3/utils.rs @@ -2,8 +2,9 @@ use crate::accountant::db_access_objects::payable_dao::PayableAccount; use crate::accountant::db_access_objects::pending_payable_dao::PendingPayable; -use crate::accountant::scanners::payable_scanner_extension::agent_web3::BlockchainAgentWeb3; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; +use crate::accountant::scanners::payable_scanner_extension::msgs::PricedQualifiedPayables; +use crate::blockchain::blockchain_agent::agent_web3::BlockchainAgentWeb3; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::PendingPayableFingerprintSeeds; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, HashAndAmount, TRANSFER_METHOD_ID, @@ -17,6 +18,7 @@ use crate::sub_lib::wallet::Wallet; use actix::Recipient; use futures::Future; use masq_lib::blockchains::chains::Chain; +use masq_lib::constants::WALLET_ADDRESS_LENGTH; use masq_lib::logger::Logger; use secp256k1secrets::SecretKey; use serde_json::Value; @@ -85,34 +87,59 @@ pub fn merged_output_data( pub fn transmission_log( chain: Chain, - accounts: &[PayableAccount], - gas_price_in_wei: u128, + qualified_payables: &PricedQualifiedPayables, + lowest_nonce_used: U256, ) -> String { - let chain_name = chain - .rec() - .literal_identifier - .chars() - .skip_while(|char| char != &'-') - .skip(1) - .collect::(); + let chain_name = chain.rec().literal_identifier; + let account_count = qualified_payables.payables.len(); + let last_nonce_used = lowest_nonce_used + U256::from(account_count - 1); + let biggest_payable = qualified_payables + .payables + .iter() + .map(|payable_with_gas_price| payable_with_gas_price.payable.balance_wei) + .max() + .unwrap(); + let max_length_as_str = biggest_payable.separate_with_commas().len(); + let payment_wei_label = "[payment wei]"; + let payment_column_width = payment_wei_label.len().max(max_length_as_str); + let introduction = once(format!( - "\ - Paying to creditors...\n\ - Transactions in the batch:\n\ + "\n\ + Paying creditors\n\ + Transactions:\n\ \n\ - gas price: {} wei\n\ - chain: {}\n\ + {:first_column_width$} {}\n\ + {:first_column_width$} {}...{}\n\ \n\ - [wallet address] [payment in wei]\n", - gas_price_in_wei, chain_name + {:first_column_width$} {: SignedTransaction { let data = sign_transaction_data(amount, recipient_wallet); let gas_limit = gas_limit(data, chain); - // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction will start making RPC calls which we don't want (Do it at your own risk). + // Warning: If you set gas_price or nonce to None in transaction_parameters, sign_transaction + // will start making RPC calls which we don't want (Do it at your own risk). let transaction_parameters = TransactionParameters { nonce: Some(nonce), to: Some(chain.rec().contract), @@ -215,12 +243,12 @@ pub fn sign_and_append_multiple_payments( chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - gas_price_in_wei: u128, mut pending_nonce: U256, - accounts: &[PayableAccount], + accounts: &PricedQualifiedPayables, ) -> Vec { let mut hash_and_amount_list = vec![]; - accounts.iter().for_each(|payable| { + accounts.payables.iter().for_each(|payable_pack| { + let payable = &payable_pack.payable; debug!( logger, "Preparing payable future of {} wei to {} with nonce {}", @@ -235,7 +263,7 @@ pub fn sign_and_append_multiple_payments( payable, consuming_wallet.clone(), pending_nonce, - gas_price_in_wei, + payable_pack.gas_price_minor, ); pending_nonce = advance_used_nonce(pending_nonce); @@ -250,19 +278,17 @@ pub fn send_payables_within_batch( chain: Chain, web3_batch: &Web3>, consuming_wallet: Wallet, - gas_price_in_wei: u128, pending_nonce: U256, new_fingerprints_recipient: Recipient, - accounts: Vec, + accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError> + 'static> { debug!( logger, - "Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}, gas_price: {}", + "Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", consuming_wallet, chain.rec().contract, chain.rec().num_chain_id, - gas_price_in_wei ); let hashes_and_paid_amounts = sign_and_append_multiple_payments( @@ -270,7 +296,6 @@ pub fn send_payables_within_batch( chain, web3_batch, consuming_wallet, - gas_price_in_wei, pending_nonce, &accounts, ); @@ -279,7 +304,6 @@ pub fn send_payables_within_batch( let hashes_and_paid_amounts_error = hashes_and_paid_amounts.clone(); let hashes_and_paid_amounts_ok = hashes_and_paid_amounts.clone(); - // TODO: We are sending hashes_and_paid_amounts to the Accountant even if the payments fail. new_fingerprints_recipient .try_send(PendingPayableFingerprintSeeds { batch_wide_timestamp: timestamp, @@ -290,7 +314,7 @@ pub fn send_payables_within_batch( info!( logger, "{}", - transmission_log(chain, &accounts, gas_price_in_wei) + transmission_log(chain, &accounts, pending_nonce) ); Box::new( @@ -302,27 +326,30 @@ pub fn send_payables_within_batch( Ok(merged_output_data( batch_response, hashes_and_paid_amounts_ok, - accounts, + accounts.into(), )) }), ) } pub fn create_blockchain_agent_web3( - gas_limit_const_part: u128, blockchain_agent_future_result: BlockchainAgentFutureResult, + gas_limit_const_part: u128, wallet: Wallet, chain: Chain, ) -> Box { + let transaction_fee_balance_in_minor_units = + blockchain_agent_future_result.transaction_fee_balance; + let masq_token_balance_in_minor_units = blockchain_agent_future_result.masq_token_balance; + let cons_wallet_balances = ConsumingWalletBalances::new( + transaction_fee_balance_in_minor_units, + masq_token_balance_in_minor_units, + ); Box::new(BlockchainAgentWeb3::new( blockchain_agent_future_result.gas_price_wei.as_u128(), gas_limit_const_part, wallet, - ConsumingWalletBalances { - transaction_fee_balance_in_minor_units: blockchain_agent_future_result - .transaction_fee_balance, - masq_token_balance_in_minor_units: blockchain_agent_future_result.masq_token_balance, - }, + cons_wallet_balances, chain, )) } @@ -332,11 +359,12 @@ mod tests { use super::*; use crate::accountant::db_access_objects::utils::from_unix_timestamp; use crate::accountant::gwei_to_wei; - use crate::accountant::scanners::payable_scanner_extension::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::accountant::test_utils::{ make_payable_account, make_payable_account_with_wallet_and_balance_and_timestamp_opt, + make_priced_qualified_payables, }; use crate::blockchain::bip32::Bip32EncryptionKeyProvider; + use crate::blockchain::blockchain_agent::agent_web3::WEB3_MAXIMAL_GAS_LIMIT_MARGIN; use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, }; @@ -360,7 +388,6 @@ mod tests { use masq_lib::constants::{DEFAULT_CHAIN, DEFAULT_GAS_PRICE}; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; - use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; use masq_lib::utils::find_free_port; use serde_json::Value; use std::net::Ipv4Addr; @@ -431,19 +458,20 @@ mod tests { .unwrap(); let web3_batch = Web3::new(Batch::new(transport)); let chain = DEFAULT_CHAIN; - let gas_price_in_gwei = DEFAULT_GAS_PRICE; let pending_nonce = 1; let consuming_wallet = make_paying_wallet(b"paying_wallet"); let account_1 = make_payable_account(1); let account_2 = make_payable_account(2); - let accounts = vec![account_1, account_2]; + let accounts = make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 222_222_222), + ]); let result = sign_and_append_multiple_payments( &logger, chain, &web3_batch, consuming_wallet, - gwei_to_wei(gas_price_in_gwei), pending_nonce.into(), &accounts, ); @@ -453,14 +481,14 @@ mod tests { vec![ HashAndAmount { hash: H256::from_str( - "94881436a9c89f48b01651ff491c69e97089daf71ab8cfb240243d7ecf9b38b2" + "374b7d023f4ac7d99e612d82beda494b0747116e9b9dc975b33b865f331ee934" ) .unwrap(), amount: 1000000000 }, HashAndAmount { hash: H256::from_str( - "3811874d2b73cecd51234c94af46bcce918d0cb4de7d946c01d7da606fe761b5" + "5708afd876bc2573f9db984ec6d0e7f8ef222dd9f115643c9b9056d8bef8bbd9" ) .unwrap(), amount: 2000000000 @@ -470,49 +498,115 @@ mod tests { } #[test] - fn transmission_log_just_works() { - init_test_logging(); - let test_name = "transmission_log_just_works"; - let gas_price = 120; - let logger = Logger::new(test_name); - let amount_1 = gwei_to_wei(900_000_000_u64); - let account_1 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w123"), - amount_1, - None, - ); - let amount_2 = 123_456_789_u128; - let account_2 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w555"), - amount_2, - None, - ); - let amount_3 = gwei_to_wei(33_355_666_u64); - let account_3 = make_payable_account_with_wallet_and_balance_and_timestamp_opt( - make_wallet("w987"), - amount_3, - None, + fn transmission_log_is_well_formatted() { + // This test only focuses on the formatting, but there are other tests asserting printing + // this in the logs + + // Case 1 + let payments = [ + gwei_to_wei(900_000_000_u64), + 123_456_789_u128, + gwei_to_wei(33_355_666_u64), + ]; + let pending_nonce = 123456789.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ + \n\ + chain: base-sepolia\n\ + nonces: 123,456,789...123,456,791\n\ + \n\ + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 900,000,000,000,000,000 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 123,456,789 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 33,355,666,000,000,000 740,740,734\n"; + + test_transmission_log( + 1, + payments, + Chain::BaseSepolia, + pending_nonce, + expected_format, ); - let accounts_to_process = vec![account_1, account_2, account_3]; - info!( - logger, - "{}", - transmission_log(TEST_DEFAULT_CHAIN, &accounts_to_process, gas_price) + // Case 2 + let payments = [ + gwei_to_wei(5_400_u64), + gwei_to_wei(10_000_u64), + 44_444_555_u128, + ]; + let pending_nonce = 100.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ + \n\ + chain: eth-mainnet\n\ + nonces: 100...102\n\ + \n\ + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 5,400,000,000,000 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 10,000,000,000,000 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 44,444,555 740,740,734\n"; + + test_transmission_log( + 2, + payments, + Chain::EthMainnet, + pending_nonce, + expected_format, ); - let log_handler = TestLogHandler::new(); - log_handler.exists_log_containing( - "INFO: transmission_log_just_works: Paying to creditors...\n\ - Transactions in the batch:\n\ + // Case 3 + let payments = [45_000_888, 1_999_999, 444_444_555]; + let pending_nonce = 1.into(); + let expected_format = "\n\ + Paying creditors\n\ + Transactions:\n\ \n\ - gas price: 120 wei\n\ - chain: sepolia\n\ + chain: polygon-mainnet\n\ + nonces: 1...3\n\ \n\ - [wallet address] [payment in wei]\n\ - 0x0000000000000000000000000000000077313233 900,000,000,000,000,000\n\ - 0x0000000000000000000000000000000077353535 123,456,789\n\ - 0x0000000000000000000000000000000077393837 33,355,666,000,000,000\n", + [wallet address] [payment wei] [gas price wei]\n\ + 0x0000000000000000000000000077616c6c657430 45,000,888 246,913,578\n\ + 0x0000000000000000000000000077616c6c657431 1,999,999 493,827,156\n\ + 0x0000000000000000000000000077616c6c657432 444,444,555 740,740,734\n"; + + test_transmission_log( + 3, + payments, + Chain::PolyMainnet, + pending_nonce, + expected_format, + ); + } + + fn test_transmission_log( + case: usize, + payments: [u128; 3], + chain: Chain, + pending_nonce: U256, + expected_result: &str, + ) { + let accounts_to_process_seeds = payments + .iter() + .enumerate() + .map(|(i, payment)| { + let wallet = make_wallet(&format!("wallet{}", i)); + let gas_price = (i as u128 + 1) * 2 * 123_456_789; + let account = make_payable_account_with_wallet_and_balance_and_timestamp_opt( + wallet, *payment, None, + ); + (account, gas_price) + }) + .collect(); + let accounts_to_process = make_priced_qualified_payables(accounts_to_process_seeds); + + let result = transmission_log(chain, &accounts_to_process, pending_nonce); + + assert_eq!( + result, expected_result, + "Test case {}: we expected this format: \"{}\", but it was: \"{}\"", + case, expected_result, result ); } @@ -573,9 +667,9 @@ mod tests { ) } - fn execute_send_payables_test( + fn test_send_payables_within_batch( test_name: &str, - accounts: Vec, + accounts: PricedQualifiedPayables, expected_result: Result, PayableTransactionError>, port: u16, ) { @@ -585,7 +679,6 @@ mod tests { REQUESTS_IN_PARALLEL, ) .unwrap(); - let gas_price = 1_000_000_000; let pending_nonce: U256 = 1.into(); let web3_batch = Web3::new(Batch::new(transport)); let (accountant, _, accountant_recording) = make_recorder(); @@ -601,7 +694,6 @@ mod tests { chain, &web3_batch, consuming_wallet.clone(), - gas_price, pending_nonce, new_fingerprints_recipient, accounts.clone(), @@ -619,23 +711,23 @@ mod tests { assert!(timestamp_after >= ppfs_message.batch_wide_timestamp); let tlh = TestLogHandler::new(); tlh.exists_log_containing( - &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}, gas_price: {}", + &format!("DEBUG: {test_name}: Common attributes of payables to be transacted: sender wallet: {}, contract: {:?}, chain_id: {}", consuming_wallet, chain.rec().contract, chain.rec().num_chain_id, - gas_price ) ); tlh.exists_log_containing(&format!( "INFO: {test_name}: {}", - transmission_log(chain, &accounts, gas_price) + transmission_log(chain, &accounts, pending_nonce) )); assert_eq!(result, expected_result); } #[test] fn send_payables_within_batch_works() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -646,24 +738,27 @@ mod tests { .start(); let expected_result = Ok(vec![ Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), + recipient_wallet: account_1.wallet.clone(), hash: H256::from_str( - "35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4", + "6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2", ) .unwrap(), }), Correct(PendingPayable { - recipient_wallet: accounts[1].wallet.clone(), + recipient_wallet: account_2.wallet.clone(), hash: H256::from_str( - "7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3", + "b67a61b29c0c48d8b63a64fda73b3247e8e2af68082c710325675d4911e113d4", ) .unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_works", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 222_222_222), + ]), expected_result, port, ); @@ -671,19 +766,22 @@ mod tests { #[test] fn send_payables_within_batch_fails_on_submit_batch_call() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let accounts = make_priced_qualified_payables(vec![ + (make_payable_account(1), 111_222_333), + (make_payable_account(2), 222_333_444), + ]); let os_code = transport_error_code(); let os_msg = transport_error_message(); let port = find_free_port(); let expected_result = Err(Sending { msg: format!("Transport error: Error(Connect, Os {{ code: {}, kind: ConnectionRefused, message: {:?} }})", os_code, os_msg).to_string(), hashes: vec![ - H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), - H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap() + H256::from_str("ec7ac48060b75889f949f5e8d301b386198218e60e2635c95cb6b0934a0887ea").unwrap(), + H256::from_str("c2d5059db0ec2fbf15f83d9157eeb0d793d6242de5e73a607935fb5660e7e925").unwrap() ], }); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_fails_on_submit_batch_call", accounts, expected_result, @@ -693,7 +791,8 @@ mod tests { #[test] fn send_payables_within_batch_all_payments_fail() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -718,8 +817,8 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[0].wallet.clone(), - hash: H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), + recipient_wallet: account_1.wallet.clone(), + hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -727,14 +826,17 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[1].wallet.clone(), - hash: H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap(), + recipient_wallet: account_2.wallet.clone(), + hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_all_payments_fail", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 111_111_111), + ]), expected_result, port, ); @@ -742,7 +844,8 @@ mod tests { #[test] fn send_payables_within_batch_one_payment_works_the_other_fails() { - let accounts = vec![make_payable_account(1), make_payable_account(2)]; + let account_1 = make_payable_account(1); + let account_2 = make_payable_account(2); let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .begin_batch() @@ -757,8 +860,8 @@ mod tests { .start(); let expected_result = Ok(vec![ Correct(PendingPayable { - recipient_wallet: accounts[0].wallet.clone(), - hash: H256::from_str("35f42b260f090a559e8b456718d9c91a9da0f234ed0a129b9d5c4813b6615af4").unwrap(), + recipient_wallet: account_1.wallet.clone(), + hash: H256::from_str("6e7fa351eef640186f76c629cb74106b3082c8f8a1a9df75ff02fe5bfd4dd1a2").unwrap(), }), Failed(RpcPayableFailure { rpc_error: Rpc(Error { @@ -766,14 +869,17 @@ mod tests { message: "The requests per second (RPS) of your requests are higher than your plan allows.".to_string(), data: None, }), - recipient_wallet: accounts[1].wallet.clone(), - hash: H256::from_str("7f3221109e4f1de8ba1f7cd358aab340ecca872a1456cb1b4f59ca33d3e22ee3").unwrap(), + recipient_wallet: account_2.wallet.clone(), + hash: H256::from_str("ca6ad0a60daeaf31cbca7ce6e499c0f4ff5870564c5e845de11834f1fc05bd4e").unwrap(), }), ]); - execute_send_payables_test( + test_send_payables_within_batch( "send_payables_within_batch_one_payment_works_the_other_fails", - accounts, + make_priced_qualified_payables(vec![ + (account_1, 111_111_111), + (account_2, 111_111_111), + ]), expected_result, port, ); diff --git a/node/src/blockchain/blockchain_interface/mod.rs b/node/src/blockchain/blockchain_interface/mod.rs index bdcbf6a91..242bf433f 100644 --- a/node/src/blockchain/blockchain_interface/mod.rs +++ b/node/src/blockchain/blockchain_interface/mod.rs @@ -6,7 +6,6 @@ pub mod lower_level_interface; use actix::Recipient; use ethereum_types::H256; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_interface::data_structures::errors::{BlockchainAgentBuildError, BlockchainError, PayableTransactionError}; use crate::blockchain::blockchain_interface::data_structures::{ProcessedPayableFallible, RetrievedBlockchainTransactions}; use crate::blockchain::blockchain_interface::lower_level_interface::LowBlockchainInt; @@ -15,7 +14,8 @@ use futures::Future; use masq_lib::blockchains::chains::Chain; use web3::types::Address; use masq_lib::logger::Logger; -use crate::accountant::db_access_objects::payable_dao::PayableAccount; +use crate::accountant::scanners::payable_scanner_extension::msgs::{PricedQualifiedPayables}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::{BlockMarker, BlockScanRange, PendingPayableFingerprintSeeds}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::TransactionReceiptResult; @@ -33,7 +33,7 @@ pub trait BlockchainInterface { recipient: Address, ) -> Box>; - fn build_blockchain_agent( + fn introduce_blockchain_agent( &self, consuming_wallet: Wallet, ) -> Box, Error = BlockchainAgentBuildError>>; @@ -48,7 +48,7 @@ pub trait BlockchainInterface { logger: Logger, agent: Box, fingerprints_recipient: Recipient, - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, ) -> Box, Error = PayableTransactionError>>; as_any_ref_in_trait!(); diff --git a/node/src/blockchain/blockchain_interface_initializer.rs b/node/src/blockchain/blockchain_interface_initializer.rs index ee87519a0..d7f452311 100644 --- a/node/src/blockchain/blockchain_interface_initializer.rs +++ b/node/src/blockchain/blockchain_interface_initializer.rs @@ -10,8 +10,9 @@ use web3::transports::Http; pub(in crate::blockchain) struct BlockchainInterfaceInitializer {} impl BlockchainInterfaceInitializer { - // TODO when we have multiple chains of fundamentally different architectures and are able to switch them, - // this should probably be replaced by a HashMap of distinct interfaces for each chain + // TODO if we ever have multiple chains of fundamentally different architectures and are able + // to switch them, this should probably be replaced by a HashMap of distinct interfaces for + // each chain pub fn initialize_interface( &self, blockchain_service_url: &str, @@ -43,24 +44,25 @@ impl BlockchainInterfaceInitializer { #[cfg(test)] mod tests { - use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; - use masq_lib::blockchains::chains::Chain; - - use futures::Future; - use std::net::Ipv4Addr; - use web3::transports::Http; - - use crate::blockchain::blockchain_interface::blockchain_interface_web3::{ - BlockchainInterfaceWeb3, REQUESTS_IN_PARALLEL, + use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayableWithGasPrice, UnpricedQualifiedPayables, }; - use crate::blockchain::blockchain_interface::BlockchainInterface; + use crate::accountant::test_utils::make_payable_account; + use crate::blockchain::blockchain_bridge::increase_gas_price_by_margin; + use crate::blockchain::blockchain_interface_initializer::BlockchainInterfaceInitializer; use crate::test_utils::make_wallet; + use futures::Future; + use masq_lib::blockchains::chains::Chain; use masq_lib::constants::DEFAULT_CHAIN; use masq_lib::test_utils::mock_blockchain_client_server::MBCSBuilder; use masq_lib::utils::find_free_port; + use std::net::Ipv4Addr; #[test] fn initialize_web3_interface_works() { + // TODO this test should definitely assert on the web3 requests sent to the server, + // that's the best way to verify that this interface belongs to the web3 architecture + // (This test amplifies the importance of GH-543) let port = find_free_port(); let _blockchain_client_server = MBCSBuilder::new(port) .ok_response("0x3B9ACA00".to_string(), 0) // gas_price = 10000000000 @@ -71,22 +73,37 @@ mod tests { ) .ok_response("0x23".to_string(), 1) .start(); - let wallet = make_wallet("123"); let chain = Chain::PolyMainnet; let server_url = &format!("http://{}:{}", &Ipv4Addr::LOCALHOST, port); - let (event_loop_handle, transport) = - Http::with_max_parallel(server_url, REQUESTS_IN_PARALLEL).unwrap(); - let subject = BlockchainInterfaceWeb3::new(transport, event_loop_handle, chain); - let blockchain_agent = subject - .build_blockchain_agent(wallet.clone()) + let result = BlockchainInterfaceInitializer {}.initialize_interface(server_url, chain); + + let account_1 = make_payable_account(12); + let account_2 = make_payable_account(34); + let unpriced_qualified_payables = + UnpricedQualifiedPayables::from(vec![account_1.clone(), account_2.clone()]); + let payable_wallet = make_wallet("payable"); + let blockchain_agent = result + .introduce_blockchain_agent(payable_wallet.clone()) .wait() .unwrap(); - - assert_eq!(blockchain_agent.consuming_wallet(), &wallet); + assert_eq!(blockchain_agent.consuming_wallet(), &payable_wallet); + let priced_qualified_payables = + blockchain_agent.price_qualified_payables(unpriced_qualified_payables); + let gas_price_with_margin = increase_gas_price_by_margin(1_000_000_000); + let expected_priced_qualified_payables = PricedQualifiedPayables { + payables: vec![ + QualifiedPayableWithGasPrice::new(account_1, gas_price_with_margin), + QualifiedPayableWithGasPrice::new(account_2, gas_price_with_margin), + ], + }; + assert_eq!( + priced_qualified_payables, + expected_priced_qualified_payables + ); assert_eq!( - blockchain_agent.agreed_fee_per_computation_unit(), - 1_000_000_000 + blockchain_agent.estimate_transaction_fee_total(&priced_qualified_payables), + 190_652_800_000_000 ); } diff --git a/node/src/blockchain/mod.rs b/node/src/blockchain/mod.rs index 4c51e726e..48698c408 100644 --- a/node/src/blockchain/mod.rs +++ b/node/src/blockchain/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. pub mod bip32; pub mod bip39; +pub mod blockchain_agent; pub mod blockchain_bridge; pub mod blockchain_interface; pub mod blockchain_interface_initializer; diff --git a/node/src/hopper/routing_service.rs b/node/src/hopper/routing_service.rs index 05d8af9d6..478441159 100644 --- a/node/src/hopper/routing_service.rs +++ b/node/src/hopper/routing_service.rs @@ -529,6 +529,7 @@ mod tests { use masq_lib::test_utils::environment_guard::EnvironmentGuard; use masq_lib::test_utils::logging::{init_test_logging, TestLogHandler}; use masq_lib::test_utils::utils::TEST_DEFAULT_CHAIN; + use std::fmt::format; use std::net::SocketAddr; use std::str::FromStr; use std::time::SystemTime; @@ -593,6 +594,7 @@ mod tests { #[test] fn logs_and_ignores_message_that_cannot_be_deserialized() { init_test_logging(); + let test_name = "logs_and_ignores_message_that_cannot_be_deserialized"; let cryptdes = make_cryptde_pair(); let route = route_from_proxy_client(&cryptdes.main.public_key(), cryptdes.main); let lcp = LiveCoresPackage::new( @@ -610,7 +612,7 @@ mod tests { data: data_enc.into(), }; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( cryptdes, RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -624,17 +626,19 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't expire CORES package with 35-byte payload to ProxyClient using main key", + &format!("ERROR: {test_name}: Couldn't expire CORES package with 35-byte payload to ProxyClient using main key"), ); } #[test] fn logs_and_ignores_message_that_cannot_be_decrypted() { init_test_logging(); + let test_name = "logs_and_ignores_message_that_cannot_be_decrypted"; let (main_cryptde, alias_cryptde) = { //initialization to real CryptDEs let pair = Bootstrapper::pub_initialize_cryptdes_for_testing(&None, &None); @@ -657,7 +661,7 @@ mod tests { data: data_enc.into(), }; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -674,11 +678,12 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't expire CORES package with 51-byte payload to ProxyClient using main key: DecryptionError(OpeningFailed)", + &format!("ERROR: {test_name}: Couldn't expire CORES package with 51-byte payload to ProxyClient using main key: DecryptionError(OpeningFailed)") ); } @@ -809,6 +814,7 @@ mod tests { let _eg = EnvironmentGuard::new(); init_test_logging(); BAN_CACHE.clear(); + let test_name = "complains_about_live_message_for_nonexistent_proxy_client"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let route = route_to_proxy_client(&main_cryptde.public_key(), main_cryptde); @@ -838,7 +844,7 @@ mod tests { let system = System::new("converts_live_message_to_expired_for_proxy_client"); let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -855,6 +861,7 @@ mod tests { 0, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -862,7 +869,7 @@ mod tests { system.run(); let tlh = TestLogHandler::new(); tlh.exists_no_log_containing("Couldn't decode CORES package in 8-byte buffer"); - tlh.exists_log_containing("WARN: RoutingService: Received CORES package from 1.2.3.4:5678 for Proxy Client, but Proxy Client isn't running"); + tlh.exists_log_containing(&format!("WARN: {test_name}: Received CORES package from 1.2.3.4:5678 for Proxy Client, but Proxy Client isn't running")); } #[test] @@ -1281,6 +1288,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_that_demands_routing_without_paying_wallet"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let origin_key = PublicKey::new(&[1, 2]); @@ -1313,9 +1322,7 @@ mod tests { sequence_number: None, data: data_enc.into(), }; - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_routing_without_paying_wallet", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1328,7 +1335,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1345,13 +1352,14 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Live CORES package with \\d+-byte payload without paying wallet", + &format!("WARN: {test_name}: Refusing to route Live CORES package with \\d+-byte payload without paying wallet"), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1366,6 +1374,7 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = "route_logs_and_ignores_cores_package_that_demands_proxy_client_routing_with_paying_wallet_that_cant_pay"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let public_key = main_cryptde.public_key(); @@ -1414,9 +1423,7 @@ mod tests { sequence_number: None, data: data_enc.into(), }; - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_proxy_client_routing_with_paying_wallet_that_cant_pay", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1429,7 +1436,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1446,13 +1453,14 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Expired CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership.", + &format!("WARN: {test_name}: Refusing to route Expired CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership."), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1467,6 +1475,7 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = "route_logs_and_ignores_cores_package_that_demands_hopper_routing_with_paying_wallet_that_cant_pay"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let current_key = main_cryptde.public_key(); @@ -1512,9 +1521,7 @@ mod tests { encodex(main_cryptde, &destination_key, &payload).unwrap(), ); - let system = System::new( - "route_logs_and_ignores_cores_package_that_demands_hopper_routing_with_paying_wallet_that_cant_pay", - ); + let system = System::new(test_name); let (proxy_client, _, proxy_client_recording_arc) = make_recorder(); let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); @@ -1527,7 +1534,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1544,6 +1551,7 @@ mod tests { 200, true, ); + subject.logger = Logger::new(test_name); subject.route_data_externally( lcp, @@ -1554,7 +1562,7 @@ mod tests { System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_matching( - "WARN: RoutingService: Refusing to route Live CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership.", + &format!("WARN: {test_name}: Refusing to route Live CORES package with \\d+-byte payload without proof of 0x0a26dc9ebb2124baf1efe9d460f1ce59cd7944bd paying wallet ownership."), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1568,6 +1576,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_from_delinquent_that_demands_external_routing"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let paying_wallet = make_paying_wallet(b"wallet"); @@ -1603,7 +1613,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1620,6 +1630,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1630,7 +1641,7 @@ mod tests { assert_eq!(dispatcher_recording.len(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); - TestLogHandler::new().exists_log_containing("WARN: RoutingService: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 7-byte payload further"); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 7-byte payload further")); } #[test] @@ -1638,6 +1649,8 @@ mod tests { let _eg = EnvironmentGuard::new(); BAN_CACHE.clear(); init_test_logging(); + let test_name = + "route_logs_and_ignores_cores_package_from_delinquent_that_demands_internal_routing"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let paying_wallet = make_paying_wallet(b"wallet"); @@ -1677,7 +1690,7 @@ mod tests { .dispatcher(dispatcher) .accountant(accountant) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1694,6 +1707,7 @@ mod tests { rate_pack_routing_byte(103), false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); @@ -1704,12 +1718,14 @@ mod tests { assert_eq!(dispatcher_recording.len(), 0); let accountant_recording = accountant_recording_arc.lock().unwrap(); assert_eq!(accountant_recording.len(), 0); - TestLogHandler::new().exists_log_containing("WARN: RoutingService: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 36-byte payload to ProxyServer"); + TestLogHandler::new().exists_log_containing(&format!("WARN: {test_name}: Node with consuming wallet 0x71d0fc7d1c570b1ed786382b551a09391c91e33d is delinquent; electing not to route 36-byte payload to ProxyServer")); } #[test] fn route_logs_and_ignores_inbound_client_data_that_doesnt_deserialize_properly() { init_test_logging(); + let test_name = + "route_logs_and_ignores_inbound_client_data_that_doesnt_deserialize_properly"; let inbound_client_data = InboundClientData { timestamp: SystemTime::now(), client_addr: SocketAddr::from_str("1.2.3.4:5678").unwrap(), @@ -1730,7 +1746,7 @@ mod tests { .neighborhood(neighborhood) .dispatcher(dispatcher) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -1744,13 +1760,14 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Couldn't decode CORES package in 0-byte buffer from 1.2.3.4:5678: DecryptionError(EmptyData)", + &format!("ERROR: {test_name}: Couldn't decode CORES package in 0-byte buffer from 1.2.3.4:5678: DecryptionError(EmptyData)"), ); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); @@ -1761,6 +1778,7 @@ mod tests { #[test] fn route_logs_and_ignores_invalid_live_cores_package() { init_test_logging(); + let test_name = "route_logs_and_ignores_invalid_live_cores_package"; let main_cryptde = main_cryptde(); let alias_cryptde = alias_cryptde(); let lcp = LiveCoresPackage::new(Route { hops: vec![] }, CryptData::new(&[])); @@ -1788,7 +1806,7 @@ mod tests { .neighborhood(neighborhood) .dispatcher(dispatcher) .build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( CryptDEPair { main: main_cryptde, alias: alias_cryptde, @@ -1805,14 +1823,15 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); subject.route(inbound_client_data); System::current().stop_with_code(0); system.run(); - TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Invalid 67-byte CORES package: RoutingError(EmptyRoute)", - ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: Invalid 67-byte CORES package: RoutingError(EmptyRoute)" + )); assert_eq!(proxy_client_recording_arc.lock().unwrap().len(), 0); assert_eq!(proxy_server_recording_arc.lock().unwrap().len(), 0); assert_eq!(neighborhood_recording_arc.lock().unwrap().len(), 0); @@ -1822,8 +1841,9 @@ mod tests { #[test] fn route_data_around_again_logs_and_ignores_bad_lcp() { init_test_logging(); + let test_name = "route_data_around_again_logs_and_ignores_bad_lcp"; let peer_actors = peer_actors_builder().build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -1837,6 +1857,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let lcp = LiveCoresPackage::new(Route { hops: vec![] }, CryptData::new(&[])); let ibcd = InboundClientData { timestamp: SystemTime::now(), @@ -1850,9 +1871,9 @@ mod tests { subject.route_data_around_again(lcp, &ibcd); - TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: bad zero-hop route: RoutingError(EmptyRoute)", - ); + TestLogHandler::new().exists_log_containing(&format!( + "ERROR: {test_name}: bad zero-hop route: RoutingError(EmptyRoute)" + )); } fn make_routing_service_subs(peer_actors: PeerActors) -> RoutingServiceSubs { @@ -1985,9 +2006,10 @@ mod tests { #[test] fn route_expired_package_handles_unmigratable_gossip() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_gossip"; let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().neighborhood(neighborhood).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2001,6 +2023,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2008,7 +2031,7 @@ mod tests { MessageType::Gossip(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_gossip"); + let system = System::new(test_name); subject.route_expired_package(Component::Neighborhood, expired_package, true); @@ -2017,7 +2040,7 @@ mod tests { let neighborhood_recording = neighborhood_recording_arc.lock().unwrap(); assert_eq!(neighborhood_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable Gossip: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable Gossip: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } @@ -2063,9 +2086,10 @@ mod tests { #[test] fn route_expired_package_handles_unmigratable_client_response() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_client_response"; let (proxy_server, _, proxy_server_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().proxy_server(proxy_server).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2079,6 +2103,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2086,7 +2111,7 @@ mod tests { MessageType::ClientResponse(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_client_response"); + let system = System::new(test_name); subject.route_expired_package(Component::ProxyServer, expired_package, true); @@ -2095,16 +2120,17 @@ mod tests { let proxy_server_recording = proxy_server_recording_arc.lock().unwrap(); assert_eq!(proxy_server_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable ClientResponsePayload: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable ClientResponsePayload: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } #[test] fn route_expired_package_handles_unmigratable_dns_resolve_failure() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_dns_resolve_failure"; let (hopper, _, hopper_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().hopper(hopper).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2118,6 +2144,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2125,7 +2152,7 @@ mod tests { MessageType::DnsResolveFailed(VersionedData::test_new(dv!(0, 0), vec![])), 0, ); - let system = System::new("route_expired_package_handles_unmigratable_dns_resolve_failure"); + let system = System::new(test_name); subject.route_expired_package(Component::ProxyServer, expired_package, true); @@ -2134,16 +2161,17 @@ mod tests { let hopper_recording = hopper_recording_arc.lock().unwrap(); assert_eq!(hopper_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable DnsResolveFailed: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable DnsResolveFailed: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } #[test] fn route_expired_package_handles_unmigratable_gossip_failure() { init_test_logging(); + let test_name = "route_expired_package_handles_unmigratable_gossip_failure"; let (neighborhood, _, neighborhood_recording_arc) = make_recorder(); let peer_actors = peer_actors_builder().neighborhood(neighborhood).build(); - let subject = RoutingService::new( + let mut subject = RoutingService::new( make_cryptde_pair(), RoutingServiceSubs { proxy_client_subs_opt: peer_actors.proxy_client_opt, @@ -2157,6 +2185,7 @@ mod tests { 200, false, ); + subject.logger = Logger::new(test_name); let expired_package = ExpiredCoresPackage::new( SocketAddr::from_str("1.2.3.4:1234").unwrap(), None, @@ -2173,7 +2202,7 @@ mod tests { let neighborhood_recording = neighborhood_recording_arc.lock().unwrap(); assert_eq!(neighborhood_recording.len(), 0); TestLogHandler::new().exists_log_containing( - "ERROR: RoutingService: Received unmigratable GossipFailure: MigrationNotFound(DataVersion { major: 0, minor: 0 }, DataVersion { major: 0, minor: 1 })", + &format!("ERROR: {test_name}: Received unmigratable GossipFailure: MigrationNotFound(DataVersion {{ major: 0, minor: 0 }}, DataVersion {{ major: 0, minor: 1 }})"), ); } } diff --git a/node/src/sub_lib/blockchain_bridge.rs b/node/src/sub_lib/blockchain_bridge.rs index 84aaabe48..669e37042 100644 --- a/node/src/sub_lib/blockchain_bridge.rs +++ b/node/src/sub_lib/blockchain_bridge.rs @@ -1,9 +1,10 @@ // Copyright (c) 2019, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::accountant::db_access_objects::payable_dao::PayableAccount; -use crate::accountant::scanners::payable_scanner_extension::blockchain_agent::BlockchainAgent; -use crate::accountant::scanners::payable_scanner_extension::msgs::QualifiedPayablesMessage; +use crate::accountant::scanners::payable_scanner_extension::msgs::{ + PricedQualifiedPayables, QualifiedPayablesMessage, +}; use crate::accountant::{RequestTransactionReceipts, ResponseSkeleton, SkeletonOptHolder}; +use crate::blockchain::blockchain_agent::BlockchainAgent; use crate::blockchain::blockchain_bridge::RetrieveTransactions; use crate::sub_lib::peer_actors::BindMessage; use actix::Message; @@ -41,14 +42,14 @@ impl Debug for BlockchainBridgeSubs { #[derive(Message)] pub struct OutboundPaymentsInstructions { - pub affordable_accounts: Vec, + pub affordable_accounts: PricedQualifiedPayables, pub agent: Box, pub response_skeleton_opt: Option, } impl OutboundPaymentsInstructions { pub fn new( - affordable_accounts: Vec, + affordable_accounts: PricedQualifiedPayables, agent: Box, response_skeleton_opt: Option, ) -> Self {