Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion fynd-rpc/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ infrastructure.
|---|---|
| `builder.rs` | `FyndRPCBuilder` wraps `FyndBuilder`, adds HTTP server config. `FyndRPC` struct runs the server with graceful shutdown |
| `config.rs` | `WorkerPoolsConfig` (TOML loader), `BlocklistConfig`, `defaults` module re-exporting `fynd-core` defaults + HTTP-specific ones |
| `protocols.rs` | `fetch_protocol_systems()` — paginated Tycho RPC call to discover available protocols; `resolve_protocols()` — higher-level wrapper used by `serve` and `scale` that expands `all_onchain`/`native_onchain` tokens and applies min-TVL filtering |
| `protocols.rs` | `fetch_protocol_systems()` — paginated Tycho RPC call to discover available protocols; `resolve_protocols()` — higher-level wrapper used by `serve` and `scale` that expands `all_onchain`/`native_onchain` tokens and applies min-TVL filtering; `fetch_token_pool_stats()` — per-token pool counts and decimals (used by the benchmark `generate-requests` subcommand) |
| `api/` | HTTP endpoint handlers and OpenAPI documentation |

## Features
Expand Down
214 changes: 213 additions & 1 deletion fynd-rpc/src/protocols.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
//! Tycho protocol system discovery.

use std::{collections::HashMap, future::Future, time::Duration};

use anyhow::{bail, Result};
use tokio::time::timeout;
use tracing::info;
use tycho_simulation::{
tycho_client::rpc::{HttpRPCClient, HttpRPCClientOptions, ProtocolSystemsParams, RPCClient},
tycho_client::rpc::{
AllTokensParams, HttpRPCClient, HttpRPCClientOptions, ProtocolComponentsPaginatedParams,
ProtocolSystemsParams, RPCClient,
},
tycho_common::models::Chain,
};

use crate::config::defaults::MIN_TOKEN_QUALITY;

/// Concurrency for the paginated Tycho component/token fetches in [`fetch_token_pool_stats`].
const FETCH_CONCURRENCY: usize = 8;

/// Expansion token: fetch every on-chain protocol system from Tycho.
const ALL_ONCHAIN: &str = "all_onchain";
/// Expansion token: like [`ALL_ONCHAIN`] but drop VM-simulated protocols (those prefixed `vm:`),
Expand Down Expand Up @@ -85,3 +96,204 @@ pub async fn resolve_protocols(
}
Ok(protocols)
}

/// Per-token liquidity statistics derived from Tycho protocol components.
///
/// `pool_count` mirrors the connector-token score in the `derive-connector-tokens` command: the
/// number of pools a token appears in, a proxy for how much traffic it attracts.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TokenPoolStats {
/// 0x-prefixed lowercase hex address.
pub address: String,
/// Token symbol as reported by Tycho (may be empty for exotic tokens).
pub symbol: String,
/// Number of decimals; used to scale synthetic amounts into raw units.
pub decimals: u32,
/// Number of pools the token appears in across the queried protocol systems.
pub pool_count: usize,
}

/// Inputs to [`fetch_token_pool_stats`].
///
/// Bundles the Tycho connection parameters with the two capacity-testing knobs: a per-fetch
/// client-side `fetch_timeout` so a degraded indexer fails fast instead of hanging, and an optional
/// `min_tvl` passed to Tycho as `tvl_gt`. The dedicated `tycho-fynd-*` indexer endpoints plan-gate
/// component queries and reject them unless `min_tvl` is set.
pub struct TokenPoolStatsParams<'a> {
/// Tycho RPC host:port (no scheme).
pub tycho_url: &'a str,
/// Optional Tycho auth key.
pub auth_key: Option<&'a str>,
/// Connect over HTTPS when true, HTTP otherwise.
pub use_tls: bool,
/// Target chain.
pub chain: Chain,
/// Protocol systems to query for components.
pub protocols: &'a [String],
/// When set, only pools whose TVL exceeds this value (native-token units) are counted, passed
/// as `tvl_gt`. Required by the `tycho-fynd-*` endpoints, which plan-gate component queries.
pub min_tvl: Option<f64>,
/// Client-side deadline applied to each per-protocol component fetch and the token-metadata
/// fetch.
pub fetch_timeout: Duration,
}

/// Awaits a single Tycho fetch under a client-side deadline.
///
/// On completion the inner RPC error is surfaced unchanged; on a timeout the `on_timeout` message
/// is returned instead. Without this a degraded indexer leaves the fetch hanging indefinitely
/// (observed byte-flat stalls exceeding 18 minutes against `tycho-base-beta`).
async fn await_with_timeout<T, E, Fut>(
fetch: Fut,
timeout_after: Duration,
on_timeout: impl FnOnce() -> String,
) -> Result<T>
where
Fut: Future<Output = std::result::Result<T, E>>,
E: std::error::Error + Send + Sync + 'static,
{
match timeout(timeout_after, fetch).await {
Ok(result) => result.map_err(anyhow::Error::from),
Err(_elapsed) => Err(anyhow::anyhow!(on_timeout())),
}
}

/// Fetches per-token pool counts and metadata from the Tycho RPC.
///
/// Queries every protocol system in `protocols` for its components (paginated), counts how many
/// pools each token appears in, then joins against Tycho's token list for symbols and decimals.
/// Tokens that appear in a pool but have no metadata (or fail the quality filter) are dropped, as
/// their decimals are needed to scale amounts.
///
/// The result is sorted by descending pool count then address so callers get a deterministic order
/// regardless of RPC page ordering.
pub async fn fetch_token_pool_stats(
params: &TokenPoolStatsParams<'_>,
) -> Result<Vec<TokenPoolStats>> {
let rpc_url = if params.use_tls {
format!("https://{}", params.tycho_url)
} else {
format!("http://{}", params.tycho_url)
};
let rpc_options =
HttpRPCClientOptions::new().with_auth_key(params.auth_key.map(|s| s.to_string()));
let rpc_client = HttpRPCClient::new(&rpc_url, rpc_options)?;

// Count pool appearances per token across every requested protocol system.
let mut pool_count: HashMap<String, usize> = HashMap::new();
for protocol in params.protocols {
info!("Fetching components for protocol system '{protocol}'...");
let mut component_params =
ProtocolComponentsPaginatedParams::new(params.chain, protocol, FETCH_CONCURRENCY);
if let Some(min_tvl) = params.min_tvl {
component_params = component_params.with_tvl_gt(min_tvl);
}
let components = await_with_timeout(
rpc_client.get_protocol_components_paginated(component_params),
params.fetch_timeout,
|| {
format!(
"timed out after {}s fetching components for protocol system '{protocol}'; \
the Tycho indexer may be degraded. Exclude it via --protocols or raise \
--fetch-timeout-secs.",
params.fetch_timeout.as_secs()
)
},
)
.await?;
for component in &components {
for token in &component.tokens {
*pool_count
.entry(token.to_string())
.or_insert(0) += 1;
}
}
}

info!("Fetching token metadata from Tycho RPC...");
let token_params =
AllTokensParams::new(params.chain, FETCH_CONCURRENCY).with_min_quality(MIN_TOKEN_QUALITY);
let tokens =
await_with_timeout(rpc_client.get_all_tokens(token_params), params.fetch_timeout, || {
format!(
"timed out after {}s fetching token metadata from Tycho; the indexer may be \
degraded. Raise --fetch-timeout-secs or retry.",
params.fetch_timeout.as_secs()
)
})
.await?;

let mut stats: Vec<TokenPoolStats> = Vec::new();
for token in &tokens {
let address = token.address.to_string();
let Some(&count) = pool_count.get(&address) else {
continue;
};
stats.push(TokenPoolStats {
address,
symbol: token.symbol.clone(),
decimals: token.decimals,
pool_count: count,
});
}

stats.sort_by(|a, b| {
b.pool_count
.cmp(&a.pool_count)
.then_with(|| a.address.cmp(&b.address))
});

if stats.is_empty() {
bail!(
"no tokens with both pool membership and metadata found for the requested protocols; \
check Tycho connectivity and the --protocols / --chain arguments"
);
}
info!("Derived pool stats for {} token(s)", stats.len());
Ok(stats)
}

#[cfg(test)]
mod tests {
use std::io;

use super::*;

#[tokio::test]
async fn await_with_timeout_reports_deadline() {
let fetch = std::future::pending::<std::result::Result<(), io::Error>>();
let err = await_with_timeout(fetch, Duration::from_millis(10), || {
"timed out fetching components for protocol system 'uniswap_v3'".to_string()
})
.await
.unwrap_err();
assert!(err
.to_string()
.contains("protocol system 'uniswap_v3'"));
}

#[tokio::test]
async fn await_with_timeout_passes_through_success() {
let value =
await_with_timeout(async { Ok::<u32, io::Error>(7) }, Duration::from_secs(30), || {
"unused".to_string()
})
.await
.unwrap();
assert_eq!(value, 7);
}

#[tokio::test]
async fn await_with_timeout_surfaces_inner_error() {
let fetch =
async { std::result::Result::<u32, io::Error>::Err(io::Error::other("rpc exploded")) };
let err =
await_with_timeout(fetch, Duration::from_secs(30), || "should not be used".to_string())
.await
.unwrap_err();
assert!(err.to_string().contains("rpc exploded"));
assert!(!err
.to_string()
.contains("should not be used"));
}
}
3 changes: 2 additions & 1 deletion tools/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ Developer and operational tooling for the Fynd solver.

See [`tools/benchmark/CLAUDE.md`](benchmark/CLAUDE.md) for the full module overview.

Five subcommands via `cargo run -p fynd-benchmark --release --`:
Six subcommands via `cargo run -p fynd-benchmark --release --`:

- **`load`** — Load-test a single solver (latency, throughput, histograms)
- **`compare`** — Compare output quality between two solver instances (amount out diff in bps)
- **`scale`** — Measure how solver throughput scales with worker thread count (in-process, no external solver needed)
- **`download-trades`** — Download the full 10k aggregator trade dataset from GitHub Releases
- **`audit`** — Compare Fynd quote quality against external aggregators (Nordstern, KyberSwap, 0x); writes a JSON report
- **`generate-requests`** — Generate a synthetic per-chain request dataset (weighted by Tycho pool counts) for capacity testing on non-Ethereum chains

---

Expand Down
5 changes: 4 additions & 1 deletion tools/benchmark/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Benchmark and comparison tooling for Fynd solvers. Requires one or more running

## Commands

Five subcommands available via `cargo run -p fynd-benchmark --release --`:
Six subcommands available via `cargo run -p fynd-benchmark --release --`:

- **`load`** — Load-test a single solver. Measures latency (round-trip, solve time, overhead) and throughput. Supports sequential, fixed-concurrency, and rate-based parallelization modes. Prints statistics and ASCII histograms to stdout; optionally exports results to JSON.

Expand All @@ -16,6 +16,8 @@ Five subcommands available via `cargo run -p fynd-benchmark --release --`:

- **`audit`** — Compare Fynd quote quality against external aggregators (Nordstern, KyberSwap, 0x). Runs over a trade dataset, records per-trade participant results (amount out, gas, protocols, route, eth_call on-chain validation), and writes a JSON report.

- **`generate-requests`** — Generate a synthetic per-chain request dataset (JSON) for capacity load testing, for chains the Ethereum-only `download-trades` set does not cover. Fetches token/pool-count data from Tycho (via `fynd_rpc::protocols::fetch_token_pool_stats`, the same pool-count query as `derive-connector-tokens`), samples token pairs weighted by pool count, and log-spaces amounts around one whole token. Deterministic for a fixed `--seed`. Output is loadable via `--requests-file`. Accepts `--chain` values `ethereum`, `base`, `unichain`, `bsc`, `arbitrum`, `polygon` (case-insensitive); `zksync` parses but needs an explicit `--tycho-url`.

Run `--help` on any subcommand for detailed options.

## Running the Audit
Expand Down Expand Up @@ -118,6 +120,7 @@ Key fields per participant:
| `exporter.rs` | Statistics calculation (`TimingStats::from_measurements` — min/max/mean/median/p95/p99/stddev), ASCII histogram rendering, and JSON export of `BenchmarkResults`. |
| `requests.rs` | Request generation and loading. Provides a default WETH→USDC request, loads embedded aggregator trades, downloads the full 10k dataset, and loads custom requests from a JSON file. |
| `scale.rs` | `scale` subcommand handler. Resolves protocols once via `resolve_protocols` (`all_onchain`/`native_onchain` expansion), then builds and tears down an in-process Fynd instance for each worker-count iteration (applying `--min-tvl`), runs load tests via `runner`, and exports scaling results to JSON. |
| `generate_requests.rs` | `generate-requests` subcommand handler. Fetches per-token pool counts and decimals from Tycho via `fynd_rpc::protocols::fetch_token_pool_stats`, samples token pairs weighted by pool count (seeded `StdRng`, no self-pairs), log-spaces amounts around one whole token, and writes a `--requests-file`-compatible JSON array. |
| `aggregator.rs` | Aggregator API clients (Nordstern, KyberSwap, 0x) used by the `audit` subcommand. |
| `pair_selector.rs` | Trade pair selection logic for `audit` runs. |
| `audit/` | `audit` subcommand handler: per-trade result collection, on-chain validation via `eth_call`, JSON report writing. |
Expand Down
1 change: 1 addition & 0 deletions tools/benchmark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ alloy = { workspace = true }
bytes = { workspace = true }
num-bigint = { workspace = true }
fastrand = "2.3"
rand = "0.9"
reqwest = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
Expand Down
Loading
Loading