Skip to content

Commit 1679ad6

Browse files
committed
docs: simplify starknet rpc readme
1 parent cba5d28 commit 1679ad6

1 file changed

Lines changed: 74 additions & 230 deletions

File tree

packages/starknet-rpc/README.md

Lines changed: 74 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,77 @@
11
# @apibara/starknet-rpc
22

3-
Event-focused Starknet JSON-RPC helpers for indexers.
3+
Starknet JSON-RPC event indexing helpers for Apibara indexers.
44

5-
This package is the phase 1 RPC path for Starknet indexing. It uses:
5+
This package supports:
66

7-
- `starknet_getEvents` over HTTP for historical backfills.
8-
- `starknet_subscribeEvents` over WebSocket for recent live indexing.
9-
- Explicit event cursors.
10-
- Explicit reorg notifications.
11-
12-
It does not use the Apibara DNA runtime as a block cache.
7+
- historical event backfill with `starknet_getEvents` over HTTP
8+
- live event indexing with `starknet_subscribeEvents` over WebSocket
9+
- explicit event cursors
10+
- reorg notifications and rollback cursors
11+
- an Apibara `RpcStreamConfig` adapter for accepted event blocks
1312

1413
## Requirements
1514

16-
Use Starknet JSON-RPC v0.10 or newer endpoints, for example URLs ending in
17-
`/rpc/v0_10` and `/ws/rpc/v0_10`.
15+
Use a Starknet JSON-RPC v0.10 or newer endpoint, for example `/rpc/v0_10`
16+
and `/ws/rpc/v0_10`.
17+
18+
The RPC node must include `block_number`, `transaction_index`, and
19+
`event_index` in event payloads. These fields are required for duplicate-safe
20+
cursoring and resume behavior.
1821

19-
The package targets modern Node.js runtimes with global `fetch` support. It
20-
ships a default WebSocket client for Node environments, and callers can still
21-
provide `webSocketFactory` when they need a custom transport.
22+
The package targets modern Node.js runtimes with global `fetch` support. A
23+
default Node WebSocket client is included, and custom transports can be provided
24+
with `webSocketFactory`.
2225

23-
This package relies on `block_number`, `transaction_index`, and `event_index`
24-
from emitted events for duplicate-safe cursoring. Older RPC versions do not
25-
include all of these fields. Pre-confirmed live indexing also requires a node
26-
that includes those cursor fields in `starknet_subscribeEvents` notifications.
27-
Pathfinder v0.10 provides them.
26+
## Recommended Usage
2827

29-
## Backfill
28+
Use `streamEvents` when you want historical backfill followed by live indexing:
3029

3130
```ts
32-
import { backfillEvents } from "@apibara/starknet-rpc";
31+
import { streamEvents } from "@apibara/starknet-rpc";
32+
33+
for await (const message of streamEvents({
34+
url: process.env.STARKNET_RPC_URL!,
35+
wsUrl: process.env.STARKNET_WS_URL!,
36+
fromBlock: { block_number: 0 },
37+
cursor: await loadCursor(),
38+
cursorFinalityStatus: await loadCursorFinalityStatus(),
39+
addresses: ["0x1234"],
40+
keys: [["0xabcdef"]],
41+
})) {
42+
if (message.type === "reorg") {
43+
await db.transaction(async (tx) => {
44+
await rollbackFrom(tx, message.reorg.startingBlockNumber);
45+
await saveCursor(tx, message.rollbackCursor);
46+
});
47+
continue;
48+
}
49+
50+
await db.transaction(async (tx) => {
51+
await insertEvent(tx, message.event);
52+
await saveCursor(tx, message.cursor);
53+
});
54+
}
55+
```
3356

34-
const cursor = await loadCursor();
57+
`streamEvents` backfills accepted events over HTTP, then switches to a live
58+
WebSocket subscription. Live subscriptions use `PRE_CONFIRMED` finality by
59+
default for low-latency indexing.
60+
61+
## HTTP Backfill
62+
63+
Use `backfillEvents` for bounded or standalone historical indexing:
64+
65+
```ts
66+
import { backfillEvents } from "@apibara/starknet-rpc";
3567

3668
for await (const message of backfillEvents({
3769
url: process.env.STARKNET_RPC_URL!,
38-
fromBlock: { block_number: cursor?.blockNumber ?? 0 },
70+
fromBlock: { block_number: 0 },
3971
toBlock: "latest",
4072
addresses: ["0x1234"],
4173
keys: [["0xabcdef"]],
42-
cursor,
74+
cursor: await loadCursor(),
4375
chunkSize: 1024,
4476
})) {
4577
await db.transaction(async (tx) => {
@@ -49,12 +81,9 @@ for await (const message of backfillEvents({
4981
}
5082
```
5183

52-
`cursor` means "last successfully persisted event". When a cursor is supplied,
53-
the backfill starts from that block and skips events at or before the cursor.
54-
Reorg handling should persist `message.rollbackCursor`; that cursor sorts before
55-
the first event in the rollback block.
84+
## Live Subscriptions
5685

57-
## Live Subscription
86+
Use `subscribeEvents` when you only need the WebSocket subscription layer:
5887

5988
```ts
6089
import { subscribeEvents } from "@apibara/starknet-rpc";
@@ -66,10 +95,6 @@ const subscription = subscribeEvents({
6695
keys: [["0xabcdef"]],
6796
idleTimeoutMs: 60_000,
6897
maxQueueSize: 10_000,
69-
reconnect: {
70-
minDelayMs: 500,
71-
maxDelayMs: 10_000,
72-
},
7398
});
7499

75100
try {
@@ -82,106 +107,20 @@ try {
82107
continue;
83108
}
84109

85-
await persistEvent(message.event, message.cursor);
86-
}
87-
} finally {
88-
await subscription.unsubscribe();
89-
}
90-
```
91-
92-
Subscriptions are for recent live indexing. If a node rejects the requested
93-
`blockId` with `TooManyBlocksBack`, use HTTP backfill first and then subscribe
94-
from a recent accepted head.
95-
96-
The WebSocket helpers close and reconnect when the connection is idle for
97-
`idleTimeoutMs` milliseconds. Set `idleTimeoutMs: 0` to disable this watchdog.
98-
Incoming messages are also bounded by `maxQueueSize`; if the consumer falls too
99-
far behind, the subscription closes instead of buffering without limit. Use
100-
`reconnect.maxAttempts` when a misconfigured endpoint should fail permanently
101-
instead of retrying forever.
102-
103-
## Combined Stream
104-
105-
```ts
106-
import { streamEvents } from "@apibara/starknet-rpc";
107-
108-
for await (const message of streamEvents({
109-
url: process.env.STARKNET_RPC_URL!,
110-
wsUrl: process.env.STARKNET_WS_URL!,
111-
fromBlock: { block_number: 0 },
112-
cursor: await loadCursor(),
113-
cursorFinalityStatus: await loadCursorFinalityStatus(),
114-
addresses: ["0x1234"],
115-
keys: [["0xabcdef"]],
116-
})) {
117-
if (message.type === "reorg") {
118110
await db.transaction(async (tx) => {
119-
await rollbackFrom(tx, message.reorg.startingBlockNumber);
120-
await saveCursor(tx, message.rollbackCursor);
111+
await insertEvent(tx, message.event);
112+
await saveCursor(tx, message.cursor);
121113
});
122-
continue;
123114
}
124-
125-
await db.transaction(async (tx) => {
126-
await insertEvent(tx, message.event);
127-
await saveCursor(tx, message.cursor);
128-
});
115+
} finally {
116+
await subscription.unsubscribe();
129117
}
130118
```
131119

