effective-indexer 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,7 @@
1
- MIT License
2
-
3
1
  Copyright (c) 2026 Aleksandr Shenshin
2
+ All rights reserved.
4
3
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
4
+ This software and associated documentation files are proprietary and confidential.
5
+ No part of this software may be copied, modified, distributed, sublicensed,
6
+ published, sold, or used in any form without prior written permission from
7
+ the copyright holder.
package/README.md CHANGED
@@ -1,7 +1,160 @@
1
1
  # Effective Indexer
2
2
 
3
+ EVM event indexing without hosted lock-in.
4
+
5
+ `effective-indexer` runs as your own worker, writes directly to your SQLite database, and gives you a typed query API.
6
+
7
+ ## Why this approach
8
+
9
+ - **Own your data**: events are stored in your DB, not in a third-party service.
10
+ - **Simple operations**: one worker process, one config file, no subgraph deployment pipeline.
11
+ - **Production-safe behavior**: checkpoint resume, reorg detection, retry/backoff, live polling.
12
+ - **Fast backfill**: parallel `eth_getLogs` with deterministic chunk ordering.
13
+ - **Typed DX**: TypeScript-first config and query surface.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install effective-indexer effect
19
+ ```
20
+
21
+ `effect` is a peer dependency.
22
+
23
+ ## 5-minute setup
24
+
25
+ ### 1) Create `indexer.config.ts`
26
+
27
+ ```ts
28
+ import { defineIndexerConfig } from "effective-indexer"
29
+ import type { Abi } from "viem"
30
+
31
+ const transferAbi: Abi = [
32
+ {
33
+ type: "event",
34
+ name: "Transfer",
35
+ inputs: [
36
+ { indexed: true, name: "from", type: "address" },
37
+ { indexed: true, name: "to", type: "address" },
38
+ { indexed: false, name: "value", type: "uint256" },
39
+ ],
40
+ },
41
+ ]
42
+
43
+ export default defineIndexerConfig({
44
+ rpcUrl: "https://rpc.mainnet.rootstock.io/{{EVM_RPC_API_KEY}}",
45
+ dbPath: "./data/events.db",
46
+ contracts: [
47
+ {
48
+ name: "Token",
49
+ address: "0xYourContractAddress",
50
+ abi: transferAbi,
51
+ events: ["Transfer"],
52
+ startBlock: 0n,
53
+ },
54
+ ],
55
+ network: {
56
+ logs: {
57
+ chunkSize: 2000,
58
+ parallelRequests: 3,
59
+ },
60
+ },
61
+ })
62
+ ```
63
+
64
+ ### 2) Create `scripts/indexer.ts`
65
+
66
+ ```ts
67
+ import config from "../indexer.config"
68
+ import { resolveIndexerConfigFromEnv, runIndexerWorker } from "effective-indexer"
69
+
70
+ const resolvedConfig = resolveIndexerConfigFromEnv(config)
71
+
72
+ runIndexerWorker(resolvedConfig).catch(error => {
73
+ console.error("Indexer worker failed:", error)
74
+ process.exit(1)
75
+ })
76
+ ```
77
+
78
+ ### 3) Add env and run
79
+
80
+ `.env`:
81
+
82
+ ```bash
83
+ EVM_RPC_API_KEY=your-rpc-api-key
84
+ # Optional full URL override:
85
+ # EVM_RPC_URL=https://rpc.mainnet.rootstock.io/<API_KEY>
86
+ ```
87
+
88
+ Run:
89
+
90
+ ```bash
91
+ node --import tsx ./scripts/indexer.ts
92
+ ```
93
+
94
+ ## Query data
95
+
96
+ ```ts
97
+ import config from "../indexer.config"
98
+ import { Indexer, resolveIndexerConfigFromEnv } from "effective-indexer"
99
+
100
+ const indexer = Indexer.create(resolveIndexerConfigFromEnv(config))
101
+
102
+ const events = await indexer.query({
103
+ contractName: "Token",
104
+ eventName: "Transfer",
105
+ order: "desc",
106
+ limit: 50,
107
+ })
108
+
109
+ console.log(events.length)
110
+ await indexer.stop()
111
+ ```
112
+
113
+ ## Public API
114
+
115
+ - `defineIndexerConfig(config)`
116
+ Identity helper for typed config files (Hardhat-style).
117
+ - `resolveIndexerConfigFromEnv(config, options?)`
118
+ Resolves `{{ENV_VAR}}` placeholders and optional RPC URL override.
119
+ - `runIndexerWorker(config, options?)`
120
+ Runs long-lived worker with built-in DB directory creation and graceful shutdown.
121
+ - `Indexer.create(config)`
122
+ Returns handle: `start()`, `stop()`, `query()`, `count()`.
123
+
124
+ ## Config essentials
125
+
126
+ - `rpcUrl`: RPC endpoint URL (supports placeholders like `{{EVM_RPC_API_KEY}}`)
127
+ - `dbPath`: SQLite path (default `./indexer.db`)
128
+ - `contracts`: non-empty list of contracts and events to index
129
+ - `network.polling`: block polling interval and confirmations
130
+ - `network.logs`: chunk size, retries, parallel requests
131
+ - `network.reorg.depth`: reorg buffer depth
132
+ - `telemetry.progress`: CLI progress rendering
133
+ - `logLevel`, `logFormat`, `enableTelemetry`
134
+
135
+ ## Operational notes
136
+
137
+ - Run a single writer process per SQLite file.
138
+ - Keep DB on persistent storage.
139
+ - Worker resumes from checkpoint after restart.
140
+ - RPC must support `eth_getLogs`.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ npm run build
146
+ npm run typecheck
147
+ npm run test
148
+ npm run check
149
+ ```
150
+
151
+ Repository: [github.com/cybervoid0/effective-indexer](https://github.com/cybervoid0/effective-indexer)
152
+ # Effective Indexer
153
+
3
154
  Lightweight EVM smart contract event indexer built with [Effect](https://effect.website).
4
155
 
156
+ Index EVM events to your own database in minutes — no hosted lock-in, no PhD required.
157
+
5
158
  Repository: [github.com/cybervoid0/effective-indexer](https://github.com/cybervoid0/effective-indexer)
6
159
 
7
160
  Indexes smart contract events into SQLite with:
@@ -87,6 +240,21 @@ Returns `IndexerHandle`:
87
240
  - `query(q?: EventQuery): Promise<ParsedEvent[]>`
88
241
  - `count(q?: EventQuery): Promise<number>`
89
242
 
243
+ ### `defineIndexerConfig(config)`
244
+
245
+ Identity helper for a typed config file (Hardhat-style DX).
246
+
247
+ ### `resolveIndexerConfigFromEnv(config, options?)`
248
+
249
+ Resolves `{{ENV_VAR}}` placeholders in `rpcUrl` and supports optional RPC override from env (`EVM_RPC_URL` by default).
250
+
251
+ ### `runIndexerWorker(config, options?)`
252
+
253
+ Runs a long-lived worker with built-in:
254
+ - SQLite directory creation
255
+ - graceful shutdown on `SIGINT` / `SIGTERM`
256
+ - keep-alive process loop
257
+
90
258
  ### `IndexerConfig`
91
259
 
92
260
  | Field | Type | Default | Description |
@@ -159,7 +327,21 @@ Run the indexer as a dedicated long-lived worker process (not in request handler
159
327
  Create `scripts/indexer.ts`:
160
328
 
161
329
  ```ts
162
- import { Indexer } from "effective-indexer"
330
+ import config from "../indexer.config"
331
+ import { resolveIndexerConfigFromEnv, runIndexerWorker } from "effective-indexer"
332
+
333
+ const resolvedConfig = resolveIndexerConfigFromEnv(config)
334
+
335
+ runIndexerWorker(resolvedConfig).catch(error => {
336
+ console.error("Indexer worker failed:", error)
337
+ process.exit(1)
338
+ })
339
+ ```
340
+
341
+ Create `indexer.config.ts`:
342
+
343
+ ```ts
344
+ import { defineIndexerConfig } from "effective-indexer"
163
345
  import type { Abi } from "viem"
164
346
 
165
347
  const transferAbi: Abi = [
@@ -174,104 +356,27 @@ const transferAbi: Abi = [
174
356
  },
175
357
  ]
176
358
 
177
- const indexer = Indexer.create({
178
- rpcUrl: process.env.EVM_RPC_URL!,
179
- dbPath: process.env.INDEXER_DB_PATH ?? "./data/events.db",
359
+ export default defineIndexerConfig({
360
+ rpcUrl: "https://rpc.mainnet.rootstock.io/{{EVM_RPC_API_KEY}}",
361
+ dbPath: "./data/events.db",
180
362
  contracts: [
181
363
  {
182
- name: process.env.INDEXER_CONTRACT_NAME ?? "Token",
183
- address: process.env.INDEXER_CONTRACT_ADDRESS!,
364
+ name: "Token",
365
+ address: "0xYourContractAddress",
184
366
  abi: transferAbi,
185
367
  events: ["Transfer"],
186
- startBlock: BigInt(process.env.INDEXER_START_BLOCK ?? "0"),
368
+ startBlock: 0n,
187
369
  },
188
370
  ],
189
- network: {
190
- polling: {
191
- intervalMs: Number(process.env.INDEXER_POLL_INTERVAL_MS ?? "12000"),
192
- confirmations: Number(process.env.INDEXER_CONFIRMATIONS ?? "1"),
193
- },
194
- logs: {
195
- chunkSize: Number(process.env.INDEXER_CHUNK_SIZE ?? "2000"),
196
- parallelRequests: Number(process.env.INDEXER_PARALLEL_REQUESTS ?? "1"),
197
- maxRetries: Number(process.env.INDEXER_MAX_RETRIES ?? "5"),
198
- retry: {
199
- baseDelayMs: Number(process.env.INDEXER_RETRY_BASE_MS ?? "1000"),
200
- maxDelayMs: Number(process.env.INDEXER_RETRY_MAX_MS ?? "30000"),
201
- },
202
- },
203
- reorg: {
204
- depth: Number(process.env.INDEXER_REORG_DEPTH ?? "20"),
205
- },
206
- },
207
- telemetry: {
208
- progress: {
209
- enabled: process.env.INDEXER_PROGRESS_ENABLED !== "false",
210
- intervalMs: Number(process.env.INDEXER_PROGRESS_INTERVAL_MS ?? "3000"),
211
- },
212
- },
213
- logLevel: (process.env.INDEXER_LOG_LEVEL ?? "info") as
214
- | "trace"
215
- | "debug"
216
- | "info"
217
- | "warning"
218
- | "error"
219
- | "none",
220
- logFormat: (process.env.INDEXER_LOG_FORMAT ?? "pretty") as
221
- | "pretty"
222
- | "json"
223
- | "structured",
224
- enableTelemetry: process.env.INDEXER_TELEMETRY !== "false",
225
- })
226
-
227
- const start = async (): Promise<void> => {
228
- await indexer.start()
229
- console.log("Indexer worker started")
230
-
231
- // Keep the process alive while indexing in background.
232
- const keepAlive = setInterval(() => undefined, 60_000)
233
-
234
- const stop = async (): Promise<void> => {
235
- clearInterval(keepAlive)
236
- await indexer.stop()
237
- process.exit(0)
238
- }
239
-
240
- process.on("SIGINT", () => {
241
- void stop()
242
- })
243
- process.on("SIGTERM", () => {
244
- void stop()
245
- })
246
- }
247
-
248
- start().catch(error => {
249
- console.error("Indexer worker failed:", error)
250
- process.exit(1)
251
371
  })
252
372
  ```
253
373
 
254
374
  Create `.env`:
255
375
 
256
376
  ```bash
257
- EVM_RPC_URL=https://your-rpc-url
258
- INDEXER_DB_PATH=./data/events.db
259
- INDEXER_CONTRACT_NAME=Token
260
- INDEXER_CONTRACT_ADDRESS=0xYourContractAddress
261
- INDEXER_START_BLOCK=0
262
- INDEXER_POLL_INTERVAL_MS=12000
263
- INDEXER_CONFIRMATIONS=1
264
- INDEXER_CHUNK_SIZE=2000
265
- INDEXER_PARALLEL_REQUESTS=1
266
- INDEXER_MAX_RETRIES=5
267
- INDEXER_RETRY_BASE_MS=1000
268
- INDEXER_RETRY_MAX_MS=30000
269
- INDEXER_REORG_DEPTH=20
270
- INDEXER_PROGRESS_ENABLED=true
271
- INDEXER_PROGRESS_INTERVAL_MS=3000
272
- INDEXER_LOG_LEVEL=info
273
- INDEXER_LOG_FORMAT=pretty
274
- INDEXER_TELEMETRY=true
377
+ EVM_RPC_API_KEY=your-rpc-api-key
378
+ # Optional full RPC URL override:
379
+ # EVM_RPC_URL=https://rpc.mainnet.rootstock.io/<API_KEY>
275
380
  ```
276
381
 
277
382
  Add scripts (with `tsx` installed):
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var promises = require('fs/promises');
4
+ var path = require('path');
3
5
  var sqlSqliteNode = require('@effect/sql-sqlite-node');
4
6
  var effect = require('effect');
5
7
  var viem = require('viem');
@@ -20,6 +22,7 @@ var ConfigError = class extends effect.Data.TaggedError("ConfigError") {
20
22
  };
21
23
 
22
24
  // src/config.ts
25
+ var defineIndexerConfig = (config) => config;
23
26
  var resolveNetwork = (config) => {
24
27
  const n = config.network;
25
28
  return {
@@ -135,6 +138,7 @@ var RpcProviderLive = effect.Layer.effect(
135
138
  })
136
139
  });
137
140
  const getLogs = (params) => effect.Effect.tryPromise({
141
+ // Use raw request to pass topics exactly as eth_getLogs expects.
138
142
  try: () => client.request({
139
143
  method: "eth_getLogs",
140
144
  params: [
@@ -712,6 +716,7 @@ var fetchLogs = (params) => effect.Stream.unwrap(
712
716
  fromBlock: chunk.from,
713
717
  toBlock: chunk.to
714
718
  }).pipe(
719
+ // Retry policy is exponential and bounded by maxDelayMs.
715
720
  effect.Effect.tapError(
716
721
  (e) => effect.Effect.gen(function* () {
717
722
  const n = yield* effect.Ref.getAndUpdate(attempt, (a) => a + 1);
@@ -910,11 +915,14 @@ var indexContract = (contract) => effect.Stream.unwrap(
910
915
  );
911
916
  }
912
917
  const chunkSize = BigInt(config.network.logs.chunkSize);
913
- const lastProcessed = yield* effect.Ref.modify(processedBlocksRef, (current) => {
914
- const advanced = current + chunkSize;
915
- const next = advanced > totalBackfillBlocks ? totalBackfillBlocks : advanced;
916
- return [next, next];
917
- });
918
+ const lastProcessed = yield* effect.Ref.modify(
919
+ processedBlocksRef,
920
+ (current) => {
921
+ const advanced = current + chunkSize;
922
+ const next = advanced > totalBackfillBlocks ? totalBackfillBlocks : advanced;
923
+ return [next, next];
924
+ }
925
+ );
918
926
  yield* progress.update(
919
927
  contract.name,
920
928
  lastProcessed,
@@ -1097,6 +1105,64 @@ var QueryApiLive = effect.Layer.effect(
1097
1105
  return { getEvents, getEventCount, getLatestBlock };
1098
1106
  })
1099
1107
  );
1108
+ var toConfigMap = (env) => {
1109
+ const map = /* @__PURE__ */ new Map();
1110
+ for (const [key, value] of Object.entries(env)) {
1111
+ if (typeof value === "string") {
1112
+ map.set(key, value);
1113
+ }
1114
+ }
1115
+ return map;
1116
+ };
1117
+ var getProvider = (env) => effect.ConfigProvider.fromMap(toConfigMap(env ?? process.env));
1118
+ var readOptionalString = (name, provider) => effect.Effect.runSync(
1119
+ effect.Effect.withConfigProvider(provider)(
1120
+ effect.Config.option(effect.Config.nonEmptyString(name))
1121
+ )
1122
+ );
1123
+ var readRequiredString = (name, provider) => {
1124
+ try {
1125
+ return effect.Effect.runSync(
1126
+ effect.Effect.withConfigProvider(provider)(effect.Config.nonEmptyString(name))
1127
+ );
1128
+ } catch {
1129
+ throw new Error(`Missing required env var: ${name}`);
1130
+ }
1131
+ };
1132
+ var readRequiredRedactedString = (name, provider) => {
1133
+ try {
1134
+ const redacted = effect.Effect.runSync(
1135
+ effect.Effect.withConfigProvider(provider)(
1136
+ effect.Config.redacted(effect.Config.nonEmptyString(name))
1137
+ )
1138
+ );
1139
+ return effect.Redacted.value(redacted);
1140
+ } catch {
1141
+ throw new Error(`Missing required env var: ${name}`);
1142
+ }
1143
+ };
1144
+ var resolveRpcUrl = (template, provider, sensitiveEnvNames) => template.replace(
1145
+ /\{\{([A-Z0-9_]+)\}\}/g,
1146
+ (_input, envName) => sensitiveEnvNames.has(envName) ? readRequiredRedactedString(envName, provider) : readRequiredString(envName, provider)
1147
+ );
1148
+ var resolveIndexerConfigFromEnv = (config, options) => {
1149
+ const provider = getProvider(options?.env);
1150
+ const rpcUrlOverrideEnv = options?.rpcUrlOverrideEnv ?? "EVM_RPC_URL";
1151
+ const rpcUrlOverride = readOptionalString(rpcUrlOverrideEnv, provider);
1152
+ const sensitiveEnvNames = new Set(
1153
+ options?.sensitiveEnvNames ?? ["EVM_RPC_API_KEY"]
1154
+ );
1155
+ if (effect.Option.isSome(rpcUrlOverride)) {
1156
+ return {
1157
+ ...config,
1158
+ rpcUrl: rpcUrlOverride.value
1159
+ };
1160
+ }
1161
+ return {
1162
+ ...config,
1163
+ rpcUrl: resolveRpcUrl(config.rpcUrl, provider, sensitiveEnvNames)
1164
+ };
1165
+ };
1100
1166
 
1101
1167
  // src/index.ts
1102
1168
  var buildLayers = (config) => {
@@ -1194,6 +1260,67 @@ var createIndexer = (config) => {
1194
1260
  }
1195
1261
  };
1196
1262
  };
1263
+ var defaultWorkerRuntime = {
1264
+ process,
1265
+ setInterval,
1266
+ clearInterval
1267
+ };
1268
+ var resolveDbPath = (config) => config.dbPath ?? "./indexer.db";
1269
+ var ensureDbDirectory = async (config) => {
1270
+ const dbPath = resolveDbPath(config);
1271
+ if (dbPath === ":memory:") {
1272
+ return;
1273
+ }
1274
+ await promises.mkdir(path.dirname(dbPath), { recursive: true });
1275
+ };
1276
+ var runIndexerWorker = async (config, options) => {
1277
+ if (options?.ensureDbDirectory ?? true) {
1278
+ await ensureDbDirectory(config);
1279
+ }
1280
+ const runtime = options?.runtime ?? defaultWorkerRuntime;
1281
+ const create = options?.createIndexer ?? createIndexer;
1282
+ const signals = options?.shutdownSignals ?? ["SIGINT", "SIGTERM"];
1283
+ const keepAliveIntervalMs = options?.keepAliveIntervalMs ?? 6e4;
1284
+ const indexer = create(config);
1285
+ await indexer.start();
1286
+ await new Promise((resolve, reject) => {
1287
+ const keepAliveTimer = runtime.setInterval(
1288
+ () => void 0,
1289
+ keepAliveIntervalMs
1290
+ );
1291
+ let stopping = false;
1292
+ const signalHandlers = /* @__PURE__ */ new Map();
1293
+ const cleanup = () => {
1294
+ runtime.clearInterval(keepAliveTimer);
1295
+ for (const signal of signals) {
1296
+ const handler = signalHandlers.get(signal);
1297
+ if (handler !== void 0) {
1298
+ runtime.process.off(signal, handler);
1299
+ }
1300
+ }
1301
+ };
1302
+ const handleStop = async () => {
1303
+ if (stopping) {
1304
+ return;
1305
+ }
1306
+ stopping = true;
1307
+ cleanup();
1308
+ try {
1309
+ await indexer.stop();
1310
+ resolve();
1311
+ } catch (error) {
1312
+ reject(error);
1313
+ }
1314
+ };
1315
+ for (const signal of signals) {
1316
+ const handler = () => {
1317
+ void handleStop();
1318
+ };
1319
+ signalHandlers.set(signal, handler);
1320
+ runtime.process.on(signal, handler);
1321
+ }
1322
+ });
1323
+ };
1197
1324
  var Indexer = {
1198
1325
  create: createIndexer
1199
1326
  };
@@ -1222,6 +1349,9 @@ exports.Storage = Storage;
1222
1349
  exports.StorageLive = StorageLive;
1223
1350
  exports.computeSnapshot = computeSnapshot;
1224
1351
  exports.createIndexer = createIndexer;
1352
+ exports.defineIndexerConfig = defineIndexerConfig;
1225
1353
  exports.resolveConfig = resolveConfig;
1354
+ exports.resolveIndexerConfigFromEnv = resolveIndexerConfigFromEnv;
1355
+ exports.runIndexerWorker = runIndexerWorker;
1226
1356
  //# sourceMappingURL=index.cjs.map
1227
1357
  //# sourceMappingURL=index.cjs.map