#![warn(unused_crate_dependencies)]

use alloy_primitives::{keccak256, Address, B256};
use reth_ethereum::{
    chainspec::ChainSpecBuilder,
    node::EthereumNode,
    primitives::{AlloyBlockHeader, SealedBlock, SealedHeader},
    provider::{
        providers::ReadOnlyConfig, AccountReader, BlockNumReader, BlockReader, BlockSource,
        HeaderProvider, ReceiptProvider, StateProvider, TransactionVariant, TransactionsProvider,
    },
    rpc::eth::primitives::Filter,
    TransactionSigned,
};

// Providers are zero cost abstractions on top of an opened MDBX Transaction
// exposing a familiar API to query the chain's information without requiring knowledge
// of the inner tables.
//
// These abstractions do not include any caching and the user is responsible for doing that.
// Other parts of the code which include caching are parts of the `EthApi` abstraction.
fn main() -> eyre::Result<()> {
    // The path to data directory, e.g. "~/.local/reth/share/mainnet"
    let datadir = std::env::var("RETH_DATADIR")?;

    // Instantiate a provider factory for Ethereum mainnet using the provided datadir path.
    let spec = ChainSpecBuilder::mainnet().build();
    let runtime = reth_ethereum::tasks::Runtime::test();
    let factory = EthereumNode::provider_factory_builder().open_read_only(
        spec.into(),
        ReadOnlyConfig::from_datadir(datadir),
        runtime,
    )?;

    // This call opens a RO transaction on the database. To write to the DB you'd need to call
    // the `provider_rw` function and look for the `Writer` variants of the traits.
    let provider = factory.provider()?;

    // Run basic queries against the DB
    let block_num = 100;
    header_provider_example(&provider, block_num)?;
    block_provider_example(&provider, block_num)?;
    txs_provider_example(&provider)?;
    receipts_provider_example(&provider)?;

    state_provider_example(factory.latest()?, &provider, provider.best_block_number()?)?;
    state_provider_example(factory.history_by_block_number(block_num)?, &provider, block_num)?;

    // Closes the RO transaction opened in the `factory.provider()` call. This is optional and
    // would happen anyway at the end of the function scope.
    drop(provider);

    Ok(())
}

/// The `HeaderProvider` allows querying the headers-related tables.
fn header_provider_example<T: HeaderProvider>(provider: T, number: u64) -> eyre::Result<()> {
    // Can query the header by number
    let header = provider.header_by_number(number)?.ok_or(eyre::eyre!("header not found"))?;

    // We can convert a header to a sealed header which contains the hash w/o needing to recompute
    // it every time.
    let sealed_header = SealedHeader::seal_slow(header);

    // Can also query the header by hash!
    let header_by_hash =
        provider.header(sealed_header.hash())?.ok_or(eyre::eyre!("header by hash not found"))?;
    assert_eq!(sealed_header.header(), &header_by_hash);

    // Can query headers by range as well, already sealed!
    let headers = provider.sealed_headers_range(100..200)?;
    assert_eq!(headers.len(), 100);

    Ok(())
}

/// The `TransactionsProvider` allows querying transaction-related information
fn txs_provider_example<T: TransactionsProvider<Transaction = TransactionSigned>>(
    provider: T,
) -> eyre::Result<()> {
    // Try the 5th tx
    let txid = 5;

    // Query a transaction by its primary ordered key in the db
    let tx = provider.transaction_by_id(txid)?.ok_or(eyre::eyre!("transaction not found"))?;

    // Can query the tx by hash
    let tx_by_hash =
        provider.transaction_by_hash(*tx.tx_hash())?.ok_or(eyre::eyre!("txhash not found"))?;
    assert_eq!(tx, tx_by_hash);

    // Can query the tx by hash with info about the block it was included in
    let (tx, meta) = provider
        .transaction_by_hash_with_meta(*tx.tx_hash())?
        .ok_or(eyre::eyre!("txhash not found"))?;
    assert_eq!(*tx.tx_hash(), meta.tx_hash);

    // Can reverse lookup the key too
    let id = provider.transaction_id(*tx.tx_hash())?.ok_or(eyre::eyre!("txhash not found"))?;
    assert_eq!(id, txid);

    // Can query the txs in the range [100, 200)
    let _txs_by_tx_range = provider.transactions_by_tx_range(100..200)?;
    // Can query the txs in the _block_ range [100, 200)]
    let _txs_by_block_range = provider.transactions_by_block_range(100..200)?;

    Ok(())
}

