diff --git a/packages/fuels-test-helpers/Cargo.toml b/packages/fuels-test-helpers/Cargo.toml index 7a41ceab25..c246db0256 100644 --- a/packages/fuels-test-helpers/Cargo.toml +++ b/packages/fuels-test-helpers/Cargo.toml @@ -31,6 +31,10 @@ tempfile = { workspace = true, default-features = false } tokio = { workspace = true, default-features = false } which = { workspace = true, default-features = false } +[dev-dependencies] +testcontainers = { version = "0.23.0", features = ["blocking"] } +ethers = { version = "=2.0.8" } + [features] default = ["fuels-accounts", "std"] std = ["fuels-accounts?/std", "fuels-core/std", "fuel-core-chain-config/std"] diff --git a/packages/fuels-test-helpers/src/fuel_bin_service.rs b/packages/fuels-test-helpers/src/fuel_bin_service.rs index 9e52bec05d..cc6dd3e0ba 100644 --- a/packages/fuels-test-helpers/src/fuel_bin_service.rs +++ b/packages/fuels-test-helpers/src/fuel_bin_service.rs @@ -15,12 +15,19 @@ use tokio::{process::Command, spawn, task::JoinHandle, time::sleep}; use crate::node_types::{DbType, NodeConfig, Trigger}; +#[derive(Debug)] +pub struct RelayerConfig { + pub relayer: String, + pub relayer_v2_listening_contracts: String, +} + #[derive(Debug)] pub(crate) struct ExtendedConfig { pub node_config: NodeConfig, pub chain_config: ChainConfig, pub state_config: StateConfig, pub snapshot_dir: TempDir, + pub relayer_config: Option, } impl ExtendedConfig { @@ -74,6 +81,15 @@ impl ExtendedConfig { } }; + if let Some(relayer_config) = &self.relayer_config { + args.push("--enable-relayer".to_string()); + args.push("--relayer".to_string()); + args.push(relayer_config.relayer.clone()); + + args.push("--relayer-v2-listening-contracts".to_string()); + args.push(relayer_config.relayer_v2_listening_contracts.clone()); + } + let body_limit = self.node_config.graphql_request_body_bytes_limit; args.push(format!("--graphql-request-body-bytes-limit={body_limit}")); @@ -150,6 +166,49 @@ impl FuelService { state_config, chain_config, snapshot_dir: tempdir()?, + relayer_config: None, + }; + + let addr = extended_config.node_config.addr; + let handle = run_node(extended_config).await?; + server_health_check(addr).await?; + + Ok(FuelService { + bound_address, + handle, + }) + } + + pub async fn new_node_with_relayer( + node_config: NodeConfig, + chain_config: ChainConfig, + state_config: StateConfig, + relayer_config: RelayerConfig, + ) -> FuelResult { + let requested_port = node_config.addr.port(); + + let bound_address = match requested_port { + 0 => get_socket_address()?, + _ if is_free(requested_port) => node_config.addr, + _ => { + return Err(error!( + IO, + "could not find a free port to start a fuel node" + )) + } + }; + + let node_config = NodeConfig { + addr: bound_address, + ..node_config + }; + + let extended_config = ExtendedConfig { + node_config, + state_config, + chain_config, + snapshot_dir: tempdir()?, + relayer_config: Some(relayer_config), }; let addr = extended_config.node_config.addr; diff --git a/packages/fuels-test-helpers/src/lib.rs b/packages/fuels-test-helpers/src/lib.rs index 659eae860d..31face4e45 100644 --- a/packages/fuels-test-helpers/src/lib.rs +++ b/packages/fuels-test-helpers/src/lib.rs @@ -20,6 +20,8 @@ mod node_types; #[cfg(not(feature = "fuel-core-lib"))] pub(crate) mod fuel_bin_service; +#[cfg(not(feature = "fuel-core-lib"))] +use fuel_bin_service::RelayerConfig; #[cfg(feature = "fuels-accounts")] mod accounts; @@ -153,6 +155,40 @@ pub async fn setup_test_provider( Provider::from(address).await } +#[cfg(not(feature = "fuel-core-lib"))] +pub async fn setup_test_provider_with_relayer( + coins: Vec, + messages: Vec, + node_config: Option, + chain_config: Option, + relayer_config: RelayerConfig, +) -> Result { + let node_config = node_config.unwrap_or_default(); + let chain_config = chain_config.unwrap_or_else(testnet_chain_config); + + let coin_configs = into_coin_configs(coins); + let message_configs = into_message_configs(messages); + + let state_config = StateConfig { + coins: coin_configs, + messages: message_configs, + ..StateConfig::local_testnet() + }; + + let srv = + FuelService::start_with_relayer(node_config, chain_config, state_config, relayer_config) + .await?; + + let address = srv.bound_address(); + + tokio::spawn(async move { + let _own_the_handle = srv; + let () = futures::future::pending().await; + }); + + Provider::from(address).await +} + // Testnet ChainConfig with increased tx size and contract size limits fn testnet_chain_config() -> ChainConfig { let mut consensus_parameters = ConsensusParameters::default(); @@ -173,11 +209,22 @@ pub fn generate_random_salt() -> [u8; 32] { #[cfg(test)] mod tests { - use std::net::{Ipv4Addr, SocketAddr}; + use std::{ + net::{Ipv4Addr, SocketAddr}, + time::{Duration, Instant}, + }; use fuel_tx::{ConsensusParameters, ContractParameters, FeeParameters, TxParameters}; use fuels_core::types::bech32::FUEL_BECH32_HRP; + use testcontainers::{ + core::{IntoContainerPort, WaitFor}, + runners::AsyncRunner, + GenericImage, ImageExt, + }; + + use ethers::providers::{Http, Provider}; + use super::*; #[tokio::test] @@ -382,4 +429,91 @@ mod tests { ); Ok(()) } + + #[tokio::test] + #[cfg(not(feature = "fuel-core-lib"))] + async fn test_setup_test_provider_with_relayer() -> Result<()> { + let container = GenericImage::new( + "ghcr.io/foundry-rs/foundry", + "nightly-4351742481c98adaa9ca3e8642e619aa986b3cee", + ) + .with_wait_for(WaitFor::message_on_stdout("Listening on 0.0.0.0:8545")) + .with_exposed_port(8545.tcp()) + .with_entrypoint("anvil") + .with_cmd(["--host", "0.0.0.0", "--slots-in-an-epoch", "1"]) + .with_mapped_port(8545, 8545.into()) + .start() + .await + .expect("Anvil did not start"); + + let eth_provider = Provider::::try_from("http://127.0.0.1:8545") + .expect("Could not instantiate ethereum provider"); + + let socket = SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 4000); + let node_config = NodeConfig { + addr: socket, + debug: true, + block_production: Trigger::Interval { + block_time: Duration::from_secs(1), + }, + ..NodeConfig::default() + }; + + let relayer_config = RelayerConfig { + relayer: "http://localhost:8545".to_string(), + relayer_v2_listening_contracts: "0x0000000000000000000000000000000000000000" + .to_string(), + }; + + let fuel_provider = setup_test_provider_with_relayer( + vec![], + vec![], + Some(node_config.clone()), + None, + relayer_config, + ) + .await?; + + let node_info = fuel_provider + .node_info() + .await + .expect("Failed to retrieve node info!"); + + assert_eq!(fuel_provider.url(), format!("http://127.0.0.1:4000")); + assert_eq!(node_info.utxo_validation, node_config.utxo_validation); + + // Mine some eth blocks + let _: () = eth_provider + // Magic number: 128 is meant to be 4 times an epoch, so it finalizes eth blocks + .request("anvil_mine", vec![ethers::types::U256::from(128u64)]) + .await + .expect("Could not mine blocks on Ethereum chain"); + + let start_time = Instant::now(); + let timeout = Duration::from_secs(10); + loop { + // Check if we've exceeded the timeout + if start_time.elapsed() > timeout { + return Err("Timeout reached while waiting for positive DA height".into()); + } + + let height = fuel_provider.latest_block_height().await?; + + let latest_block = fuel_provider + .block_by_height(height.into()) + .await? + .ok_or("Latest block not found")?; + + if latest_block.header.da_height > 0 { + break; + } + + // Wait for the production of a new fuel block + tokio::time::sleep(Duration::from_secs(1)).await; + } + + let _ = container.stop().await; + + Ok(()) + } } diff --git a/packages/fuels-test-helpers/src/service.rs b/packages/fuels-test-helpers/src/service.rs index 0e40b812a4..067b944600 100644 --- a/packages/fuels-test-helpers/src/service.rs +++ b/packages/fuels-test-helpers/src/service.rs @@ -7,7 +7,7 @@ use fuel_core_services::State; use fuels_core::types::errors::{error, Result}; #[cfg(not(feature = "fuel-core-lib"))] -use crate::fuel_bin_service::FuelService as BinFuelService; +use crate::fuel_bin_service::{FuelService as BinFuelService, RelayerConfig}; use crate::NodeConfig; pub struct FuelService { @@ -43,6 +43,29 @@ impl FuelService { }) } + #[cfg(not(feature = "fuel-core-lib"))] + pub async fn start_with_relayer( + node_config: NodeConfig, + chain_config: ChainConfig, + state_config: StateConfig, + relayer_config: RelayerConfig, + ) -> Result { + let service = BinFuelService::new_node_with_relayer( + node_config, + chain_config, + state_config, + relayer_config, + ) + .await?; + + let bound_address = service.bound_address; + + Ok(FuelService { + service, + bound_address, + }) + } + pub async fn stop(&self) -> Result { #[cfg(feature = "fuel-core-lib")] let result = self.service.send_stop_signal_and_await_shutdown().await;