degenbot_cli_core/
block.rs1use std::future::Future;
16
17use alloy::eips::BlockNumberOrTag;
18use alloy::primitives::Address;
19use degenbot_rpc::provider::AlloyProvider;
20
21use crate::error::CliError;
22
23pub const DEFAULT_CHUNK_SIZE: u64 = 10_000;
25
26pub const DEFAULT_TO_BLOCK: &str = "latest:-64";
28
29pub const DEFAULT_VERIFY_ALL_INTERVAL: u64 = 1_000_000;
31
32const RPC_MAX_RETRIES: u32 = 5;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum BlockTag {
38 Latest,
40 Earliest,
42 Pending,
44 Safe,
46 Finalized,
48}
49
50impl BlockTag {
51 #[must_use]
53 pub fn parse(raw: &str) -> Option<Self> {
54 match raw {
55 "latest" => Some(Self::Latest),
56 "earliest" => Some(Self::Earliest),
57 "pending" => Some(Self::Pending),
58 "safe" => Some(Self::Safe),
59 "finalized" => Some(Self::Finalized),
60 _ => None,
61 }
62 }
63
64 #[must_use]
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::Latest => "latest",
69 Self::Earliest => "earliest",
70 Self::Pending => "pending",
71 Self::Safe => "safe",
72 Self::Finalized => "finalized",
73 }
74 }
75
76 #[must_use]
78 pub const fn to_alloy(self) -> BlockNumberOrTag {
79 match self {
80 Self::Latest => BlockNumberOrTag::Latest,
81 Self::Earliest => BlockNumberOrTag::Earliest,
82 Self::Pending => BlockNumberOrTag::Pending,
83 Self::Safe => BlockNumberOrTag::Safe,
84 Self::Finalized => BlockNumberOrTag::Finalized,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ToBlockSpec {
92 Number(u64),
94 Tip,
97 TagOffset {
99 tag: BlockTag,
101 offset: i64,
103 },
104}
105
106pub fn parse_to_block(raw: &str) -> Result<ToBlockSpec, CliError> {
113 if !raw.is_empty() && raw.bytes().all(|b| b.is_ascii_digit()) {
114 return raw
115 .parse::<u64>()
116 .map(ToBlockSpec::Number)
117 .map_err(|_| CliError::InvalidBlockTag(raw.to_string()));
118 }
119 let (tag_raw, offset) = match raw.split_once(':') {
120 Some((tag, offset)) => (
121 tag,
122 offset
123 .trim()
124 .parse::<i64>()
125 .map_err(|_| CliError::InvalidBlockTag(tag.to_string()))?,
126 ),
127 None => (raw, 0),
128 };
129 let tag =
130 BlockTag::parse(tag_raw).ok_or_else(|| CliError::InvalidBlockTag(tag_raw.to_string()))?;
131 if offset == 0 {
132 Ok(ToBlockSpec::Tip)
133 } else {
134 Ok(ToBlockSpec::TagOffset { tag, offset })
135 }
136}
137
138pub fn resolve_to_block(spec: ToBlockSpec, rpc_url: &str) -> Result<Option<u64>, CliError> {
148 match spec {
149 ToBlockSpec::Number(block) => Ok(Some(block)),
150 ToBlockSpec::Tip => Ok(None),
151 ToBlockSpec::TagOffset { tag, offset } => {
152 let base = fetch_tag_block_number(tag, rpc_url)?;
153 let signed = i128::from(base) + i128::from(offset);
154 let resolved = u64::try_from(signed).map_err(|_| {
155 CliError::BlockResolution(format!(
156 "block tag {}:{offset} resolved out of range (tag block {base})",
157 tag.as_str()
158 ))
159 })?;
160 Ok(Some(resolved))
161 }
162 }
163}
164
165fn fetch_tag_block_number(tag: BlockTag, rpc_url: &str) -> Result<u64, CliError> {
167 let provider = match block_on(AlloyProvider::new(rpc_url, RPC_MAX_RETRIES)) {
168 Ok(Ok(provider)) => provider,
169 Ok(Err(err)) => return Err(CliError::BlockResolution(err.to_string())),
170 Err(err) => return Err(err),
171 };
172 let arc = provider.provider_arc();
173 let result = block_on(async move { arc.get_block_by_number(tag.to_alloy()).await })?;
174 let block = result
175 .map_err(|err| CliError::BlockResolution(err.to_string()))?
176 .ok_or_else(|| {
177 CliError::BlockResolution(format!(
178 "eth_getBlockByNumber({}) returned no block",
179 tag.as_str()
180 ))
181 })?;
182 Ok(block.header.number)
183}
184
185pub(crate) fn block_on<F: Future>(fut: F) -> Result<F::Output, CliError> {
193 if tokio::runtime::Handle::try_current().is_ok() {
194 return Err(CliError::RuntimeNested);
195 }
196 let runtime = tokio::runtime::Builder::new_multi_thread()
197 .enable_all()
198 .build()
199 .map_err(|err| CliError::BlockResolution(err.to_string()))?;
200 Ok(runtime.block_on(fut))
201}
202
203pub fn resolve_chain_selector(selector: &str) -> Result<u64, CliError> {
210 let lower = selector.trim().to_ascii_lowercase();
211 if !lower.is_empty() {
212 if let Ok(chain_id) = lower.parse::<u64>() {
213 return Ok(chain_id);
214 }
215 }
216 match lower.as_str() {
217 "base" => Ok(8453),
218 "ethereum" | "eth" | "mainnet" => Ok(1),
219 "arbitrum" | "arb" | "arb1" => Ok(42_161),
220 _ => Err(CliError::UnknownChain {
221 chain: selector.to_string(),
222 }),
223 }
224}
225
226#[must_use]
228pub fn checksum(address: Address) -> String {
229 address.to_checksum(None)
230}