use std::future::Future;
use alloy::eips::BlockNumberOrTag;
use alloy::primitives::Address;
use degenbot_rpc::provider::AlloyProvider;
use crate::error::{CliError, UnknownVariant};
pub const DEFAULT_CHUNK_SIZE: u64 = 10_000;
pub const DEFAULT_TO_BLOCK: &str = "latest:-64";
pub const DEFAULT_VERIFY_ALL_INTERVAL: u64 = 1_000_000;
const RPC_MAX_RETRIES: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockTag {
Latest,
Earliest,
Pending,
Safe,
Finalized,
}
impl BlockTag {
pub const ALL: [Self; 5] = [
Self::Latest,
Self::Earliest,
Self::Pending,
Self::Safe,
Self::Finalized,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Latest => "latest",
Self::Earliest => "earliest",
Self::Pending => "pending",
Self::Safe => "safe",
Self::Finalized => "finalized",
}
}
#[must_use]
pub const fn to_alloy(self) -> BlockNumberOrTag {
match self {
Self::Latest => BlockNumberOrTag::Latest,
Self::Earliest => BlockNumberOrTag::Earliest,
Self::Pending => BlockNumberOrTag::Pending,
Self::Safe => BlockNumberOrTag::Safe,
Self::Finalized => BlockNumberOrTag::Finalized,
}
}
}
impl TryFrom<&str> for BlockTag {
type Error = UnknownVariant;
fn try_from(raw: &str) -> Result<Self, Self::Error> {
match raw {
"latest" => Ok(Self::Latest),
"earliest" => Ok(Self::Earliest),
"pending" => Ok(Self::Pending),
"safe" => Ok(Self::Safe),
"finalized" => Ok(Self::Finalized),
_ => Err(UnknownVariant {
raw: raw.to_owned(),
known: Self::ALL.iter().map(|tag| tag.as_str()).collect(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToBlockSpec {
Number(u64),
Tip,
TagOffset {
tag: BlockTag,
offset: i64,
},
}
pub fn parse_to_block(raw: &str) -> Result<ToBlockSpec, CliError> {
if !raw.is_empty() && raw.bytes().all(|b| b.is_ascii_digit()) {
return raw
.parse::<u64>()
.map(ToBlockSpec::Number)
.map_err(|_| CliError::InvalidBlockTag(raw.to_string()));
}
let (tag_raw, offset) = match raw.split_once(':') {
Some((tag, offset)) => (
tag,
offset
.trim()
.parse::<i64>()
.map_err(|_| CliError::InvalidBlockTag(tag.to_string()))?,
),
None => (raw, 0),
};
let tag =
BlockTag::try_from(tag_raw).map_err(|_| CliError::InvalidBlockTag(tag_raw.to_string()))?;
if offset == 0 {
Ok(ToBlockSpec::Tip)
} else {
Ok(ToBlockSpec::TagOffset { tag, offset })
}
}
pub fn resolve_to_block(spec: ToBlockSpec, rpc_url: &str) -> Result<Option<u64>, CliError> {
match spec {
ToBlockSpec::Number(block) => Ok(Some(block)),
ToBlockSpec::Tip => Ok(None),
ToBlockSpec::TagOffset { tag, offset } => {
let base = fetch_tag_block_number(tag, rpc_url)?;
let signed = i128::from(base) + i128::from(offset);
let resolved = u64::try_from(signed).map_err(|_| {
CliError::BlockResolution(format!(
"block tag {}:{offset} resolved out of range (tag block {base})",
tag.as_str()
))
})?;
Ok(Some(resolved))
}
}
}
fn fetch_tag_block_number(tag: BlockTag, rpc_url: &str) -> Result<u64, CliError> {
let result = block_on(async move {
let provider = match AlloyProvider::new(rpc_url, RPC_MAX_RETRIES).await {
Ok(provider) => provider,
Err(err) => return Err(CliError::BlockResolution(err.to_string())),
};
let arc = provider.provider_arc();
match arc.get_block_by_number(tag.to_alloy()).await {
Ok(block) => Ok(block),
Err(err) => Err(CliError::BlockResolution(err.to_string())),
}
})??;
let block = result.ok_or_else(|| {
CliError::BlockResolution(format!(
"eth_getBlockByNumber({}) returned no block",
tag.as_str()
))
})?;
Ok(block.header.number)
}
pub(crate) fn block_on<F: Future>(fut: F) -> Result<F::Output, CliError> {
if tokio::runtime::Handle::try_current().is_ok() {
return Err(CliError::RuntimeNested);
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|err| CliError::BlockResolution(err.to_string()))?;
Ok(runtime.block_on(fut))
}
pub fn resolve_chain_selector(selector: &str) -> Result<u64, CliError> {
let lower = selector.trim().to_ascii_lowercase();
if !lower.is_empty() {
if let Ok(chain_id) = lower.parse::<u64>() {
return Ok(chain_id);
}
}
match lower.as_str() {
"base" => Ok(8453),
"ethereum" | "eth" | "mainnet" => Ok(1),
"arbitrum" | "arb" | "arb1" => Ok(42_161),
_ => Err(CliError::UnknownChain {
chain: selector.to_string(),
}),
}
}
#[must_use]
pub fn checksum(address: Address) -> String {
address.to_checksum(None)
}
#[cfg(test)]
mod tests {
#![expect(clippy::unwrap_used)]
use super::*;
#[test]
fn block_tag_try_from_round_trips_every_tag() {
for tag in BlockTag::ALL {
assert_eq!(BlockTag::try_from(tag.as_str()), Ok(tag));
}
}
#[test]
fn block_tag_try_from_unknown_names_the_raw_value_and_known_set() {
let err = BlockTag::try_from("noon").unwrap_err();
assert_eq!(err.raw, "noon");
let message = err.to_string();
assert!(
message.contains("noon"),
"raw value missing from: {message}"
);
assert!(
message.contains("latest") && message.contains("finalized"),
"known set missing from: {message}"
);
}
}