Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file.
Empty file.
177 changes: 177 additions & 0 deletions apps/indexer/.cursor/rules/hyperindex.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
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, createEffect } from "envio";

// Define an effect. It can have any name you want.
export const getSomething = 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]),
rateLimit: false,
cache: true,
},
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
```
Loading