Subgrounds basics

This page was written to provide an overview of the main concepts in Subgrounds and to serve as a quick reference for those concepts.

Subgrounds

The Subgrounds class provides the toplevel Subgrounds API and most Subgrounds users will be using this class exclusively. This class is used to load (i.e.: introspect) GraphQL APIs (subgraph APIs or vanilla GraphQL APIs) as well as execute querying operations. Moreover, this class is meant to be used as a singleton, i.e.: initialized once and reused throughout a project.

The code cell below demonstrates how to initialize your Subgrounds object and load a GraphQL API.

>>> from subgrounds import Subgrounds

# Initialize Subgrounds
>>> sg = Subgrounds()

# Load a subgraph using its API URL
>>> aaveV2 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/aave/protocol-v2')

# Load a vanilla GraphQL API using its API URL
>>> snapshot = sg.load_api('https://hub.snapshot.org/graphql')

FieldPaths

FieldPaths are the main building blocks used to construct Subgrounds queries. A FieldPath represents a selection path through a GraphQL schema starting from the root Query entity (see The Query and Mutation types) all the way down to a scalar leaf.

FieldPaths are created by simply selecting attributes starting from the subgraph object returned by the Subgrounds.load_subgraph or Subgrounds.load_api methods:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Create a FieldPath
>>> pools_tvl = uniswapV3.Query.pools.totalValueLockedUSD

In the example above, the pools_tvl variable is a FieldPath object representing the following GraphQL query:

query {
  pools {
    totalValueLockedUSD
  }
}

Note that FieldPaths don't have to be selected from root to leaf each time and partial FieldPaths can be reused:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Create a FieldPath
>>> pools_tvl = uniswapV3.Query.pools.totalValueLockedUSD

# Create a partial FieldPath
>>> pools = uniswapV3.Query.pools

# This FieldPath is equivalent to the `pools_tvl` FieldPath
>>> pools_tvl2 = pools.totalValueLockedUSD

FieldPath arguments

Field arguments can be specified via FieldPaths by "calling" the field in question:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Create a partial FieldPath of `pools` created after block 14720000 
# ordered by their TVL in descending order
>>> new_pools = uniswapV3.Query.pools(
...   orderBy=uniswapV3.Pool.totalValueLockedUSD,
...   orderDirection='desc',
...   where=[
...     uniswapV3.Pool.createdAtBlockNumber > 14720000
...   ]
... )

# Select the `totalValueLockedUSD` of the
>>> pools_tvl = new_pools.totalValueLockedUSD

In the example above, the pools_tvl FieldPath represents the following GraphQL query:

query {
  pools(
    orderBy: totalValueLockedUSD,
    orderDirection: desc,
    where: {
      createdAtBlockNumber_gt: 14720000
    }
  ) {
    totalValueLockedUSD
  }
}

Notice that the values for the orderBy and where arguments are FieldPath themselves. This allows users to construct complex queries in pure Python by using the Subgraph object returned when loading an API. Note however that the FieldPaths used as argument values are relative FieldPath, i.e.: they do not start from the root Query entity, but rather start from a user defined entity type (in this case the Pool entity). It is important to make sure that the relative FieldPath used as values for the orderBy and where arguments match the entity type of the field on which the arguments are applied (in our example, the pools field is of type Pool). If this is not respected, a type error exception will be thrown.

Argument values can also be supplied in their "raw" form, without the use of relative FieldPaths:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# This partial FieldPath is equivalant to the `new_pools` FieldPath
# in the previous example
>>> new_pools2 = uniswapV3.Query.pools(
...   orderBy='totalValueLockedUSD',
...   orderDirection='desc',
...   where={
...     'createdAtBlockNumber_gt': 14720000
...   }
... )

Querying data

The Subgrounds class provides three methods for querying data:

  1. query

  2. query_df

  3. query_json

All three methods take the same arguments (ignoring optional arguments), namely a list of FieldPaths and differ only in the way the returned data is formatted. This section will go over each method individually.

Subgrounds.query

The query method returns the data in its simplest form which, depending on the FieldPaths given as argument, will be either: 1) a single value; 2) a list of values; or 3) a tuple containing single values or lists of values. It is important to consider the shape of the queried data (e.g.: single entities, list of entities...) as the shape of the returned data will depend on it.

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# 1) Single value: Query the quantity of WETH locked on Uniswap V3 
>>> sg.query([
...   uniswapV3.Query.token(id='0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2').totalValueLocked
... ])
505722.3421353012

# Partial FieldPath of top 10 most traded tokens 
>>> most_traded_tokens = uniswapV3.Query.tokens(
...   orderBy=uniswapV3.Token.volumeUSD,
...   orderDirection='desc',
...   first=10
... )

# 2) List of values: Query symbol of top 10 most traded tokens
>>> sg.query([
...   most_traded_tokens.symbol
... ])
['WETH', 'USDC', 'USDT', 'DAI', 'WBTC', 'FEI', 'UST', 'APE', 'LOOKS', 'HEX']

# 3) Tuple of values: Query symbol and TVL of top 10 most traded tokens
>>> sg.query([
...   most_traded_tokens.symbol,
...   most_traded_tokens.totalValueLocked
... ])
(['WETH', 'USDC', 'USDT', 'DAI', 'WBTC', 'FEI', 'UST', 'APE', 'LOOKS', 'HEX'],
 [506128.5352471704,
  774159914.44222,
  353261712.912211,
  384420834.9304443,
  11069.25245941,
  42232365.752994545,
  8030872.929855888,
  2604424.599749661,
  11610722.749456603,
  131046877.18519919])

Subgrounds.query_df

Subgrounds provides a simple way to query subgraph data directly into a pandas DataFrame via the query_df method. Just like the query method, query_df takes as argument a list of FieldPaths and returns one or more DataFrames depending on the shape of the queried data. query_df will attempt to flatten all the data to a single DataFrame (effectively mimicking the SQL JOIN operation) but when that is not possible, two or more DataFrames will be returned.

Example with single DataFrame returned

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Select top 10 pools by cummulative volume 
>>> top_10_pools = uniswapV3.Query.pools(
...   orderBy=uniswapV3.Pool.volumeUSD,
...   orderDirection='desc',
...   first=10
... )

# Query data flattened to a single DataFrame
>>> sg.query_df([
...   top_10_pools.id,
...   top_10_pools.token0.symbol,
...   top_10_pools.token1.symbol,
... ])
                                     pools_id pools_token0_symbol pools_token1_symbol
0  0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640                USDC                WETH
1  0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8                USDC                WETH
2  0x11b815efb8f581194ae79006d24e0d814b7697f6                WETH                USDT
3  0x4e68ccd3e89f51c3074ca5072bbac773960dfa36                WETH                USDT
4  0x60594a405d53811d3bc4766596efd80fd545a270                 DAI                WETH
5  0x4585fe77225b41b697c938b018e2ac67ac5a20c0                WBTC                WETH
6  0x3416cf6c708da44db2624d63ea0aaef7113527c6                USDC                USDT
7  0xcbcdf9626bc03e24f779434178a73a0b4bad62ed                WBTC                WETH
8  0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8                 DAI                WETH
9  0x7858e59e0c01ea06df3af3d20ac7b0003275d4bf                USDC                USDT

Example with multiple DataFrames returned

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Select WETH-USDC pool
>>> weth_usdc = uniswapV3.Query.pool(id='0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640')

# Define FieldPaths for last 10 mints and burns
>>> weth_usdc_last10_mints = weth_usdc.mints(
...   orderBy=uniswapV3.Mint.timestamp,
...   orderDirection='desc',
...   first=10
... )

>>> weth_usdc_last10_burns = weth_usdc.burns(
...   orderBy=uniswapV3.Burn.timestamp,
...   orderDirection='desc',
...   first=10
... )

# Query flattened data. Since we are selecting two unnested list fields (i.e.: one is not nested inside the other) we will get back two DataFrames
>>> [mints, burns] = sg.query_df([
...   weth_usdc_last10_mints.timestamp,
...   weth_usdc_last10_mints.amount0,
...   weth_usdc_last10_mints.amount1,
...   weth_usdc_last10_burns.timestamp,
...   weth_usdc_last10_burns.amount0,
...   weth_usdc_last10_burns.amount1,
... ])

>>> mints
   pool_mints_timestamp  pool_mints_amount0  pool_mints_amount1
