Differences (The Graph vs. Envio)
If you're looking to support both subgraphs (The Graph) and indexers (Envio) into your apps, see the "Different Results (*)" 📌 section below to understand how results from these two APIs may differ for the same entities.
General​
In contrast to The Graph, Envio offers a slightly different Developer Experience (DX) in terms of indexer implementation. While the query specifics don't change a lot for consumers, here are a few items to consider before developing on top of the Sablier indexers/subgraphs:
Querying Language​
Both solutions create a GraphQL API for consumers to read data from. The Graph uses a customized subset of GraphQL operations, which makes is impossible to use the same queries between both indexers and subgraphs, whereas Envio deploys a GraphQL API over a Postgres DB. Here are some examples:
Example | The Graph | Envio |
---|---|---|
Pagination | first , skip | limit , offset |
Filter by id | stream(id) | Stream(where: {id: {_eq: 1}}) |
Single vs. Plural | stream(id){} , streams{} | Stream(where...){} for both |
Nested items | campaigns{ id, asset: {id, symbol}} | Campaign{ asset_id, asset: {id, symbol}} |
Filter similar items | assets(id: $id) | Asset(where: {id: {_iregex: $id}}) |
Notes:
- For the 3rd example, querying for a single item vs. a collection will have separate keywords for The Graph. With
Envio, you'll have to identify within the app itself if the query is supposed to yield one or multiple items. As you
may tell, with Envio you'll query for
Stream
(capitalized), while with The Graph you'll query forstream
orstreams
. - For the 4th example, Envio Indexers will yield the contents of the
name
of an object with ({}
), while the<name>_id
label will ask for the identifier of the object (e.g.,asset
vsasset_id
). Keep this in mind for "where" filtering.
Handler Language​
- The Graph: uses Assembly Script
- Envio: Sablier uses TypeScript by default, but you can also use JavaScript or Rescript
With Envio, one important mental model is in the concept of loaders vs. handlers. To optimize querying speeds, Envio
will ask you to write 2 methods: (i) a loader
, where you can express which existing entities you're expecting to use
and mutate, and (ii) a handler
where you manage these entities and/or create new ones.
Example: In the case of a Withdraw event, we weill pre-load the Stream
entity and maybe the Watcher
, while in
the second part we'll handle the creation of a new Action
of type Withdraw, we'll update the Stream
's
withdrawnAmount
and increase the Watcher
's index.
Specifics​
Initializer Contracts​
We've architected this indexer around a set of pre-configured contracts.
Similar to The Graph, we start by pre-configuring a set of contracts. While Envio's indexer doesn't have the same requirement of pre-configuring contracts to listen to, we'll keep this feature to ensure we can query against those entities, even if they'll be empty at start.
We'll ensure contracts have been initialized (see the watcher.ts
helper) by making a call against the initializer at
the start of each method. It should only come into play within the create handlers.
Versioning and Lockup Flavors​
While for The Graph's subgraphs we track flavor-first (see subgraph.template.yaml
for the configuration of
SablierV2LockupLinear
and SablierV2LockupDynamic
), for Envio's indexers we'll have a version-first approach.
Therefore, LockupLinear
and LockupDynamic
will be bundled under the same Lockup<Version>
contract tracker (see
./config.template.mustache
). Different versions of the protocol will be tracked separately, which is why we have
Lockup_V20
(v2.0) and Lockup_V21
(v2.1) in our configuration. Later on, inside the handler logic, we'll separate
contracts by flavor.
└── contracts/
├── LockupV20/
│ ├── event: CreateLockupDynamicStream
│ └── event: CreateLockupLinearStream
└── LockupV21/
├── event: CreateLockupDynamicStream
└── event: CreateLockupLinearStream
└── contracts/
├── LockupDynamic/
│ ├── event: CreateLockupDynamicStreamV20
│ └── event: CreateLockupDynamicStreamV21
└── LockupLinear/
├── event: CreateLockupLinearStreamV20
└── event: CreateLockupLinearStreamV21
Contract Calls​
Outside of data provided directly by the event arguments, one can access additional information through contract calls.
While The Graph provides a dedicated API (binding an ABI to an address, calling contract.try_method()
), Envio is less
opinionated. A good example of contract calls can be found in our helpers/asset.ts
where we use viem
and a custom
caching mechanism to search for asset details.
Different Results (*)​
Due to the cross-chain indexing aspect, entities in Envio will need to have a chainId suffix attached to them to ensure they're unique across the board. At the same time, there are some minor features missing, which will cause some differences listed below.
- For Envio indexers, some entities will have different identifiers (from what The Graph's subgraph have):
protocol-envio
: theAction
,Asset
,Batch
,Batcher
,Contract
have a-chainId
appended to the IDmerkle-envio
: theAction
,Asset
andFactory
have a-chainId
appended to the ID
To avoid writing separate systems when assigning variables to queries, you can use slightly different filters. For
example, given the different id
s of an Asset
(address
in The Graph and address-chainId
in Envio) you can query
for certain assets with:
asset(id: $assetId)
for The GraphAsset(where: {id: {_iregex: $assetId}})
With assetId
in both cases being assigned to the Asset's address
.