envio 3.7.0 → 3.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.
Files changed (43) hide show
  1. package/index.d.ts +331 -142
  2. package/package.json +6 -6
  3. package/src/ChainState.res +9 -1
  4. package/src/ChainState.res.mjs +6 -1
  5. package/src/Config.res +19 -10
  6. package/src/Config.res.mjs +17 -7
  7. package/src/Envio.res +54 -76
  8. package/src/EventConfigBuilder.res +159 -30
  9. package/src/EventConfigBuilder.res.mjs +120 -24
  10. package/src/HandlerRegister.res +20 -11
  11. package/src/HandlerRegister.res.mjs +22 -5
  12. package/src/Hasura.res +30 -22
  13. package/src/Hasura.res.mjs +11 -5
  14. package/src/InMemoryStore.res +1 -6
  15. package/src/InMemoryStore.res.mjs +1 -3
  16. package/src/Internal.res +33 -13
  17. package/src/Internal.res.mjs +12 -16
  18. package/src/Main.res +2 -5
  19. package/src/MemoryStorage.res +40 -8
  20. package/src/MemoryStorage.res.mjs +38 -16
  21. package/src/Persistence.res +4 -2
  22. package/src/PgStorage.res +6 -1
  23. package/src/PgStorage.res.mjs +1 -1
  24. package/src/SimulateItems.res +261 -9
  25. package/src/SimulateItems.res.mjs +218 -47
  26. package/src/TestIndexer.res +26 -10
  27. package/src/TestIndexer.res.mjs +34 -12
  28. package/src/bindings/ClickHouse.res +33 -6
  29. package/src/bindings/ClickHouse.res.mjs +25 -4
  30. package/src/db/InternalTable.res +11 -0
  31. package/src/db/InternalTable.res.mjs +7 -0
  32. package/src/sources/EvmHyperSyncSource.res +3 -2
  33. package/src/sources/SimulateSource.res +8 -4
  34. package/src/sources/SimulateSource.res.mjs +7 -3
  35. package/src/sources/Svm.res +34 -7
  36. package/src/sources/Svm.res.mjs +32 -4
  37. package/src/sources/SvmHyperSyncClient.res +10 -12
  38. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  39. package/src/sources/SvmHyperSyncSource.res +91 -34
  40. package/src/sources/SvmHyperSyncSource.res.mjs +77 -37
  41. package/src/sources/TransactionStore.res +38 -0
  42. package/src/sources/TransactionStore.res.mjs +5 -0
  43. package/svm.schema.json +27 -78
@@ -5,11 +5,14 @@ import * as Config from "./Config.res.mjs";
5
5
  import * as Address from "./Address.res.mjs";
6
6
  import * as ChainId from "./ChainId.res.mjs";
7
7
  import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as BlockStore from "./sources/BlockStore.res.mjs";
8
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
10
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
11
  import * as HandlerRegister from "./HandlerRegister.res.mjs";
11
12
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
12
13
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
14
+ import * as TransactionStore from "./sources/TransactionStore.res.mjs";
15
+ import * as SvmHyperSyncSource from "./sources/SvmHyperSyncSource.res.mjs";
13
16
 
14
17
  let evmSimulateBlockSchema = S$RescriptSchema.schema(s => ({
15
18
  number: s.m(S$RescriptSchema.Option.getOr(S$RescriptSchema.$$null(S$RescriptSchema.int), 0)),
@@ -168,6 +171,16 @@ function deriveSrcAddress(providedSrcAddress, eventConfig, chainConfig, config)
168
171
  }
169
172
  }
170
173
 
174
+ function liveRegistrationsFor(config, chainId, eventConfig) {
175
+ return HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig).filter(reg => {
176
+ if (Stdlib_Option.isSome(reg.handler) || Stdlib_Option.isSome(reg.contractRegister)) {
177
+ return !HandlerRegister.isDroppedByWhere(config, reg);
178
+ } else {
179
+ return false;
180
+ }
181
+ });
182
+ }
183
+
171
184
  function parse(simulateItems, config, chainConfig, onEventRegistrations) {
172
185
  let chainId = chainConfig.id;
173
186
  let startBlock = chainConfig.startBlock;
@@ -178,21 +191,161 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
178
191
  contents: 0
179
192
  };
180
193
  let items = [];
194
+ let svmTxs = [];
195
+ let svmActivities = [];
196
+ let svmBlocks = [];
181
197
  let seenCoordinates = {};
182
198
  simulateItems.forEach((rawJson, itemIndex) => {
183
- let match = rawJson.contract;
184
- let match$1 = rawJson.event;
185
- if (match === undefined) {
186
- return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
187
- }
188
- if (match$1 === undefined) {
189
- return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
199
+ let match = config.ecosystem.name;
200
+ let match$1 = rawJson.program;
201
+ let match$2 = rawJson.instruction;
202
+ switch (match) {
203
+ case "evm" :
204
+ case "fuel" :
205
+ break;
206
+ case "svm" :
207
+ if (match$1 === undefined) {
208
+ return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "program" and "instruction" fields.`);
209
+ }
210
+ if (match$2 === undefined) {
211
+ return Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "program" and "instruction" fields.`);
212
+ }
213
+ let ec = findEventConfig(config, match$1, match$2);
214
+ let eventConfig = ec !== undefined ? ec : Stdlib_JsError.throwWithMessage(`simulate: Instruction "` + match$2 + `" not found on program "` + match$1 + `". Check that the program and instruction names match your config.yaml.`);
215
+ let blockJson = rawJson.block;
216
+ let s = rawJson.slot;
217
+ let slot;
218
+ if (s !== undefined) {
219
+ slot = s;
220
+ } else if (blockJson == null) {
221
+ slot = currentBlock.contents;
222
+ } else {
223
+ let v = blockJson["slot"];
224
+ slot = v !== undefined ? Stdlib_Option.getOr((v == null) ? undefined : Primitive_option.some(v), currentBlock.contents) : currentBlock.contents;
225
+ }
226
+ currentBlock.contents = slot;
227
+ let tx = rawJson.transaction;
228
+ let transaction = tx !== undefined ? Primitive_option.valFromOption(tx) : ({});
229
+ let transactionIndex = Stdlib_Option.getOr(transaction.transactionIndex, 0);
230
+ let path = Stdlib_Option.getOr(rawJson.path, [0]);
231
+ let programId = Stdlib_Option.getOr(rawJson.programId, eventConfig.programId);
232
+ let args = rawJson.accountArguments;
233
+ let accountArguments;
234
+ if (args !== undefined) {
235
+ accountArguments = args;
236
+ } else {
237
+ let named = rawJson.accounts;
238
+ accountArguments = named !== undefined ? eventConfig.accounts.map(name => {
239
+ let match = named[name];
240
+ if (match !== undefined) {
241
+ return match.address;
242
+ } else {
243
+ return "";
244
+ }
245
+ }) : [];
246
+ }
247
+ let data = Stdlib_Option.getOr(rawJson.data, Stdlib_Option.getOr(eventConfig.discriminator, "0x"));
248
+ let decoded = Stdlib_Option.map(rawJson.args, args => ({
249
+ name: match$2,
250
+ argsJson: JSON.stringify(args),
251
+ accountsJson: "{}",
252
+ extraAccounts: []
253
+ }));
254
+ let logs = Stdlib_Option.map(rawJson.logs, logs => logs.map(log => ({
255
+ kind: log.kind,
256
+ message: log.message
257
+ })));
258
+ let liveRegistrations = liveRegistrationsFor(config, chainId, eventConfig);
259
+ if (Utils.$$Array.isEmpty(liveRegistrations)) {
260
+ Stdlib_JsError.throwWithMessage(`simulate: no handler runs for instruction "` + match$2 + `" on program "` + match$1 + `". Register a handler with indexer.onInstruction before simulating it.`);
261
+ }
262
+ svmTxs.push({
263
+ slot: slot,
264
+ transactionIndex: transactionIndex,
265
+ signature: transaction.signature,
266
+ allSignatures: transaction.allSignatures,
267
+ feePayer: transaction.feePayer,
268
+ success: transaction.success,
269
+ err: transaction.err,
270
+ fee: transaction.fee,
271
+ computeUnitsConsumed: transaction.computeUnitsConsumed,
272
+ accountKeys: transaction.accountKeys,
273
+ recentBlockhash: transaction.recentBlockhash,
274
+ version: transaction.version
275
+ });
276
+ let rows = transaction.accountActivities;
277
+ if (rows !== undefined) {
278
+ rows.forEach(activity => {
279
+ svmActivities.push({
280
+ slot: slot,
281
+ transactionIndex: transactionIndex,
282
+ account: activity.address,
283
+ accountIndex: activity.transactionAccountIndex,
284
+ isSigner: activity.isSigner,
285
+ isWritable: activity.isWritable,
286
+ preBalance: Stdlib_Option.flatMap(activity.lamports, l => l.pre),
287
+ postBalance: Stdlib_Option.flatMap(activity.lamports, l => l.post),
288
+ mint: Stdlib_Option.flatMap(activity.token, t => t.mint),
289
+ owner: Stdlib_Option.flatMap(activity.token, t => t.owner),
290
+ decimals: Stdlib_Option.flatMap(activity.token, t => t.decimals),
291
+ preAmount: Stdlib_Option.flatMap(activity.token, t => t.preAmount),
292
+ postAmount: Stdlib_Option.flatMap(activity.token, t => t.postAmount)
293
+ });
294
+ });
295
+ }
296
+ let block = rawJson.block;
297
+ let blockTime = block !== undefined ? block.time : undefined;
298
+ let block$1 = rawJson.block;
299
+ let blockHash = block$1 !== undefined ? block$1.hash : undefined;
300
+ svmBlocks.push({
301
+ blockNumber: slot,
302
+ blockHash: blockHash,
303
+ blockTimestamp: blockTime
304
+ });
305
+ liveRegistrations.forEach(reg => {
306
+ let onEventRegistrationIndex = onEventRegistrations.length;
307
+ let newrecord = {...reg};
308
+ newrecord.index = onEventRegistrationIndex;
309
+ onEventRegistrations.push(newrecord);
310
+ let payload = SvmHyperSyncSource.toSvmInstruction({
311
+ onEventRegistrationIndex: onEventRegistrationIndex,
312
+ slot: slot,
313
+ transactionIndex: transactionIndex,
314
+ path: path,
315
+ programId: programId,
316
+ accounts: accountArguments,
317
+ data: data,
318
+ isInner: Stdlib_Option.getOr(rawJson.isInner, false),
319
+ decoded: decoded,
320
+ logs: logs
321
+ }, match$1, match$2, eventConfig, newrecord.fieldSelection);
322
+ payload["srcAddress"] = programId;
323
+ items.push({
324
+ kind: 0,
325
+ onEventRegistration: newrecord,
326
+ chainId: chainId,
327
+ blockNumber: slot,
328
+ logIndex: transactionIndex,
329
+ orderPath: path,
330
+ transactionIndex: transactionIndex,
331
+ payload: payload
332
+ });
333
+ });
334
+ return;
190
335
  }
191
- let ec = findEventConfig(config, match, match$1);
192
- let eventConfig = ec !== undefined ? ec : Stdlib_JsError.throwWithMessage(`simulate: Event "` + match$1 + `" not found on contract "` + match + `". Check that the contract and event names match your config.yaml.`);
336
+ let match$3 = rawJson.contract;
337
+ let match$4 = rawJson.event;
338
+ let match$5 = match$3 !== undefined && match$4 !== undefined ? [
339
+ match$3,
340
+ match$4
341
+ ] : Stdlib_JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`);
342
+ let eventName = match$5[1];
343
+ let contractName = match$5[0];
344
+ let ec$1 = findEventConfig(config, contractName, eventName);
345
+ let eventConfig$1 = ec$1 !== undefined ? ec$1 : Stdlib_JsError.throwWithMessage(`simulate: Event "` + eventName + `" not found on contract "` + contractName + `". Check that the contract and event names match your config.yaml.`);
193
346
  let json = rawJson.params;
194
347
  let paramsJson = json !== undefined ? json : ({});
195
- let params = S$RescriptSchema.convertOrThrow(paramsJson, eventConfig.simulateParamsSchema);
348
+ let params = S$RescriptSchema.convertOrThrow(paramsJson, eventConfig$1.simulateParamsSchema);
196
349
  let li = rawJson.logIndex;
197
350
  let logIndex;
198
351
  if (li !== undefined) {
@@ -205,45 +358,45 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
205
358
  currentLogIndex.contents = li$1 + 1 | 0;
206
359
  logIndex = li$1;
207
360
  }
208
- let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig, chainConfig, config);
209
- let blockJson = rawJson.block;
210
- let blockJson$1 = (blockJson == null) ? undefined : Primitive_option.some(blockJson);
361
+ let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig$1, chainConfig, config);
362
+ let blockJson$1 = rawJson.block;
363
+ let blockJson$2 = (blockJson$1 == null) ? undefined : Primitive_option.some(blockJson$1);
211
364
  let transactionJson = rawJson.transaction;
212
365
  let transactionJson$1 = (transactionJson == null) ? undefined : Primitive_option.some(transactionJson);
213
- let match$2 = config.ecosystem.name;
214
- let match$3;
215
- switch (match$2) {
366
+ let match$6 = config.ecosystem.name;
367
+ let match$7;
368
+ switch (match$6) {
216
369
  case "evm" :
217
- let block = parseEvmSimulateBlock(currentBlock.contents, blockJson$1);
218
- match$3 = [
219
- block,
220
- block.number
370
+ let block$2 = parseEvmSimulateBlock(currentBlock.contents, blockJson$2);
371
+ match$7 = [
372
+ block$2,
373
+ block$2.number
221
374
  ];
222
375
  break;
223
376
  case "fuel" :
224
- let block$1 = parseFuelSimulateBlock(currentBlock.contents, blockJson$1);
225
- match$3 = [
226
- block$1,
227
- block$1.height
377
+ let block$3 = parseFuelSimulateBlock(currentBlock.contents, blockJson$2);
378
+ match$7 = [
379
+ block$3,
380
+ block$3.height
228
381
  ];
229
382
  break;
230
383
  case "svm" :
231
- match$3 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
384
+ match$7 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
232
385
  break;
233
386
  }
234
- let blockNumber = match$3[1];
235
- let block$2 = match$3[0];
236
- let match$4 = config.ecosystem.name;
237
- let transaction;
238
- switch (match$4) {
387
+ let blockNumber = match$7[1];
388
+ let block$4 = match$7[0];
389
+ let match$8 = config.ecosystem.name;
390
+ let transaction$1;
391
+ switch (match$8) {
239
392
  case "evm" :
240
- transaction = parseEvmSimulateTransaction(transactionJson$1);
393
+ transaction$1 = parseEvmSimulateTransaction(transactionJson$1);
241
394
  break;
242
395
  case "fuel" :
243
- transaction = parseFuelSimulateTransaction(transactionJson$1);
396
+ transaction$1 = parseFuelSimulateTransaction(transactionJson$1);
244
397
  break;
245
398
  case "svm" :
246
- transaction = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
399
+ transaction$1 = Stdlib_JsError.throwWithMessage("simulate is not supported for SVM ecosystem");
247
400
  break;
248
401
  }
249
402
  currentBlock.contents = blockNumber;
@@ -254,20 +407,20 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
254
407
  } else {
255
408
  seenCoordinates[coordinate] = itemIndex;
256
409
  }
257
- let liveRegistrations = HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig).filter(reg => {
410
+ let liveRegistrations$1 = HandlerRegister.getSimulateOnEventRegistrations(config, chainId, eventConfig$1).filter(reg => {
258
411
  if (Stdlib_Option.isSome(reg.handler) || Stdlib_Option.isSome(reg.contractRegister)) {
259
412
  return !HandlerRegister.isDroppedByWhere(config, reg);
260
413
  } else {
261
414
  return false;
262
415
  }
263
416
  });
264
- if (Utils.$$Array.isEmpty(liveRegistrations)) {
265
- let match$5 = ChainMap.values(config.chainMap).length;
266
- Stdlib_JsError.throwWithMessage(`simulate: no handler runs for event "` + match$1 + `" on contract "` + match + `"` + (
267
- match$5 !== 1 ? ` on chain ` + ChainId.toString(chainId) : ""
417
+ if (Utils.$$Array.isEmpty(liveRegistrations$1)) {
418
+ let match$9 = ChainMap.values(config.chainMap).length;
419
+ Stdlib_JsError.throwWithMessage(`simulate: no handler runs for event "` + eventName + `" on contract "` + contractName + `"` + (
420
+ match$9 !== 1 ? ` on chain ` + ChainId.toString(chainId) : ""
268
421
  ) + `. Register a handler with indexer.onEvent (and check any \`where\` filter isn't excluding this chain) before simulating it.`);
269
422
  }
270
- liveRegistrations.forEach(reg => {
423
+ liveRegistrations$1.forEach(reg => {
271
424
  let onEventRegistrationIndex = onEventRegistrations.length;
272
425
  let newrecord = {...reg};
273
426
  newrecord.index = onEventRegistrationIndex;
@@ -280,19 +433,34 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
280
433
  logIndex: logIndex,
281
434
  transactionIndex: 0,
282
435
  payload: {
283
- contractName: eventConfig.contractName,
284
- eventName: eventConfig.name,
436
+ contractName: eventConfig$1.contractName,
437
+ eventName: eventConfig$1.name,
285
438
  params: params,
286
439
  chainId: chainId,
287
440
  srcAddress: srcAddress,
288
441
  logIndex: logIndex,
289
- transaction: Primitive_option.some(transaction),
290
- block: Primitive_option.some(block$2)
442
+ transaction: Primitive_option.some(transaction$1),
443
+ block: Primitive_option.some(block$4)
291
444
  }
292
445
  });
293
446
  });
294
447
  });
295
- return items;
448
+ let match = config.ecosystem.name;
449
+ switch (match) {
450
+ case "evm" :
451
+ case "fuel" :
452
+ return {
453
+ items: items,
454
+ transactionStore: undefined,
455
+ blockStore: undefined
456
+ };
457
+ case "svm" :
458
+ return {
459
+ items: items,
460
+ transactionStore: Primitive_option.some(TransactionStore.fromSvmJs(svmTxs, svmActivities)),
461
+ blockStore: Primitive_option.some(BlockStore.fromJs(svmBlocks, "svm", false))
462
+ };
463
+ }
296
464
  }
297
465
 
298
466
  function patchConfig(config, processConfig, registrationsByChainId) {
@@ -329,12 +497,14 @@ function patchConfig(config, processConfig, registrationsByChainId) {
329
497
  let newrecord = {...chainConfig};
330
498
  newrecord.endBlock = endBlock;
331
499
  newrecord.startBlock = startBlock;
332
- let items = parse(simulateRaw, config, newrecord, chainRegistrations.onEventRegistrations);
500
+ let match = parse(simulateRaw, config, newrecord, chainRegistrations.onEventRegistrations);
333
501
  let newrecord$1 = {...newrecord};
334
502
  newrecord$1.sourceConfig = {
335
503
  TAG: "SimulateSourceConfig",
336
- items: items,
337
- endBlock: endBlock
504
+ items: match.items,
505
+ endBlock: endBlock,
506
+ transactionStore: match.transactionStore,
507
+ blockStore: match.blockStore
338
508
  };
339
509
  return newrecord$1;
340
510
  });
@@ -356,6 +526,7 @@ export {
356
526
  dummySrcAddress,
357
527
  firstContractAddress,
358
528
  deriveSrcAddress,
529
+ liveRegistrationsFor,
359
530
  parse,
360
531
  patchConfig,
361
532
  }
@@ -10,6 +10,12 @@ type fuelChainConfig = {
10
10
  simulate?: array<Envio.fuelSimulateItem>,
11
11
  }
12
12
 
13
+ type svmChainConfig = {
14
+ startBlock?: int,
15
+ endBlock?: int,
16
+ simulate?: array<Envio.svmSimulateItem>,
17
+ }
18
+
13
19
  // Internal type used for block range validation and state management
14
20
  type chainConfig = {
15
21
  startBlock: int,
@@ -314,22 +320,32 @@ let getSimulateEndBlock = (
314
320
  ~startBlock: int,
315
321
  ): int => {
316
322
  let maxBlock = ref(startBlock)
323
+ let blockNumberKey = switch config.ecosystem.name {
324
+ | Svm => "slot"
325
+ | _ => config.ecosystem.blockNumberName
326
+ }
327
+ let bump = (n: option<int>) =>
328
+ switch n {
329
+ | Some(v) if v > maxBlock.contents => maxBlock := v
330
+ | _ => ()
331
+ }
332
+ let getInt = (d: dict<JSON.t>, key) =>
333
+ d
334
+ ->Dict.get(key)
335
+ ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
317
336
  simulateItems->Array.forEach(rawJson => {
337
+ let itemDict = rawJson->(Utils.magic: JSON.t => dict<JSON.t>)
338
+ // SVM items carry the slot at the top level (`block.slot` is the override).
339
+ switch config.ecosystem.name {
340
+ | Svm => itemDict->getInt("slot")->bump
341
+ | _ => ()
342
+ }
318
343
  let blockJson: option<JSON.t> =
319
344
  (rawJson->(Utils.magic: JSON.t => {..}))["block"]
320
345
  ->(Utils.magic: 'a => Nullable.t<JSON.t>)
321
346
  ->Nullable.toOption
322
347
  switch blockJson {
323
- | Some(bj) =>
324
- let blockDict = bj->(Utils.magic: JSON.t => dict<JSON.t>)
325
- let n: option<int> =
326
- blockDict
327
- ->Dict.get(config.ecosystem.blockNumberName)
328
- ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
329
- switch n {
330
- | Some(v) if v > maxBlock.contents => maxBlock := v
331
- | _ => ()
332
- }
348
+ | Some(bj) => bj->(Utils.magic: JSON.t => dict<JSON.t>)->getInt(blockNumberKey)->bump
333
349
  | None => ()
334
350
  }
335
351
  })
@@ -230,22 +230,44 @@ function getSimulateEndBlock(simulateItems, config, startBlock) {
230
230
  let maxBlock = {
231
231
  contents: startBlock
232
232
  };
233
- simulateItems.forEach(rawJson => {
234
- let blockJson = rawJson.block;
235
- if (blockJson == null) {
236
- return;
237
- }
238
- let n = Stdlib_Option.flatMap(blockJson[config.ecosystem.blockNumberName], v => {
239
- if (v == null) {
240
- return;
241
- } else {
242
- return Primitive_option.some(v);
243
- }
244
- });
233
+ let match = config.ecosystem.name;
234
+ let blockNumberKey;
235
+ switch (match) {
236
+ case "evm" :
237
+ case "fuel" :
238
+ blockNumberKey = config.ecosystem.blockNumberName;
239
+ break;
240
+ case "svm" :
241
+ blockNumberKey = "slot";
242
+ break;
243
+ }
244
+ let bump = n => {
245
245
  if (n !== undefined && n > maxBlock.contents) {
246
246
  maxBlock.contents = n;
247
247
  return;
248
248
  }
249
+ };
250
+ let getInt = (d, key) => Stdlib_Option.flatMap(d[key], v => {
251
+ if (v == null) {
252
+ return;
253
+ } else {
254
+ return Primitive_option.some(v);
255
+ }
256
+ });
257
+ simulateItems.forEach(rawJson => {
258
+ let match = config.ecosystem.name;
259
+ switch (match) {
260
+ case "evm" :
261
+ case "fuel" :
262
+ break;
263
+ case "svm" :
264
+ bump(getInt(rawJson, "slot"));
265
+ break;
266
+ }
267
+ let blockJson = rawJson.block;
268
+ if (!(blockJson == null)) {
269
+ return bump(getInt(blockJson, blockNumberKey));
270
+ }
249
271
  });
250
272
  return maxBlock.contents;
251
273
  }
@@ -357,6 +357,17 @@ let setUpdatesOrThrow = async (
357
357
  // The '{cluster}' macro resolves to each node's configured cluster name.
358
358
  let onClusterClause = (~onCluster: bool) => onCluster ? ` ON CLUSTER '{cluster}'` : ""
359
359
 
360
+ // ReplicatedMergeTree drops an insert whose block hash is already in Keeper, and
361
+ // mutations don't clear those hashes. Crash recovery trims the history tail past
362
+ // the committed Postgres checkpoint with ALTER ... DELETE and then replays it, so
363
+ // an identical replayed block would be discarded while still reporting success —
364
+ // a permanent gap that nothing surfaces. Trim-then-replay is what makes recovery
365
+ // correct here, and the duplicates dedup would have caught are already collapsed
366
+ // by the entity view's `LIMIT 1 BY`. Plain MergeTree goes by
367
+ // non_replicated_deduplication_window (0 by default), so it needs no clause.
368
+ let replicatedTableSettingsClause = (~replicated: bool) =>
369
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
370
+
360
371
  // Strip both engine arguments `(...)` and a trailing `SETTINGS ...` clause to
361
372
  // get the bare engine name, e.g. `Replicated('/p','{shard}','{replica}') SETTINGS x=1`
362
373
  // and `Replicated SETTINGS x=1` both yield `Replicated`.
@@ -395,9 +406,9 @@ let makeCreateHistoryTableQuery = (
395
406
  }
396
407
  })
397
408
 
398
- let (partitionBy, orderBy, ttl) = switch entityConfig.storage.clickhouseOptions {
399
- | Some(options) => (options.partitionBy, options.orderBy, options.ttl)
400
- | None => (None, None, None)
409
+ let (partitionBy, orderBy, ttl, skippingIndexes) = switch entityConfig.storage.clickhouseOptions {
410
+ | Some(options) => (options.partitionBy, options.orderBy, options.ttl, options.skippingIndexes)
411
+ | None => (None, None, None, None)
401
412
  }
402
413
 
403
414
  // Schema field name -> ClickHouse column name, so @storage(clickhouse: {...})
@@ -461,6 +472,22 @@ let makeCreateHistoryTableQuery = (
461
472
  | None => ""
462
473
  }
463
474
 
475
+ // Data skipping indexes live inside the column list, after the last column.
476
+ // GRANULARITY is omitted when unset, leaving ClickHouse's default of 1.
477
+ let skippingIndexDefinitions = switch skippingIndexes {
478
+ | Some(skippingIndexes) =>
479
+ skippingIndexes
480
+ ->Array.map(index => {
481
+ let granularityClause = switch index.granularity {
482
+ | Some(granularity) => ` GRANULARITY ${granularity->Int.toString}`
483
+ | None => ""
484
+ }
485
+ `,\n INDEX \`${index.name}\` ${index.expr->resolveExpressionColumns} TYPE ${index.type_}${granularityClause}`
486
+ })
487
+ ->Array.joinUnsafe("")
488
+ | None => ""
489
+ }
490
+
464
491
  `CREATE TABLE IF NOT EXISTS ${database}.\`${EntityHistory.historyTableName(
465
492
  ~entityName=entityConfig.name,
466
493
  ~entityIndex=entityConfig.index,
@@ -475,10 +502,10 @@ let makeCreateHistoryTableQuery = (
475
502
  ~fieldType=Enum({config: EntityHistory.RowAction.config->Table.fromGenericEnumConfig}),
476
503
  ~isNullable=false,
477
504
  ~isArray=false,
478
- )}
505
+ )}${skippingIndexDefinitions}
479
506
  )
480
507
  ENGINE = ${tableEngine}${partitionByClause}
481
- ORDER BY (${orderByColumns})${ttlClause}`
508
+ ORDER BY (${orderByColumns})${ttlClause}${replicatedTableSettingsClause(~replicated)}`
482
509
  }
483
510
 
484
511
  // Generate CREATE TABLE query for checkpoints
@@ -522,7 +549,7 @@ let makeCreateCheckpointsTableQuery = (
522
549
  )}
523
550
  )
524
551
  ENGINE = ${tableEngine}
525
- ORDER BY (${idField})`
552
+ ORDER BY (${idField})${replicatedTableSettingsClause(~replicated)}`
526
553
  }
527
554
 
528
555
  // Generate CREATE VIEW query for entity current state
@@ -273,6 +273,14 @@ function onClusterClause(onCluster) {
273
273
  }
274
274
  }
275
275
 
276
+ function replicatedTableSettingsClause(replicated) {
277
+ if (replicated) {
278
+ return "\nSETTINGS replicated_deduplication_window = 0";
279
+ } else {
280
+ return "";
281
+ }
282
+ }
283
+
276
284
  function databaseEngineName(engineSpec) {
277
285
  return engineSpec.trim().split("(")[0].split(" ")[0].trim();
278
286
  }
@@ -295,12 +303,15 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
295
303
  let match = options !== undefined ? [
296
304
  options.partitionBy,
297
305
  options.orderBy,
298
- options.ttl
306
+ options.ttl,
307
+ options.skippingIndexes
299
308
  ] : [
309
+ undefined,
300
310
  undefined,
301
311
  undefined,
302
312
  undefined
303
313
  ];
314
+ let skippingIndexes = match[3];
304
315
  let ttl = match[2];
305
316
  let orderBy = match[1];
306
317
  let partitionBy = match[0];
@@ -336,6 +347,11 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
336
347
  });
337
348
  let partitionByClause = partitionBy !== undefined ? `\nPARTITION BY ` + resolveExpressionColumns(partitionBy) : "";
338
349
  let ttlClause = ttl !== undefined ? `\nTTL ` + resolveExpressionColumns(ttl) : "";
350
+ let skippingIndexDefinitions = skippingIndexes !== undefined ? skippingIndexes.map(index => {
351
+ let granularity = index.granularity;
352
+ let granularityClause = granularity !== undefined ? ` GRANULARITY ` + granularity.toString() : "";
353
+ return `,\n INDEX \`` + index.name + `\` ` + resolveExpressionColumns(index.expr) + ` TYPE ` + index.type + granularityClause;
354
+ }).join("") : "";
339
355
  return `CREATE TABLE IF NOT EXISTS ` + database + `.\`` + EntityHistory.historyTableName(entityConfig.name, entityConfig.index) + `\`` + (
340
356
  onCluster ? ` ON CLUSTER '{cluster}'` : ""
341
357
  ) + ` (
@@ -344,10 +360,12 @@ function makeCreateHistoryTableQuery(entityConfig, database, replicatedOpt, onCl
344
360
  \`` + EntityHistory.changeFieldName + `\` ` + getClickHouseFieldType({
345
361
  type: "Enum",
346
362
  config: EntityHistory.RowAction.config
347
- }, false, false, undefined) + `
363
+ }, false, false, undefined) + skippingIndexDefinitions + `
348
364
  )
349
365
  ENGINE = ` + tableEngine + partitionByClause + `
350
- ORDER BY (` + orderByColumns + `)` + ttlClause;
366
+ ORDER BY (` + orderByColumns + `)` + ttlClause + (
367
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
368
+ );
351
369
  }
352
370
 
353
371
  function makeCreateCheckpointsTableQuery(database, replicatedOpt, onClusterOpt, chainIdModeOpt) {
@@ -365,7 +383,9 @@ function makeCreateCheckpointsTableQuery(database, replicatedOpt, onClusterOpt,
365
383
  \`` + "events_processed" + `\` ` + getClickHouseFieldType("UInt64", false, false, undefined) + `
366
384
  )
367
385
  ENGINE = ` + tableEngine + `
368
- ORDER BY (` + "id" + `)`;
386
+ ORDER BY (` + "id" + `)` + (
387
+ replicated ? "\nSETTINGS replicated_deduplication_window = 0" : ""
388
+ );
369
389
  }
370
390
 
371
391
  function makeCreateViewQuery(entityConfig, database, onClusterOpt) {
@@ -505,6 +525,7 @@ export {
505
525
  setCheckpointsOrThrow,
506
526
  setUpdatesOrThrow,
507
527
  onClusterClause,
528
+ replicatedTableSettingsClause,
508
529
  databaseEngineName,
509
530
  makeCreateHistoryTableQuery,
510
531
  makeCreateCheckpointsTableQuery,