All packages
uniswap-database-changes-pool-price-optimism

uniswap-database-changes-pool-price-optimism

ciyengar3
v0.0.2/2 downloads

Package ref

uniswap-database-changes-pool-price-optimism@v0.0.2

Run package

CLI

Run db_out from the command line.

substreams run uniswap-database-changes-pool-price-optimism@v0.0.2 db_out -e optimism
Authenticate by running substreams auth or directly on thegraph.market (see docs).

README

uniswap-pool-price

The last spot price of every Uniswap pool, a pool_price_block row for each block with a price event plus an empty heartbeat row about once a minute, for consumers that only need prices. Schema: proto/evm/uniswap/pool_price/pool-price.proto (PoolPriceBlock, oneof 2002).

How a price is chosen

Every Uniswap price change has an event that carries the post-state price: v3/v4 price only moves on Swap and starts at Initialize, and v2 emits Sync on every reserve change. For each pool, the event with the highest ordinal in the block sets the row's price. Only events are read, so extended and base blocks give the same rows.

Each entry in pools has the field names and types of block_digest's PoolChangeset: version, pool_address, pool_id (v4), last_ordinal, sqrt_price_x96 / tick (v3/v4) or reserve0 / reserve1 (v2), plus what set the price:

  • v3/v4 with swap_count > 0: a swap.
  • v3/v4 with initialized set and swap_count = 0: the starting price from Initialize, before any trade. Anyone can initialize a pool at any price, so weigh these by liquidity before trusting them.
  • v2 with swap_count > 0: the pair traded in the block. A mint, burn or sync after the swap can still set the final reserves.
  • v2 with swap_count = 0: only a mint, burn or sync moved the reserves.

v3/v4 entries a swap set also carry liquidity, the active liquidity the last Swap reports. It's liquidity at that swap, not at the end of the block, since a Mint, Burn or ModifyLiquidity later in the block changes it without an event this package reads. v2 and Initialize-only entries have none.

Prices are raw units, since token decimals aren't in the stream.

Modules

  • map_pool_prices: the digest (evm.uniswap.pool_price.Block) from the pools package's uniswap_v{2,3,4}:map_events, imported unchanged from the deployed pools spkg so the parents are served from cache. Those map_events already filter to Uniswap pools (v2/v3 on store_pools, v4 on the PoolManager address).
  • db_out: the pool_price_block row, for a block with a price event or a heartbeat block (block_num % heartbeat_blocks == 0, an empty row). It also takes the block, unused, so it runs on blocks with no Uniswap events to write their heartbeats (Substreams skips a module whose map inputs are all empty). Params ordering_key (pool_price:<chain_id> in generated manifests) and heartbeat_blocks (about one minute of blocks per chain in chains.json, 0 writes every block). A gap between rows means no Uniswap price moved in those blocks, and a gap longer than heartbeat_blocks means the stream is behind.

PoolPriceBlock is GCS-delivered: the sink uploads each row to the payload bucket and publishes a GcsPayloadRef pointer, one object per block.

Building

make pack NETWORK=base       # ../local/uniswap-labs-database-changes-pool-price-base.spkg
make run NETWORK=base
make test

scripts/build.sh resolves the pools version the prod sink streams for the chain (pool-state's resolve-pools-version.py, POOLS_VERSION / POOLS_SPKG override it) and fails unless the imported parents hash like the deployed ones. chains.json has every chain with a prod pools sink (the chains we compute realtime prices for), each with its chain id, the sink's primary Substreams endpoint and a recent start block: 2026-10-27 for base and mainnet, 2026-11-02 for the rest. A pool's first price arrives with its first event after that block.

Measured

On 200 Base blocks from 53,150,000, against pool-state's db_out changeset (whose prices come from storage on these extended blocks):

  • 200 rows, pool_count right in each, pools sorted.
  • 2,199 pool entries. Price, swap_count and initialized equal the changeset's in every one, except five zero-amount v4 swaps. Those left slot0 untouched, so the changeset has no price, and the event's unchanged price is correct.
  • The changeset's 35 slot0 rewrites with no price event (reentrancy lock, fee update; 32 v3, 3 v4) have no entry, as intended.
  • liquidity on all 2,054 v3/v4 swap entries. Where the changeset has an end-of-block liquidity (84 entries) it's equal in 73. The other 11 all had a Mint, Burn or ModifyLiquidity in the same block.
  • Egress 686 KiB against db_out's 4.0 MiB.

Modules

Execution graph

8 modules
mapMain

db_out

A pool_price_block row per block with a price event, plus an empty heartbeat row every heartbeat_blocks blocks.

from #159027000

Output

sf.substreams.sink.database.v1.DatabaseChanges

Inputs

paramssf.substreams.v1.Clocksf.ethereum.type.v2.Blockmap_pool_prices
Show 6 dependencies