use super::*; use crate::{ mock_db::MockDBProvider, ports::{ BlockImporter, MockConsensusParametersProvider, }, MockDb, }; use fuel_core_services::{ stream::BoxStream, Service as ServiceTrait, }; use fuel_core_txpool::types::GasPrice; use fuel_core_types::{ blockchain::SealedBlock, entities::coins::coin::Coin, fuel_crypto::rand::{ rngs::StdRng, SeedableRng, }, fuel_tx::{ Cacheable, Input, Transaction, TransactionBuilder, Word, }, services::{ block_importer::ImportResult, p2p::GossipsubMessageAcceptance, }, }; use std::cell::RefCell; type GossipedTransaction = GossipData; pub struct TestContext { pub(crate) service: Service< MockP2P, MockDBProvider, MockTxPoolGasPrice, MockConsensusParametersProvider, >, mock_db: MockDb, rng: RefCell, } #[derive(Debug, Clone)] pub struct MockTxPoolGasPrice { pub gas_price: Option, } impl MockTxPoolGasPrice { pub fn new(gas_price: GasPrice) -> Self { Self { gas_price: Some(gas_price), } } pub fn new_none() -> Self { Self { gas_price: None } } } impl GasPriceProviderConstraint for MockTxPoolGasPrice { fn gas_price(&self, _block_height: BlockHeight) -> Option { self.gas_price } } impl TestContext { pub async fn new() -> Self { TestContextBuilder::new().build_and_start().await } pub fn service( &self, ) -> &Service< MockP2P, MockDBProvider, MockTxPoolGasPrice, MockConsensusParametersProvider, > { &self.service } pub fn setup_script_tx(&self, tip: Word) -> Transaction { let (_, gas_coin) = self.setup_coin(); let mut tx = TransactionBuilder::script(vec![], vec![]) .max_fee_limit(tip) .tip(tip) .script_gas_limit(1000) .add_input(gas_coin) .finalize_as_transaction(); tx.precompute(&Default::default()) .expect("Should be able to cache"); tx } pub fn setup_coin(&self) -> (Coin, Input) { crate::test_helpers::setup_coin(&mut self.rng.borrow_mut(), Some(&self.mock_db)) } } mockall::mock! { pub P2P {} impl PeerToPeer for P2P { type GossipedTransaction = GossipedTransaction; fn broadcast_transaction(&self, transaction: Arc) -> anyhow::Result<()>; fn gossiped_transaction_events(&self) -> BoxStream; fn notify_gossip_transaction_validity( &self, message_info: GossipsubMessageInfo, validity: GossipsubMessageAcceptance, ) -> anyhow::Result<()>; } } impl MockP2P { pub fn new_with_txs(txs: Vec) -> Self { let mut p2p = MockP2P::default(); p2p.expect_gossiped_transaction_events().returning(move || { let txs_clone = txs.clone(); let stream = fuel_core_services::stream::unfold(txs_clone, |mut txs| async { let tx = txs.pop(); if let Some(tx) = tx { Some((GossipData::new(tx, vec![], vec![]), txs)) } else { core::future::pending().await } }); Box::pin(stream) }); p2p } } mockall::mock! { pub Importer {} impl BlockImporter for Importer { fn block_events(&self) -> BoxStream; } } impl MockImporter { fn with_blocks(blocks: Vec) -> Self { let mut importer = MockImporter::default(); importer.expect_block_events().returning(move || { let blocks = blocks.clone(); let stream = fuel_core_services::stream::unfold(blocks, |mut blocks| async { let block = blocks.pop(); if let Some(sealed_block) = block { let result: SharedImportResult = Arc::new( ImportResult::new_from_local(sealed_block, vec![], vec![]), ); Some((result, blocks)) } else { core::future::pending().await } }); Box::pin(stream) }); importer } } pub struct TestContextBuilder { config: Option, mock_db: MockDb, rng: StdRng, p2p: Option, importer: Option, } impl Default for TestContextBuilder { fn default() -> Self { Self::new() } } impl TestContextBuilder { pub fn new() -> Self { Self { config: None, mock_db: MockDb::default(), rng: StdRng::seed_from_u64(10), p2p: None, importer: None, } } pub fn with_config(mut self, config: Config) -> Self { self.config = Some(config); self } pub fn with_importer(&mut self, importer: MockImporter) { self.importer = Some(importer) } pub fn with_p2p(&mut self, p2p: MockP2P) { self.p2p = Some(p2p) } pub fn setup_script_tx(&mut self, tip: Word) -> Transaction { let (_, gas_coin) = self.setup_coin(); TransactionBuilder::script(vec![], vec![]) .tip(tip) .max_fee_limit(tip) .script_gas_limit(1000) .add_input(gas_coin) .finalize_as_transaction() } pub fn setup_coin(&mut self) -> (Coin, Input) { crate::test_helpers::setup_coin(&mut self.rng, Some(&self.mock_db)) } pub fn build(self) -> TestContext { let rng = RefCell::new(self.rng); let gas_price = 0; let config = self.config.unwrap_or_default(); let mock_db = self.mock_db; let mut p2p = self.p2p.unwrap_or_else(|| MockP2P::new_with_txs(vec![])); // set default handlers for p2p methods after test is set up, so they will be last on the FIFO // ordering of methods handlers: https://docs.rs/mockall/0.12.1/mockall/index.html#matching-multiple-calls p2p.expect_notify_gossip_transaction_validity() .returning(move |_, _| Ok(())); p2p.expect_broadcast_transaction() .returning(move |_| Ok(())); let importer = self .importer .unwrap_or_else(|| MockImporter::with_blocks(vec![])); let gas_price_provider = MockTxPoolGasPrice::new(gas_price); let mut consensus_parameters_provider = MockConsensusParametersProvider::default(); consensus_parameters_provider .expect_latest_consensus_parameters() .returning(|| Arc::new(Default::default())); let service = new_service( config, MockDBProvider(mock_db.clone()), importer, p2p, Default::default(), gas_price_provider, consensus_parameters_provider, ); TestContext { service, mock_db, rng, } } pub async fn build_and_start(self) -> TestContext { let context = self.build(); context.service.start_and_await().await.unwrap(); context } }