Skip to content

Fetch uncached V3 pools on demand so cold quotes find liquidity#4560

Open
AryanGodara wants to merge 2 commits into
mainfrom
aryan/v3-on-demand-pool-fetch
Open

Fetch uncached V3 pools on demand so cold quotes find liquidity#4560
AryanGodara wants to merge 2 commits into
mainfrom
aryan/v3-on-demand-pool-fetch

Conversation

@AryanGodara

Copy link
Copy Markdown
Member

Description

The driver's Uniswap V3 fetcher warm-caches the top max-pools-to-initialize pools ranked by raw liquidity. Raw liquidity isn't comparable across pools (it scales with token decimals and price range), so well-funded pools with a low raw value are excluded from the warm cache. When a quote requests such a pair, checkpoint.get() returns nothing and fetch returns zero AMMs, failing the quote until a later maintenance tick happens to back-fill the pool.

This change makes fetch fetch the requested pairs' uncached pools on demand instead of deferring to maintenance, so the first quote for any pair finds liquidity.

Changes

  • PoolsCheckpointHandler::get also returns the ids of pools that exist for the requested pairs but aren't cached.
  • fetch fetches those on demand (new fetch_pools) before building the result; on failure it serves whatever is cached.
  • fetch_pools skips pools whose conversion fails (e.g. ticks not yet available) instead of aborting the batch; update_missing_pools reuses it, so periodic maintenance gains the same resilience.
  • Unit tests for the on-demand path and the skip-on-failure behaviour.

Stacked on #4559 (bootstrap split) and #4555 (migration isolation) so one image carries all three for combined staging validation.

How to test

Unit + e2e tests.
Did a local driver against an ink-seeded pool-indexer. it loads the previously-missing pools on the first quote.

@AryanGodara AryanGodara self-assigned this Jun 24, 2026
@AryanGodara AryanGodara force-pushed the aryan/pool-indexer-split-bootstrap branch from c628569 to a1aa54b Compare June 24, 2026 22:49
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from ec454b8 to 1d243f4 Compare June 24, 2026 22:51
@AryanGodara AryanGodara marked this pull request as ready for review June 25, 2026 08:35
@AryanGodara AryanGodara requested a review from a team as a code owner June 25, 2026 08:35

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements on-demand fetching for Uniswap V3 pools that are missing from the warm cache, allowing them to be fetched at the indexer's current block instead of waiting for the next maintenance cycle. However, two high-severity issues were identified: first, if no pools exist for the requested token pairs, the checkpoint block number defaults to 0, which triggers a massive event replay from block 1 and can cause database overload or OOM; second, passing 0 as the target block to fetch current pools breaks the subgraph client backend by querying the genesis block. Both issues require immediate fixes to ensure stability and compatibility.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/liquidity-sources/src/uniswap_v3/pool_fetching.rs
Comment thread crates/liquidity-sources/src/uniswap_v3/pool_fetching.rs
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 1d243f4 to 5bcf94d Compare June 25, 2026 09:09
@AryanGodara AryanGodara force-pushed the aryan/pool-indexer-split-bootstrap branch from a1aa54b to 30abab7 Compare June 25, 2026 11:03
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 5bcf94d to 5df6d08 Compare June 25, 2026 11:03
@AryanGodara AryanGodara force-pushed the aryan/pool-indexer-split-bootstrap branch from 30abab7 to eb207c2 Compare June 25, 2026 12:37
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 5df6d08 to 706dac6 Compare June 25, 2026 12:37
@AryanGodara AryanGodara force-pushed the aryan/pool-indexer-split-bootstrap branch from eb207c2 to ea474c9 Compare June 25, 2026 12:54
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 706dac6 to 8bfe6c4 Compare June 25, 2026 12:54
@AryanGodara AryanGodara force-pushed the aryan/pool-indexer-split-bootstrap branch from ea474c9 to 8b03564 Compare June 25, 2026 14:01
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 8bfe6c4 to cf905d5 Compare June 25, 2026 14:01
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed.