132-
`streamEvents` performs an inclusive HTTP-to-WS handoff:
133-
134-
1. Reads the current accepted head.
135-
2. Backfills with `starknet_getEvents` through that head.
136-
3. Subscribes with `starknet_subscribeEvents` from the same head.
137-
4. Deduplicates replayed events using the persisted cursor identity.
138-
139-
This avoids a missing-event window between HTTP and WebSocket indexing.
140-
141-
If the WebSocket handoff block has fallen outside the node's subscription
142-
history window, `streamEvents` catches the `TooManyBlocksBack` response, runs
143-
another HTTP catch-up pass to the latest accepted head, and retries the live
144-
subscription from the newer block.
145-
146-
`streamEvents` retries transport-level HTTP failures with exponential backoff.
147-
JSON-RPC errors are surfaced to the caller. Direct `getEvents` and
148-
`backfillEvents` calls do not add a global rate limiter; callers indexing large
149-
histories should choose a node and `chunkSize` appropriate for their rate limits.
150-
151120
## Apibara RPC Stream
152121

153-
`StarknetRpcStream` implements the same `RpcStreamConfig` shape used by
154-
`@apibara/evm-rpc`:
155-
156-
```ts
157-
import { createRpcClient } from "@apibara/protocol/rpc";
158-
import { StarknetRpcStream } from "@apibara/starknet-rpc";
159-
160-
const client = createRpcClient(
161-
new StarknetRpcStream({
162-
url: process.env.STARKNET_RPC_URL!,
163-
getEventsRangeSize: 1_000n,
164-
headRefreshIntervalMs: 1_000,
165-
}),
166-
);
167-
168-
for await (const message of client.streamData({
169-
filter: [
170-
{
171-
addresses: ["0x1234"],
172-
keys: [["0xabcdef"]],
173-
},
174-
],
175-
startingCursor: { orderKey: 0n },
176-
})) {
177-
// message.data.data contains event-focused StarknetRpcBlock values.
178-
}
179-
```
180-
181-
This adapter is for accepted event blocks fetched over HTTP. Use `streamEvents`
182-
when the indexer needs the low-latency pre-confirmed WebSocket path.
183-
184-
Apibara CLI indexers can use the adapter without a DNA `streamUrl`:
122+
`StarknetRpcStream` implements Apibara's `RpcStreamConfig` interface for
123+
accepted event blocks fetched over HTTP:
185124

186125
```ts
187126
import { defineIndexer } from "apibara/indexer";
@@ -204,129 +143,34 @@ export default defineIndexer(
204143
});
205144
```
206145

207-
## Cursor Contract
146+
Use `streamEvents` instead when you need the pre-confirmed WebSocket path.
208147

209-
Persist the cursor in the same transaction as the indexed rows derived from the
210-
event:
148+
## Cursor and Reorg Contract
211149

212-
```sql
213-
create table indexer_cursor (
214-
id text primary key,
215-
block_number integer not null,
216-
transaction_index integer not null,
217-
transaction_hash text not null,
218-
event_index integer not null,
219-
finality_status text
220-
);
221-
```
150+
Persist the cursor in the same transaction as the rows derived from the event.
222151

223-
Cursor ordering and resume semantics use:
152+
Cursor ordering uses:
224153

225154
```text
226155
block_number + transaction_index + event_index
227156
```
228157

229-
The stable event identity is:
158+
Stable event identity uses:
230159

231160
```text
232161
block_number + transaction_hash + event_index
233162
```
234163

