Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(opt8n): Implement opt8n server #66

Merged
merged 16 commits into from
Aug 21, 2024
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
4 changes: 4 additions & 0 deletions bin/opt8n/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,3 +38,5 @@ revm.workspace = true
# OP Types
op-test-vectors.workspace = true
op-alloy-rpc-types.workspace = true
thiserror.workspace = true
reqwest.workspace = true
3 changes: 3 additions & 0 deletions bin/opt8n/src/cmd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod repl;
pub mod script;
pub mod server;
111 changes: 111 additions & 0 deletions bin/opt8n/src/cmd/repl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use anvil::cmd::NodeArgs;
use clap::{CommandFactory, FromArgMatches, Parser};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, BufReader};

use crate::opt8n::{Opt8n, Opt8nArgs};

#[derive(Parser, Clone, Debug)]
pub struct ReplArgs {
#[command(flatten)]
opt8n_args: Opt8nArgs,
#[command(flatten)]
pub node_args: NodeArgs,
}

impl ReplArgs {
pub async fn run(&self) -> color_eyre::Result<()> {
let mut opt8n = Opt8n::new(
Some(self.node_args.clone()),
self.opt8n_args.output.clone(),
self.opt8n_args.genesis.clone(),
)
.await?;

repl(&mut opt8n).await?;

Ok(())
}
}

#[derive(Parser, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[clap(rename_all = "snake_case", infer_subcommands = true, multicall = true)]
pub enum ReplCommand {
#[command(visible_alias = "a")]
Anvil {
#[arg(index = 1, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(visible_alias = "c")]
Cast {
#[arg(index = 1, allow_hyphen_values = true)]
args: Vec<String>,
},
Dump,
RpcEndpoint,
// TODO: implement clear
// TODO: implement reset
#[command(visible_alias = "e")]
Exit,
}

/// Listens for commands, and new blocks from the block stream.
pub async fn repl(opt8n: &mut Opt8n) -> color_eyre::Result<()> {
let mut new_blocks = opt8n.eth_api.backend.new_block_notifications();

loop {
tokio::select! {
command = receive_command() => {
match command {
Ok(ReplCommand::Exit) => break,
Ok(command) => execute(opt8n, command).await?,
Err(e) => eprintln!("Error: {:?}", e),
}
}

new_block = new_blocks.next() => {
if let Some(new_block) = new_block {
if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(new_block.hash) {
opt8n.generate_execution_fixture(block).await?;
}
}
}
}
}

Ok(())
}

async fn receive_command() -> color_eyre::Result<ReplCommand> {
let line = BufReader::new(tokio::io::stdin())
.lines()
.next_line()
.await?
.unwrap();
let words = shellwords::split(&line)?;

let matches = ReplCommand::command().try_get_matches_from(words)?;
Ok(ReplCommand::from_arg_matches(&matches)?)
}

async fn execute(opt8n: &mut Opt8n, command: ReplCommand) -> color_eyre::Result<()> {
match command {
ReplCommand::Dump => {
opt8n.mine_block().await;
}
ReplCommand::Anvil { mut args } => {
args.insert(0, "anvil".to_string());
let command = NodeArgs::command_for_update();
let matches = command.try_get_matches_from(args)?;
let node_args = NodeArgs::from_arg_matches(&matches)?;
node_args.run().await?;
}
ReplCommand::Cast { .. } => {}
ReplCommand::RpcEndpoint => {
println!("{}", opt8n.node_handle.http_endpoint());
}
ReplCommand::Exit => unreachable!(),
}
Ok(())
}
133 changes: 133 additions & 0 deletions bin/opt8n/src/cmd/script.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use anvil::cmd::NodeArgs;
use clap::{Parser, ValueHint};
use color_eyre::eyre::eyre;
use futures::StreamExt;

use crate::opt8n::{Opt8n, Opt8nArgs};

#[derive(Parser, Clone, Debug)]
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(self) -> color_eyre::Result<()> {
let opt8n = Opt8n::new(
Some(self.node_args.clone()),
self.opt8n_args.output.clone(),
self.opt8n_args.genesis.clone(),
)
.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(
script_args.opts.silent,
script_args.json,
))?;

script_args.broadcast = true;
script_args.evm_opts.sender = Some(
opt8n
.node_handle
.genesis_accounts()
.last()
.expect("Could not get genesis account"),
);
script_args.unlocked = true;
script_args.evm_opts.fork_url = Some(opt8n.node_handle.http_endpoint());

run_script(opt8n, Box::new(script_args)).await?;

Ok(())
}
}

/// Run a Forge script with the given arguments, and generate an execution fixture
/// from the broadcasted transactions.
pub async fn run_script(
opt8n: Opt8n,
script_args: Box<forge_script::ScriptArgs>,
) -> color_eyre::Result<()> {
let mut new_blocks = opt8n.eth_api.backend.new_block_notifications();

// Run the forge script and broadcast the transactions to the anvil node
let mut opt8n = broadcast_transactions(opt8n, script_args).await?;

// Mine the block and generate the execution fixture
opt8n.mine_block().await;

let block = new_blocks.next().await.ok_or(eyre!("No new block"))?;
if let Some(block) = opt8n.eth_api.backend.get_block_by_hash(block.hash) {
opt8n.generate_execution_fixture(block).await?;
}

Ok(())
}

async fn broadcast_transactions(
opt8n: Opt8n,
script_args: Box<forge_script::ScriptArgs>,
) -> color_eyre::Result<Opt8n> {
// Run the script, compile the transactions and broadcast to the anvil instance
let compiled = script_args.preprocess().await?.compile()?;

let pre_simulation = compiled
.link()
.await?
.prepare_execution()
.await?
.execute()
.await?
.prepare_simulation()
.await?;

let bundled = pre_simulation.fill_metadata().await?.bundle().await?;

let tx_count = bundled
.sequence
.sequences()
.iter()
.fold(0, |sum, sequence| sum + sequence.transactions.len());

// TODO: break into function
let broadcast = bundled.broadcast();

let pending_transactions = tokio::task::spawn(async move {
loop {
let pending_tx_count = opt8n
.eth_api
.txpool_content()
.await
.expect("Failed to get txpool content")
.pending
.len();

if pending_tx_count == tx_count {
return opt8n;
}
}
});

let opt8n = tokio::select! {
_ = broadcast => {
// TODO: Gracefully handle this error
return Err(eyre!("Script failed early"));
},
opt8n = pending_transactions => {
opt8n?
}
};

Ok(opt8n)
}
Loading