0            1652114746        3.179282e+05          548.114506
1            1652113488        1.194870e+06          400.000000
2            1652113014        1.249154e+06          760.212396
3            1652112429        2.112058e+06         1891.033562
4            1652111846        3.646868e+05          140.593137
5            1652110962        5.740381e+04           23.700000
6            1652110962        3.522151e+05          548.186728
7            1652109116        5.223502e+04           20.894748
8            1652106977        1.204390e+04           10.525402
9            1652106531        7.187740e+02            0.993683

>>> burns
   pool_burns_timestamp  pool_burns_amount0  pool_burns_amount1
0            1652115279       278038.482233           57.092565
1            1652114890         1754.280892            6.006157
2            1652114731            0.000000          697.289317
3            1652114233            0.000000           19.743883
4            1652113956            0.000000            0.017788
5            1652112925         4743.955507            0.780928
6            1652112719            0.000000          587.274826
7            1652112509            0.000000           23.297411
8            1652112509            0.000000          760.767874
9            1652112459            0.000000          631.697170

Subgrounds.query_json

Subgrounds allows users to query data in its raw JSON format using the query_json method:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

>>> top_2_traded_tokens = uniswapV3.Query.tokens(
...   orderBy=uniswapV3.Token.volumeUSD,
...   orderDirection='desc',
...   first=2
... )

>>> sg.query_json([
...   top_2_traded_tokens.symbol
... ])
[{'x6e2162a85075a2b3': [{'symbol': 'WETH',
    'id': '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
    'volumeUSD': '456255840800.0050364446410698045662'},
   {'symbol': 'USDC',
    'id': '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
    'volumeUSD': '324987692399.2943551375705643641231'}]}]

Note

Subgrounds uses GraphQL aliases to differentiate queries selecting the same fields but with different arguments, which is why the JSON response data contains the key x6e2162a85075a2b3 instead of the "expected" tokens key (see Subgrounds query aliases for more information). Subgrounds also selects additional fields automatically due to how automatic pagination is implemented. In the previous example, the id and volumeUSD fields were automatically added to the query by Subgrounds. However, these particularities are only apparent when using query_json, hence why using this method is not recommended unless it is absolutely necessary to get the raw JSON data.

Combining FieldPaths

When passing a list of FieldPaths as argument to one of the aforementioned functions, the FieldPaths are merged together in a single query if the FieldPaths originate from the same subgraph:

>>> uniswapV3 = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3')

# Partial FieldPath selecting the top 4 most traded tokens on Uniswap V3
>>> most_traded_tokens = uniswapV3.Query.tokens(
...   orderBy=uniswapV3.Token.volumeUSD,
...   orderDirection='desc',
...   first=4
... )

# Partial FieldPath selecting the top 2 pools by TVL of the top 4 most traded tokens
# (notice the FieldPath starts from most_traded_tokens) 
>>> most_liquid_pairs = most_traded_tokens.whitelistPools(
...   orderBy=uniswapV3.Pool.totalValueLockedUSD,
...   orderDirection='desc',
...   first=2
... )

# Querying the symbol of the top 4 most traded tokens, their 2 most liquid 
# pools' token symbols and their 2 most liquid pool's TVL in USD
>>> sg.query_df([
...   most_traded_tokens.symbol,
...   most_liquid_pairs.token0.symbol,
...   most_liquid_pairs.token1.symbol,
...   most_liquid_pairs.totalValueLockedUSD
... ])
  tokens_symbol tokens_whitelistPools_token0_symbol tokens_whitelistPools_token1_symbol  tokens_whitelistPools_totalValueLockedUSD
0          WETH                                USDC                                WETH                               4.068723e+08
1          WETH                                WBTC                                WETH                               3.311227e+08
2          USDC                                 DAI                                USDC                               3.284779e+08
3          USDC                                USDC                                WETH                               4.068723e+08
4          USDT                                WETH                                USDT                               2.055448e+08
5          USDT                                USDC                                USDT                               1.980053e+08
6           DAI                                 DAI                                USDC                               3.284779e+08
7           DAI                                 DAI                                WETH                               9.759597e+07

Under the hood, when executing the previous code, Subgrounds will combine the queried FieldPaths into the following GraphQL query:

query {
  tokens(first: 4, orderBy: volumeUSD, orderDirection: desc) {
    symbol
    whitelistPools(first: 2, orderBy: totalValueLockedUSD, orderDirection: desc) {
      token0 {
        symbol
      }
      token1 {
        symbol
      }
      totalValueLockedUSD
    }
  }
}

SyntheticFields

One of Subgrounds' unique features is the ability to define schema-based (i.e.: pre-querying) transformations using SyntheticFields.

SyntheticFields can be created using Python arithmetic operators on relative FieldPaths (i.e.: FieldPaths starting from an entity and not the root Query object) and must be added to the entity which they enhance. Once added to an entity, SyntheticFields can be queried just like regular GraphQL fields. The example below demonstrates how to create a simple SyntheticField to calculate the swap price of Swap events stored on the Sushiswap subgraph:

>>> sushiswap = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/sushiswap/exchange')

# Define a synthetic field named price1 (the swap price of token1,
# in terms of token0) on Swap entities
>>> sushiswap.Swap.price1 = abs(sushiswap.Swap.amount0Out - sushiswap.Swap.amount0In) / abs(sushiswap.Swap.amount1Out - sushiswap.Swap.amount1In)

# Build query to get the last 10 swaps of the WETH-USDC pair on Sushiswap 
>>> weth_usdc = sushiswap.Query.pair(id='0x397ff1542f962076d0bfe58ea045ffa2d347aca0')

>>> last_10_swaps = weth_usdc.swaps(
...   orderBy=sushiswap.Swap.timestamp,
...   orderDirection='desc',
...   first=10
... )

# Query swap prices using the SyntheticField price1 just like they were regular fields
>>> sg.query_df([
...   last_10_swaps.timestamp,
...   last_10_swaps.price1
... ])
   pair_swaps_timestamp  pair_swaps_price1
0            1654267855        1762.526840
1            1654267855        1760.132097
2            1654267766        1747.485689
3            1654267562        1758.131683
4            1654267528        1758.081544
5            1654267493        1742.260312
6            1654267493        1749.891244
7            1654267493        1755.254957
8            1654267364        1748.934985
9            1654267356        1750.284126

SyntheticFields can also be created using the constructor, allowing for much more complex transformations.

>>> from datetime import datetime
>>> from subgrounds.subgraph import SyntheticField

>>> sushiswap = sg.load_subgraph('https://api.thegraph.com/subgraphs/name/sushiswap/exchange')

# Create a SyntheticField on the Swap entity called `datetime`, which will format 
# the timestamp field into something more human readable
>>> sushiswap.Swap.datetime = SyntheticField(
...   lambda timestamp: str(datetime.fromtimestamp(timestamp)),
...   SyntheticField.STRING,
...   sushiswap.Swap.timestamp
... )

>>> last_10_swaps = sushiswap.Query.swaps(
...   orderBy=sushiswap.Swap.timestamp,
...   orderDirection='desc',
...   first=10
... )

>>> sg.query_df([
...   last_10_swaps.datetime,
...   last_10_swaps.to,
...   last_10_swaps.pair.token0.symbol,
...   last_10_swaps.pair.token1.symbol
... ])
        swaps_datetime                                    swaps_to swaps_pair_token0_symbol swaps_pair_token1_symbol
0  2022-06-03 11:04:25  0x98c3d3183c4b8a650614ad179a1a98be0a8d6b8e                     OOKI                     WETH
1  2022-06-03 11:03:29  0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f                    CHIMP                     WETH
2  2022-06-03 11:03:29  0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f                     WETH                     USDT
3  2022-06-03 11:02:59  0x0eae044f00b0af300500f090ea00027097d03000                     ICHI                     WETH
4  2022-06-03 11:02:59  0xfb3bd022d5dacf95ee28a6b07825d4ff9c5b3814                     USDC                     WETH
5  2022-06-03 11:02:59  0x11111112542d85b3ef69ae05771c2dccff4faa26                     WETH                     USDT
6  2022-06-03 11:02:21  0xd7c09e006a2891880331b0f6224071c1e890a98a                     WETH                     ROOK
7  2022-06-03 11:02:21  0x60c809705e045572ffe3c44badd4d680165960fa                      OHM                      DAI
8  2022-06-03 11:01:00  0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f                      BIT                     WETH
9  2022-06-03 11:00:54  0xdef171fe48cf0115b1d80b88dc8eab59176fee57                     NEWO                     USDC