235-
`event_index` is scoped to the transaction, not the whole block. Do not use
236-
`block_number + event_index` as a unique key.
237-
238-
When using the default pre-confirmed live stream, persist the event finality
239-
next to the cursor and pass it back as `cursorFinalityStatus` on restart. If the
240-
cursor finality is unknown and the live stream uses `PRE_CONFIRMED`,
241-
`streamEvents` conservatively emits a synthetic rollback for the cursor block
242-
and replays that block over HTTP before returning to WebSocket live indexing.
243-
This prevents a missed reorg from skipping replacement events that share the
244-
same `block_number + transaction_index + event_index` ordering.
245-
246-
If cursor finality is not persisted, this conservative replay happens on every
247-
restart of a pre-confirmed stream. Rollback and transform handlers should be
248-
idempotent, especially when they perform external side effects. Persisting
249-
`finality_status` and passing `cursorFinalityStatus` avoids unnecessary
250-
cursor-block rollbacks once the cursor is known to be accepted.
251-
252-
Pre-confirmed subscriptions may deliver the same stable event identity again
253-
when finality or `block_hash` changes. The subscription helpers dedupe exact
254-
replays, but they emit these finality updates so callers can update the
255-
persisted row and cursor finality.
164+
`event_index` is scoped to the transaction, not the whole block.
256165

257-
## Schema Recommendations
166+
For pre-confirmed indexing, persist cursor finality and pass it back as
167+
`cursorFinalityStatus` on restart. This lets the stream resume without skipping
168+
accepted replacement events after reorgs.
258169

259-
For event-derived tables, store:
260-
261-
- `block_number`
262-
- `block_hash`
263-
- `transaction_index`
264-
- `transaction_hash`
265-
- `event_index`
266-
- contract address
267-
- normalized selector or first key
268-
269-
Use a unique constraint on `(block_number, transaction_hash, event_index)` for
270-
raw event rows. Domain tables can use their own keys, but they should also store
271-
the event cursor fields that created or last updated each row, including
272-
`transaction_index` for ordering.
273-
274-
## Reorg Rollback
275-
276-
On a reorg message, roll back all chain-derived rows where:
170+
When receiving a reorg message, delete or roll back all chain-derived rows where:
277171

278172
```sql
279173
block_number >= starting_block_number
280174
```
281175

282-
Update the persisted cursor to `message.rollbackCursor` in the same transaction
283-
as the chain-row rollback:
284-
285-
```ts
286-
await db.transaction(async (tx) => {
287-
await tx.sql`
288-
delete from indexed_events
289-
where block_number >= ${message.reorg.startingBlockNumber}
290-
`;
291-
292-
await saveCursor(tx, message.rollbackCursor);
293-
});
294-
```
295-
296-
After the caller handles the rollback, `streamEvents` resumes from the reorg
297-
starting block with HTTP backfill and then returns to WebSocket live indexing.
298-
If a process restarts with a persisted pre-confirmed cursor beyond the current
299-
accepted head, `streamEvents` emits a synthetic reorg message and resets its
300-
dedupe state to `message.rollbackCursor` before subscribing live. This prevents
301-
replacement events at the same cursor ordering from being skipped.
302-
If the accepted head has already caught up to the cursor block, the synthetic
303-
rollback starts at the cursor block and the HTTP backfill leg replays that block.
304-
305-
## Finality
306-
307-
Phase 1 is designed for low-latency Starknet event indexing:
308-
309-
- `backfillEvents` and the HTTP backfill leg of `streamEvents` use
310-
`starknet_getEvents` for accepted historical events.
311-
- `subscribeEvents` and the live WebSocket leg of `streamEvents` subscribe with
312-
`finalityStatus: "PRE_CONFIRMED"` by default.
313-
314-
Callers can override `finalityStatus` when they need accepted-only live
315-
indexing, but the default live path is pre-confirmed so applications can observe
316-
new events as soon as the Starknet node publishes them over
317-
`starknet_subscribeEvents`.
318-
319-
Pre-confirmed events can be reorged. Callers must persist cursors atomically
320-
with indexed rows and honor reorg messages by rolling back all chain-derived rows
321-
where `block_number >= starting_block_number`.
322-
323-
## Live Integration Tests
324-
325-
Optional live integration tests should read URLs from:
326-
327-
```sh
328-
TEST_STARKNET_RPC_URL=...
329-
TEST_STARKNET_WS_URL=...
330-
```
331-
332-
Do not commit full-node URLs into tests or examples.
176+
Then persist `message.rollbackCursor` in the same transaction.

0 commit comments

Comments
 (0)