Installation

This tutorial shows how to setup an Apibara project from scratch. The goal is to start indexing data as quickly as possible and to understand the basic structure of a project. By the end of this tutorial, you will have a basic indexer that streams data from two networks (Ethereum and Starknet).

Installation

This tutorial starts with a fresh Typescript project. Let's start by creating it.

Terminal
mkdir my-indexer
cd my-indexer
npm init -y

After that, you can add the Apibara NPM packages.

Terminal
npm add --save apibara@next @apibara/protocol@next @apibara/indexer@next
added 325 packages, and audited 327 packages in 11s

73 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Apibara Config

Your indexers' configuration goes in the apibara.config.ts file. You can leave the configuration empty for now.

apibara.config.ts
import { defineConfig } from "apibara/config";

export default defineConfig({});

EVM Indexer

Let's create the first EVM indexer. All indexers must go in the indexers directory and have a name that ends with .indexer.ts or .indexer.js. The Apibara CLI will automatically detect the indexers in this directory and make them available to the project.

Since Apibara is chain-agnostic, we need to add the EVM package to stream Ethereum data.

Terminal
npm add --save @apibara/evm@next
added 2 packages, and audited 329 packages in 4s

73 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Then you can create the first indexer.

Terminal
mkdir indexers
touch indexers/rocket-pool.indexer.ts
rocket-pool.indexer.ts
import { EvmStream } from "@apibara/evm";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";

export default defineIndexer(EvmStream)({
  streamUrl: "https://ethereum.preview.apibara.org",
  startingCursor: {
    orderKey: 21_000_000n
  },
  filter: {
    logs: [{
      address: "0xae78736Cd615f374D3085123A210448E74Fc6393"
    }]
  },
  async transform({ block }) {
    const logger = useLogger();
    const { logs, header } = block;
    logger.log(`Block number ${header?.blockNumber}`)
    for (const log of logs) {
      logger.log(`Log ${log.logIndex} from ${log.address} tx=${log.transactionHash}`)
    }
  },
});

Notice the following:

  • The indexer file exports a single indexer.
  • The defineIndexer function takes the stream as parameter. In this case, the EvmStream is used. This is needed because Apibara supports multiple networks with different data types.
  • streamUrl specifies where the data comes from. You can connect to streams hosted by us, or to self-hosted streams.
  • startingCursor specifies from which block to start streaming.
  • The filter specifies which data to receive. You can read more about the available data for EVM chains in the EVM documentation.
  • The transform function is called for each block. It receives the block as parameter. This is where your indexer processes the data.
  • The useLogger hook returns an indexer-specific logger.

There are more indexer options available, you can find them in the documentation (TODO: link).

Running the indexer

During development, you will use the apibara CLI to build and run indexers. For convenience, you should add the following scripts to your package.json file.

package.json
{
  "scripts": {
    "dev": "apibara dev",
    "build": "apibara build",
    "start": "apibara start"
  }
}
  • dev: runs all indexers in development mode. Indexers are automatically reloaded and restarted when they change.
  • build: builds the indexers for production.
  • start: runs a single indexer in production mode. Notice you must first build the indexers.

Now, run the indexer in development mode.

Terminal
npm run dev
> [email protected] dev
> apibara dev

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3587 ms
✔ Restarting indexers
rocket-pool  | log Block number 21000071
rocket-pool  | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool  | log Block number 21000097
rocket-pool  | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool  | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool  | log Block number 21000111
rocket-pool  | log Log 589 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xa01ec6551e76364f6cf687f52823d66b1c07f7a47ce157a9cd9e441691a021f0
...

Starknet indexer

You can index data on different networks from the same project. Let's add an indexer for Starknet. Like before, you need to add the package to support the network.

Terminal
npm add --save @apibara/starknet@next
added 1 package, and audited 330 packages in 5s

73 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

After that, you can implement the indexer. In this case, the indexer listens for all events emitted by the STRK staking contract.

strk-staking.indexer.ts
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";

export default defineIndexer(StarknetStream)({
  streamUrl: "https://starknet.preview.apibara.org",
  startingCursor: {
    orderKey: 900_000n,
  },
  filter: {
    events: [{
      address: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
    }],
  },
  async transform({ block }) {
    const logger = useLogger();
    const { events, header } = block;
    logger.log(`Block number ${header?.blockNumber}`)
    for (const event of events) {
      logger.log(`Event ${event.eventIndex} tx=${event.transactionHash}`)
    }
  },
});

You can now run the indexer. In this case, you can specify which indexer you want to run with the --indexers option. When the flag is omitted, all indexers are run concurrently.

Terminal
npm run dev -- --indexers strk-staking
> [email protected] dev
> apibara dev --indexers=strk-staking

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3858 ms
✔ Restarting indexers
strk-staking | log Block number 929092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 929119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
...

Production build

The apibara build command is used to build a production version of the indexer. There are two main changes for the production build:

  • No hot code reloading is available.
  • Only one indexer is started. If your project has multiple indexers, it should start them independently.
Terminal
npm run build
> [email protected] build
> apibara build

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
◐ Building 2 indexers
✔ Build succeeded!
ℹ You can start the indexers with apibara start

Once the indexers are built, you can run them in two (equivalent) ways:

  • The apibara start command by specifying which indexer to run with the --indexer flag. In this tutorial we are going to use this method.
  • Running .apibara/build/start.mjs with Node. This is useful when building Docker images for your indexers.
Terminal
npm run start -- --indexer rocket-pool
> [email protected] start
> apibara start --indexer rocket-pool

◐ Starting indexer rocket-pool
rocket-pool  | log Block number 21000071
rocket-pool  | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool  | log Block number 21000097
rocket-pool  | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool  | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
...

Runtime configuration & presets

Apibara provides a mechanism for indexers to load their configuration from the apibara.config.ts file:

  • Add the configuration under the runtimeConfig key in apibara.config.ts.
  • Change your indexer's module to return a function that, given the runtime configuration, returns the indexer.

You can update the configuration to define values that are configurable by your indexer. This example is going to store the DNA stream URL and contract address in the configuration.

apibara.config.ts
import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {
    streamUrl: "https://starknet.preview.apibara.org",
    contractAddress: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
  },
});

Then update the indexer to return a function that returns the indexer. Your editor is going to show a type error since the types of config.streamUrl and config.contractAddress are unknown, the next session is going to explain how to solve that issue.

strk-staking.indexer.ts
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";
import { ApibaraRuntimeConfig } from "apibara/types";

export default function (config: ApibaraRuntimeConfig) {
  return defineIndexer(StarknetStream)({
    streamUrl: config.streamUrl,
    startingCursor: {
      orderKey: 900_000n,
    },
    filter: {
      events: [{
        address: config.contractAddress as `0x${string}`
      }],
    },
    async transform({ block }) {
      // Unchanged.
    },
  });
};

Typescript & type safety

You may have noticed that the CLI generates types in .apibara/types before building the indexers (both in development and production mode). These types contain the type definition of your runtime configuration. You can instruct Typescript to use them by adding the following tsconfig.json to your project.

tsconfig.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler"
  },
  "include": [
    "**/*.ts",
    ".apibara/types"
  ],
  "exclude": [
    "node_modules"
  ],
}

After restarting the Typescript language server you will have a type-safe runtime configuration right into your indexer!

Presets

Having a single runtime configuration is useful but not enough for real-world indexers. The CLI provides a way to have multiple "presets" and select which one to use at runtime. This is useful, for example, if you're deploying the same indexers on multiple networks where only the DNA stream URL and contract addresses change.

You can any number of presets in the configuration and use the --preset flag to select which one to use. For example, you can add a sepolia preset that contains the URL of the Starknet Sepolia DNA stream. If a preset doesn't specify a key, then the value from the root configuration is used.

apibara.config.ts
import { defineConfig } from "apibara/config";

export default defineConfig({
  runtimeConfig: {
    streamUrl: "https://starknet.preview.apibara.org",
    contractAddress: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a" as `0x${string}`,
  },
  presets: {
    sepolia: {
      runtimeConfig: {
        streamUrl: "https://starknet-sepolia.preview.apibara.org",
      },
    }
  },
});

You can then run the indexer in development mode using the sepolia preset.

Terminal
npm run dev -- --indexers=strk-staking --preset=sepolia
> [email protected] dev
> apibara dev --indexers=strk-staking --preset=sepolia

✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3858 ms
✔ Restarting indexers
strk-staking | log Block number 100092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 100119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
...

Storing data & persisting state across restarts

All indexers implemented in this tutorial are stateless. They don't store any data to a database and if you restart them they will restart indexing from the beginning.

You can refer to our storage section to learn more about writing data to a database and persisting the indexer's state across restarts.

Last modified
Apibara

Apibara is the fastest platform to build production-grade indexers that connect onchain data to web2 services.

© 2024 GNC Labs Limited. All rights reserved.