Block Level and Daily Balances

In this guide, we're going to look at how to create a view of balances at a block level and daily level.

By using the snapshot data in ethereum.core.fact_token_balances we can create a block level view of balances or a daily view. The approach for both methods is similar, but we can start at a block level.

Please note: Balances data is very large. Queries without where filters on block_number of block_timestamp may take a long time. Please do your best to only query what you really need. However, we do realize that some use cases require full history.

We are going to use the same USDC-WETH Uniswap v3 pool from the prior example. We'll start by pulling the decimal transformed balances for the pool from the last 7 days.

select
  block_number,
  block_timestamp,
  user_address,
  contract_address,
  c.symbol,
  balance / pow(10, c.decimals) as balance
from
  ethereum.core.fact_token_balances b
  left join ethereum.core.dim_contracts c on b.contract_address = c.address
where
  user_address = '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640'
  and block_timestamp >= current_date() - 7
  and contract_address in (
    '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
    '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
  )

The next step involves filling all the gaps in blocks with the prior value. This pool has a lot of volume as of writing, so there is not much filling to do, as there is already almost a swap every block. However, something like an EOA (wallet) will have much less volume, and therefore has more filling to do. We will fill by block_number here, but if you only needed daily balances, you could fill by block_timestamp::date.

Once we have our spine of block_number, user_address, contract_address, and symbol, we can left join in our balances data, and fill the gaps with the prior valid value. Let's do this with a few CTEs.

with balances as (
  select
    block_number,
    block_timestamp,
    user_address,
    contract_address,
    c.symbol,
    balance / pow(10, c.decimals) as balance
  from
    ethereum.core.fact_token_balances b
    left join ethereum.core.dim_contracts c on b.contract_address = c.address
  where
    user_address = '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640'
    and block_timestamp >= current_date() - 7
    and contract_address in (
      '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
      '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
    )
),
-- create a spine of all the possible combos
spine as (
  select
    block_number,
    block_timestamp,
    user_address,
    contract_address,
    symbol
  from
    ethereum.core.fact_blocks b
    join (
      select
        distinct contract_address,
        user_address,
        symbol
      from
        balances
    ) on 1 = 1
  where
    block_number between (
      select
        min(block_number)
      from
        balances
    )
    and (
      select
        max(block_number)
      from
        balances
    )
),
-- join all options with transfer based data and fill gaps 
block_level_balances as (
  select
    s.block_number,
    s.block_timestamp,
    s.user_address,
    s.contract_address,
    s.symbol,
    b.balance,
    LAST_VALUE(b.balance ignore nulls) over(
      PARTITION BY s.user_address,
      s.contract_address
      ORDER BY
        s.block_number ASC rows unbounded preceding
    ) as balance_filled
  from
    spine s
    left join balances b using (block_number, user_address, contract_address)
)
select
  *
from
  block_level_balances;

This gives us the WETH and USDC balance, per block, for the Uniswap V3 pool. We could take this a step further by filtering to just the last balace per day using the following final step.

select
  block_timestamp :: date as block_date,
  block_number,
  user_address,
  contract_address,
  symbol,
  balance_filled as balance
from
  block_level_balances qualify (
    row_number() over (
      partition by user_address,
      contract_address,
      block_date
      order by
        block_number desc
    ) = 1
  )
order by
  block_date,
  user_address,
  contract_address

If you wanted to analyze the total supply of a token, you would want to remove the user_address filter. There are many possibilities with these tables, but remember to use as many filters as possible for optimal performance.

Last updated