@eclesia/indexer-engine 2.14.4 → 2.16.0
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/dist/_virtual/rolldown_runtime.cjs +20 -15
- package/dist/_virtual/rolldown_runtime.mjs +10 -30
- package/dist/constants.cjs +31 -2
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +27 -3
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.mts +27 -3
- package/dist/constants.d.mts.map +1 -1
- package/dist/constants.mjs +27 -3
- package/dist/constants.mjs.map +1 -1
- package/dist/emitter/index.cjs +25 -7
- package/dist/emitter/index.cjs.map +1 -1
- package/dist/emitter/index.d.cts +8 -1
- package/dist/emitter/index.d.cts.map +1 -1
- package/dist/emitter/index.d.mts +8 -1
- package/dist/emitter/index.d.mts.map +1 -1
- package/dist/emitter/index.mjs +25 -7
- package/dist/emitter/index.mjs.map +1 -1
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/indexer/index.cjs +498 -225
- package/dist/indexer/index.cjs.map +1 -1
- package/dist/indexer/index.d.cts +111 -7
- package/dist/indexer/index.d.cts.map +1 -1
- package/dist/indexer/index.d.mts +111 -7
- package/dist/indexer/index.d.mts.map +1 -1
- package/dist/indexer/index.mjs +496 -224
- package/dist/indexer/index.mjs.map +1 -1
- package/dist/metrics/index.cjs +14 -0
- package/dist/metrics/index.cjs.map +1 -1
- package/dist/metrics/index.d.cts +7 -0
- package/dist/metrics/index.d.cts.map +1 -1
- package/dist/metrics/index.d.mts +7 -0
- package/dist/metrics/index.d.mts.map +1 -1
- package/dist/metrics/index.mjs +14 -0
- package/dist/metrics/index.mjs.map +1 -1
- package/dist/mocks/index.cjs +3 -1
- package/dist/mocks/index.cjs.map +1 -1
- package/dist/mocks/index.d.cts +2 -2
- package/dist/mocks/index.d.mts +2 -2
- package/dist/mocks/index.mjs +4 -2
- package/dist/mocks/index.mjs.map +1 -1
- package/dist/mocks/rpc-client.cjs +185 -20
- package/dist/mocks/rpc-client.cjs.map +1 -1
- package/dist/mocks/rpc-client.d.cts +26 -10
- package/dist/mocks/rpc-client.d.cts.map +1 -1
- package/dist/mocks/rpc-client.d.mts +26 -10
- package/dist/mocks/rpc-client.d.mts.map +1 -1
- package/dist/mocks/rpc-client.mjs +184 -22
- package/dist/mocks/rpc-client.mjs.map +1 -1
- package/dist/promise-queue/index.cjs +10 -4
- package/dist/promise-queue/index.cjs.map +1 -1
- package/dist/promise-queue/index.d.cts +1 -4
- package/dist/promise-queue/index.d.cts.map +1 -1
- package/dist/promise-queue/index.d.mts +1 -4
- package/dist/promise-queue/index.d.mts.map +1 -1
- package/dist/promise-queue/index.mjs +10 -4
- package/dist/promise-queue/index.mjs.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +28 -5
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.mts +28 -5
- package/dist/types/index.d.mts.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/utils/index.cjs +6 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +3 -2
- package/dist/utils/index.d.mts +3 -2
- package/dist/utils/index.mjs +7 -2
- package/dist/utils/index.mjs.map +1 -1
- package/dist/utils/text.cjs +31 -0
- package/dist/utils/text.cjs.map +1 -1
- package/dist/utils/text.d.cts +24 -1
- package/dist/utils/text.d.cts.map +1 -1
- package/dist/utils/text.d.mts +24 -1
- package/dist/utils/text.d.mts.map +1 -1
- package/dist/utils/text.mjs +30 -1
- package/dist/utils/text.mjs.map +1 -1
- package/dist/utils/timeout.cjs +32 -0
- package/dist/utils/timeout.cjs.map +1 -0
- package/dist/utils/timeout.d.cts +18 -0
- package/dist/utils/timeout.d.cts.map +1 -0
- package/dist/utils/timeout.d.mts +18 -0
- package/dist/utils/timeout.d.mts.map +1 -0
- package/dist/utils/timeout.mjs +30 -0
- package/dist/utils/timeout.mjs.map +1 -0
- package/package.json +35 -34
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/ascii.cjs +0 -32
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/ascii.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/ascii.mjs +0 -29
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/ascii.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/base64.cjs +0 -67
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/base64.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/base64.mjs +0 -64
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/base64.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/bech32.cjs +0 -80
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/bech32.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/bech32.mjs +0 -77
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/bech32.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/hex.cjs +0 -33
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/hex.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/hex.mjs +0 -30
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/hex.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/index.cjs +0 -106
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/index.mjs +0 -103
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/index.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/rfc3339.cjs +0 -52
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/rfc3339.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/rfc3339.mjs +0 -49
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/rfc3339.mjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/utf8.cjs +0 -31
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/utf8.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/utf8.mjs +0 -28
- package/dist/node_modules/.pnpm/@cosmjs_encoding@0.36.0/node_modules/@cosmjs/encoding/build/utf8.mjs.map +0 -1
- package/dist/node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.cjs +0 -98
- package/dist/node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.mjs +0 -95
- package/dist/node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.mjs.map +0 -1
package/dist/indexer/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { CONNECT_TIMEOUT_MS, DEFAULT_BATCH_SIZE, DEFAULT_HEALTH_CHECK_PORT, DEFAULT_POLLING_INTERVAL_MS, DEFAULT_PROMETHEUS_PORT, DEFAULT_START_HEIGHT, GENESIS_BATCH_SIZE, PAGINATION_LIMITS, PERIODIC_INTERVALS,
|
|
1
|
+
import { CONNECT_TIMEOUT_MS, DEFAULT_BATCH_SIZE, DEFAULT_BIND_HOST, DEFAULT_HEALTH_CHECK_PORT, DEFAULT_POLLING_INTERVAL_MS, DEFAULT_PROMETHEUS_PORT, DEFAULT_START_HEIGHT, GENESIS_BATCH_SIZE, IDLE_CHECK_INTERVAL_MS, MAX_FAILURES_PER_BLOCK, PAGINATION_LIMITS, PERIODIC_INTERVALS, RETRY_BASE_DELAY_MS, RETRY_MAX_DELAY_MS, RPC_TIMEOUT_MS } from "../constants.mjs";
|
|
2
2
|
import { EclesiaEmitter } from "../emitter/index.mjs";
|
|
3
|
-
import { RPCError } from "../errors/index.mjs";
|
|
3
|
+
import { ConfigurationError, RPCError } from "../errors/index.mjs";
|
|
4
4
|
import { IndexerMetrics } from "../metrics/index.mjs";
|
|
5
5
|
import { CircularBuffer } from "../promise-queue/index.mjs";
|
|
6
|
-
import { decodeAttr } from "../utils/text.mjs";
|
|
6
|
+
import { decodeAttr, hasBlockEventMode, redactUrl } from "../utils/text.mjs";
|
|
7
|
+
import { retryDelay, withTimeout } from "../utils/timeout.mjs";
|
|
7
8
|
import "../utils/index.mjs";
|
|
8
9
|
import { validateFilePath, validatePort, validatePositiveInteger, validateUrl } from "../validation/index.mjs";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
@@ -14,12 +15,11 @@ import { QueryValidatorsRequest, QueryValidatorsResponse } from "cosmjs-types/co
|
|
|
14
15
|
import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js";
|
|
15
16
|
import Fastify from "fastify";
|
|
16
17
|
import { chain } from "stream-chain";
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import { v4 } from "uuid";
|
|
18
|
+
import pick from "stream-json/filters/pick.js";
|
|
19
|
+
import parser from "stream-json/parser.js";
|
|
20
|
+
import streamArray from "stream-json/streamers/stream-array.js";
|
|
21
|
+
import streamValues from "stream-json/streamers/stream-values.js";
|
|
22
|
+
import batch from "stream-json/utils/batch.js";
|
|
23
23
|
import * as winston from "winston";
|
|
24
24
|
|
|
25
25
|
//#region src/indexer/index.ts
|
|
@@ -46,7 +46,7 @@ const defaultIndexerConfig = {
|
|
|
46
46
|
* Core blockchain indexer that connects to Tendermint RPC and processes blocks
|
|
47
47
|
* Extends EclesiaEmitter to provide event-driven architecture for modules
|
|
48
48
|
*/
|
|
49
|
-
var
|
|
49
|
+
var EclesiaIndexer = class extends EclesiaEmitter {
|
|
50
50
|
/** Indexer configuration settings */
|
|
51
51
|
config;
|
|
52
52
|
/** Fastify HTTP server for health checks */
|
|
@@ -81,6 +81,31 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
81
81
|
subscription = null;
|
|
82
82
|
/** Timeout handler for block reception */
|
|
83
83
|
blockTimeout = null;
|
|
84
|
+
/** Timer for the next poll in polling mode */
|
|
85
|
+
pollTimer = null;
|
|
86
|
+
/** Bumped on every (re)start and stop so a polling chain from a previous run exits */
|
|
87
|
+
pollGeneration = 0;
|
|
88
|
+
/** Bumped on every start() so callbacks left over from a previous run cannot trigger recovery in this one */
|
|
89
|
+
runGeneration = 0;
|
|
90
|
+
/**
|
|
91
|
+
* Rejecters of waits parked in waitForBlockData(). Each entry is removed as soon as its block
|
|
92
|
+
* arrives, so a healthy run keeps this empty instead of accumulating one entry per block.
|
|
93
|
+
*/
|
|
94
|
+
blockWaiters = /* @__PURE__ */ new Set();
|
|
95
|
+
/** Pending restart timer */
|
|
96
|
+
retryTimer = null;
|
|
97
|
+
/** Resolves when the indexer has stopped for good: stop() was called, endHeight was reached, or it gave up */
|
|
98
|
+
stopped = Promise.resolve();
|
|
99
|
+
resolveStopped = () => {};
|
|
100
|
+
/** Next height the fetcher will request; advances as fetches are enqueued */
|
|
101
|
+
nextFetchHeight = 0;
|
|
102
|
+
/** Whether a fetcher loop is active, and for which run */
|
|
103
|
+
fetcherRunning = false;
|
|
104
|
+
fetcherGeneration = 0;
|
|
105
|
+
fetcherToken = 0;
|
|
106
|
+
/** Height of the block whose processing failed most recently, and how many times in a row */
|
|
107
|
+
lastFailedHeight;
|
|
108
|
+
sameHeightFailures = 0;
|
|
84
109
|
/**
|
|
85
110
|
* Creates a new Eclesia indexer instance
|
|
86
111
|
* @param config - Indexer configuration options
|
|
@@ -94,40 +119,44 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
94
119
|
if (config.prometheusPort !== void 0) validatePort(config.prometheusPort, "prometheusPort");
|
|
95
120
|
if (config.startHeight !== void 0) validatePositiveInteger(config.startHeight, "startHeight");
|
|
96
121
|
if (config.pollingInterval !== void 0) validatePositiveInteger(config.pollingInterval, "pollingInterval");
|
|
122
|
+
const provided = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
97
123
|
this.config = {
|
|
98
124
|
...defaultIndexerConfig,
|
|
99
|
-
...
|
|
125
|
+
...provided
|
|
100
126
|
};
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
const errorFormat = winston.format((info) => {
|
|
128
|
+
const error = info.error;
|
|
129
|
+
if (error instanceof Error) info.error = {
|
|
130
|
+
name: error.name,
|
|
131
|
+
message: error.message,
|
|
132
|
+
stack: error.stack
|
|
133
|
+
};
|
|
134
|
+
else if (error !== void 0 && (typeof error !== "object" || error === null)) info.error = { message: String(error) };
|
|
135
|
+
return info;
|
|
136
|
+
});
|
|
137
|
+
const textFormat = winston.format.printf(({ level, message, timestamp, error }) => {
|
|
138
|
+
const detail = error;
|
|
139
|
+
const suffix = detail ? "\n" + (detail.stack ?? detail.message ?? "") : "";
|
|
140
|
+
return `${timestamp} [${level.toUpperCase()}]:\t${message}${suffix}`;
|
|
104
141
|
});
|
|
105
142
|
this.log = winston.createLogger({
|
|
106
143
|
level: this.config.logLevel,
|
|
107
144
|
defaultMeta: { service: "Eclesia Indexer" },
|
|
108
|
-
transports: [
|
|
109
|
-
new winston.transports.File({
|
|
110
|
-
filename: "error.log",
|
|
111
|
-
level: "error"
|
|
112
|
-
}),
|
|
113
|
-
new winston.transports.File({ filename: "combined.log" }),
|
|
114
|
-
new winston.transports.Console({ format: winston.format.combine(winston.format.splat(), winston.format.timestamp(), eclesiaFormat, winston.format.colorize({ all: true })) })
|
|
115
|
-
]
|
|
145
|
+
transports: [new winston.transports.Console({ format: this.config.logFormat === "json" ? winston.format.combine(winston.format.splat(), winston.format.timestamp(), errorFormat(), winston.format.json()) : winston.format.combine(winston.format.splat(), winston.format.timestamp(), errorFormat(), textFormat, winston.format.colorize({ all: true })) })]
|
|
116
146
|
});
|
|
147
|
+
const protocol = new URL(this.config.rpcUrl).protocol;
|
|
148
|
+
if (!this.config.usePolling && (protocol === "http:" || protocol === "https:")) {
|
|
149
|
+
this.log.warn("rpcUrl " + redactUrl(this.config.rpcUrl) + " is HTTP, which cannot deliver block subscriptions; polling every " + this.config.pollingInterval + " ms instead (use a ws:// or wss:// URL for WebSocket mode)");
|
|
150
|
+
this.config.usePolling = true;
|
|
151
|
+
}
|
|
117
152
|
const queueErrorHandler = (e) => {
|
|
118
153
|
this.prometheus?.recordError("rpc");
|
|
119
|
-
this.log.error("Error enqueueing block data
|
|
154
|
+
this.log.error("Error enqueueing block data", { error: e });
|
|
120
155
|
};
|
|
121
156
|
if (this.config.minimal) this.blockQueue = new CircularBuffer(this.config.batchSize, queueErrorHandler);
|
|
122
157
|
else this.blockQueue = new CircularBuffer(this.config.batchSize, queueErrorHandler);
|
|
123
158
|
this.on("_unhandled", (msg) => {
|
|
124
|
-
if (msg.uuid)
|
|
125
|
-
this.log.verbose("Unhandled event: " + msg.type);
|
|
126
|
-
this.emit("uuid", {
|
|
127
|
-
status: true,
|
|
128
|
-
uuid: msg.uuid
|
|
129
|
-
});
|
|
130
|
-
}
|
|
159
|
+
if (msg.type !== "uuid" && this.log.isVerboseEnabled()) this.log.verbose("Unhandled event: " + msg.type);
|
|
131
160
|
});
|
|
132
161
|
if (this.config.enablePrometheus) {
|
|
133
162
|
this.prometheus = new IndexerMetrics();
|
|
@@ -139,10 +168,10 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
139
168
|
const prometheusPort = this.config.prometheusPort ?? (process.env.PROMETHEUS_PORT ? parseInt(process.env.PROMETHEUS_PORT, 10) : DEFAULT_PROMETHEUS_PORT);
|
|
140
169
|
this.prometheusServer.listen({
|
|
141
170
|
port: prometheusPort,
|
|
142
|
-
host:
|
|
171
|
+
host: this.config.prometheusHost ?? DEFAULT_BIND_HOST
|
|
143
172
|
}, (err) => {
|
|
144
173
|
if (err) {
|
|
145
|
-
this.log.error("Prometheus error
|
|
174
|
+
this.log.error("Prometheus server error", { error: err });
|
|
146
175
|
this.prometheus?.recordError("metrics_server");
|
|
147
176
|
this.emit("fatal-error", {
|
|
148
177
|
error: err,
|
|
@@ -154,16 +183,16 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
154
183
|
if (this.config.enableHealthcheck) {
|
|
155
184
|
this.fastify = Fastify({ logger: false });
|
|
156
185
|
this.fastify.get("/health", async (_request, reply) => {
|
|
157
|
-
const code = this.healthCheck.status == "OK" ? 200 : 503;
|
|
186
|
+
const code = this.healthCheck.status == "OK" || this.healthCheck.status == "WAITING" ? 200 : 503;
|
|
158
187
|
reply.code(code).send(this.healthCheck);
|
|
159
188
|
});
|
|
160
189
|
const healthPort = this.config.healthCheckPort ?? (process.env.HEALTH_CHECK_PORT ? parseInt(process.env.HEALTH_CHECK_PORT, 10) : DEFAULT_HEALTH_CHECK_PORT);
|
|
161
190
|
this.fastify.listen({
|
|
162
191
|
port: healthPort,
|
|
163
|
-
host:
|
|
192
|
+
host: this.config.healthCheckHost ?? DEFAULT_BIND_HOST
|
|
164
193
|
}, (err) => {
|
|
165
194
|
if (err) {
|
|
166
|
-
this.log.error("Health check server error
|
|
195
|
+
this.log.error("Health check server error", { error: err });
|
|
167
196
|
this.prometheus?.recordError("health_check_server");
|
|
168
197
|
this.emit("fatal-error", {
|
|
169
198
|
error: err,
|
|
@@ -175,10 +204,110 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
175
204
|
}
|
|
176
205
|
setStatus(status) {
|
|
177
206
|
this.healthCheck.status = status;
|
|
207
|
+
this.prometheus?.setWaiting(status === "WAITING");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Marks the current run for recovery and wakes the main loop if it is parked waiting for a
|
|
211
|
+
* block. Callbacks left over from a previous run pass their generation and are ignored.
|
|
212
|
+
*/
|
|
213
|
+
requestRecovery(reason, generation = this.runGeneration) {
|
|
214
|
+
if (generation !== this.runGeneration) {
|
|
215
|
+
this.log.debug("Ignoring recovery request from a previous run: " + reason);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!this.tryToRecover) this.log.warn("Recovery requested: " + reason);
|
|
219
|
+
this.tryToRecover = true;
|
|
220
|
+
this.wakeBlockWaiters("Recovery requested while waiting for block data");
|
|
221
|
+
}
|
|
222
|
+
/** Rejects every wait parked in waitForBlockData() */
|
|
223
|
+
wakeBlockWaiters(reason) {
|
|
224
|
+
const waiters = [...this.blockWaiters];
|
|
225
|
+
this.blockWaiters.clear();
|
|
226
|
+
for (const reject of waiters) reject(new RPCError(reason));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Refuses to index a chain other than the configured one. An RPC pool that mixes networks, or
|
|
230
|
+
* a wrong URL, would otherwise write a different chain's blocks into the database.
|
|
231
|
+
*/
|
|
232
|
+
assertChainId(network) {
|
|
233
|
+
if (this.config.chainId !== void 0 && network !== this.config.chainId) throw new ConfigurationError("RPC serves chain " + network + " but chainId is configured as " + this.config.chainId, {
|
|
234
|
+
expected: this.config.chainId,
|
|
235
|
+
actual: network
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Counts consecutive processing failures per block height */
|
|
239
|
+
noteBlockFailure(height) {
|
|
240
|
+
if (height === this.lastFailedHeight) this.sameHeightFailures++;
|
|
241
|
+
else {
|
|
242
|
+
this.lastFailedHeight = height;
|
|
243
|
+
this.sameHeightFailures = 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** True once one block has failed maxFailuresPerBlock times in a row */
|
|
247
|
+
isStuck() {
|
|
248
|
+
return this.lastFailedHeight !== void 0 && this.sameHeightFailures >= (this.config.maxFailuresPerBlock ?? MAX_FAILURES_PER_BLOCK);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Waits for the next dequeued block but wakes early when recovery or stop is requested, so a
|
|
252
|
+
* loop parked on an empty queue never waits for a block that will not come.
|
|
253
|
+
*/
|
|
254
|
+
waitForBlockData(dequeued) {
|
|
255
|
+
if (this.tryToRecover || !this.started) return Promise.reject(new RPCError("Recovery requested while waiting for block data"));
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
this.blockWaiters.add(reject);
|
|
258
|
+
dequeued.then((value) => {
|
|
259
|
+
this.blockWaiters.delete(reject);
|
|
260
|
+
resolve(value);
|
|
261
|
+
}, (error) => {
|
|
262
|
+
this.blockWaiters.delete(reject);
|
|
263
|
+
reject(error);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** (Re)arms the idle check that runs when no block has been announced for a while */
|
|
268
|
+
armIdleCheck() {
|
|
269
|
+
if (this.blockTimeout) clearTimeout(this.blockTimeout);
|
|
270
|
+
this.blockTimeout = setTimeout(() => {
|
|
271
|
+
this.checkLiveness();
|
|
272
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Runs when no block has been announced for IDLE_CHECK_INTERVAL_MS. A chain that has stopped
|
|
276
|
+
* producing blocks (halt, upgrade, slow chain) is not an error: the indexer reports WAITING and
|
|
277
|
+
* checks again later. Recovery is requested only when the chain has moved on without us, which
|
|
278
|
+
* means the subscription is dead, or when the RPC cannot be reached at all.
|
|
279
|
+
*/
|
|
280
|
+
async checkLiveness() {
|
|
281
|
+
if (!this.started) return;
|
|
282
|
+
const generation = this.runGeneration;
|
|
283
|
+
try {
|
|
284
|
+
const status = await withTimeout(this.client.status(), RPC_TIMEOUT_MS, new RPCError("RPC status call timed out"));
|
|
285
|
+
if (!this.started || generation !== this.runGeneration) return;
|
|
286
|
+
const chainHeight = status.syncInfo.latestBlockHeight;
|
|
287
|
+
if (chainHeight > this.latestHeight) {
|
|
288
|
+
this.requestRecovery("chain is at " + chainHeight + " but nothing was announced since " + this.latestHeight, generation);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.log.info("No new block for " + IDLE_CHECK_INTERVAL_MS / 1e3 + " s, chain height is still " + chainHeight);
|
|
292
|
+
this.setStatus("WAITING");
|
|
293
|
+
this.armIdleCheck();
|
|
294
|
+
} catch (e) {
|
|
295
|
+
this.log.error("Liveness check failed", { error: e });
|
|
296
|
+
this.requestRecovery("RPC unreachable during liveness check", generation);
|
|
297
|
+
}
|
|
178
298
|
}
|
|
179
|
-
blockListener = {
|
|
180
|
-
|
|
181
|
-
|
|
299
|
+
blockListener = {
|
|
300
|
+
next: (data) => {
|
|
301
|
+
this.newBlockReceived(data.header.height);
|
|
302
|
+
},
|
|
303
|
+
error: (error) => {
|
|
304
|
+
this.log.error("Block subscription error", { error });
|
|
305
|
+
this.requestRecovery("block subscription errored");
|
|
306
|
+
},
|
|
307
|
+
complete: () => {
|
|
308
|
+
if (this.started) this.requestRecovery("block subscription closed by the node");
|
|
309
|
+
}
|
|
310
|
+
};
|
|
182
311
|
isMinimal(_blockqueue) {
|
|
183
312
|
if (this.config.minimal) return true;
|
|
184
313
|
else return false;
|
|
@@ -195,38 +324,55 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
195
324
|
} catch (_e) {}
|
|
196
325
|
this.log.verbose("Disconnected from RPC");
|
|
197
326
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.log.info("Attempting to connected to RPC: " + this.config.rpcUrl);
|
|
202
|
-
this.client = await Promise.race([connectComet(this.config.rpcUrl), connectTimeoutPromise]);
|
|
203
|
-
await Promise.race([this.client.status(), connectTimeoutPromise]);
|
|
327
|
+
this.log.info("Attempting to connect to RPC: " + redactUrl(this.config.rpcUrl));
|
|
328
|
+
this.client = await this.connectWithTimeout();
|
|
329
|
+
await withTimeout(this.client.status(), CONNECT_TIMEOUT_MS, new RPCError("RPC status call timed out"));
|
|
204
330
|
this.log.info("Connected to RPC for ad hoc queries");
|
|
205
|
-
this.blockClient = await
|
|
206
|
-
await
|
|
331
|
+
this.blockClient = await this.connectWithTimeout();
|
|
332
|
+
await withTimeout(this.blockClient.status(), CONNECT_TIMEOUT_MS, new RPCError("RPC status call timed out"));
|
|
207
333
|
this.log.info("Connected to RPC for block & validator info");
|
|
208
334
|
return true;
|
|
209
335
|
} catch (error) {
|
|
210
|
-
this.log.error("RPC connection error
|
|
336
|
+
this.log.error("RPC connection error", { error });
|
|
211
337
|
this.prometheus?.recordError("rpc");
|
|
212
|
-
this.
|
|
338
|
+
this.requestRecovery("RPC connection failed");
|
|
213
339
|
return false;
|
|
214
340
|
}
|
|
215
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Opens one CometBFT client with its own timeout. If the timeout wins, the client
|
|
344
|
+
* that may still arrive is disconnected so a slow RPC never leaks a socket.
|
|
345
|
+
*/
|
|
346
|
+
async connectWithTimeout() {
|
|
347
|
+
let timedOut = false;
|
|
348
|
+
const pending = connectComet(this.config.rpcUrl);
|
|
349
|
+
pending.then((client) => {
|
|
350
|
+
if (timedOut) try {
|
|
351
|
+
client.disconnect();
|
|
352
|
+
} catch (_e) {}
|
|
353
|
+
}).catch(() => {});
|
|
354
|
+
try {
|
|
355
|
+
return await withTimeout(pending, CONNECT_TIMEOUT_MS, new RPCError("RPC connection timed out"));
|
|
356
|
+
} catch (e) {
|
|
357
|
+
timedOut = true;
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
216
361
|
async initialize() {
|
|
217
362
|
if (!this.initialized) {
|
|
218
363
|
try {
|
|
219
364
|
if (this.config.init) await this.config.init();
|
|
220
365
|
} catch (e) {
|
|
221
|
-
this.log.error("Failed to initialize indexer
|
|
366
|
+
this.log.error("Failed to initialize indexer", { error: e });
|
|
222
367
|
this.prometheus?.recordError("init_error");
|
|
223
368
|
this.setStatus("FAILED");
|
|
224
369
|
throw e;
|
|
225
370
|
}
|
|
226
371
|
if (await this.config.shouldProcessGenesis()) try {
|
|
227
372
|
if (this.config.genesisPath) await this.parseGenesis();
|
|
373
|
+
else this.log.warn("shouldProcessGenesis() returned true but no genesisPath is configured, skipping genesis import");
|
|
228
374
|
} catch (e) {
|
|
229
|
-
this.log.error("Failed to parse genesis
|
|
375
|
+
this.log.error("Failed to parse genesis", { error: e });
|
|
230
376
|
this.prometheus?.recordError("genesis_error");
|
|
231
377
|
this.setStatus("FAILED");
|
|
232
378
|
throw e;
|
|
@@ -234,8 +380,40 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
234
380
|
this.initialized = true;
|
|
235
381
|
}
|
|
236
382
|
}
|
|
237
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Stops the indexer and releases everything that would keep the process alive:
|
|
385
|
+
* the block subscription, polling and inactivity timers, both RPC clients and the
|
|
386
|
+
* health and metrics servers. Safe to call more than once.
|
|
387
|
+
*/
|
|
388
|
+
async stop() {
|
|
238
389
|
this.started = false;
|
|
390
|
+
this.resolveStopped();
|
|
391
|
+
if (this.retryTimer) {
|
|
392
|
+
clearTimeout(this.retryTimer);
|
|
393
|
+
this.retryTimer = null;
|
|
394
|
+
}
|
|
395
|
+
this.wakeBlockWaiters("Indexer stopped while waiting for block data");
|
|
396
|
+
this.stopPolling();
|
|
397
|
+
if (this.blockTimeout) {
|
|
398
|
+
clearTimeout(this.blockTimeout);
|
|
399
|
+
this.blockTimeout = null;
|
|
400
|
+
}
|
|
401
|
+
if (this.subscription) {
|
|
402
|
+
try {
|
|
403
|
+
this.subscription.removeListener(this.blockListener);
|
|
404
|
+
} catch (_e) {}
|
|
405
|
+
this.subscription = null;
|
|
406
|
+
}
|
|
407
|
+
for (const client of [this.client, this.blockClient]) try {
|
|
408
|
+
client?.disconnect();
|
|
409
|
+
} catch (_e) {}
|
|
410
|
+
const servers = [this.fastify, this.prometheusServer];
|
|
411
|
+
this.fastify = null;
|
|
412
|
+
this.prometheusServer = null;
|
|
413
|
+
await Promise.all(servers.map((server) => server?.close().catch((e) => {
|
|
414
|
+
this.log.warn("Error closing HTTP server", { error: e });
|
|
415
|
+
})));
|
|
416
|
+
this.log.info("Indexer stopped");
|
|
239
417
|
}
|
|
240
418
|
clearBlockQueue() {
|
|
241
419
|
if (this.blockQueue) {
|
|
@@ -255,65 +433,88 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
255
433
|
this.log.verbose("Removed existing block listener and subscription");
|
|
256
434
|
}
|
|
257
435
|
if (!this.config.usePolling) this.subscription = this.client.subscribeNewBlock ? this.client.subscribeNewBlock() : null;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.
|
|
262
|
-
this.log.info("Current chain height: " + this.latestHeight);
|
|
436
|
+
const status = await withTimeout(this.client.status(), RPC_TIMEOUT_MS, new RPCError("RPC status call timed out"));
|
|
437
|
+
this.assertChainId(status.nodeInfo.network);
|
|
438
|
+
this.latestHeight = status.syncInfo.latestBlockHeight;
|
|
439
|
+
this.log.info("Connected to " + status.nodeInfo.network + ", current chain height: " + this.latestHeight);
|
|
263
440
|
this.heightToProcess = await this.config.getNextHeight();
|
|
264
|
-
|
|
441
|
+
this.nextFetchHeight = this.heightToProcess;
|
|
442
|
+
if (this.config.usePolling) this.startPolling();
|
|
265
443
|
else if (this.subscription) this.subscription.addListener(this.blockListener);
|
|
266
444
|
else {
|
|
267
445
|
this.prometheus?.recordError("rpc");
|
|
268
446
|
throw new Error("Could not subscribe to new blocks");
|
|
269
447
|
}
|
|
448
|
+
this.armIdleCheck();
|
|
270
449
|
} catch (e) {
|
|
271
|
-
this.log.error("Failed to set up block listening
|
|
450
|
+
this.log.error("Failed to set up block listening", { error: e });
|
|
272
451
|
this.prometheus?.recordError("rpc");
|
|
273
452
|
this.setStatus("FAILED");
|
|
274
453
|
throw e;
|
|
275
454
|
}
|
|
276
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Resolves once the indexer has stopped for good: stop() was called, endHeight was reached, or
|
|
458
|
+
* a fatal-error was emitted. Restarts with backoff do not resolve it. Use it to keep a caller
|
|
459
|
+
* waiting for the whole run rather than for the first loop exit.
|
|
460
|
+
*/
|
|
461
|
+
whenStopped() {
|
|
462
|
+
return this.stopped;
|
|
463
|
+
}
|
|
277
464
|
async start() {
|
|
465
|
+
if (!this.started) this.stopped = new Promise((resolve) => {
|
|
466
|
+
this.resolveStopped = resolve;
|
|
467
|
+
});
|
|
278
468
|
this.started = true;
|
|
469
|
+
this.runGeneration++;
|
|
470
|
+
const generation = this.runGeneration;
|
|
471
|
+
this.tryToRecover = false;
|
|
472
|
+
this.blockWaiters.clear();
|
|
279
473
|
this.clearBlockQueue();
|
|
280
474
|
await this.initialize();
|
|
281
475
|
try {
|
|
282
476
|
await this.setupBlockListening();
|
|
283
477
|
this.log.debug("Starting main processing loop");
|
|
284
|
-
this.tryToRecover = false;
|
|
285
478
|
this.fetcher().catch((e) => {
|
|
286
479
|
this.setStatus("FAILED");
|
|
287
480
|
this.prometheus?.recordError("rpc");
|
|
288
|
-
|
|
481
|
+
this.log.error("Error in fetching service", { error: e });
|
|
482
|
+
this.requestRecovery("fetcher failed", generation);
|
|
289
483
|
});
|
|
290
|
-
} catch (
|
|
291
|
-
|
|
484
|
+
} catch (e) {
|
|
485
|
+
this.requestRecovery("block listening setup failed: " + e, generation);
|
|
486
|
+
}
|
|
487
|
+
let lastProcessed;
|
|
488
|
+
while (this.started && !this.tryToRecover) {
|
|
489
|
+
let txOpen = false;
|
|
490
|
+
let failingHeight;
|
|
292
491
|
try {
|
|
293
492
|
this.prometheus?.updateRetryCount(this.retryCount);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let
|
|
493
|
+
if (this.blockQueue.synced && this.blockQueue.size() <= 1) this.setStatus("WAITING");
|
|
494
|
+
let height;
|
|
495
|
+
let timestamp;
|
|
297
496
|
if (this.isMinimal(this.blockQueue)) {
|
|
298
|
-
const
|
|
299
|
-
setTimeout(reject, QUEUE_DEQUEUE_TIMEOUT_MS, []);
|
|
300
|
-
});
|
|
301
|
-
const toProcess = await Promise.race([this.blockQueue.dequeue(), timeoutPromise]);
|
|
497
|
+
const toProcess = await this.waitForBlockData(this.blockQueue.dequeue());
|
|
302
498
|
this.log.silly("Retrieved block data");
|
|
303
499
|
if (!toProcess || !toProcess[0] || !toProcess[1]) throw new RPCError("Could not fetch block(minimal)");
|
|
304
500
|
height = toProcess[0].block.header.height;
|
|
501
|
+
failingHeight = height;
|
|
305
502
|
timestamp = toRfc3339WithNanoseconds(toProcess[0].block.header.time);
|
|
503
|
+
await this.config.beginTransaction();
|
|
504
|
+
txOpen = true;
|
|
505
|
+
this.log.silly("Started db tx");
|
|
306
506
|
await this.processBlock(toProcess[0], toProcess[1]);
|
|
307
507
|
} else {
|
|
308
|
-
const
|
|
309
|
-
setTimeout(reject, QUEUE_DEQUEUE_TIMEOUT_MS, []);
|
|
310
|
-
});
|
|
311
|
-
const toProcess = await Promise.race([this.blockQueue.dequeue(), timeoutPromise]);
|
|
508
|
+
const toProcess = await this.waitForBlockData(this.blockQueue.dequeue());
|
|
312
509
|
this.log.silly("Retrieved block data");
|
|
313
510
|
if (!toProcess || !toProcess[0] || !toProcess[1] || !toProcess[2]) throw new RPCError("Could not fetch block(full)");
|
|
314
511
|
this.log.silly("Decoded block");
|
|
315
512
|
height = toProcess[0].block.header.height;
|
|
513
|
+
failingHeight = height;
|
|
316
514
|
timestamp = toRfc3339WithNanoseconds(toProcess[0].block.header.time);
|
|
515
|
+
await this.config.beginTransaction();
|
|
516
|
+
txOpen = true;
|
|
517
|
+
this.log.silly("Started db tx");
|
|
317
518
|
await this.processBlock(toProcess[0], toProcess[1], QueryValidatorsResponse.decode(toProcess[2]).validators);
|
|
318
519
|
}
|
|
319
520
|
if (height % PERIODIC_INTERVALS.LARGE == 0) await this.asyncEmit("periodic/large", {
|
|
@@ -333,65 +534,86 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
333
534
|
});
|
|
334
535
|
this.log.silly("Handled periodic events");
|
|
335
536
|
await this.config.endTransaction(true);
|
|
537
|
+
txOpen = false;
|
|
538
|
+
lastProcessed = height;
|
|
539
|
+
this.lastFailedHeight = void 0;
|
|
540
|
+
this.sameHeightFailures = 0;
|
|
336
541
|
this.log.silly("Committed db tx");
|
|
337
542
|
} catch (e) {
|
|
338
|
-
|
|
339
|
-
this.log.error("Block processing error: " + e);
|
|
340
|
-
this.setStatus("FAILED");
|
|
341
|
-
try {
|
|
543
|
+
if (txOpen) try {
|
|
342
544
|
await this.config.endTransaction(false);
|
|
343
545
|
} catch (dbe) {
|
|
344
546
|
this.prometheus?.recordError("database");
|
|
345
|
-
this.log.error("Error ending transaction
|
|
547
|
+
this.log.error("Error ending transaction", { error: dbe });
|
|
346
548
|
}
|
|
347
|
-
this.
|
|
549
|
+
if (!this.started) break;
|
|
550
|
+
if (failingHeight !== void 0) this.noteBlockFailure(failingHeight);
|
|
551
|
+
this.prometheus?.recordError("block");
|
|
552
|
+
this.log.error("Block processing error", { error: e });
|
|
553
|
+
this.setStatus("FAILED");
|
|
554
|
+
this.requestRecovery("block processing failed", generation);
|
|
348
555
|
break;
|
|
349
556
|
}
|
|
350
557
|
this.retryCount = 0;
|
|
351
558
|
this.setStatus("OK");
|
|
559
|
+
if (this.config.endHeight !== void 0 && lastProcessed !== void 0 && lastProcessed >= this.config.endHeight) {
|
|
560
|
+
this.log.info("Reached configured end height " + this.config.endHeight + ". Stopping indexer.");
|
|
561
|
+
await this.stop();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
352
564
|
}
|
|
353
565
|
if (!this.started) {
|
|
354
566
|
this.log.info("Indexer manually stopped.");
|
|
355
567
|
return;
|
|
356
568
|
}
|
|
357
|
-
if (this.
|
|
358
|
-
|
|
359
|
-
this.
|
|
569
|
+
if (this.isStuck()) {
|
|
570
|
+
const height = this.lastFailedHeight;
|
|
571
|
+
this.log.error("Block " + height + " failed " + this.sameHeightFailures + " times in a row. This is a deterministic failure in a handler or the data, not an outage. Giving up.");
|
|
572
|
+
this.started = false;
|
|
573
|
+
this.emit("fatal-error", {
|
|
574
|
+
error: /* @__PURE__ */ new Error("Block " + height + " failed " + this.sameHeightFailures + " times in a row"),
|
|
575
|
+
message: "Block processing is stuck",
|
|
576
|
+
retryCount: this.retryCount,
|
|
577
|
+
height
|
|
578
|
+
});
|
|
579
|
+
this.resolveStopped();
|
|
360
580
|
return;
|
|
361
581
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this.retryCount
|
|
365
|
-
this.
|
|
366
|
-
setTimeout(() => this.start(), this.retryCount * 5e3);
|
|
367
|
-
} else {
|
|
368
|
-
this.log.info("Indexer failed too many times. Exiting.");
|
|
582
|
+
this.retryCount++;
|
|
583
|
+
if (this.config.maxRetries !== void 0 && this.retryCount > this.config.maxRetries) {
|
|
584
|
+
this.log.error("Indexer failed " + this.retryCount + " times in a row, giving up (maxRetries=" + this.config.maxRetries + ")");
|
|
585
|
+
this.started = false;
|
|
369
586
|
this.emit("fatal-error", {
|
|
370
587
|
error: /* @__PURE__ */ new Error("Max retry attempts exceeded"),
|
|
371
588
|
message: "Indexer failed too many times",
|
|
372
589
|
retryCount: this.retryCount
|
|
373
590
|
});
|
|
591
|
+
this.resolveStopped();
|
|
592
|
+
return;
|
|
374
593
|
}
|
|
594
|
+
const delay = retryDelay(this.retryCount, RETRY_BASE_DELAY_MS, RETRY_MAX_DELAY_MS);
|
|
595
|
+
this.log.warn("Indexer is restarting in " + delay / 1e3 + " s (attempt " + this.retryCount + ")");
|
|
596
|
+
this.retryTimer = setTimeout(() => {
|
|
597
|
+
this.retryTimer = null;
|
|
598
|
+
this.start().catch((e) => {
|
|
599
|
+
this.log.error("Restart failed", { error: e });
|
|
600
|
+
});
|
|
601
|
+
}, delay);
|
|
375
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Emits an event and waits for its handlers. Handlers run one after another in registration
|
|
605
|
+
* order: they share one database connection and one transaction, so interleaving them at
|
|
606
|
+
* await points would let two handlers read and write the same rows in an unpredictable order,
|
|
607
|
+
* and a failure in one would leave the others mid-flight while the block is rolled back. The
|
|
608
|
+
* first rejection propagates and stops the remaining handlers.
|
|
609
|
+
*/
|
|
376
610
|
asyncEmit = async (type, event) => {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (ev.uuid == event.uuid) if (ev.status) {
|
|
384
|
-
listenersResponded++;
|
|
385
|
-
if (listenersResponded == listenerCount) {
|
|
386
|
-
this.off("uuid", returnFunc);
|
|
387
|
-
resolve();
|
|
388
|
-
}
|
|
389
|
-
} else reject(ev.error);
|
|
390
|
-
};
|
|
391
|
-
this.on("uuid", returnFunc);
|
|
392
|
-
});
|
|
393
|
-
this.emit(type, event);
|
|
394
|
-
return prom;
|
|
611
|
+
const handlers = this.handlersFor(type);
|
|
612
|
+
if (handlers.length === 0) {
|
|
613
|
+
this.emit(type, event);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
for (const handler of handlers) await handler(event);
|
|
395
617
|
};
|
|
396
618
|
async processBlock(block, block_results, validators) {
|
|
397
619
|
const endTimer = this.prometheus?.timeBlockProcessing();
|
|
@@ -411,8 +633,8 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
411
633
|
let beginBlockEvents;
|
|
412
634
|
let endBlockEvents;
|
|
413
635
|
if (block_results.finalizeBlockEvents) {
|
|
414
|
-
beginBlockEvents = block_results.finalizeBlockEvents.filter((x) => x
|
|
415
|
-
endBlockEvents = block_results.finalizeBlockEvents.filter((x) => x
|
|
636
|
+
beginBlockEvents = block_results.finalizeBlockEvents.filter((x) => hasBlockEventMode(x, "BeginBlock"));
|
|
637
|
+
endBlockEvents = block_results.finalizeBlockEvents.filter((x) => hasBlockEventMode(x, "EndBlock"));
|
|
416
638
|
} else {
|
|
417
639
|
beginBlockEvents = block_results.beginBlockEvents;
|
|
418
640
|
endBlockEvents = block_results.endBlockEvents;
|
|
@@ -448,7 +670,13 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
448
670
|
timestamp
|
|
449
671
|
});
|
|
450
672
|
}
|
|
451
|
-
let events =
|
|
673
|
+
let events = [];
|
|
674
|
+
if (txlog) try {
|
|
675
|
+
const parsed = JSON.parse(txlog);
|
|
676
|
+
if (Array.isArray(parsed)) events = parsed;
|
|
677
|
+
} catch (_e) {
|
|
678
|
+
this.log.silly("Tx log is not JSON, using msg_index attributes instead");
|
|
679
|
+
}
|
|
452
680
|
if (events.length == 0) {
|
|
453
681
|
const eventsToAdd = [];
|
|
454
682
|
this.log.silly("No events found in tx log. Parsing events for msg_index");
|
|
@@ -470,8 +698,8 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
470
698
|
}
|
|
471
699
|
const msgs = tx.body?.messages;
|
|
472
700
|
if (msgs) for (let i = 0; i < msgs.length; i++) {
|
|
473
|
-
this.log.silly("Indexer broadcasting msg for handling: " + msgs[i].typeUrl);
|
|
474
|
-
const msgevents = msgs.length > 1 ? events.find((x) => x.msg_index == i)?.events : events[0]
|
|
701
|
+
if (this.log.isSillyEnabled()) this.log.silly("Indexer broadcasting msg for handling: " + msgs[i].typeUrl);
|
|
702
|
+
const msgevents = msgs.length > 1 ? events.find((x) => x.msg_index == i)?.events : events[0]?.events ?? [];
|
|
475
703
|
await this.asyncEmit(msgs[i].typeUrl, {
|
|
476
704
|
value: {
|
|
477
705
|
tx: msgs[i].value,
|
|
@@ -483,7 +711,7 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
483
711
|
if (msgs[i].typeUrl == "/cosmos.authz.v1beta1.MsgExec") {
|
|
484
712
|
const authzMsgs = MsgExec.decode(msgs[i].value).msgs;
|
|
485
713
|
if (authzMsgs) for (let r = 0; r < authzMsgs.length; r++) {
|
|
486
|
-
this.log.silly("Indexer broadcasting msg for handling: " + authzMsgs[r].typeUrl);
|
|
714
|
+
if (this.log.isSillyEnabled()) this.log.silly("Indexer broadcasting msg for handling: " + authzMsgs[r].typeUrl);
|
|
487
715
|
const authzMsgEvents = msgevents?.reduce((events$1, evt) => {
|
|
488
716
|
if (evt.attributes.filter((x) => decodeAttr(x.key) == "authz_msg_index" && decodeAttr(x.value) == "" + r).length > 0) events$1.push(evt);
|
|
489
717
|
return events$1;
|
|
@@ -511,136 +739,166 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
511
739
|
endTimer?.();
|
|
512
740
|
this.prometheus?.updateBlockMetrics(height, this.latestHeight, this.blockQueue.size());
|
|
513
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* Fetches every height from nextFetchHeight up to latestHeight, waiting for queue space
|
|
744
|
+
* before each fetch. Serves the initial catch-up and live blocks alike: a new announcement
|
|
745
|
+
* only moves latestHeight and starts this loop if it is not already running, so bursts and
|
|
746
|
+
* skipped announcements are handled by the same code and the queue can never overflow.
|
|
747
|
+
*/
|
|
514
748
|
async fetcher() {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (this.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
});
|
|
526
|
-
const toIndex = Promise.race([Promise.all([this.blockClient.block(i), this.blockClient.blockResults(i)]), timeoutPromise]).catch((e) => {
|
|
527
|
-
this.log.error("Error fetching block: " + i + " : " + e);
|
|
528
|
-
this.prometheus?.recordError("rpc");
|
|
529
|
-
this.tryToRecover = true;
|
|
530
|
-
return Promise.resolve([]);
|
|
531
|
-
});
|
|
532
|
-
this.blockQueue.enqueue(toIndex);
|
|
533
|
-
} else {
|
|
534
|
-
const timeoutPromise = new Promise((resolve, reject) => {
|
|
535
|
-
setTimeout(reject, RPC_TIMEOUT_MS, false);
|
|
536
|
-
});
|
|
537
|
-
const q = QueryValidatorsRequest.fromPartial({ pagination: { limit: PAGINATION_LIMITS.VALIDATORS } });
|
|
538
|
-
const vals = QueryValidatorsRequest.encode(q).finish();
|
|
539
|
-
const toIndex = Promise.race([Promise.all([
|
|
540
|
-
this.blockClient.block(i),
|
|
541
|
-
this.blockClient.blockResults(i),
|
|
542
|
-
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, i, false)
|
|
543
|
-
]), timeoutPromise]).catch((e) => {
|
|
544
|
-
this.log.error("Error fetching block: " + i + " : " + e);
|
|
545
|
-
this.tryToRecover = true;
|
|
546
|
-
return Promise.resolve([]);
|
|
547
|
-
});
|
|
548
|
-
this.blockQueue.enqueue(toIndex);
|
|
749
|
+
if (this.fetcherRunning && this.fetcherGeneration === this.runGeneration) return;
|
|
750
|
+
const generation = this.runGeneration;
|
|
751
|
+
const token = ++this.fetcherToken;
|
|
752
|
+
this.fetcherRunning = true;
|
|
753
|
+
this.fetcherGeneration = generation;
|
|
754
|
+
try {
|
|
755
|
+
while (this.nextFetchHeight <= this.latestHeight && (this.config.endHeight === void 0 || this.nextFetchHeight <= this.config.endHeight)) {
|
|
756
|
+
if (this.tryToRecover || !this.started || generation !== this.runGeneration) {
|
|
757
|
+
this.log.verbose("Exiting fetcher loop. Attempting to recover indexer");
|
|
758
|
+
break;
|
|
549
759
|
}
|
|
550
|
-
|
|
551
|
-
this.log.
|
|
552
|
-
|
|
760
|
+
const i = this.nextFetchHeight;
|
|
761
|
+
this.log.debug("Fetching: " + i);
|
|
762
|
+
try {
|
|
763
|
+
if (this.isMinimal(this.blockQueue)) {
|
|
764
|
+
const toIndex = withTimeout(Promise.all([this.blockClient.block(i), this.blockClient.blockResults(i)]), RPC_TIMEOUT_MS, new RPCError("Timed out fetching block " + i)).catch((e) => {
|
|
765
|
+
this.log.error("Error fetching block " + i, { error: e });
|
|
766
|
+
this.prometheus?.recordError("rpc");
|
|
767
|
+
this.requestRecovery("fetch failed for block " + i, generation);
|
|
768
|
+
return Promise.resolve([]);
|
|
769
|
+
});
|
|
770
|
+
this.blockQueue.enqueue(toIndex);
|
|
771
|
+
} else {
|
|
772
|
+
const toIndex = withTimeout(Promise.all([
|
|
773
|
+
this.blockClient.block(i),
|
|
774
|
+
this.blockClient.blockResults(i),
|
|
775
|
+
this.fetchValidatorSet(i)
|
|
776
|
+
]), RPC_TIMEOUT_MS, new RPCError("Timed out fetching block " + i)).catch((e) => {
|
|
777
|
+
this.log.error("Error fetching block " + i, { error: e });
|
|
778
|
+
this.prometheus?.recordError("rpc");
|
|
779
|
+
this.requestRecovery("fetch failed for block " + i, generation);
|
|
780
|
+
return Promise.resolve([]);
|
|
781
|
+
});
|
|
782
|
+
this.blockQueue.enqueue(toIndex);
|
|
783
|
+
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
this.log.error("Fetching error", { error: e });
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
this.nextFetchHeight = i + 1;
|
|
789
|
+
await this.blockQueue.continue();
|
|
553
790
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.log.
|
|
557
|
-
break;
|
|
791
|
+
if (!this.tryToRecover && this.started && generation === this.runGeneration && !this.blockQueue.synced) {
|
|
792
|
+
this.blockQueue.setSynced();
|
|
793
|
+
this.log.info("Synced to latest height");
|
|
558
794
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
this.blockQueue.setSynced();
|
|
562
|
-
this.log.info("Synced to latest height");
|
|
795
|
+
} finally {
|
|
796
|
+
if (this.fetcherToken === token) this.fetcherRunning = false;
|
|
563
797
|
}
|
|
564
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Runs an ABCI query. A transport failure (RPC down, timeout, empty reply) requests a recovery.
|
|
801
|
+
* A reply with a non-zero code is the chain answering "no" (pruned height, unknown path, bad
|
|
802
|
+
* key): it is thrown as an RPCError with the code and log, and no recovery is requested for
|
|
803
|
+
* ad-hoc queries, so modules can catch it. Block-pipeline queries reject into the fetcher,
|
|
804
|
+
* which requests recovery itself.
|
|
805
|
+
*/
|
|
565
806
|
async callABCI(path, data, height, adHoc = true) {
|
|
807
|
+
let abciq;
|
|
808
|
+
const endTimer = this.prometheus?.timeRpcCall(path) ?? void 0;
|
|
566
809
|
try {
|
|
567
|
-
|
|
568
|
-
const abciq = await (adHoc ? this.client : this.blockClient).abciQuery({
|
|
810
|
+
abciq = await (adHoc ? this.client : this.blockClient).abciQuery({
|
|
569
811
|
path,
|
|
570
812
|
data,
|
|
571
813
|
height
|
|
572
814
|
});
|
|
815
|
+
} catch (e) {
|
|
816
|
+
this.setStatus("FAILED");
|
|
817
|
+
this.prometheus?.recordError("rpc");
|
|
818
|
+
this.requestRecovery("ABCI query failed for " + path);
|
|
819
|
+
throw new RPCError("RPC not responding. Query at: " + path + " (" + e + ")");
|
|
820
|
+
} finally {
|
|
573
821
|
endTimer?.();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
this.tryToRecover = true;
|
|
577
|
-
this.setStatus("FAILED");
|
|
578
|
-
this.prometheus?.recordError("rpc");
|
|
579
|
-
throw new Error("RPC not responding. Query at: " + path);
|
|
580
|
-
}
|
|
581
|
-
} catch (_e) {
|
|
582
|
-
this.tryToRecover = true;
|
|
822
|
+
}
|
|
823
|
+
if (!abciq) {
|
|
583
824
|
this.setStatus("FAILED");
|
|
584
|
-
|
|
825
|
+
this.prometheus?.recordError("rpc");
|
|
826
|
+
this.requestRecovery("empty ABCI response for " + path);
|
|
827
|
+
throw new RPCError("RPC not responding. Query at: " + path);
|
|
585
828
|
}
|
|
829
|
+
if (abciq.code) {
|
|
830
|
+
this.prometheus?.recordError("rpc");
|
|
831
|
+
throw new RPCError("ABCI query " + path + " failed with code " + abciq.code + (abciq.log ? ": " + abciq.log : ""));
|
|
832
|
+
}
|
|
833
|
+
return abciq.value;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Fetches the complete validator set at a height, following pagination, and returns it
|
|
837
|
+
* re-encoded as a single QueryValidatorsResponse so the block queue payload keeps its shape.
|
|
838
|
+
* Chains with more validators than one page (1000) were silently truncated before.
|
|
839
|
+
*/
|
|
840
|
+
async fetchValidatorSet(height) {
|
|
841
|
+
const validators = [];
|
|
842
|
+
let key;
|
|
843
|
+
do {
|
|
844
|
+
const request = QueryValidatorsRequest.fromPartial({ pagination: key ? {
|
|
845
|
+
limit: PAGINATION_LIMITS.VALIDATORS,
|
|
846
|
+
key
|
|
847
|
+
} : { limit: PAGINATION_LIMITS.VALIDATORS } });
|
|
848
|
+
const page = QueryValidatorsResponse.decode(await this.callABCI("/cosmos.staking.v1beta1.Query/Validators", QueryValidatorsRequest.encode(request).finish(), height, false));
|
|
849
|
+
validators.push(...page.validators);
|
|
850
|
+
key = page.pagination?.nextKey && page.pagination.nextKey.length > 0 ? page.pagination.nextKey : void 0;
|
|
851
|
+
} while (key);
|
|
852
|
+
return QueryValidatorsResponse.encode(QueryValidatorsResponse.fromPartial({ validators })).finish();
|
|
586
853
|
}
|
|
587
854
|
newBlockReceived(height) {
|
|
588
|
-
|
|
589
|
-
this.blockTimeout = setTimeout(() => {
|
|
590
|
-
this.tryToRecover = true;
|
|
591
|
-
}, 3e4);
|
|
855
|
+
this.armIdleCheck();
|
|
592
856
|
this.log.info("Received new block: %d", height);
|
|
593
|
-
if (height
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}));
|
|
607
|
-
else {
|
|
608
|
-
const q = QueryValidatorsRequest.fromPartial({ pagination: { limit: 1000n } });
|
|
609
|
-
const vals = QueryValidatorsRequest.encode(q).finish();
|
|
610
|
-
this.blockQueue.enqueue(Promise.all([
|
|
611
|
-
this.client.block(height),
|
|
612
|
-
this.client.blockResults(height),
|
|
613
|
-
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, height, false)
|
|
614
|
-
]).catch((e) => {
|
|
615
|
-
this.log.error("Error fetching block: " + height + " : " + e);
|
|
616
|
-
return Promise.resolve([]);
|
|
617
|
-
}));
|
|
618
|
-
}
|
|
619
|
-
} catch (e) {
|
|
620
|
-
this.log.error("New Block Received error: " + e);
|
|
621
|
-
}
|
|
622
|
-
} else this.latestHeight = height;
|
|
857
|
+
if (height <= this.latestHeight) return;
|
|
858
|
+
this.latestHeight = height;
|
|
859
|
+
if (this.tryToRecover || !this.started) return;
|
|
860
|
+
this.fetcher().catch((e) => {
|
|
861
|
+
this.log.error("Error in fetching service", { error: e });
|
|
862
|
+
this.requestRecovery("fetcher failed");
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/** Starts a single polling chain, retiring any chain left over from a previous run */
|
|
866
|
+
startPolling() {
|
|
867
|
+
this.stopPolling();
|
|
868
|
+
const generation = ++this.pollGeneration;
|
|
869
|
+
this.pollForBlock(generation);
|
|
623
870
|
}
|
|
624
|
-
|
|
871
|
+
stopPolling() {
|
|
872
|
+
this.pollGeneration++;
|
|
873
|
+
if (this.pollTimer) {
|
|
874
|
+
clearTimeout(this.pollTimer);
|
|
875
|
+
this.pollTimer = null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async pollForBlock(generation) {
|
|
879
|
+
if (!this.started || generation !== this.pollGeneration) return;
|
|
625
880
|
try {
|
|
626
881
|
const status = await this.client.status();
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
this.pollForBlock();
|
|
630
|
-
}, this.config.pollingInterval);
|
|
882
|
+
if (!this.started || generation !== this.pollGeneration) return;
|
|
883
|
+
if (status.syncInfo.latestBlockHeight > this.latestHeight) this.newBlockReceived(status.syncInfo.latestBlockHeight);
|
|
631
884
|
} catch (e) {
|
|
632
|
-
this.log.error("Error polling for new block
|
|
633
|
-
this.
|
|
885
|
+
this.log.error("Error polling for new block", { error: e });
|
|
886
|
+
this.prometheus?.recordError("rpc");
|
|
887
|
+
this.requestRecovery("polling failed");
|
|
888
|
+
return;
|
|
634
889
|
}
|
|
890
|
+
this.pollTimer = setTimeout(() => {
|
|
891
|
+
this.pollForBlock(generation);
|
|
892
|
+
}, this.config.pollingInterval);
|
|
635
893
|
}
|
|
636
894
|
readGenesis() {
|
|
637
|
-
if (this.config.genesisPath) return fs.createReadStream(this.config.genesisPath)
|
|
895
|
+
if (this.config.genesisPath) return fs.createReadStream(this.config.genesisPath);
|
|
638
896
|
else throw new Error("Genesis path not set");
|
|
639
897
|
}
|
|
640
898
|
async setArrayReader(path, processor) {
|
|
641
899
|
return new Promise((resolve, reject) => {
|
|
642
900
|
try {
|
|
643
|
-
const pickers = path.split(".").map((filter) =>
|
|
901
|
+
const pickers = path.split(".").map((filter) => pick({ filter }));
|
|
644
902
|
let counter = 0;
|
|
645
903
|
let chunkCounter = 0;
|
|
646
904
|
const chunkProcessor = async (data) => {
|
|
@@ -652,48 +910,59 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
652
910
|
await this.config.endTransaction(true);
|
|
653
911
|
await this.config.beginTransaction();
|
|
654
912
|
}
|
|
913
|
+
return data;
|
|
655
914
|
};
|
|
656
915
|
chain([
|
|
657
916
|
this.readGenesis(),
|
|
917
|
+
parser(),
|
|
658
918
|
...pickers,
|
|
659
|
-
|
|
660
|
-
|
|
919
|
+
streamArray(),
|
|
920
|
+
batch({ batchSize: GENESIS_BATCH_SIZE }),
|
|
661
921
|
chunkProcessor
|
|
662
922
|
]).on("data", (data) => {
|
|
663
923
|
if (data && Array.isArray(data)) counter = counter + data.length;
|
|
664
924
|
}).on("end", () => {
|
|
665
925
|
this.log.info(`Processed ${counter} entries in ${chunkCounter} chunks`);
|
|
666
926
|
resolve(true);
|
|
927
|
+
}).on("error", (e) => {
|
|
928
|
+
this.log.error("Error reading genesis array " + path, { error: e });
|
|
929
|
+
reject(e);
|
|
667
930
|
});
|
|
668
|
-
} catch (
|
|
669
|
-
this.log.verbose("Error in setArrayReader: " +
|
|
670
|
-
reject();
|
|
931
|
+
} catch (e) {
|
|
932
|
+
this.log.verbose("Error in setArrayReader: " + e);
|
|
933
|
+
reject(e);
|
|
671
934
|
}
|
|
672
935
|
});
|
|
673
936
|
}
|
|
674
937
|
async setValueReader(path, processor) {
|
|
675
938
|
return new Promise((resolve, reject) => {
|
|
676
939
|
try {
|
|
677
|
-
const pickers = path.split(".").map((filter) =>
|
|
940
|
+
const pickers = path.split(".").map((filter) => pick({ filter }));
|
|
678
941
|
let counter = 0;
|
|
679
942
|
chain([
|
|
680
943
|
this.readGenesis(),
|
|
944
|
+
parser(),
|
|
681
945
|
...pickers,
|
|
682
|
-
|
|
946
|
+
streamValues(),
|
|
683
947
|
processor
|
|
684
948
|
]).on("data", (_data) => {
|
|
685
949
|
counter++;
|
|
686
950
|
}).on("end", () => {
|
|
687
951
|
this.log.info(`Processed ${counter} entries`);
|
|
688
952
|
resolve(true);
|
|
953
|
+
}).on("error", (e) => {
|
|
954
|
+
this.log.error("Error reading genesis value " + path, { error: e });
|
|
955
|
+
reject(e);
|
|
689
956
|
});
|
|
690
|
-
} catch (
|
|
691
|
-
|
|
957
|
+
} catch (e) {
|
|
958
|
+
this.log.verbose("Error in setValueReader: " + e);
|
|
959
|
+
reject(e);
|
|
692
960
|
}
|
|
693
961
|
});
|
|
694
962
|
}
|
|
695
963
|
async parseGenesis() {
|
|
696
964
|
this.log.info("Parsing genesis");
|
|
965
|
+
await this.config.onGenesisStart?.();
|
|
697
966
|
await this.config.beginTransaction();
|
|
698
967
|
try {
|
|
699
968
|
this.log.info("Starting genesis import");
|
|
@@ -721,6 +990,7 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
721
990
|
}
|
|
722
991
|
return data;
|
|
723
992
|
});
|
|
993
|
+
await this.config.onGenesisComplete?.();
|
|
724
994
|
await this.config.endTransaction(true);
|
|
725
995
|
this.log.info("Finished importing");
|
|
726
996
|
} catch (e) {
|
|
@@ -728,14 +998,16 @@ var EcleciaIndexer = class extends EclesiaEmitter {
|
|
|
728
998
|
await this.config.endTransaction(false);
|
|
729
999
|
} catch (dbe) {
|
|
730
1000
|
this.prometheus?.recordError("database");
|
|
731
|
-
this.log.error("Error ending transaction
|
|
1001
|
+
this.log.error("Error ending transaction", { error: dbe });
|
|
732
1002
|
}
|
|
733
1003
|
this.log.error("Failed to import genesis");
|
|
734
1004
|
throw e;
|
|
735
1005
|
}
|
|
736
1006
|
}
|
|
737
1007
|
};
|
|
1008
|
+
/** @deprecated Misspelling kept for compatibility, use EclesiaIndexer. Removed in 3.0. */
|
|
1009
|
+
const EcleciaIndexer = EclesiaIndexer;
|
|
738
1010
|
|
|
739
1011
|
//#endregion
|
|
740
|
-
export { EcleciaIndexer, defaultIndexerConfig };
|
|
1012
|
+
export { EcleciaIndexer, EclesiaIndexer, defaultIndexerConfig };
|
|
741
1013
|
//# sourceMappingURL=index.mjs.map
|