The Problem
Subgraphs often involve two types of entities:
- Entities that are created from events and which are not known at subgraph design time.
- 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:
- They must exist for the subgraph to function properly
- Developers know which entities to create at subgraph design time
- 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
}