diff --git a/Cargo.lock b/Cargo.lock index 03f6c68..828921c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4670,19 +4670,23 @@ dependencies = [ "alloy-rpc-types", "anvil", "anvil-core", + "axum", "cast", "clap", "color-eyre", "forge-script", "foundry-common", "futures", + "http-body-util", "hyper 1.4.1", "op-alloy-rpc-types", "op-test-vectors", + "reqwest", "revm 12.1.0", "serde", "serde_json", "shellwords", + "thiserror", "tokio", "tracing", ] @@ -5426,10 +5430,12 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.26.0", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", "winreg", @@ -7282,6 +7288,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/Cargo.toml b/Cargo.toml index ca3e256..97a90f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ tokio = { version = "1", features = ["full"] } futures = "0.3" clap = { version = "4", features = ["derive"] } shellwords = "1" -reqwest = "0.12" +reqwest = { version = "0.12", features = ["stream"] } tracing-subscriber = "0.3.18" hashbrown = "0.14.5" diff --git a/bin/opt8n/Cargo.toml b/bin/opt8n/Cargo.toml index 3d1e2dd..2cbc4cd 100644 --- a/bin/opt8n/Cargo.toml +++ b/bin/opt8n/Cargo.toml @@ -16,6 +16,8 @@ tracing.workspace = true tokio.workspace = true futures.workspace = true color-eyre.workspace = true +axum = "0.7.5" +http-body-util = "0.1.2" # CLI clap.workspace = true @@ -36,4 +38,6 @@ revm.workspace = true # OP Types op-test-vectors.workspace = true op-alloy-rpc-types.workspace = true -hyper = "1.4.1" +thiserror.workspace = true +reqwest.workspace = true +hyper = "1.4.1" \ No newline at end of file diff --git a/bin/opt8n/src/cmd/script.rs b/bin/opt8n/src/cmd/script.rs index e75b094..b412eca 100644 --- a/bin/opt8n/src/cmd/script.rs +++ b/bin/opt8n/src/cmd/script.rs @@ -1,5 +1,5 @@ use anvil::cmd::NodeArgs; -use clap::Parser; +use clap::{Parser, ValueHint}; use color_eyre::eyre::eyre; use futures::StreamExt; @@ -9,14 +9,14 @@ use crate::opt8n::{Opt8n, Opt8nArgs}; pub struct ScriptArgs { #[command(flatten)] opt8n_args: Opt8nArgs, - #[command(flatten)] - inner: forge_script::ScriptArgs, + #[arg(value_hint = ValueHint::FilePath)] + pub path: String, #[command(flatten)] pub node_args: NodeArgs, } impl ScriptArgs { - pub async fn run(mut self) -> color_eyre::Result<()> { + pub async fn run(self) -> color_eyre::Result<()> { let opt8n = Opt8n::new( Some(self.node_args.clone()), self.opt8n_args.output.clone(), @@ -24,23 +24,28 @@ impl ScriptArgs { ) .await?; + let mut script_args = forge_script::ScriptArgs { + path: self.path.clone(), + ..Default::default() + }; + foundry_common::shell::set_shell(foundry_common::shell::Shell::from_args( - self.inner.opts.silent, - self.inner.json, + script_args.opts.silent, + script_args.json, ))?; - self.inner.broadcast = true; - self.inner.evm_opts.sender = Some( + script_args.broadcast = true; + script_args.evm_opts.sender = Some( opt8n .node_handle .genesis_accounts() .last() .expect("Could not get genesis account"), ); - self.inner.unlocked = true; - self.inner.evm_opts.fork_url = Some(opt8n.node_handle.http_endpoint()); + script_args.unlocked = true; + script_args.evm_opts.fork_url = Some(opt8n.node_handle.http_endpoint()); - run_script(opt8n, Box::new(self.inner)).await?; + run_script(opt8n, Box::new(script_args)).await?; Ok(()) } diff --git a/bin/opt8n/src/cmd/server.rs b/bin/opt8n/src/cmd/server.rs index 7ec514b..1bc23da 100644 --- a/bin/opt8n/src/cmd/server.rs +++ b/bin/opt8n/src/cmd/server.rs @@ -1,7 +1,21 @@ use anvil::cmd::NodeArgs; +use axum::body::{Body, Bytes}; +use axum::extract::State; +use axum::http::{Request, StatusCode}; +use axum::response::{IntoResponse, Response}; use clap::Parser; +use color_eyre::eyre::eyre; +use color_eyre::owo_colors::OwoColorize; +use futures::StreamExt; +use http_body_util::BodyExt; +use std::net::SocketAddr; +use std::sync::Arc; +use thiserror::Error; +use tokio::net::TcpListener; +use tokio::sync::mpsc::Sender; +use tokio::sync::Mutex; -use crate::opt8n::Opt8nArgs; +use crate::opt8n::{Opt8n, Opt8nArgs}; #[derive(Parser, Clone, Debug)] pub struct ServerArgs { @@ -13,6 +27,153 @@ pub struct ServerArgs { impl ServerArgs { pub async fn run(&self) -> color_eyre::Result<()> { - unimplemented!() + let opt8n = Opt8n::new( + Some(self.node_args.clone()), + self.opt8n_args.output.clone(), + self.opt8n_args.genesis.clone(), + ) + .await?; + + let opt8n = Arc::new(Mutex::new(opt8n)); + + let (dump_tx, mut dump_rx) = tokio::sync::mpsc::channel::<()>(1); + + let dump_fixture_router = axum::Router::new() + .route("/dump_fixture", axum::routing::post(dump_execution_fixture)) + .with_state((opt8n.clone(), dump_tx)); + + let router = axum::Router::new() + .route("/mine_prestate", axum::routing::post(mine_prestate)) + .fallback(fallback_handler) + .with_state(opt8n); + + let router = dump_fixture_router.merge(router); + + let addr: SocketAddr = ([127, 0, 0, 1], 0).into(); + let listener = TcpListener::bind(addr).await?; + let local_addr = listener.local_addr()?; + + let server = axum::serve(listener, router.into_make_service()); + let _ = println!("Opt8n server listening on: {:#?}", local_addr).green(); + + tokio::select! { + err = server => { + todo!("Handle server error: {:#?}", err); + } + + _ = dump_rx.recv() => { + let _ = println!("Exuction fixture dumped to: {:#?}", self.opt8n_args.output).green(); + + } + } + + Ok(()) + } +} + +async fn dump_execution_fixture( + State((opt8n, dump_tx)): State<(Arc>, Sender<()>)>, +) -> Result<(), ServerError> { + mine_block(opt8n).await?; + dump_tx.send(()).await.map_err(ServerError::SendError)?; + + Ok(()) +} + +async fn mine_prestate(State(opt8n): State>>) -> Result<(), ServerError> { + mine_block(opt8n.clone()).await?; + Ok(()) +} + +async fn mine_block(opt8n: Arc>) -> Result<(), ServerError> { + let mut opt8n = opt8n.lock().await; + + let mut new_blocks = opt8n.eth_api.backend.new_block_notifications(); + + opt8n.mine_block().await; + + let block = new_blocks + .next() + .await + .ok_or(eyre!("No new block")) + .map_err(ServerError::Opt8nError)?; + if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(block.hash) { + opt8n + .generate_execution_fixture(block) + .await + .map_err(ServerError::Opt8nError)?; + } + + Ok(()) +} + +async fn fallback_handler( + State(opt8n): State>>, + req: Request, +) -> Result<(), ServerError> { + let anvil_endpoint = opt8n.lock().await.node_handle.http_endpoint(); + proxy_to_anvil(req, anvil_endpoint).await?; + Ok(()) +} + +pub async fn proxy_to_anvil( + req: Request, + anvil_endpoint: String, +) -> Result, ServerError> { + let http_client = reqwest::Client::new(); + + let (headers, body) = req.into_parts(); + let body = body + .collect() + .await + .map_err(ServerError::AxumError)? + .to_bytes(); + + let axum_req: Request = Request::from_parts(headers, body); + let mut req = reqwest::Request::try_from(axum_req).expect("TODO: handle error"); + req.url_mut().set_fragment(Some(&anvil_endpoint)); + + let res: Response = http_client + .execute(req) + .await + .expect("TODO: handle error ") + .into(); + + let (headers, body) = res.into_parts(); + + let body = body + .collect() + .await + .map_err(ServerError::ReqwestError)? + .to_bytes() + .into(); + + Ok(Response::from_parts(headers, body)) +} + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("Opt8n error: {0}")] + Opt8nError(color_eyre::Report), + #[error("Axum error: {0}")] + AxumError(axum::Error), + #[error("Reqwest error: {0}")] + ReqwestError(reqwest::Error), + #[error("Senderror: {0}")] + SendError(tokio::sync::mpsc::error::SendError<()>), +} + +impl IntoResponse for ServerError { + fn into_response(self) -> Response { + let message = match self { + ServerError::Opt8nError(err) => err.to_string(), + ServerError::ReqwestError(err) => err.to_string(), + ServerError::AxumError(err) => err.to_string(), + ServerError::SendError(err) => err.to_string(), + }; + + let body = Body::from(message); + + (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() } } diff --git a/examples/exec-scripts/script/Counter.s.sol b/examples/exec-scripts/script/Counter.s.sol new file mode 100644 index 0000000..06b2e92 --- /dev/null +++ b/examples/exec-scripts/script/Counter.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +}