Playgrounds Analytics proposal: Subgraph DevEx: Subgraph initialization function

The Problem

Subgraphs often involve two types of entities:

  1. Entities that are created from events and which are not known at subgraph design time.
  2. Entities that are initialized in a deterministic manner and which are known at subgraph design time.

This proposal concerns the second type of entities.

These entities often model static contracts like factory contracts (e.g.: the Factory entity in the Uniswap V3 subgraph) or entities used to keep track of subgraph-wide statistics (e.g.: the LendingProtocol entity in Messari’s standardized lending subgraphs).

Additionally, these entities generally have three design constraints in common:

  1. They must exist for the subgraph to function properly
  2. Developers know which entities to create at subgraph design time
  3. These entities should be created only once

With these design constraints in mind, there are several patterns a subgraph developer can adopt during the design process to enforce these constraints. A typical design pattern is the “getOrCreate” pattern.

How does the “getOrCreate” pattern work? First, let’s consider a typical DEX with two types of smart contracts: liquidity pool contracts which handle the actual swapping of assets, and a factory contract which is used to create liquidity pools.

A subgraph modeling this protocol would usually have, at minimum, two entities modeling both contracts. Entities modelling liquidity pools would be created from events emitted by the factory contract, but the entity modelling the factory contract itself cannot be created using events. This is where the “getOrCreate” pattern usually comes in. For example, one might do something like this:

export function getOrCreateFactory(): Factory {
  let factory = Factory.load(FACTORY_ADDRESS)
  if (factory === null) {
    factory = new Factory(FACTORY_ADDRESS)
    // Initialize factory
    factory.save()
  }

  return factory
}

export function handlePoolCreated(event: PoolCreated): void {
  let factory = getOrCreateFactory()

  // Handle event
}

This code snippet ensures that the Factory entity will always be initialized since it will be created whenever the handlePoolCreated event handler is first triggered. However, this approach is not ideal from the point of view of subgraph development experience. It creates tight coupling between initialization and indexing logic, introduces the potential for bugs, and increases the overall complexity of the subgraph.

The need for a suboptimal solution like the “getOrCreate” pattern stems from the lack of proper initialization functionality when developing subgraphs.

This is the problem we are proposing a solution to.

The Solution

Subgraph manifests should allow developers to provide a subgraph initialization function that is executed only once before the subgraph starts indexing any data.

specVersion: 0.0.5
description: Uniswap V3 subgraph
repository: https://github.com/0xPlaygrounds/uniswap-v3-subgraph
schema:
  file: ./schema.graphql
# NEW!
init:
  file: ./src/init.ts
  function: myInitFunction
...

This would remove the need for the getOrCreate pattern and would enforce the constraints listed above and would neatly separate initialization and indexing logic.

Looking back at our toy example from above, this is how this new feature could potentially be used:

// src/init.ts
export myInitFunction(): void {
  let factory = new Factory(FACTORY_ADDRESS)
  // Initialize factory entity
  factory.save()
}

// src/factory.ts
export function handlePoolCreated(event: PoolCreated): void {
  // We know the entity exists, no need for logic that verifies this
  let factory = Factory.load(FACTORY_ADDRESS)

  // Handle event
}
1 Like

Hey @stopher, thanks for the write up, this is indeed a good idea, we have been tracking this request here: Initialization handlers · Issue #3221 · graphprotocol/graph-node · GitHub, definitely good to get more validation