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> {
174 let result = block_on(async move {
175 let provider = match AlloyProvider::new(rpc_url, RPC_MAX_RETRIES).await {
176 Ok(provider) => provider,
177 Err(err) => return Err(CliError::BlockResolution(err.to_string())),
178 };
179 let arc = provider.provider_arc();
180 match arc.get_block_by_number(tag.to_alloy()).await {
181 Ok(block) => Ok(block),
182 Err(err) => Err(CliError::BlockResolution(err.to_string())),
183 }
184 })??;
185 let block = result.ok_or_else(|| {
186 CliError::BlockResolution(format!(
187 "eth_getBlockByNumber({}) returned no block",
188 tag.as_str()
189 ))
190 })?;
191 Ok(block.header.number)
192}
193
194pub(crate) fn block_on<F: Future>(fut: F) -> Result<F::Output, CliError> {
202 if tokio::runtime::Handle::try_current().is_ok() {
203 return Err(CliError::RuntimeNested);
204 }
205 let runtime = tokio::runtime::Builder::new_multi_thread()
206 .enable_all()
207 .build()
208 .map_err(|err| CliError::BlockResolution(err.to_string()))?;
209 Ok(runtime.block_on(fut))
210}
211
212pub fn resolve_chain_selector(selector: &str) -> Result<u64, CliError> {
219 let lower = selector.trim().to_ascii_lowercase();
220 if !lower.is_empty() {
221 if let Ok(chain_id) = lower.parse::<u64>() {
222 return Ok(chain_id);
223 }
224 }
225 match lower.as_str() {
226 "base" => Ok(8453),
227 "ethereum" | "eth" | "mainnet" => Ok(1),
228 "arbitrum" | "arb" | "arb1" => Ok(42_161),
229 _ => Err(CliError::UnknownChain {
230 chain: selector.to_string(),
231 }),
232 }
233}
234
235#[must_use]
237pub fn checksum(address: Address) -> String {
238 address.to_checksum(None)
239}