ethl 0.1.21

Tools for capturing, processing, archiving, and replaying Ethereum events
Documentation
use alloy::providers::{Provider, ProviderBuilder};
use alloy::transports::mock::Asserter;
use anyhow::Result;
use ethl::rpc::{
    RpcError,
    config::{CaptureTarget, ProviderSettings},
    heads::resolve_capture_target,
};

/// Minimal synthetic block JSON for `eth_getBlockByNumber`.
/// Only fields required by alloy's serde deserialization are included.
fn block_json(number: u64) -> serde_json::Value {
    serde_json::json!({
        "hash":             "0x0000000000000000000000000000000000000000000000000000000000000001",
        "parentHash":       "0x0000000000000000000000000000000000000000000000000000000000000000",
        "sha3Uncles":       "0x0000000000000000000000000000000000000000000000000000000000000000",
        "miner":            "0x0000000000000000000000000000000000000000",
        "stateRoot":        "0x0000000000000000000000000000000000000000000000000000000000000000",
        "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "receiptsRoot":     "0x0000000000000000000000000000000000000000000000000000000000000000",
        "logsBloom":        format!("0x{}", "0".repeat(512)),
        "difficulty":       "0x0",
        "number":           format!("{:#x}", number),
        "gasLimit":         "0x0",
        "gasUsed":          "0x0",
        "timestamp":        "0x0",
        "extraData":        "0x",
        "mixHash":          "0x0000000000000000000000000000000000000000000000000000000000000000",
        "nonce":            "0x0000000000000000",
        "transactions":     [],
        "uncles":           []
    })
}

fn mock_settings(asserter: Asserter) -> ProviderSettings {
    let provider = ProviderBuilder::new()
        .disable_recommended_fillers()
        .connect_mocked_client(asserter)
        .erased();
    ProviderSettings::from_mock(provider)
}

// === CaptureTarget::Latest ===

#[tokio::test]
async fn latest_returns_tip() -> Result<()> {
    let asserter = Asserter::new();
    asserter.push_success(&serde_json::json!("0x80")); // tip = 128
    let mut settings = mock_settings(asserter);
    settings.capture_target = CaptureTarget::Latest;

    let block = resolve_capture_target(&settings).await?;
    assert_eq!(block, 128);
    Ok(())
}

// === CaptureTarget::Lag ===

#[tokio::test]
async fn lag_subtracts_from_tip() -> Result<()> {
    let asserter = Asserter::new();
    asserter.push_success(&serde_json::json!("0x80")); // tip = 128
    let mut settings = mock_settings(asserter);
    settings.capture_target = CaptureTarget::Lag(10);

    let block = resolve_capture_target(&settings).await?;
    assert_eq!(block, 118); // 128 - 10
    Ok(())
}

#[tokio::test]
async fn lag_saturates_at_zero() -> Result<()> {
    let asserter = Asserter::new();
    asserter.push_success(&serde_json::json!("0x05")); // tip = 5
    let mut settings = mock_settings(asserter);
    settings.capture_target = CaptureTarget::Lag(100);

    let block = resolve_capture_target(&settings).await?;
    assert_eq!(block, 0);
    Ok(())
}

// === CaptureTarget::Finalized ===

#[tokio::test]
async fn finalized_returns_finalized_block_number() -> Result<()> {
    let asserter = Asserter::new();
    // eth_getBlockByNumber("finalized") → block at 64 (= 0x40)
    asserter.push_success(&block_json(64));
    let mut settings = mock_settings(asserter);
    settings.capture_target = CaptureTarget::Finalized;

    let block = resolve_capture_target(&settings).await?;
    assert_eq!(block, 64);
    Ok(())
}

#[tokio::test]
async fn finalized_null_response_is_error() -> Result<()> {
    let asserter = Asserter::new();
    // Provider returns null (no finalized block, e.g. pre-merge or unsupported).
    asserter.push_success(&serde_json::Value::Null);
    let mut settings = mock_settings(asserter);
    settings.capture_target = CaptureTarget::Finalized;

    let result = resolve_capture_target(&settings).await;
    assert!(
        matches!(result, Err(RpcError::Other(_))),
        "expected RpcError::Other for null finalized block, got {result:?}"
    );
    Ok(())
}