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
3668for 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
6089import { 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
75100try {
@@ -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
187126import { 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
226155block_number + transaction_index + event_index
227156```
228157
229- The stable event identity is :
158+ Stable event identity uses :
230159
231160``` text
232161block_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
279173block_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