@github-actions github-actions Bot added the stale label Jul 3, 2026
@AryanGodara AryanGodara removed the stale label Jul 3, 2026
Base automatically changed from aryan/pool-indexer-split-bootstrap to main July 3, 2026 09:25
@AryanGodara AryanGodara requested a review from a team as a code owner July 3, 2026 09:25
@AryanGodara AryanGodara force-pushed the aryan/v3-on-demand-pool-fetch branch from 8b910f1 to c860437 Compare July 3, 2026 11:12
Comment on lines +263 to +269
// `0` means "latest available" (see the V3PoolDataSource contract). The
// subgraph needs a concrete block — `block: { number: 0 }` queries genesis
// — so resolve a recent safe block first.
let target_block = match target_block {
0 => self.get_safe_block().await?,
n => n,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// `0` means "latest available" (see the V3PoolDataSource contract). The
// subgraph needs a concrete block — `block: { number: 0 }` queries genesis
// — so resolve a recent safe block first.
let target_block = match target_block {
0 => self.get_safe_block().await?,
n => n,
};
// `0` means "latest available" for V3PoolDataSource
let target_block = match target_block {
// inversely, here `0` would be "genesis", so fetch the latest safe block
0 => self.get_safe_block().await?,
n => n,
};

fn get(
&self,
token_pairs: &HashSet<TokenPair>,
) -> (HashMap<Address, Arc<PoolInfo>>, Vec<Address>, u64) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at what point a triple should become a struct so we get proper naming?

(existing_pools, pools_checkpoint.block_number)
pools_checkpoint
.missing_pools
.extend(missing_pools.iter().copied());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vec::extend takes an IntoIterator which Vec should implement, so a single clone should work here

Comment on lines +268 to +270
/// skipping any that can't be converted yet (e.g. no ticks). Doesn't touch
/// the cache. Pass the checkpoint block to cache for replay, or 0 to take
/// the indexer's current snapshot without blocking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't touch the cache. Pass the checkpoint block to cache for replay,

This is confusing, if this function doesnt touch the cache either dont mention it, or write a separate paragraph mentioning how it can be used/leveraged for caching purposes

Comment on lines +276 to +278
if pool_ids.is_empty() {
return Ok(Vec::new());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO get_pools_with_ticks_by_ids should be written in such a way that this isn't needed

when you later collect its going to be an empty vec that ends equally empty

let (missing, block_number) = {
let checkpoint = self.pools_checkpoint.lock().unwrap();
(
checkpoint.missing_pools.iter().copied().collect::<Vec<_>>(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a clone to me

Comment on lines +464 to +465
// No pools registered for these pairs: `get` returns block 0, so skip the
// replay below (it would otherwise scan events from block 1) and return.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like this, please double check I didn't butcher the doc meaning (I think I understood what it meant)

Suggested change
// No pools registered for these pairs: `get` returns block 0, so skip the
// replay below (it would otherwise scan events from block 1) and return.
// if `get` returns block 0 that means there's no pools for these pairs
// we skip the replay because it would scan events from block 1 onwards

}

// The warm cache only holds the top pools by raw liquidity, so plenty of
// real pairs are absent. Fetch those on demand instead of waiting for the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're not fetching "on demand" here, just fetching them

Comment on lines +906 to +914
// Cold: not cached, flagged missing.
let (cached, missing, _) = handler.get(&HashSet::from([pair]));
assert!(cached.is_empty());
assert_eq!(missing, vec![pool]);

// On-demand fetch resolves it.
let fetched = handler.fetch_current(&missing).await;
assert_eq!(fetched.len(), 1);
assert_eq!(fetched[0].0, pool);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt fetch_current work regardless if its in the cached or not?
if yes this test isn't really testing much other that you:

  • can list non-cached pools
  • fetch them

it seems like you're reconstructing logic that should be a single method 🤔

/// minus the reorg buffer, the indexer serves finalized), so waiting
/// for it would hang the quote. Returns empty on failure so the quote
/// falls back to cached pools.
async fn fetch_current(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async fn fetch_current(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> {
async fn fetch_at_current_block(&self, pool_ids: &[Address]) -> Vec<(Address, Arc<PoolInfo>)> {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants