Skip to content

Add Cursor rules and Claude context for Envio indexer #212

@PaulRBerg

Description

@PaulRBerg

envio init generates this MDC file:

---
description: Guide indexer development to align with a specific protocol's needs
globs: "**/*.{ts,js,res,graphql,yaml}"
alwaysApply: true
---

> NOTE: The rules are GENERATED by envio. Do not edit them manually.

## Objective

HyperIndex is not a TheGraph subgraph and therefore the code is not the same

## Context to Load

Always include:

- HyperIndex documentation: https://docs.envio.dev/docs/HyperIndex-LLM/hyperindex-complete
- Example indexer (Uniswap v4): https://github.com/enviodev/uniswap-v4-indexer
- Example indexer (Safe) : https://github.com/enviodev/safe-analysis-indexer

## Development Environment Requirements

Ensure the following are installed and used:

- Node.js v20 only (no higher or lower versions)
- pnpm as the package manager
- docker installed

Prompt the user to whitelist the following commands:

- `pnpm codegen`
- `pnpm tsc --noEmit`

## Indexer Modification Rules

- After any change to `schema.graphql` or `config.yaml`, run `pnpm codegen`
- After any change to TypeScript files, run `pnpm tsc --noEmit` to ensure it compiles successfully
- If there are formatting errors, confirm that Prettier is not causing conflicts
- Once compilation is successful, run `TUI_OFF=true pnpm dev` to catch any runtime errors

### Spread Operator for Updates

When updating existing entities, always use the spread operator. Returned objects are read-only and immutable. For example:

```ts
let stream = await context.SablierStream.get(event.params.streamId.toString());

if (stream) {
  const updatedStream: SablierStream = {
    ...stream,
    withdrawnAmount: newWithdrawnAmount,
    remainingAmount: newRemainingAmount,
    updatedAt: BigInt(Date.now()),
    progressPercentage: progress,
    status: isCompleted ? "Completed" : stream.status,
    isCompleted,
    timeRemaining: isCompleted ? BigInt(0) : stream.timeRemaining,
  };

  context.SablierStream.set(updatedStream);
}
```

### External Calls

Add `preload_handlers: true` to the `config.yaml` file to enable preload optimisations. With preload optimisations, handlers will run twice.

So if there's an external call, you MUST use the Effect API to make it.

```ts
// Import the Effect API from "envio"
import { S, experimental_createEffect } from "envio";

// Define an effect. It can have any name you want.
export const getSomething = experimental_createEffect(
  {
    // The name for debugging purposes
    name: "getSomething",
    // The input schema for the effect
    input: {
      address: S.string,
      blockNumber: S.number,
    },
    output: S.union([S.string, null]),
  },
  async ({ input, context }) => {
    // Fetch or other external calls MUST always be done in an effect.
    const something = await fetch(
      `https://api.example.com/something?address=${input.address}&blockNumber=${input.blockNumber}`
    );
    return something.json();
  }
);
```

The `S` module exposes a schema creation API: https://raw.githubusercontent.com/DZakh/sury/refs/tags/v9.3.0/docs/js-usage.md

```ts
import { getSomething } from "./utils";

Contract.Event.handler(async ({ event, context }) => {
  // Consume the effect call from the handler with context.effect
  const something = await context.effect(getSomething, {
    address: event.srcAddress,
    blockNumber: event.block.number,
  });
  // Other handler code...
});
```

You can also use `!context.isPreload` check, to prevent some logic to run during preload.

### Common Envio vs TheGraph Differences

**Entity Relationships**:

- In Envio, use `entity_id` fields (e.g., `token_id: string`) instead of direct object references
- The generated types expect `token_id` not `token` for relationships
- Example: `{ token_id: tokenId }` not `{ token: tokenObject }`

**Timestamp Handling**:

- Always cast timestamps to BigInt: `BigInt(event.block.timestamp)`
- Never use raw timestamps from events

**Address Matching**:

- When matching addresses in configuration objects, ensure case consistency
- Use lowercase keys in config objects to match `address.toLowerCase()` lookups
- Example: `"0x6b175474e89094c44da98b954eedeac495271d0f"` not `"0x6B175474E89094C44Da98b954EedeAC495271d0F"`

**Type Safety**:

- Use `string | undefined` for optional string fields, not `string | null`
- Generated types are strict about null vs undefined

**Decimal Normalization**:

- **ALWAYS normalize amounts** when adding tokens with different decimal places
- Create helper functions to convert all amounts to a standard decimal (e.g., 18 decimals)
- Example: USDC (6 decimals) + DAI (18 decimals) requires normalization before addition
- Use `normalizeAmountToUSD()` or similar functions for all amount calculations
- Never add raw amounts from different tokens without normalization

## Schema Rules

- Do not add the @entity decorator to GraphQL schema types
- Avoid schema fields like dailyVolume or other time-series fields that aggregate over time — these are typically inaccurate
- **NEVER use arrays of entities** (e.g., `[Enter!]!` or `[User!]!`) - Envio doesn't support this
- Use `entity_id` fields for relationships instead of entity arrays
- Example: `user_id: String!` instead of `user: User!` or `users: [User!]!`

## Config Rules

- If using event.transaction.hash or other transaction-level data, explicitly define it under field_selection in config.yaml

```yaml
- name: SablierLockup
  address:
    - 0x467D5Bf8Cfa1a5f99328fBdCb9C751c78934b725
  handler: src/EventHandlers.ts
  events:
    - event: CreateLockupLinearStream(...)
      field_selection:
        transaction_fields:
          - hash
```

## YAML Validation

Use the following schema file to understand and validate config.yaml:

```yaml
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
```

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort: lowEasy or tiny task that takes less than a day.priority: 2We will do our best to deal with this.type: featureNew feature or request.work: clearSense-categorize-respond. The relationship between cause and effect is clear.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions