Skip to content

eth_watch/eth_watch_example_test.cpp

Functions

Name
int main()

Functions Documentation

function main

int main()

Source code

// Copyright 2026 Genius Ventures, Inc.
// SPDX-License-Identifier: MIT

#include <discv4/chain_peers.hpp>
#include <eth/eth_watch_cli.hpp>
#include <eth/eth_watch_service.hpp>

#include <boost/asio/io_context.hpp>

#include <functional>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

namespace {

struct TestRunner
{
    int run = 0;
    int failed = 0;

    void start(const std::string& name)
    {
        ++run;
        current = name;
        std::cout << "[ RUN      ] " << current << "\n";
    }

    void pass()
    {
        std::cout << "[       OK ] " << current << "\n";
    }

    void fail(const std::string& message)
    {
        ++failed;
        std::cout << "[  FAILED  ] " << current << ": " << message << "\n";
    }

    int finish() const
    {
        std::cout << "[==========] " << run << " test(s) ran\n";
        if (failed == 0)
        {
            std::cout << "[  PASSED  ] " << run << " test(s)\n";
            return 0;
        }
        std::cout << "[  FAILED  ] " << failed << " test(s)\n";
        return 1;
    }

    std::string current;
};

std::string make_enode(const std::string& ip, uint16_t port, char fill)
{
    return std::string("enode://")
        + std::string(128, fill)
        + "@"
        + ip
        + ":"
        + std::to_string(port);
}

std::string make_chain_json(
    const std::string& chain_name,
    uint64_t           network_id,
    const std::string& genesis_hex,
    const std::string& fork_id,
    const std::string& nodes_json,
    const std::string& bootnodes_json)
{
    return std::string("{\"") + chain_name + "\":{"
        + "\"networkId\":" + std::to_string(network_id) + ","
        + "\"genesisHex\":\"" + genesis_hex + "\","
        + "\"forkId\":\"" + fork_id + "\","
        + "\"forkNext\":\"0\","
        + "\"nodes\":" + nodes_json + ","
        + "\"bootnodes\":" + bootnodes_json
        + "}}";
}

std::string peer_json(const std::string& ip, uint16_t port, char fill)
{
    return std::string("{\"enode\":\"") + make_enode(ip, port, fill) + "\"}";
}

std::optional<discv4::ChainPeerConfig> load_config(
    const std::string& chain_name,
    const std::string& nodes_json,
    const std::string& bootnodes_json,
    uint64_t           network_id = 11155111,
    const std::string& genesis_hex = "25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9",
    const std::string& fork_id = "268956b6")
{
    return discv4::load_chain_peer_config_from_json_text(
        chain_name,
        make_chain_json(chain_name, network_id, genesis_hex, fork_id, nodes_json, bootnodes_json));
}

discv4::DialFn no_op_dial_fn()
{
    return [](
        discv4::ValidatedPeer,
        std::function<void(rlpx::DisconnectReason)> done,
        std::function<void(std::shared_ptr<rlpx::RlpxSession>)>,
        boost::asio::yield_context)
    {
        done(rlpx::DisconnectReason::kTcpError);
    };
}

bool expect(bool condition, const std::string& message, TestRunner& runner)
{
    if (!condition)
    {
        runner.fail(message);
        return false;
    }
    return true;
}

void test_service_starts_from_cached_chain_metadata(TestRunner& runner)
{
    runner.start("EthWatchExample.CachedChainMetadata");

    auto chain_config = load_config(
        "ethereum-sepolia",
        "[" + peer_json("10.0.0.1", 30303, 'a') + "]",
        "[" + peer_json("10.0.0.2", 30304, 'b') + "]");
    if (!expect(chain_config.has_value(), "failed to load chain metadata", runner)) { return; }

    auto watches = eth::cli::build_service_watch_specs({
        {"0x9af8050220D8C355CA3c6dC00a78B474cd3e3c70", "Transfer(address,address,uint256)"},
        {"0x9af8050220D8C355CA3c6dC00a78B474cd3e3c70", "BridgeSourceBurned(address,uint256,uint256,uint256,uint256)"},
    });
    if (!expect(watches.has_value(), "failed to build watch specs", runner)) { return; }

    eth::EthWatchConnectionConfig connection{};
    connection.max_connections_per_chain = 1;
    connection.max_total_connections = 1;

    auto service_config = eth::cli::build_service_config(
        connection,
        std::move(*watches),
        {*chain_config});
    service_config.dial_fn_factory = [](const discv4::ChainPeerConfig&) { return no_op_dial_fn(); };

    boost::asio::io_context io;
    eth::EthWatchService service;
    if (!expect(service.initialize(std::move(service_config), [](const eth::WatchEventNotification&) {}),
                "service rejected cache-backed config", runner)) { return; }
    service.run(io);

    auto queue = service.peer_queue("ethereum-sepolia");
    if (!expect(queue != nullptr, "peer queue not created", runner)) { return; }
    if (!expect(queue->cached_peer_count() == 1U, "cached peer was not enqueued", runner)) { return; }
    if (!expect(!queue->needs_discovery(), "cached-peer chain should not require fallback discovery", runner)) { return; }
    if (!expect(service.discv4_fallback_count() == 0U, "unexpected fallback discovery startup", runner)) { return; }

    runner.pass();
}

void test_gnosis_empty_nodes_uses_discovery_fallback(TestRunner& runner)
{
    runner.start("EthWatchExample.GnosisDiscoveryFallback");

    auto chain_config = load_config(
        "gnosis-chain",
        "[]",
        "[" + peer_json("10.0.0.3", 30305, 'c') + "]",
        100,
        "4f1dd23188aab3a0b3768e6a2b5f6cbf3fcb259af45d37b228a8a0ae61161f80",
        "06000064");
    if (!expect(chain_config.has_value(), "failed to load Gnosis metadata", runner)) { return; }
    if (!expect(chain_config->nodes.empty(), "Gnosis fixture should have empty nodes", runner)) { return; }
    if (!expect(chain_config->bootnodes.size() == 1U, "Gnosis fixture should have a bootnode", runner)) { return; }

    eth::EthWatchServiceConfig service_config{};
    service_config.chains.push_back(*chain_config);
    service_config.dial_fn_factory = [](const discv4::ChainPeerConfig&) { return no_op_dial_fn(); };
    service_config.discv4_fallback_starter = [](
        boost::asio::io_context&,
        const discv4::ChainPeerConfig& chain,
        std::shared_ptr<eth::EthPeerQueue> queue)
    {
        return chain.canonical_name == "gnosis-chain"
            && chain.nodes.empty()
            && !chain.bootnodes.empty()
            && queue
            && queue->needs_discovery();
    };

    boost::asio::io_context io;
    eth::EthWatchService service;
    if (!expect(service.initialize(std::move(service_config), [](const eth::WatchEventNotification&) {}),
                "service rejected discovery-fallback config", runner)) { return; }
    service.run(io);

    auto queue = service.peer_queue("gnosis-chain");
    if (!expect(queue != nullptr, "Gnosis peer queue not created", runner)) { return; }
    if (!expect(queue->cached_peer_count() == 0U, "Gnosis should not have cached peers", runner)) { return; }
    if (!expect(queue->discovery_bootnodes().size() == 1U, "Gnosis bootnode not preserved", runner)) { return; }
    if (!expect(queue->needs_discovery(), "Gnosis queue should need discovery", runner)) { return; }
    if (!expect(service.discv4_fallback_count() == 1U, "fallback discovery did not start", runner)) { return; }

    runner.pass();
}

void test_all_chain_service_config(TestRunner& runner)
{
    runner.start("EthWatchExample.AllChainsConfig");

    const std::vector<std::string> chains{
        "ethereum-mainnet",
        "polygon-mainnet",
        "bnb-smart-chain",
        "base-mainnet",
    };
    const std::vector<char> node_fills{'1', '2', '3', '4'};
    const std::vector<char> bootnode_fills{'5', '6', '7', '8'};

    std::vector<discv4::ChainPeerConfig> chain_configs;
    for (size_t i = 0; i < chains.size(); ++i)
    {
        const auto& chain = chains[i];
        auto config = load_config(
            chain,
            "[" + peer_json("10.0.1." + std::to_string(i + 1), 30303, node_fills[i]) + "]",
            "[" + peer_json("10.0.2." + std::to_string(i + 1), 30304, bootnode_fills[i]) + "]",
            1);
        if (!expect(config.has_value(), "failed to load metadata for " + chain, runner)) { return; }
        chain_configs.push_back(*config);
    }

    auto watches = eth::cli::build_service_watch_specs({
        {"", "Transfer(address,address,uint256)"},
    });
    if (!expect(watches.has_value(), "failed to build all-chain watch specs", runner)) { return; }

    auto service_config = eth::cli::build_service_config(
        eth::EthWatchConnectionConfig{},
        std::move(*watches),
        std::move(chain_configs));
    service_config.dial_fn_factory = [](const discv4::ChainPeerConfig&) { return no_op_dial_fn(); };

    boost::asio::io_context io;
    eth::EthWatchService service;
    if (!expect(service.initialize(std::move(service_config), [](const eth::WatchEventNotification&) {}),
                "service rejected all-chain config", runner)) { return; }
    service.run(io);

    if (!expect(service.runtime_chain_count() == chains.size(), "wrong runtime chain count", runner)) { return; }
    if (!expect(service.peer_queue_count() == chains.size(), "wrong peer queue count", runner)) { return; }
    if (!expect(service.scheduler_count() == chains.size(), "wrong scheduler count", runner)) { return; }

    runner.pass();
}

} // namespace

int main()
{
    TestRunner runner;
    std::cout << "[==========] eth_watch C++ example tests\n";
    test_service_starts_from_cached_chain_metadata(runner);
    test_gnosis_empty_nodes_uses_discovery_fallback(runner);
    test_all_chain_service_config(runner);
    return runner.finish();
}

Updated on 2026-06-05 at 17:22:18 -0700