@eclesia/indexer-engine 2.7.7 → 2.8.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 +34 -0
- package/dist/_virtual/rolldown_runtime.js +13 -0
- package/dist/emitter/index.cjs +57 -0
- package/dist/emitter/index.cjs.map +1 -0
- package/dist/emitter/index.d.cts +21 -0
- package/dist/emitter/index.d.cts.map +1 -0
- package/dist/emitter/index.d.ts +19 -15
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js +54 -73
- package/dist/emitter/index.js.map +1 -0
- package/dist/index.cjs +23 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7 -44
- package/dist/indexer/index.cjs +570 -0
- package/dist/indexer/index.cjs.map +1 -0
- package/dist/indexer/index.d.cts +54 -0
- package/dist/indexer/index.d.cts.map +1 -0
- package/dist/indexer/index.d.ts +50 -46
- package/dist/indexer/index.d.ts.map +1 -1
- package/dist/indexer/index.js +551 -724
- package/dist/indexer/index.js.map +1 -0
- package/dist/promise-queue/index.cjs +140 -0
- package/dist/promise-queue/index.cjs.map +1 -0
- package/dist/promise-queue/index.d.cts +39 -0
- package/dist/promise-queue/index.d.cts.map +1 -0
- package/dist/promise-queue/index.d.ts +36 -33
- package/dist/promise-queue/index.d.ts.map +1 -1
- package/dist/promise-queue/index.js +138 -172
- package/dist/promise-queue/index.js.map +1 -0
- package/dist/types/index.cjs +12 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +102 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +88 -83
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +6 -2
- package/dist/types/index.js.map +1 -0
- package/dist/utils/bech32.cjs +27 -0
- package/dist/utils/bech32.cjs.map +1 -0
- package/dist/utils/bech32.d.cts +7 -0
- package/dist/utils/bech32.d.cts.map +1 -0
- package/dist/utils/bech32.d.ts +3 -1
- package/dist/utils/bech32.d.ts.map +1 -1
- package/dist/utils/bech32.js +17 -21
- package/dist/utils/bech32.js.map +1 -0
- package/dist/utils/bigint.cjs +9 -0
- package/dist/utils/bigint.cjs.map +1 -0
- package/dist/utils/bigint.d.cts +5 -0
- package/dist/utils/bigint.d.cts.map +1 -0
- package/dist/utils/bigint.d.ts +3 -1
- package/dist/utils/bigint.d.ts.map +1 -1
- package/dist/utils/bigint.js +6 -5
- package/dist/utils/bigint.js.map +1 -0
- package/dist/utils/index.cjs +22 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +11 -0
- package/dist/utils/index.d.cts.map +1 -0
- package/dist/utils/index.d.ts +10 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +17 -19
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/text.cjs +11 -0
- package/dist/utils/text.cjs.map +1 -0
- package/dist/utils/text.d.cts +5 -0
- package/dist/utils/text.d.cts.map +1 -0
- package/dist/utils/text.d.ts +4 -1
- package/dist/utils/text.d.ts.map +1 -1
- package/dist/utils/text.js +8 -13
- package/dist/utils/text.js.map +1 -0
- package/package.json +24 -6
- package/dist/index.d.ts.map +0 -1
package/dist/indexer/index.js
CHANGED
|
@@ -1,726 +1,553 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { EclesiaEmitter } from "../emitter/index.js";
|
|
2
|
+
import { CircularBuffer } from "../promise-queue/index.js";
|
|
3
|
+
import { decodeAttr } from "../utils/text.js";
|
|
4
|
+
import "../utils/index.js";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { connectComet, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
|
|
8
|
+
import { MsgExec } from "cosmjs-types/cosmos/authz/v1beta1/tx.js";
|
|
9
|
+
import { QueryValidatorsRequest, QueryValidatorsResponse } from "cosmjs-types/cosmos/staking/v1beta1/query.js";
|
|
10
|
+
import { Tx } from "cosmjs-types/cosmos/tx/v1beta1/tx.js";
|
|
11
|
+
import Fastify from "fastify";
|
|
12
|
+
import { chain } from "stream-chain";
|
|
13
|
+
import * as Parser from "stream-json";
|
|
14
|
+
import * as Pick from "stream-json/filters/Pick.js";
|
|
15
|
+
import * as StreamArray from "stream-json/streamers/StreamArray.js";
|
|
16
|
+
import * as StreamValues from "stream-json/streamers/StreamValues.js";
|
|
17
|
+
import * as Batch from "stream-json/utils/Batch.js";
|
|
18
|
+
import { v4 } from "uuid";
|
|
19
|
+
import * as winston from "winston";
|
|
20
|
+
|
|
21
|
+
//#region src/indexer/index.ts
|
|
22
|
+
const defaultIndexerConfig = {
|
|
23
|
+
startHeight: 1,
|
|
24
|
+
batchSize: 500,
|
|
25
|
+
modules: [],
|
|
26
|
+
getNextHeight: () => 1,
|
|
27
|
+
logLevel: "info",
|
|
28
|
+
usePolling: false,
|
|
29
|
+
pollingInterval: 5e3,
|
|
30
|
+
processGenesis: false,
|
|
31
|
+
minimal: true,
|
|
32
|
+
init: () => Promise.resolve(),
|
|
33
|
+
beginTransaction: () => Promise.resolve(),
|
|
34
|
+
endTransaction: (_status) => Promise.resolve()
|
|
4
35
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
var EcleciaIndexer = class extends EclesiaEmitter {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
super();
|
|
39
|
+
this.initialized = false;
|
|
40
|
+
this.retryCount = 0;
|
|
41
|
+
this.tryToRecover = false;
|
|
42
|
+
this.healthCheck = { status: "CONNECTING" };
|
|
43
|
+
this.subscription = null;
|
|
44
|
+
this.blockListener = { next: (data) => {
|
|
45
|
+
this.newBlockReceived(data.header.height);
|
|
46
|
+
} };
|
|
47
|
+
this.asyncEmit = async (type, event) => {
|
|
48
|
+
event.uuid = v4();
|
|
49
|
+
let listenerCount = this.handled.get(type);
|
|
50
|
+
if (!listenerCount) listenerCount = 1;
|
|
51
|
+
let listenersResponded = 0;
|
|
52
|
+
const prom = new Promise((resolve, reject) => {
|
|
53
|
+
const returnFunc = (ev) => {
|
|
54
|
+
if (ev.uuid == event.uuid) if (ev.status) {
|
|
55
|
+
listenersResponded++;
|
|
56
|
+
if (listenersResponded == listenerCount) {
|
|
57
|
+
this.off("uuid", returnFunc);
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
} else reject(ev.error);
|
|
61
|
+
};
|
|
62
|
+
this.on("uuid", returnFunc);
|
|
63
|
+
});
|
|
64
|
+
this.emit(type, event);
|
|
65
|
+
return prom;
|
|
66
|
+
};
|
|
67
|
+
this.config = {
|
|
68
|
+
...defaultIndexerConfig,
|
|
69
|
+
...config
|
|
70
|
+
};
|
|
71
|
+
if (this.config.minimal) this.blockQueue = new CircularBuffer(this.config.batchSize);
|
|
72
|
+
else this.blockQueue = new CircularBuffer(this.config.batchSize);
|
|
73
|
+
const { printf } = winston.format;
|
|
74
|
+
const eclesiaFormat = printf(({ level, message, timestamp }) => {
|
|
75
|
+
return `${timestamp} [${level.toUpperCase()}]:\t${message}`;
|
|
76
|
+
});
|
|
77
|
+
this.log = winston.createLogger({
|
|
78
|
+
level: this.config.logLevel,
|
|
79
|
+
defaultMeta: { service: "Eclesia Indexer" },
|
|
80
|
+
transports: [
|
|
81
|
+
new winston.transports.File({
|
|
82
|
+
filename: "error.log",
|
|
83
|
+
level: "error"
|
|
84
|
+
}),
|
|
85
|
+
new winston.transports.File({ filename: "combined.log" }),
|
|
86
|
+
new winston.transports.Console({ format: winston.format.combine(winston.format.splat(), winston.format.timestamp(), eclesiaFormat, winston.format.colorize({ all: true })) })
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
this.fastify = Fastify({ logger: false });
|
|
90
|
+
this.on("_unhandled", (msg) => {
|
|
91
|
+
if (msg.uuid) {
|
|
92
|
+
this.log.verbose("Unhandled event: " + msg.type);
|
|
93
|
+
this.emit("uuid", {
|
|
94
|
+
status: true,
|
|
95
|
+
uuid: msg.uuid
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.fastify.get("/health", async (_request, reply) => {
|
|
100
|
+
const code = this.healthCheck.status == "OK" ? 200 : 503;
|
|
101
|
+
reply.code(code).send(this.healthCheck);
|
|
102
|
+
});
|
|
103
|
+
this.fastify.listen({
|
|
104
|
+
port: 80,
|
|
105
|
+
host: "0.0.0.0"
|
|
106
|
+
}, (err) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
this.log.error(err);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
setStatus(status) {
|
|
114
|
+
this.healthCheck.status = status;
|
|
115
|
+
}
|
|
116
|
+
isMinimal(_blockqueue) {
|
|
117
|
+
if (this.config.minimal) return true;
|
|
118
|
+
else return false;
|
|
119
|
+
}
|
|
120
|
+
async connect() {
|
|
121
|
+
try {
|
|
122
|
+
if (this.client && this.tryToRecover) {
|
|
123
|
+
this.log.verbose("Recover from error. Attempting to disconnect from RPC");
|
|
124
|
+
this.client.disconnect();
|
|
125
|
+
this.blockClient.disconnect();
|
|
126
|
+
this.log.verbose("Disconnected from RPC");
|
|
127
|
+
}
|
|
128
|
+
this.client = await connectComet(this.config.rpcUrl);
|
|
129
|
+
this.log.info("Connected to RPC for ad hoc queries");
|
|
130
|
+
this.blockClient = await connectComet(this.config.rpcUrl);
|
|
131
|
+
this.log.info("Connected to RPC for block & validator info");
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.log.error(error);
|
|
135
|
+
this.tryToRecover = true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async start() {
|
|
140
|
+
if (this.blockQueue) {
|
|
141
|
+
this.blockQueue.clear();
|
|
142
|
+
this.log.verbose("Starting, clearing block queue");
|
|
143
|
+
}
|
|
144
|
+
if (!this.initialized) {
|
|
145
|
+
try {
|
|
146
|
+
if (this.config.init) await this.config.init();
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this.log.error("Failed to initialize indexer: " + e);
|
|
149
|
+
this.setStatus("FAILED");
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
if (this.config.processGenesis) try {
|
|
153
|
+
if (this.config.genesisPath) await this.parseGenesis();
|
|
154
|
+
} catch (e) {
|
|
155
|
+
this.log.error("Failed to parse genesis: " + e);
|
|
156
|
+
this.setStatus("FAILED");
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
this.initialized = true;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
await this.connect();
|
|
163
|
+
if (!this.config.usePolling && this.subscription) {
|
|
164
|
+
this.subscription.removeListener(this.blockListener);
|
|
165
|
+
this.subscription = null;
|
|
166
|
+
this.log.verbose("Removed existing block listener and subscription");
|
|
167
|
+
}
|
|
168
|
+
this.subscription = this.client.subscribeNewBlock ? this.client.subscribeNewBlock() : null;
|
|
169
|
+
this.latestHeight = (await this.client.status()).syncInfo.latestBlockHeight;
|
|
170
|
+
this.log.info("Current chain height: " + this.latestHeight);
|
|
171
|
+
this.heightToProcess = await this.config.getNextHeight();
|
|
172
|
+
if (this.config.usePolling) this.pollForBlock();
|
|
173
|
+
else if (this.subscription) this.subscription.addListener(this.blockListener);
|
|
174
|
+
else throw new Error("Could not subscribe to new blocks");
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.log.error("Failed to set up block listening: " + e);
|
|
177
|
+
this.setStatus("FAILED");
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
this.tryToRecover = false;
|
|
181
|
+
this.fetcher().catch((e) => {
|
|
182
|
+
this.setStatus("FAILED");
|
|
183
|
+
throw new Error("Error in fetching service: " + e);
|
|
184
|
+
});
|
|
185
|
+
const hrTime = process.hrtime();
|
|
186
|
+
let ms = hrTime[0] * 1e6 + hrTime[1] / 1e3;
|
|
187
|
+
while (this.blockQueue.size() > 0 && !this.tryToRecover) {
|
|
188
|
+
try {
|
|
189
|
+
if (this.tryToRecover) throw new Error("Exiting processing loop. Attempting to recover indexer");
|
|
190
|
+
await this.config.beginTransaction();
|
|
191
|
+
this.log.silly("Started db tx");
|
|
192
|
+
let height, timestamp;
|
|
193
|
+
if (this.isMinimal(this.blockQueue)) {
|
|
194
|
+
const toProcess = await this.blockQueue.dequeue();
|
|
195
|
+
this.log.silly("Retrieved block data");
|
|
196
|
+
if (!toProcess || !toProcess[0] || !toProcess[1]) throw new Error("Could not fetch block");
|
|
197
|
+
height = toProcess[0].block.header.height;
|
|
198
|
+
timestamp = toRfc3339WithNanoseconds(toProcess[0].block.header.time);
|
|
199
|
+
await this.processBlock(toProcess[0], toProcess[1]);
|
|
200
|
+
} else {
|
|
201
|
+
const toProcess = await this.blockQueue.dequeue();
|
|
202
|
+
this.log.silly("Retrieved block data");
|
|
203
|
+
if (!toProcess || !toProcess[0] || !toProcess[1] || !toProcess[2]) throw new Error("Could not fetch block");
|
|
204
|
+
this.log.silly("Decoded block");
|
|
205
|
+
height = toProcess[0].block.header.height;
|
|
206
|
+
timestamp = toRfc3339WithNanoseconds(toProcess[0].block.header.time);
|
|
207
|
+
await this.processBlock(toProcess[0], toProcess[1], QueryValidatorsResponse.decode(toProcess[2]).validators);
|
|
208
|
+
}
|
|
209
|
+
if (height % 1e3 == 0) {
|
|
210
|
+
const hrTime$1 = process.hrtime();
|
|
211
|
+
const newms = hrTime$1[0] * 1e6 + hrTime$1[1] / 1e3;
|
|
212
|
+
const duration = newms - ms;
|
|
213
|
+
ms = newms;
|
|
214
|
+
const rate = 1e9 / duration;
|
|
215
|
+
this.log.info("Processing:" + rate.toFixed(2) + "blocks/sec");
|
|
216
|
+
await this.asyncEmit("periodic/1000", {
|
|
217
|
+
value: null,
|
|
218
|
+
height,
|
|
219
|
+
timestamp
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (height % 100 == 0) await this.asyncEmit("periodic/100", {
|
|
223
|
+
value: null,
|
|
224
|
+
height,
|
|
225
|
+
timestamp
|
|
226
|
+
});
|
|
227
|
+
if (height % 50 == 0) await this.asyncEmit("periodic/50", {
|
|
228
|
+
value: null,
|
|
229
|
+
height,
|
|
230
|
+
timestamp
|
|
231
|
+
});
|
|
232
|
+
this.log.silly("Handled periodic events");
|
|
233
|
+
await this.config.endTransaction(true);
|
|
234
|
+
this.log.silly("Committed db tx");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
this.log.error("" + e);
|
|
237
|
+
this.setStatus("FAILED");
|
|
238
|
+
try {
|
|
239
|
+
await this.config.endTransaction(false);
|
|
240
|
+
} catch (dbe) {
|
|
241
|
+
this.log.error("Error ending transaction. Must be a DB error: " + dbe);
|
|
242
|
+
}
|
|
243
|
+
this.tryToRecover = true;
|
|
244
|
+
this.retryCount++;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
this.retryCount = 0;
|
|
248
|
+
this.setStatus("OK");
|
|
249
|
+
}
|
|
250
|
+
if (this.retryCount < 3) {
|
|
251
|
+
this.log.debug("Indexer retryCount: " + this.retryCount);
|
|
252
|
+
this.log.info("Indexer is restarting");
|
|
253
|
+
setTimeout(() => this.start(), this.retryCount * 5e3);
|
|
254
|
+
} else {
|
|
255
|
+
this.log.info("Indexer failed too many times. Exiting.");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async processBlock(block, block_results, validators) {
|
|
260
|
+
let processStart = [0, 0];
|
|
261
|
+
if (this.config.logLevel == "silly") processStart = process.hrtime();
|
|
262
|
+
const height = block.block.header.height;
|
|
263
|
+
this.log.debug("Processing block: %d", height);
|
|
264
|
+
this.log.silly("Started db tx");
|
|
265
|
+
const timestamp = toRfc3339WithNanoseconds(block.block.header.time);
|
|
266
|
+
await this.asyncEmit("block", {
|
|
267
|
+
value: {
|
|
268
|
+
block,
|
|
269
|
+
block_results
|
|
270
|
+
},
|
|
271
|
+
height,
|
|
272
|
+
timestamp
|
|
273
|
+
});
|
|
274
|
+
this.log.silly("Modules handled block event");
|
|
275
|
+
await this.asyncEmit("begin_block", {
|
|
276
|
+
value: {
|
|
277
|
+
events: block_results.beginBlockEvents,
|
|
278
|
+
validators
|
|
279
|
+
},
|
|
280
|
+
height,
|
|
281
|
+
timestamp
|
|
282
|
+
});
|
|
283
|
+
this.log.silly("Modules handled begin_block events");
|
|
284
|
+
await this.asyncEmit("tx_events", {
|
|
285
|
+
value: block_results.results,
|
|
286
|
+
height,
|
|
287
|
+
timestamp
|
|
288
|
+
});
|
|
289
|
+
this.log.silly("Modules handled tx events");
|
|
290
|
+
for (let t = 0; t < block.block.txs.length; t++) {
|
|
291
|
+
const tx = Tx.decode(block.block.txs[t]);
|
|
292
|
+
const result = block_results.results[t].code;
|
|
293
|
+
const txlog = block_results.results[t].log;
|
|
294
|
+
if (result != 0) continue;
|
|
295
|
+
if (tx.body && tx.body.memo != "") {
|
|
296
|
+
const txHash = createHash("sha256").update(block.block.txs[t]).digest("hex");
|
|
297
|
+
await this.asyncEmit("tx_memo", {
|
|
298
|
+
value: {
|
|
299
|
+
txHash,
|
|
300
|
+
txBody: tx.body
|
|
301
|
+
},
|
|
302
|
+
height,
|
|
303
|
+
timestamp
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const events = txlog ? JSON.parse(txlog) : [];
|
|
307
|
+
const msgs = tx.body?.messages;
|
|
308
|
+
if (msgs) for (let i = 0; i < msgs.length; i++) {
|
|
309
|
+
this.log.silly("Indexer broadcasting msg for handling: " + msgs[i].typeUrl);
|
|
310
|
+
const msgevents = msgs.length > 1 ? events.find((x) => x.msg_index == i)?.events : events[0].events;
|
|
311
|
+
await this.asyncEmit(msgs[i].typeUrl, {
|
|
312
|
+
value: {
|
|
313
|
+
tx: msgs[i].value,
|
|
314
|
+
events: msgevents
|
|
315
|
+
},
|
|
316
|
+
height,
|
|
317
|
+
timestamp
|
|
318
|
+
});
|
|
319
|
+
if (msgs[i].typeUrl == "/cosmos.authz.v1beta1.MsgExec") {
|
|
320
|
+
const authzMsgs = MsgExec.decode(msgs[i].value).msgs;
|
|
321
|
+
if (authzMsgs) for (let r = 0; r < authzMsgs.length; r++) {
|
|
322
|
+
this.log.silly("Indexer broadcasting msg for handling: " + authzMsgs[r].typeUrl);
|
|
323
|
+
const authzMsgEvents = msgevents?.reduce((events$1, evt) => {
|
|
324
|
+
if (evt.attributes.filter((x) => decodeAttr(x.key) == "authz_msg_index" && decodeAttr(x.value) == "" + r).length > 0) events$1.push(evt);
|
|
325
|
+
return events$1;
|
|
326
|
+
}, []);
|
|
327
|
+
await this.asyncEmit(authzMsgs[r].typeUrl, {
|
|
328
|
+
value: {
|
|
329
|
+
tx: authzMsgs[r].value,
|
|
330
|
+
events: authzMsgEvents
|
|
331
|
+
},
|
|
332
|
+
height,
|
|
333
|
+
timestamp
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.log.silly("Modules handled msg events");
|
|
340
|
+
await this.asyncEmit("end_block", {
|
|
341
|
+
value: block_results.endBlockEvents,
|
|
342
|
+
height,
|
|
343
|
+
timestamp
|
|
344
|
+
});
|
|
345
|
+
this.log.silly("Modules handled end_block events");
|
|
346
|
+
if (this.config.logLevel == "silly") {
|
|
347
|
+
const processEnd = process.hrtime(processStart);
|
|
348
|
+
const processTime = processEnd[0] * 1e3 + processEnd[1] / 1e6;
|
|
349
|
+
this.log.silly("Processed block %d in %d ms", height, processTime.toFixed(2));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async fetcher() {
|
|
353
|
+
for (let i = this.heightToProcess; i <= this.latestHeight; i++) {
|
|
354
|
+
this.log.debug("Fetching: " + i);
|
|
355
|
+
if (this.tryToRecover) {
|
|
356
|
+
this.log.verbose("Exiting fetcher loop. Attempting to recover indexer");
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
if (this.isMinimal(this.blockQueue)) {
|
|
361
|
+
const timeoutPromise = new Promise((resolve, reject) => {
|
|
362
|
+
setTimeout(reject, 2e4, false);
|
|
363
|
+
});
|
|
364
|
+
const toIndex = Promise.race([Promise.all([this.blockClient.block(i), this.blockClient.blockResults(i)]), timeoutPromise]).catch((e) => {
|
|
365
|
+
this.log.error("Error fetching block: " + i + " : " + e);
|
|
366
|
+
this.tryToRecover = true;
|
|
367
|
+
return Promise.resolve([]);
|
|
368
|
+
});
|
|
369
|
+
this.blockQueue.enqueue(toIndex);
|
|
370
|
+
} else {
|
|
371
|
+
const timeoutPromise = new Promise((resolve, reject) => {
|
|
372
|
+
setTimeout(reject, 2e4, false);
|
|
373
|
+
});
|
|
374
|
+
const q = QueryValidatorsRequest.fromPartial({ pagination: { limit: 1000n } });
|
|
375
|
+
const vals = QueryValidatorsRequest.encode(q).finish();
|
|
376
|
+
const toIndex = Promise.race([Promise.all([
|
|
377
|
+
this.blockClient.block(i),
|
|
378
|
+
this.blockClient.blockResults(i),
|
|
379
|
+
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, i, false)
|
|
380
|
+
]), timeoutPromise]).catch((e) => {
|
|
381
|
+
this.log.error("Error fetching block: " + i + " : " + e);
|
|
382
|
+
this.tryToRecover = true;
|
|
383
|
+
return Promise.resolve([]);
|
|
384
|
+
});
|
|
385
|
+
this.blockQueue.enqueue(toIndex);
|
|
386
|
+
}
|
|
387
|
+
await this.blockQueue.continue();
|
|
388
|
+
if (this.tryToRecover) {
|
|
389
|
+
this.retryCount++;
|
|
390
|
+
throw new Error("RPC not responding");
|
|
391
|
+
}
|
|
392
|
+
} catch (e) {
|
|
393
|
+
this.log.error(e);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (!this.tryToRecover) {
|
|
398
|
+
this.blockQueue.setSynced();
|
|
399
|
+
this.log.info("Synced to latest height");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async callABCI(path, data, height, adHoc = true) {
|
|
403
|
+
try {
|
|
404
|
+
const abciq = await (adHoc ? this.client : this.blockClient).abciQuery({
|
|
405
|
+
path,
|
|
406
|
+
data,
|
|
407
|
+
height
|
|
408
|
+
});
|
|
409
|
+
if (abciq) return abciq.value;
|
|
410
|
+
else {
|
|
411
|
+
this.tryToRecover = true;
|
|
412
|
+
this.setStatus("FAILED");
|
|
413
|
+
throw new Error("RPC not responding. Query at: " + path);
|
|
414
|
+
}
|
|
415
|
+
} catch (_e) {
|
|
416
|
+
this.tryToRecover = true;
|
|
417
|
+
this.setStatus("FAILED");
|
|
418
|
+
this.retryCount++;
|
|
419
|
+
throw new Error("RPC not responding. Query at: " + path);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
newBlockReceived(height) {
|
|
423
|
+
this.log.info("Received new block: %d", height);
|
|
424
|
+
if (this.blockQueue.synced && !this.tryToRecover) {
|
|
425
|
+
if (this.blockQueue.size() + 1 == this.config.batchSize) {
|
|
426
|
+
this.log.error("Block queue is full. Cannot add new block");
|
|
427
|
+
this.tryToRecover = true;
|
|
428
|
+
this.retryCount++;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
if (this.isMinimal(this.blockQueue)) this.blockQueue.enqueue(Promise.all([this.client.block(height), this.client.blockResults(height)]).catch((e) => {
|
|
433
|
+
this.log.error("Error fetching block: " + height + " : " + e);
|
|
434
|
+
return Promise.resolve([]);
|
|
435
|
+
}));
|
|
436
|
+
else {
|
|
437
|
+
const q = QueryValidatorsRequest.fromPartial({ pagination: { limit: 1000n } });
|
|
438
|
+
const vals = QueryValidatorsRequest.encode(q).finish();
|
|
439
|
+
this.blockQueue.enqueue(Promise.all([
|
|
440
|
+
this.client.block(height),
|
|
441
|
+
this.client.blockResults(height),
|
|
442
|
+
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, height, false)
|
|
443
|
+
]).catch((e) => {
|
|
444
|
+
this.log.error("Error fetching block: " + height + " : " + e);
|
|
445
|
+
return Promise.resolve([]);
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {
|
|
449
|
+
this.log.error("" + e);
|
|
450
|
+
}
|
|
451
|
+
} else this.latestHeight = height;
|
|
452
|
+
}
|
|
453
|
+
async pollForBlock() {
|
|
454
|
+
const status = await this.client.status();
|
|
455
|
+
if (status.syncInfo.latestBlockHeight > this.latestHeight) while (this.latestHeight < status.syncInfo.latestBlockHeight) this.newBlockReceived(this.latestHeight + 1);
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
this.pollForBlock();
|
|
458
|
+
}, this.config.pollingInterval);
|
|
459
|
+
}
|
|
460
|
+
readGenesis() {
|
|
461
|
+
if (this.config.genesisPath) return fs.createReadStream(this.config.genesisPath).pipe(Parser.parser());
|
|
462
|
+
else throw new Error("Genesis path not set");
|
|
463
|
+
}
|
|
464
|
+
async setArrayReader(path, processor) {
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
try {
|
|
467
|
+
const pickers = path.split(".").map((filter) => Pick.pick({ filter }));
|
|
468
|
+
let counter = 0;
|
|
469
|
+
chain([
|
|
470
|
+
this.readGenesis(),
|
|
471
|
+
...pickers,
|
|
472
|
+
StreamArray.streamArray(),
|
|
473
|
+
Batch.batch({ batchSize: 1e3 }),
|
|
474
|
+
processor
|
|
475
|
+
]).on("data", (data) => {
|
|
476
|
+
if (data && Array.isArray(data)) counter = counter + data.length;
|
|
477
|
+
}).on("end", () => {
|
|
478
|
+
this.log.info(`Processed ${counter} entries`);
|
|
479
|
+
resolve(true);
|
|
480
|
+
});
|
|
481
|
+
} catch (_e) {
|
|
482
|
+
this.log.verbose("Error in setArrayReader: " + _e);
|
|
483
|
+
reject();
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
async setValueReader(path, processor) {
|
|
488
|
+
return new Promise((resolve, reject) => {
|
|
489
|
+
try {
|
|
490
|
+
const pickers = path.split(".").map((filter) => Pick.pick({ filter }));
|
|
491
|
+
let counter = 0;
|
|
492
|
+
chain([
|
|
493
|
+
this.readGenesis(),
|
|
494
|
+
...pickers,
|
|
495
|
+
StreamValues.streamValues(),
|
|
496
|
+
processor
|
|
497
|
+
]).on("data", (_data) => {
|
|
498
|
+
counter++;
|
|
499
|
+
}).on("end", () => {
|
|
500
|
+
this.log.info(`Processed ${counter} entries`);
|
|
501
|
+
resolve(true);
|
|
502
|
+
});
|
|
503
|
+
} catch (_e) {
|
|
504
|
+
reject();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async parseGenesis() {
|
|
509
|
+
this.log.info("Parsing genesis");
|
|
510
|
+
await this.config.beginTransaction();
|
|
511
|
+
try {
|
|
512
|
+
this.log.info("Starting genesis import");
|
|
513
|
+
this.log.debug("Importing genesis file...");
|
|
514
|
+
for (const [key, _value] of this.handled) if (key.startsWith("genesis/")) {
|
|
515
|
+
const genesisEntry = key.split("/");
|
|
516
|
+
this.log.verbose("Importing " + key + "...");
|
|
517
|
+
if (genesisEntry[1] == "array") await this.setArrayReader(genesisEntry[2], async (data) => {
|
|
518
|
+
await this.asyncEmit(key, { value: data.map((x) => x.value) });
|
|
519
|
+
return data;
|
|
520
|
+
});
|
|
521
|
+
else await this.setValueReader(genesisEntry[2], async (data) => {
|
|
522
|
+
await this.asyncEmit(key, { value: data.value });
|
|
523
|
+
return data;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
this.log.info("Importing gen TXs...");
|
|
527
|
+
await this.setArrayReader("app_state.genutil.gen_txs", async (data) => {
|
|
528
|
+
for (let j = 0; j < data.length; j++) {
|
|
529
|
+
const gentx = data[j].value;
|
|
530
|
+
for (let i = 0; i < gentx.body.messages.length; i++) {
|
|
531
|
+
const msg = gentx.body.messages[i];
|
|
532
|
+
await this.asyncEmit("gentx" + msg["@type"], { value: msg });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return data;
|
|
536
|
+
});
|
|
537
|
+
await this.config.endTransaction(true);
|
|
538
|
+
this.log.info("Finished importing");
|
|
539
|
+
} catch (e) {
|
|
540
|
+
try {
|
|
541
|
+
await this.config.endTransaction(false);
|
|
542
|
+
} catch (dbe) {
|
|
543
|
+
this.log.error("Error ending transaction. Must be a DB error: " + dbe);
|
|
544
|
+
}
|
|
545
|
+
this.log.error("Failed to import genesis");
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
40
549
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.retryCount = 0;
|
|
46
|
-
this.tryToRecover = false;
|
|
47
|
-
this.healthCheck = {
|
|
48
|
-
status: "CONNECTING",
|
|
49
|
-
};
|
|
50
|
-
this.subscription = null;
|
|
51
|
-
this.blockListener = {
|
|
52
|
-
next: (data) => {
|
|
53
|
-
this.newBlockReceived(data.header.height);
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
this.asyncEmit = async (type, event) => {
|
|
57
|
-
event.uuid = (0, uuid_1.v4)();
|
|
58
|
-
/*
|
|
59
|
-
* More than 1 listener can be registered for an event type
|
|
60
|
-
* Fortunately these are all set up during module init() so we have a consistent count
|
|
61
|
-
* so we can count responses to resolve when complete
|
|
62
|
-
* values are irrelevant as promise resolution is only used for flow control
|
|
63
|
-
*/
|
|
64
|
-
let listenerCount = this.handled.get(type);
|
|
65
|
-
if (!listenerCount) {
|
|
66
|
-
// Setting listenerCount to 1 (the unhandled listener)
|
|
67
|
-
listenerCount = 1;
|
|
68
|
-
}
|
|
69
|
-
let listenersResponded = 0;
|
|
70
|
-
const prom = new Promise((resolve, reject) => {
|
|
71
|
-
const returnFunc = (ev) => {
|
|
72
|
-
if (ev.uuid == event.uuid) {
|
|
73
|
-
if (ev.status) {
|
|
74
|
-
listenersResponded++;
|
|
75
|
-
if (listenersResponded == listenerCount) {
|
|
76
|
-
// All listeners have done their thing so we can remove listener, resolve and continue execution
|
|
77
|
-
this.off("uuid", returnFunc);
|
|
78
|
-
resolve();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
// At least 1 listener is reporting an error. Reject and handle exception at the original asyncEmit location
|
|
83
|
-
reject(ev.error);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
this.on("uuid", returnFunc);
|
|
88
|
-
});
|
|
89
|
-
this.emit(type, event);
|
|
90
|
-
return prom;
|
|
91
|
-
};
|
|
92
|
-
this.config = {
|
|
93
|
-
...exports.defaultIndexerConfig,
|
|
94
|
-
...config,
|
|
95
|
-
};
|
|
96
|
-
if (this.config.minimal) {
|
|
97
|
-
this.blockQueue = new promise_queue_1.CircularBuffer(this.config.batchSize);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
this.blockQueue = new promise_queue_1.CircularBuffer(this.config.batchSize);
|
|
101
|
-
}
|
|
102
|
-
const { printf, } = winston_1.default.format;
|
|
103
|
-
const eclesiaFormat = printf(({ level, message, timestamp, }) => {
|
|
104
|
-
return `${timestamp} [${level.toUpperCase()}]:\t${message}`;
|
|
105
|
-
});
|
|
106
|
-
this.log = winston_1.default.createLogger({
|
|
107
|
-
level: this.config.logLevel,
|
|
108
|
-
defaultMeta: {
|
|
109
|
-
service: "Eclesia Indexer",
|
|
110
|
-
},
|
|
111
|
-
transports: [
|
|
112
|
-
new winston_1.default.transports.File({
|
|
113
|
-
filename: "error.log",
|
|
114
|
-
level: "error",
|
|
115
|
-
}),
|
|
116
|
-
new winston_1.default.transports.File({
|
|
117
|
-
filename: "combined.log",
|
|
118
|
-
}),
|
|
119
|
-
new winston_1.default.transports.Console({
|
|
120
|
-
format: winston_1.default.format.combine(winston_1.default.format.splat(), winston_1.default.format.timestamp(), eclesiaFormat, winston_1.default.format.colorize({
|
|
121
|
-
all: true,
|
|
122
|
-
})),
|
|
123
|
-
}),
|
|
124
|
-
],
|
|
125
|
-
});
|
|
126
|
-
this.fastify = (0, fastify_1.default)({
|
|
127
|
-
logger: false,
|
|
128
|
-
});
|
|
129
|
-
this.on("_unhandled", (msg) => {
|
|
130
|
-
if (msg.uuid) {
|
|
131
|
-
this.log.verbose("Unhandled event: " + msg.type);
|
|
132
|
-
this.emit("uuid", {
|
|
133
|
-
status: true,
|
|
134
|
-
uuid: msg.uuid,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
this.fastify.get("/health", async (_request, reply) => {
|
|
139
|
-
const code = this.healthCheck.status == "OK"
|
|
140
|
-
? 200
|
|
141
|
-
: 503;
|
|
142
|
-
reply.code(code).send(this.healthCheck);
|
|
143
|
-
});
|
|
144
|
-
this.fastify.listen({
|
|
145
|
-
port: 80,
|
|
146
|
-
host: "0.0.0.0",
|
|
147
|
-
}, (err) => {
|
|
148
|
-
if (err) {
|
|
149
|
-
this.log.error(err);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
setStatus(status) {
|
|
155
|
-
this.healthCheck.status = status;
|
|
156
|
-
}
|
|
157
|
-
isMinimal(_blockqueue) {
|
|
158
|
-
if (this.config.minimal) {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async connect() {
|
|
166
|
-
try {
|
|
167
|
-
if (this.client && this.tryToRecover) {
|
|
168
|
-
this.log.verbose("Recover from error. Attempting to disconnect from RPC");
|
|
169
|
-
this.client.disconnect();
|
|
170
|
-
this.blockClient.disconnect();
|
|
171
|
-
this.log.verbose("Disconnected from RPC");
|
|
172
|
-
}
|
|
173
|
-
this.client = await (0, tendermint_rpc_2.connectComet)(this.config.rpcUrl);
|
|
174
|
-
this.log.info("Connected to RPC for ad hoc queries");
|
|
175
|
-
this.blockClient = await (0, tendermint_rpc_2.connectComet)(this.config.rpcUrl);
|
|
176
|
-
this.log.info("Connected to RPC for block & validator info");
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
this.log.error(error);
|
|
181
|
-
this.tryToRecover = true;
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async start() {
|
|
186
|
-
if (this.blockQueue) {
|
|
187
|
-
this.blockQueue.clear();
|
|
188
|
-
this.log.verbose("Starting, clearing block queue");
|
|
189
|
-
}
|
|
190
|
-
if (!this.initialized) {
|
|
191
|
-
try {
|
|
192
|
-
if (this.config.init) {
|
|
193
|
-
await this.config.init();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch (e) {
|
|
197
|
-
this.log.error("Failed to initialize indexer: " + e);
|
|
198
|
-
this.setStatus("FAILED");
|
|
199
|
-
throw e;
|
|
200
|
-
}
|
|
201
|
-
if (this.config.processGenesis) {
|
|
202
|
-
try {
|
|
203
|
-
if (this.config.genesisPath) {
|
|
204
|
-
await this.parseGenesis();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (e) {
|
|
208
|
-
this.log.error("Failed to parse genesis: " + e);
|
|
209
|
-
this.setStatus("FAILED");
|
|
210
|
-
throw e;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
this.initialized = true;
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
await this.connect();
|
|
217
|
-
if (!this.config.usePolling && this.subscription) {
|
|
218
|
-
this.subscription.removeListener(this.blockListener);
|
|
219
|
-
this.subscription = null;
|
|
220
|
-
this.log.verbose("Removed existing block listener and subscription");
|
|
221
|
-
}
|
|
222
|
-
this.subscription = this.client.subscribeNewBlock
|
|
223
|
-
? this.client.subscribeNewBlock()
|
|
224
|
-
: null;
|
|
225
|
-
const status = await this.client.status();
|
|
226
|
-
this.latestHeight = status.syncInfo.latestBlockHeight;
|
|
227
|
-
this.log.info("Current chain height: " + this.latestHeight);
|
|
228
|
-
this.heightToProcess = await this.config.getNextHeight();
|
|
229
|
-
if (this.config.usePolling) {
|
|
230
|
-
this.pollForBlock();
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
if (this.subscription) {
|
|
234
|
-
this.subscription.addListener(this.blockListener);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
throw new Error("Could not subscribe to new blocks");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
catch (e) {
|
|
242
|
-
this.log.error("Failed to set up block listening: " + e);
|
|
243
|
-
this.setStatus("FAILED");
|
|
244
|
-
throw e;
|
|
245
|
-
}
|
|
246
|
-
this.tryToRecover = false;
|
|
247
|
-
this.fetcher().catch((e) => {
|
|
248
|
-
this.setStatus("FAILED");
|
|
249
|
-
throw new Error("Error in fetching service: " + e);
|
|
250
|
-
});
|
|
251
|
-
const hrTime = process.hrtime();
|
|
252
|
-
let ms = hrTime[0] * 1000000 + hrTime[1] / 1000;
|
|
253
|
-
while (this.blockQueue.size() > 0 && !this.tryToRecover) {
|
|
254
|
-
// await the dequeued promise is essentially awaiting fetched data for that block
|
|
255
|
-
try {
|
|
256
|
-
if (this.tryToRecover) {
|
|
257
|
-
throw new Error("Exiting processing loop. Attempting to recover indexer");
|
|
258
|
-
}
|
|
259
|
-
// Index block inside a db transaction to ensure data consistency
|
|
260
|
-
await this.config.beginTransaction();
|
|
261
|
-
this.log.silly("Started db tx");
|
|
262
|
-
let height, timestamp;
|
|
263
|
-
if (this.isMinimal(this.blockQueue)) {
|
|
264
|
-
const toProcess = await this.blockQueue.dequeue();
|
|
265
|
-
this.log.silly("Retrieved block data");
|
|
266
|
-
if (!toProcess || !toProcess[0] || !toProcess[1]) {
|
|
267
|
-
throw new Error("Could not fetch block");
|
|
268
|
-
}
|
|
269
|
-
height = toProcess[0].block.header.height;
|
|
270
|
-
timestamp = (0, tendermint_rpc_1.toRfc3339WithNanoseconds)(toProcess[0].block.header.time);
|
|
271
|
-
await this.processBlock(toProcess[0], toProcess[1]);
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
const toProcess = await this.blockQueue.dequeue();
|
|
275
|
-
this.log.silly("Retrieved block data");
|
|
276
|
-
if (!toProcess || !toProcess[0] || !toProcess[1] || !toProcess[2]) {
|
|
277
|
-
throw new Error("Could not fetch block");
|
|
278
|
-
}
|
|
279
|
-
this.log.silly("Decoded block");
|
|
280
|
-
height = toProcess[0].block.header.height;
|
|
281
|
-
timestamp = (0, tendermint_rpc_1.toRfc3339WithNanoseconds)(toProcess[0].block.header.time);
|
|
282
|
-
await this.processBlock(toProcess[0], toProcess[1], query_1.QueryValidatorsResponse.decode(toProcess[2]).validators);
|
|
283
|
-
}
|
|
284
|
-
// Emit events to trigger periodic operations every 50, 100 and 1000 blocks
|
|
285
|
-
if (height % 1000 == 0) {
|
|
286
|
-
const hrTime = process.hrtime();
|
|
287
|
-
const newms = hrTime[0] * 1000000 + hrTime[1] / 1000;
|
|
288
|
-
const duration = newms - ms;
|
|
289
|
-
ms = newms;
|
|
290
|
-
const rate = 1000000000 / duration;
|
|
291
|
-
this.log.info("Processing:" + rate.toFixed(2) + "blocks/sec");
|
|
292
|
-
await this.asyncEmit("periodic/1000", {
|
|
293
|
-
value: null,
|
|
294
|
-
height,
|
|
295
|
-
timestamp,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
if (height % 100 == 0) {
|
|
299
|
-
await this.asyncEmit("periodic/100", {
|
|
300
|
-
value: null,
|
|
301
|
-
height,
|
|
302
|
-
timestamp,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
if (height % 50 == 0) {
|
|
306
|
-
await this.asyncEmit("periodic/50", {
|
|
307
|
-
value: null,
|
|
308
|
-
height,
|
|
309
|
-
timestamp,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
this.log.silly("Handled periodic events");
|
|
313
|
-
await this.config.endTransaction(true);
|
|
314
|
-
this.log.silly("Committed db tx");
|
|
315
|
-
}
|
|
316
|
-
catch (e) {
|
|
317
|
-
this.log.error("" + e);
|
|
318
|
-
this.setStatus("FAILED");
|
|
319
|
-
try {
|
|
320
|
-
await this.config.endTransaction(false);
|
|
321
|
-
}
|
|
322
|
-
catch (dbe) {
|
|
323
|
-
this.log.error("Error ending transaction. Must be a DB error: " + dbe);
|
|
324
|
-
}
|
|
325
|
-
this.tryToRecover = true;
|
|
326
|
-
this.retryCount++;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
this.retryCount = 0;
|
|
330
|
-
this.setStatus("OK");
|
|
331
|
-
}
|
|
332
|
-
if (this.retryCount < 3) {
|
|
333
|
-
this.log.debug("Indexer retryCount: " + this.retryCount);
|
|
334
|
-
this.log.info("Indexer is restarting");
|
|
335
|
-
setTimeout(() => this.start(), this.retryCount * 5000);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
this.log.info("Indexer failed too many times. Exiting.");
|
|
339
|
-
process.exit(1);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
async processBlock(block, block_results, validators) {
|
|
343
|
-
let processStart = [0, 0];
|
|
344
|
-
if (this.config.logLevel == "silly") {
|
|
345
|
-
processStart = process.hrtime();
|
|
346
|
-
}
|
|
347
|
-
const height = block.block.header.height;
|
|
348
|
-
this.log.debug("Processing block: %d", height);
|
|
349
|
-
this.log.silly("Started db tx");
|
|
350
|
-
// Initialize height & timestamp to be used for this block-processing run
|
|
351
|
-
const timestamp = (0, tendermint_rpc_1.toRfc3339WithNanoseconds)(block.block.header.time);
|
|
352
|
-
// Use & await asyncEmit to ensure db insertions in order
|
|
353
|
-
/*
|
|
354
|
-
* Emit block information to any interested modules.
|
|
355
|
-
* Primarily the required block module listens to this
|
|
356
|
-
*/
|
|
357
|
-
await this.asyncEmit("block", {
|
|
358
|
-
value: {
|
|
359
|
-
block,
|
|
360
|
-
block_results,
|
|
361
|
-
},
|
|
362
|
-
height,
|
|
363
|
-
timestamp,
|
|
364
|
-
});
|
|
365
|
-
this.log.silly("Modules handled block event");
|
|
366
|
-
// Deal with begin_block events first
|
|
367
|
-
await this.asyncEmit("begin_block", {
|
|
368
|
-
value: {
|
|
369
|
-
events: block_results.beginBlockEvents,
|
|
370
|
-
validators,
|
|
371
|
-
},
|
|
372
|
-
height,
|
|
373
|
-
timestamp,
|
|
374
|
-
});
|
|
375
|
-
this.log.silly("Modules handled begin_block events");
|
|
376
|
-
// Then individual tx_events
|
|
377
|
-
await this.asyncEmit("tx_events", {
|
|
378
|
-
value: block_results.results,
|
|
379
|
-
height,
|
|
380
|
-
timestamp,
|
|
381
|
-
});
|
|
382
|
-
this.log.silly("Modules handled tx events");
|
|
383
|
-
// Emit details and result for each tx msg separately
|
|
384
|
-
for (let t = 0; t < block.block.txs.length; t++) {
|
|
385
|
-
const tx = tx_2.Tx.decode(block.block.txs[t]);
|
|
386
|
-
const result = block_results.results[t].code;
|
|
387
|
-
const txlog = block_results.results[t].log;
|
|
388
|
-
if (result != 0) {
|
|
389
|
-
// Tx failed. Ignore
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if (tx.body && tx.body.memo != "") {
|
|
393
|
-
const txHash = (0, node_crypto_1.createHash)("sha256").update(block.block.txs[t])
|
|
394
|
-
.digest("hex");
|
|
395
|
-
await this.asyncEmit("tx_memo", {
|
|
396
|
-
value: {
|
|
397
|
-
txHash,
|
|
398
|
-
txBody: tx.body,
|
|
399
|
-
},
|
|
400
|
-
height,
|
|
401
|
-
timestamp,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
// parsing log rather than using events directly in order to have msg_index available to filter appropriate events for each msg
|
|
405
|
-
const events = txlog
|
|
406
|
-
? JSON.parse(txlog)
|
|
407
|
-
: [];
|
|
408
|
-
const msgs = tx.body?.messages;
|
|
409
|
-
if (msgs) {
|
|
410
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
411
|
-
this.log.silly("Indexer broadcasting msg for handling: " + msgs[i].typeUrl);
|
|
412
|
-
const msgevents = msgs.length > 1
|
|
413
|
-
? events.find(x => x.msg_index == i)?.events
|
|
414
|
-
: events[0].events;
|
|
415
|
-
await this.asyncEmit(msgs[i].typeUrl, {
|
|
416
|
-
value: {
|
|
417
|
-
tx: msgs[i].value,
|
|
418
|
-
events: msgevents,
|
|
419
|
-
},
|
|
420
|
-
height,
|
|
421
|
-
timestamp,
|
|
422
|
-
});
|
|
423
|
-
if (msgs[i].typeUrl == "/cosmos.authz.v1beta1.MsgExec") {
|
|
424
|
-
const authzMsgs = tx_1.MsgExec.decode(msgs[i].value).msgs;
|
|
425
|
-
if (authzMsgs) {
|
|
426
|
-
for (let r = 0; r < authzMsgs.length; r++) {
|
|
427
|
-
this.log.silly("Indexer broadcasting msg for handling: " + authzMsgs[r].typeUrl);
|
|
428
|
-
const authzMsgEvents = msgevents?.reduce((events, evt) => {
|
|
429
|
-
if (evt.attributes.filter(x => (0, utils_1.decodeAttr)(x.key) == "authz_msg_index" && (0, utils_1.decodeAttr)(x.value) == "" + r).length > 0) {
|
|
430
|
-
events.push(evt);
|
|
431
|
-
}
|
|
432
|
-
return events;
|
|
433
|
-
}, []);
|
|
434
|
-
await this.asyncEmit(authzMsgs[r].typeUrl, {
|
|
435
|
-
value: {
|
|
436
|
-
tx: authzMsgs[r].value,
|
|
437
|
-
events: authzMsgEvents,
|
|
438
|
-
},
|
|
439
|
-
height,
|
|
440
|
-
timestamp,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
this.log.silly("Modules handled msg events");
|
|
449
|
-
// Then deal with end_block events
|
|
450
|
-
await this.asyncEmit("end_block", {
|
|
451
|
-
value: block_results.endBlockEvents,
|
|
452
|
-
height,
|
|
453
|
-
timestamp,
|
|
454
|
-
});
|
|
455
|
-
this.log.silly("Modules handled end_block events");
|
|
456
|
-
if (this.config.logLevel == "silly") {
|
|
457
|
-
const processEnd = process.hrtime(processStart);
|
|
458
|
-
const processTime = processEnd[0] * 1000 + processEnd[1] / 1000000;
|
|
459
|
-
this.log.silly("Processed block %d in %d ms", height, processTime.toFixed(2));
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
async fetcher() {
|
|
463
|
-
for (let i = this.heightToProcess; i <= this.latestHeight; i++) {
|
|
464
|
-
this.log.debug("Fetching: " + i);
|
|
465
|
-
if (this.tryToRecover) {
|
|
466
|
-
this.log.verbose("Exiting fetcher loop. Attempting to recover indexer");
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
try {
|
|
470
|
-
if (this.isMinimal(this.blockQueue)) {
|
|
471
|
-
const timeoutPromise = new Promise((resolve, reject) => {
|
|
472
|
-
setTimeout(reject, 20000, false);
|
|
473
|
-
});
|
|
474
|
-
const toIndex = Promise.race([Promise.all([this.blockClient.block(i), this.blockClient.blockResults(i)]), timeoutPromise]).catch((e) => {
|
|
475
|
-
this.log.error("Error fetching block: " + i + " : " + e);
|
|
476
|
-
this.tryToRecover = true;
|
|
477
|
-
return Promise.resolve([]);
|
|
478
|
-
});
|
|
479
|
-
this.blockQueue.enqueue(toIndex);
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
const timeoutPromise = new Promise((resolve, reject) => {
|
|
483
|
-
setTimeout(reject, 20000, false);
|
|
484
|
-
});
|
|
485
|
-
const q = query_1.QueryValidatorsRequest.fromPartial({
|
|
486
|
-
pagination: {
|
|
487
|
-
limit: 1000n,
|
|
488
|
-
},
|
|
489
|
-
});
|
|
490
|
-
const vals = query_1.QueryValidatorsRequest.encode(q).finish();
|
|
491
|
-
const toIndex = Promise.race([
|
|
492
|
-
Promise.all([
|
|
493
|
-
this.blockClient.block(i),
|
|
494
|
-
this.blockClient.blockResults(i),
|
|
495
|
-
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, i, false),
|
|
496
|
-
]),
|
|
497
|
-
timeoutPromise,
|
|
498
|
-
]).catch((e) => {
|
|
499
|
-
this.log.error("Error fetching block: " + i + " : " + e);
|
|
500
|
-
this.tryToRecover = true;
|
|
501
|
-
return Promise.resolve([]);
|
|
502
|
-
});
|
|
503
|
-
this.blockQueue.enqueue(toIndex);
|
|
504
|
-
}
|
|
505
|
-
await this.blockQueue.continue();
|
|
506
|
-
if (this.tryToRecover) {
|
|
507
|
-
this.retryCount++;
|
|
508
|
-
throw new Error("RPC not responding");
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
catch (e) {
|
|
512
|
-
this.log.error(e);
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
if (!this.tryToRecover) {
|
|
517
|
-
this.blockQueue.setSynced();
|
|
518
|
-
this.log.info("Synced to latest height");
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
async callABCI(path, data, height, adHoc = true) {
|
|
522
|
-
try {
|
|
523
|
-
const abciq = await (adHoc
|
|
524
|
-
? this.client
|
|
525
|
-
: this.blockClient).abciQuery({
|
|
526
|
-
path,
|
|
527
|
-
data,
|
|
528
|
-
height: height,
|
|
529
|
-
});
|
|
530
|
-
if (abciq) {
|
|
531
|
-
return abciq.value;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
this.tryToRecover = true;
|
|
535
|
-
this.setStatus("FAILED");
|
|
536
|
-
throw new Error("RPC not responding. Query at: " + path);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
catch (_e) {
|
|
540
|
-
this.tryToRecover = true;
|
|
541
|
-
this.setStatus("FAILED");
|
|
542
|
-
this.retryCount++;
|
|
543
|
-
throw new Error("RPC not responding. Query at: " + path);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
newBlockReceived(height) {
|
|
547
|
-
this.log.info("Received new block: %d", height);
|
|
548
|
-
// If we are synced, add to end of queue
|
|
549
|
-
if (this.blockQueue.synced && !this.tryToRecover) {
|
|
550
|
-
if (this.blockQueue.size() + 1 == this.config.batchSize) {
|
|
551
|
-
this.log.error("Block queue is full. Cannot add new block");
|
|
552
|
-
this.tryToRecover = true;
|
|
553
|
-
this.retryCount++;
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
try {
|
|
557
|
-
if (this.isMinimal(this.blockQueue)) {
|
|
558
|
-
this.blockQueue.enqueue(Promise.all([this.client.block(height), this.client.blockResults(height)]).catch((e) => {
|
|
559
|
-
this.log.error("Error fetching block: " + height + " : " + e);
|
|
560
|
-
return Promise.resolve([]);
|
|
561
|
-
}));
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
const q = query_1.QueryValidatorsRequest.fromPartial({
|
|
565
|
-
pagination: {
|
|
566
|
-
limit: 1000n,
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
const vals = query_1.QueryValidatorsRequest.encode(q).finish();
|
|
570
|
-
this.blockQueue.enqueue(Promise.all([
|
|
571
|
-
this.client.block(height),
|
|
572
|
-
this.client.blockResults(height),
|
|
573
|
-
this.callABCI("/cosmos.staking.v1beta1.Query/Validators", vals, height, false),
|
|
574
|
-
]).catch((e) => {
|
|
575
|
-
this.log.error("Error fetching block: " + height + " : " + e);
|
|
576
|
-
return Promise.resolve([]);
|
|
577
|
-
}));
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
catch (e) {
|
|
581
|
-
this.log.error("" + e);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
this.latestHeight = height;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
async pollForBlock() {
|
|
589
|
-
const status = await this.client.status();
|
|
590
|
-
if (status.syncInfo.latestBlockHeight > this.latestHeight) {
|
|
591
|
-
while (this.latestHeight < status.syncInfo.latestBlockHeight) {
|
|
592
|
-
this.newBlockReceived(this.latestHeight + 1);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
setTimeout(() => {
|
|
596
|
-
this.pollForBlock();
|
|
597
|
-
}, this.config.pollingInterval);
|
|
598
|
-
}
|
|
599
|
-
readGenesis() {
|
|
600
|
-
if (this.config.genesisPath) {
|
|
601
|
-
return node_fs_1.default.createReadStream(this.config.genesisPath).pipe((0, stream_json_1.parser)());
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
throw new Error("Genesis path not set");
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async setArrayReader(path, processor) {
|
|
608
|
-
const readPromise = new Promise((resolve, reject) => {
|
|
609
|
-
try {
|
|
610
|
-
const filters = path.split(".");
|
|
611
|
-
const pickers = filters.map(filter => (0, Pick_1.pick)({
|
|
612
|
-
filter,
|
|
613
|
-
}));
|
|
614
|
-
let counter = 0;
|
|
615
|
-
(0, stream_chain_1.chain)([
|
|
616
|
-
this.readGenesis(),
|
|
617
|
-
...pickers,
|
|
618
|
-
(0, StreamArray_1.streamArray)(),
|
|
619
|
-
(0, Batch_1.batch)({
|
|
620
|
-
batchSize: 1000,
|
|
621
|
-
}),
|
|
622
|
-
processor,
|
|
623
|
-
])
|
|
624
|
-
.on("data", (data) => {
|
|
625
|
-
if (data && Array.isArray(data)) {
|
|
626
|
-
counter = counter + data.length;
|
|
627
|
-
}
|
|
628
|
-
})
|
|
629
|
-
.on("end", () => {
|
|
630
|
-
this.log.info(`Processed ${counter} entries`);
|
|
631
|
-
resolve(true);
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
catch (_e) {
|
|
635
|
-
this.log.verbose("Error in setArrayReader: " + _e);
|
|
636
|
-
reject();
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
return readPromise;
|
|
640
|
-
}
|
|
641
|
-
async setValueReader(path, processor) {
|
|
642
|
-
const readPromise = new Promise((resolve, reject) => {
|
|
643
|
-
try {
|
|
644
|
-
const filters = path.split(".");
|
|
645
|
-
const pickers = filters.map(filter => (0, Pick_1.pick)({
|
|
646
|
-
filter,
|
|
647
|
-
}));
|
|
648
|
-
let counter = 0;
|
|
649
|
-
(0, stream_chain_1.chain)([this.readGenesis(), ...pickers, (0, StreamValues_1.streamValues)(), processor])
|
|
650
|
-
.on("data", (_data) => {
|
|
651
|
-
counter++;
|
|
652
|
-
})
|
|
653
|
-
.on("end", () => {
|
|
654
|
-
this.log.info(`Processed ${counter} entries`);
|
|
655
|
-
resolve(true);
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
catch (_e) {
|
|
659
|
-
reject();
|
|
660
|
-
}
|
|
661
|
-
});
|
|
662
|
-
return readPromise;
|
|
663
|
-
}
|
|
664
|
-
async parseGenesis() {
|
|
665
|
-
this.log.info("Parsing genesis");
|
|
666
|
-
await this.config.beginTransaction();
|
|
667
|
-
try {
|
|
668
|
-
this.log.info("Starting genesis import");
|
|
669
|
-
this.log.debug("Importing genesis file...");
|
|
670
|
-
for (const [key, _value] of this.handled) {
|
|
671
|
-
if (key.startsWith("genesis/")) {
|
|
672
|
-
const genesisEntry = key.split("/");
|
|
673
|
-
this.log.verbose("Importing " + key + "...");
|
|
674
|
-
if (genesisEntry[1] == "array") {
|
|
675
|
-
await this.setArrayReader(genesisEntry[2],
|
|
676
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
677
|
-
async (data) => {
|
|
678
|
-
await this.asyncEmit(key, {
|
|
679
|
-
value: data.map((x) => x.value),
|
|
680
|
-
});
|
|
681
|
-
return data;
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
await this.setValueReader(genesisEntry[2],
|
|
686
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
687
|
-
async (data) => {
|
|
688
|
-
await this.asyncEmit(key, {
|
|
689
|
-
value: data.value,
|
|
690
|
-
});
|
|
691
|
-
return data;
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
this.log.info("Importing gen TXs...");
|
|
697
|
-
await this.setArrayReader("app_state.genutil.gen_txs",
|
|
698
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
699
|
-
async (data) => {
|
|
700
|
-
for (let j = 0; j < data.length; j++) {
|
|
701
|
-
const gentx = data[j].value;
|
|
702
|
-
for (let i = 0; i < gentx.body.messages.length; i++) {
|
|
703
|
-
const msg = gentx.body.messages[i];
|
|
704
|
-
await this.asyncEmit(("gentx" + msg["@type"]), {
|
|
705
|
-
value: msg,
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return data;
|
|
710
|
-
});
|
|
711
|
-
await this.config.endTransaction(true);
|
|
712
|
-
this.log.info("Finished importing");
|
|
713
|
-
}
|
|
714
|
-
catch (e) {
|
|
715
|
-
try {
|
|
716
|
-
await this.config.endTransaction(false);
|
|
717
|
-
}
|
|
718
|
-
catch (dbe) {
|
|
719
|
-
this.log.error("Error ending transaction. Must be a DB error: " + dbe);
|
|
720
|
-
}
|
|
721
|
-
this.log.error("Failed to import genesis");
|
|
722
|
-
throw e;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
exports.EcleciaIndexer = EcleciaIndexer;
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
export { EcleciaIndexer, defaultIndexerConfig };
|
|
553
|
+
//# sourceMappingURL=index.js.map
|