/// The `BlockReader` allows querying the headers-related tables.
fn block_provider_example<T: BlockReader<Block = reth_ethereum::Block>>(
    provider: T,
    number: u64,
) -> eyre::Result<()> {
    // Can query a block by number
    let block = provider.block(number.into())?.ok_or(eyre::eyre!("block num not found"))?;
    assert_eq!(block.number, number);

    // Can query a block with its senders, this is useful when you want to execute a block and do
    // not want to manually recover the senders for each transaction (as each transaction is
    // stored on disk with its v,r,s but not its `from` field.).
    let _recovered_block = provider
        .sealed_block_with_senders(number.into(), TransactionVariant::WithHash)?
        .ok_or(eyre::eyre!("block num not found"))?;

    // Can seal the block to cache the hash, like the Header above.
    let sealed_block = SealedBlock::seal_slow(block.clone());

    // Can also query the block by hash directly
    let block_by_hash = provider
        .block_by_hash(sealed_block.hash())?
        .ok_or(eyre::eyre!("block by hash not found"))?;
    assert_eq!(block, block_by_hash);

    // Or by relying in the internal conversion
    let block_by_hash2 = provider
        .block(sealed_block.hash().into())?
        .ok_or(eyre::eyre!("block by hash not found"))?;
    assert_eq!(block, block_by_hash2);

    // Or you can also specify the datasource. For this provider this always returns `None`, but
    // the blockchain tree is also able to access pending state not available in the db yet.
    let block_by_hash3 = provider
        .find_block_by_hash(sealed_block.hash(), BlockSource::Any)?
        .ok_or(eyre::eyre!("block hash not found"))?;
    assert_eq!(block, block_by_hash3);
    Ok(())
}

/// The `ReceiptProvider` allows querying the receipts tables.
fn receipts_provider_example<
    T: ReceiptProvider<Receipt = reth_ethereum::Receipt>
        + TransactionsProvider<Transaction = TransactionSigned>
        + HeaderProvider,
>(
    provider: T,
) -> eyre::Result<()> {
    let txid = 5;
    let header_num = 100;

    // Query a receipt by txid
    let receipt = provider.receipt(txid)?.ok_or(eyre::eyre!("tx receipt not found"))?;

    // Can query receipt by txhash too
    let tx = provider.transaction_by_id(txid)?.unwrap();
    let receipt_by_hash = provider
        .receipt_by_hash(*tx.tx_hash())?
        .ok_or(eyre::eyre!("tx receipt by hash not found"))?;
    assert_eq!(receipt, receipt_by_hash);

    // Can query all the receipts in a block
    let _receipts = provider
        .receipts_by_block(100.into())?
        .ok_or(eyre::eyre!("no receipts found for block"))?;

    // Can check if an address/topic filter is present in a header, if it is we query the block and
    // receipts and do something with the data
    // 1. get the bloom from the header
    let header = provider.header_by_number(header_num)?.unwrap();
    let bloom = header.logs_bloom();

    // 2. Construct the address/topics filters. topic0 always refers to the event signature, so
    // filter it with event_signature() (or use the .event() helper). The remaining helpers map to
    // the indexed parameters in declaration order (topic1 -> first indexed param, etc).
    let contract_addr = Address::random();
    let indexed_from = Address::random();
    let indexed_to = Address::random();
    let transfer_signature = keccak256("Transfer(address,address,uint256)");

    // This matches ERC-20 Transfer events emitted by contract_addr where both indexed addresses are
    // fixed. If your event declares a third indexed parameter, continue with topic3(...).
    let filter = Filter::new()
        .address(contract_addr)
        .event_signature(transfer_signature)
        .topic1(indexed_from)
        .topic2(indexed_to);

    // 3. If the address & topics filters match do something. We use the outer check against the
    // bloom filter stored in the header to avoid having to query the receipts table when there
    // is no instance of any event that matches the filter in the header.
    if filter.matches_bloom(bloom) {
        let receipts = provider.receipt(header_num)?.ok_or(eyre::eyre!("receipt not found"))?;
        for log in &receipts.logs {
            if filter.matches(log) {
                // Do something with the log e.g. decode it.
                println!("Matching log found! {log:?}")
            }
        }
    }

    Ok(())
}

/// The `StateProvider` allows querying the state tables.
fn state_provider_example<T: StateProvider + AccountReader, H: HeaderProvider>(
    provider: T,
    headers: &H,
    number: u64,
) -> eyre::Result<()> {
    let address = Address::random();
    let storage_key = B256::random();
    let slots = [storage_key];

    let header = headers.header_by_number(number)?.ok_or(eyre::eyre!("header not found"))?;
    let state_root = header.state_root();

    // Can get account / storage state with simple point queries
    let account = provider.basic_account(&address)?;
    let code = provider.account_code(&address)?;
    let storage_value = provider.storage(address, storage_key)?;

    println!(
        "state at block #{number}: addr={address:?}, nonce={}, balance={}, storage[{:?}]={:?}, has_code={}",
        account.as_ref().map(|acc| acc.nonce).unwrap_or_default(),
        account.as_ref().map(|acc| acc.balance).unwrap_or_default(),
        storage_key,
        storage_value,
        code.is_some()
    );

    // Returns a bundled proof with the account's info
    let proof = provider.proof(Default::default(), address, &slots)?;

    // Can verify the returned proof against the state root
    proof.verify(state_root)?;
    println!("account proof verified against state root {state_root:?}");

    Ok(())
}
