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
@@ -88,20 +88,10 @@ let allSvmTransactionFields = [
88
88
  "accountKeys",
89
89
  "recentBlockhash",
90
90
  "version",
91
- "tokenBalances",
92
- "allSignatures"
91
+ "allSignatures",
92
+ "accountActivities"
93
93
  ];
94
94
 
95
- let svmTransactionFieldSchema = S$RescriptSchema.$$enum(allSvmTransactionFields);
96
-
97
- let allSvmBlockFields = [
98
- "height",
99
- "parentSlot",
100
- "parentHash"
101
- ];
102
-
103
- let svmBlockFieldSchema = S$RescriptSchema.$$enum(allSvmBlockFields);
104
-
105
95
  let evmNullableBlockFields = new Set([
106
96
  "nonce",
107
97
  "difficulty",
@@ -141,10 +131,16 @@ let evmNullableTransactionFields = new Set([
141
131
  "type"
142
132
  ]);
143
133
 
144
- function makeFieldSelection(blockFields, transactionFields, blockMaskFn, transactionMaskFn) {
134
+ function makeFieldSelection(blockFields, transactionFields, instructionFieldsOpt, accountActivityFieldsOpt, logFieldsOpt, blockMaskFn, transactionMaskFn) {
135
+ let instructionFields = instructionFieldsOpt !== undefined ? Primitive_option.valFromOption(instructionFieldsOpt) : new Set();
136
+ let accountActivityFields = accountActivityFieldsOpt !== undefined ? Primitive_option.valFromOption(accountActivityFieldsOpt) : new Set();
137
+ let logFields = logFieldsOpt !== undefined ? Primitive_option.valFromOption(logFieldsOpt) : new Set();
145
138
  return {
146
139
  blockFields: blockFields,
147
140
  transactionFields: transactionFields,
141
+ instructionFields: instructionFields,
142
+ accountActivityFields: accountActivityFields,
143
+ logFields: logFields,
148
144
  blockMask: blockMaskFn(blockFields),
149
145
  transactionMask: transactionMaskFn(transactionFields)
150
146
  };
@@ -162,6 +158,9 @@ function unionFieldSelection(a, b) {
162
158
  return {
163
159
  blockFields: unionFields(a.blockFields, b.blockFields),
164
160
  transactionFields: unionFields(a.transactionFields, b.transactionFields),
161
+ instructionFields: unionFields(a.instructionFields, b.instructionFields),
162
+ accountActivityFields: unionFields(a.accountActivityFields, b.accountActivityFields),
163
+ logFields: unionFields(a.logFields, b.logFields),
165
164
  blockMask: FieldMask.orMask(a.blockMask, b.blockMask),
166
165
  transactionMask: FieldMask.orMask(a.transactionMask, b.transactionMask)
167
166
  };
@@ -329,9 +328,6 @@ export {
329
328
  allEvmTransactionFields,
330
329
  evmTransactionFieldSchema,
331
330
  allSvmTransactionFields,
332
- svmTransactionFieldSchema,
333
- allSvmBlockFields,
334
- svmBlockFieldSchema,
335
331
  evmNullableBlockFields,
336
332
  evmNullableTransactionFields,
337
333
  makeFieldSelection,
package/src/Main.res CHANGED
@@ -239,7 +239,7 @@ let getGlobalIndexer = (): 'indexer => {
239
239
  "event": unknown,
240
240
  "wildcard": option<bool>,
241
241
  "where": option<JSON.t>,
242
- "fields": option<Internal.evmFieldsSelection>,
242
+ "fields": option<unknown>,
243
243
  }
244
244
  )
245
245
  // Detect format: if "contract" is a string, it's the TS format
@@ -295,7 +295,7 @@ let getGlobalIndexer = (): 'indexer => {
295
295
  "program": unknown,
296
296
  "instruction": unknown,
297
297
  "where": option<JSON.t>,
298
- "fields": option<Internal.evmFieldsSelection>,
298
+ "fields": option<unknown>,
299
299
  }
300
300
  )
301
301
  let (programName, instructionName) = if typeof(raw["program"]) === #string {
@@ -308,9 +308,6 @@ let getGlobalIndexer = (): 'indexer => {
308
308
  (inst["contract"], inst["_0"])
309
309
  }
310
310
  let where = raw["where"]
311
- // SVM takes its selection from the config, so `fields` is carried through
312
- // only to be rejected by the registration — dropping it here would leave a
313
- // plain-JS caller with a silently ignored option.
314
311
  let fields = raw["fields"]
315
312
  let eventOptions: option<Internal.eventOptions<_>> = switch (where, fields) {
316
313
  | (None, None) => None
@@ -232,11 +232,24 @@ let toInitialState = (state: t, ~cleanRun): Persistence.initialState => {
232
232
  let handleLoad = (state: t, ~tableName: string, ~filter: EntityFilter.t): array<
233
233
  Internal.entity,
234
234
  > => {
235
- // Loads for non-entity tables (e.g. effect caches `envio_effect_<name>`) reach
236
- // here too. Nothing persists those, so there's nothing to return an empty
237
- // result makes the effect recompute instead of crashing on a missing config.
235
+ // Effect caches (`envio_effect_<name>`) are loaded through the same call, and
236
+ // they have no entity config they're served from the cache `writeBatch`
237
+ // filled, so a resumed indexer reuses cached outputs instead of recomputing
238
+ // them the way Postgres would.
238
239
  switch state.entityConfigs->Dict.get(tableName) {
239
- | None => []
240
+ | None =>
241
+ switch state.effectCache->Dict.get(tableName) {
242
+ | None => []
243
+ | Some(cacheDict) =>
244
+ cacheDict
245
+ ->Dict.valuesToArray
246
+ ->Array.filter(item =>
247
+ filter->EntityFilter.matches(
248
+ ~entity=item->(Utils.magic: Internal.effectCacheItem => dict<EntityFilter.FieldValue.t>),
249
+ )
250
+ )
251
+ ->(Utils.magic: array<Internal.effectCacheItem> => array<Internal.entity>)
252
+ }
240
253
  | Some(entityConfig) =>
241
254
  let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
242
255
  let matched =
@@ -320,6 +333,17 @@ let writeBatch = (
320
333
  | None => ()
321
334
  }
322
335
 
336
+ // The rollback diff restates what the reverted state already is, so it is not
337
+ // a change history should record — and an id it touches needs no backfill
338
+ // either, because the diff already carries its reverted value. Postgres draws
339
+ // the same line, keyed on the diff's checkpoint id.
340
+ let diffCheckpointId = rollback->Option.map(({diffCheckpointId}) => diffCheckpointId)
341
+ let isDiff = (change: Change.t<Internal.entity>) =>
342
+ switch diffCheckpointId {
343
+ | Some(diffCheckpointId) => change->Change.getCheckpointId === diffCheckpointId
344
+ | None => false
345
+ }
346
+
323
347
  updatedEntities->Array.forEach(({entityConfig, scope, changes}: Persistence.updatedEntity) => {
324
348
  let entityDict = state->getEntityDict(~name=entityConfig.name)
325
349
  let historyRows = state->getHistory(~name=entityConfig.name)
@@ -327,9 +351,17 @@ let writeBatch = (
327
351
  // onto the stored entity the same way the Postgres write path does.
328
352
  let chainIdField = entityConfig.table->Table.getChainIdField
329
353
 
354
+ let idsWithDiff = Utils.Set.make()
355
+ changes->Array.forEach(change =>
356
+ if isDiff(change) {
357
+ idsWithDiff->Utils.Set.add(change->Change.getEntityId->EntityId.toKey)->ignore
358
+ }
359
+ )
360
+
330
361
  changes->Array.forEach(change => {
331
362
  let entityId = change->Change.getEntityId
332
- if shouldSaveHistory {
363
+ let shouldSaveChangeHistory = shouldSaveHistory && !isDiff(change)
364
+ if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityId->EntityId.toKey)) {
333
365
  state->backfillHistory(~entityConfig, ~scope, ~entityId, ~rows=historyRows)
334
366
  }
335
367
  switch change {
@@ -340,7 +372,7 @@ let writeBatch = (
340
372
  | _ => entity
341
373
  }
342
374
  entityDict->Dict.set(rowKey(~scope, ~entityId), storedEntity)
343
- if shouldSaveHistory {
375
+ if shouldSaveChangeHistory {
344
376
  historyRows
345
377
  ->Array.push({
346
378
  entityId,
@@ -353,7 +385,7 @@ let writeBatch = (
353
385
  }
354
386
  | Delete({checkpointId}) =>
355
387
  entityDict->Utils.Dict.deleteInPlace(rowKey(~scope, ~entityId))
356
- if shouldSaveHistory {
388
+ if shouldSaveChangeHistory {
357
389
  historyRows
358
390
  ->Array.push({
359
391
  entityId,
@@ -472,7 +504,7 @@ let getRollbackData = (
472
504
  | Some({action: DELETE, entityId, scope}) =>
473
505
  removals->Array.push({Persistence.entityId, scope})->ignore
474
506
  | Some({action: SET, entity: Some(entity)}) =>
475
- restored->Array.push(entity->(Utils.magic: Internal.entity => unknown))->ignore
507
+ restored->Array.push(entity)->ignore
476
508
  | Some({action: SET, entity: None}) => ()
477
509
  }
478
510
  })
@@ -187,20 +187,25 @@ function toInitialState(state, cleanRun) {
187
187
 
188
188
  function handleLoad(state, tableName, filter) {
189
189
  let entityConfig = state.entityConfigs[tableName];
190
- if (entityConfig === undefined) {
191
- return [];
190
+ if (entityConfig !== undefined) {
191
+ let entityDict = Stdlib_Option.getOr(state.entities[entityConfig.name], {});
192
+ let matched = Object.values(entityDict).filter(entity => EntityFilter.matches(filter, entity));
193
+ let field = Table.getChainIdField(entityConfig.table);
194
+ if (field !== undefined) {
195
+ return matched.map(entity => {
196
+ let copy = Utils.Dict.shallowCopy(entity);
197
+ Utils.Dict.deleteInPlace(copy, field.fieldName);
198
+ return copy;
199
+ });
200
+ } else {
201
+ return matched;
202
+ }
192
203
  }
193
- let entityDict = Stdlib_Option.getOr(state.entities[entityConfig.name], {});
194
- let matched = Object.values(entityDict).filter(entity => EntityFilter.matches(filter, entity));
195
- let field = Table.getChainIdField(entityConfig.table);
196
- if (field !== undefined) {
197
- return matched.map(entity => {
198
- let copy = Utils.Dict.shallowCopy(entity);
199
- Utils.Dict.deleteInPlace(copy, field.fieldName);
200
- return copy;
201
- });
204
+ let cacheDict = state.effectCache[tableName];
205
+ if (cacheDict !== undefined) {
206
+ return Object.values(cacheDict).filter(item => EntityFilter.matches(filter, item));
202
207
  } else {
203
- return matched;
208
+ return [];
204
209
  }
205
210
  }
206
211
 
@@ -234,15 +239,32 @@ function writeBatch(state, batch, rollback, isInReorgThreshold, config, updatedE
234
239
  if (rollback !== undefined) {
235
240
  applyRollback(state, rollback.targetCheckpointId);
236
241
  }
242
+ let diffCheckpointId = Stdlib_Option.map(rollback, param => param.diffCheckpointId);
243
+ let isDiff = change => {
244
+ if (diffCheckpointId !== undefined) {
245
+ return change.checkpointId === diffCheckpointId;
246
+ } else {
247
+ return false;
248
+ }
249
+ };
237
250
  updatedEntities.forEach(param => {
251
+ let changes = param.changes;
238
252
  let scope = param.scope;
239
253
  let entityConfig = param.entityConfig;
240
254
  let entityDict = getEntityDict(state, entityConfig.name);
241
255
  let historyRows = getHistory(state, entityConfig.name);
242
256
  let chainIdField = Table.getChainIdField(entityConfig.table);
243
- param.changes.forEach(change => {
257
+ let idsWithDiff = new Set();
258
+ changes.forEach(change => {
259
+ if (isDiff(change)) {
260
+ idsWithDiff.add(EntityId.toKey(change.entityId));
261
+ return;
262
+ }
263
+ });
264
+ changes.forEach(change => {
244
265
  let entityId = change.entityId;
245
- if (shouldSaveHistory) {
266
+ let shouldSaveChangeHistory = shouldSaveHistory && !isDiff(change);
267
+ if (shouldSaveHistory && !idsWithDiff.has(EntityId.toKey(entityId))) {
246
268
  backfillHistory(state, entityConfig, scope, entityId, historyRows);
247
269
  }
248
270
  if (change.type === "SET") {
@@ -250,7 +272,7 @@ function writeBatch(state, batch, rollback, isInReorgThreshold, config, updatedE
250
272
  let match = Internal.chainScopeChainId(scope);
251
273
  let storedEntity = chainIdField !== undefined && match !== undefined ? Internal.stampChainId(entity, chainIdField.fieldName, Primitive_option.valFromOption(match)) : entity;
252
274
  entityDict[rowKey(scope, entityId)] = storedEntity;
253
- if (shouldSaveHistory) {
275
+ if (shouldSaveChangeHistory) {
254
276
  historyRows.push({
255
277
  entityId: entityId,
256
278
  scope: scope,
@@ -264,7 +286,7 @@ function writeBatch(state, batch, rollback, isInReorgThreshold, config, updatedE
264
286
  }
265
287
  }
266
288
  Utils.Dict.deleteInPlace(entityDict, rowKey(scope, entityId));
267
- if (shouldSaveHistory) {
289
+ if (shouldSaveChangeHistory) {
268
290
  historyRows.push({
269
291
  entityId: entityId,
270
292
  scope: scope,
@@ -147,11 +147,13 @@ type storage = {
147
147
  "new_progress_block_number": int,
148
148
  }>,
149
149
  >,
150
- // Get rollback data for entity
150
+ // Rollback data for an entity, as decoded entities rather than storage rows:
151
+ // only the storage knows how it encoded them, so each one decodes its own
152
+ // before handing them back.
151
153
  getRollbackData: (
152
154
  ~entityConfig: Internal.entityConfig,
153
155
  ~rollbackTargetCheckpointId: Internal.checkpointId,
154
- ) => promise<(array<rollbackRemoval>, array<unknown>)>,
156
+ ) => promise<(array<rollbackRemoval>, array<Internal.entity>)>,
155
157
  // Write batch to storage
156
158
  writeBatch: (
157
159
  ~batch: Batch.t,
package/src/PgStorage.res CHANGED
@@ -2308,7 +2308,12 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2308
2308
  }
2309
2309
  })
2310
2310
 
2311
- (removals, restoredEntitiesResult)
2311
+ (
2312
+ removals,
2313
+ restoredEntitiesResult
2314
+ ->S.parseOrThrow(entityConfig.table->Table.pgRowsSchema)
2315
+ ->(Utils.magic: array<unknown> => array<Internal.entity>),
2316
+ )
2312
2317
  }
2313
2318
 
2314
2319
  let writeBatchMethod = async (
@@ -1464,7 +1464,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::` + addrCh
1464
1464
  });
1465
1465
  return [
1466
1466
  removals,
1467
- restoredEntitiesResult
1467
+ S$RescriptSchema.parseOrThrow(restoredEntitiesResult, Table.pgRowsSchema(entityConfig.table))
1468
1468
  ];
1469
1469
  };
1470
1470
  let writeBatchMethod = async (batch, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities, chainMetaData, onWrite) => {
@@ -174,6 +174,8 @@ type rawSimulateItem
174
174
 
175
175
  @get external getContract: rawSimulateItem => option<string> = "contract"
176
176
  @get external getEvent: rawSimulateItem => option<string> = "event"
177
+ @get external getProgram: rawSimulateItem => option<string> = "program"
178
+ @get external getInstruction: rawSimulateItem => option<string> = "instruction"
177
179
 
178
180
  let findEventConfig = (~config: Config.t, ~contractName: string, ~eventName: string) => {
179
181
  let found = ref(None)
@@ -240,18 +242,71 @@ let deriveSrcAddress = (
240
242
  }
241
243
  }
242
244
 
245
+ type parseResult = {
246
+ items: array<Internal.item>,
247
+ transactionStore: option<TransactionStore.t>,
248
+ blockStore: option<BlockStore.t>,
249
+ }
250
+
251
+ type svmSimActivity = {
252
+ address: string,
253
+ transactionAccountIndex?: int,
254
+ isSigner?: bool,
255
+ isWritable?: bool,
256
+ lamports?: {pre?: bigint, post?: bigint},
257
+ token?: {
258
+ mint?: string,
259
+ owner?: string,
260
+ decimals?: int,
261
+ preAmount?: bigint,
262
+ postAmount?: bigint,
263
+ },
264
+ }
265
+
266
+ type svmSimTransaction = {
267
+ transactionIndex?: int,
268
+ signature?: string,
269
+ allSignatures?: array<string>,
270
+ feePayer?: string,
271
+ success?: bool,
272
+ err?: string,
273
+ fee?: bigint,
274
+ computeUnitsConsumed?: bigint,
275
+ accountKeys?: array<string>,
276
+ recentBlockhash?: string,
277
+ version?: string,
278
+ accountActivities?: array<svmSimActivity>,
279
+ }
280
+
281
+ let liveRegistrationsFor = (
282
+ ~config: Config.t,
283
+ ~chainId: ChainId.t,
284
+ ~eventConfig: Internal.eventConfig,
285
+ ) =>
286
+ HandlerRegister.getSimulateOnEventRegistrations(
287
+ ~config,
288
+ ~chainId,
289
+ ~eventConfig,
290
+ )->Array.filter(reg =>
291
+ (reg.handler->Option.isSome || reg.contractRegister->Option.isSome) &&
292
+ !HandlerRegister.isDroppedByWhere(~config, reg)
293
+ )
294
+
243
295
  let parse = (
244
296
  ~simulateItems: array<JSON.t>,
245
297
  ~config: Config.t,
246
298
  ~chainConfig: Config.chain,
247
299
  ~onEventRegistrations: array<Internal.onEventRegistration>,
248
- ): array<Internal.item> => {
300
+ ): parseResult => {
249
301
  let chainId = chainConfig.id
250
302
  let startBlock = chainConfig.startBlock
251
303
  let currentBlock = ref(startBlock)
252
304
  let currentLogIndex = ref(0)
253
305
 
254
306
  let items = []
307
+ let svmTxs: array<TransactionStore.svmTxInput> = []
308
+ let svmActivities: array<TransactionStore.svmActivityInput> = []
309
+ let svmBlocks: array<BlockStore.inputBlock> = []
255
310
  // Coordinate "block:logIndex" -> the index of the first item that claimed it,
256
311
  // used to reject two items resolving to the same (block, logIndex).
257
312
  let seenCoordinates = Dict.make()
@@ -259,8 +314,189 @@ let parse = (
259
314
  simulateItems->Array.forEachWithIndex((rawJson, itemIndex) => {
260
315
  let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
261
316
 
262
- switch (raw->getContract, raw->getEvent) {
263
- | (Some(contractName), Some(eventName)) =>
317
+ switch (config.ecosystem.name, raw->getProgram, raw->getInstruction) {
318
+ | (Svm, Some(programName), Some(instructionName)) =>
319
+ let eventConfig = switch findEventConfig(
320
+ ~config,
321
+ ~contractName=programName,
322
+ ~eventName=instructionName,
323
+ ) {
324
+ | Some(ec) => ec
325
+ | None =>
326
+ JsError.throwWithMessage(
327
+ `simulate: Instruction "${instructionName}" not found on program "${programName}". ` ++ `Check that the program and instruction names match your config.yaml.`,
328
+ )
329
+ }
330
+ let svmEventConfig =
331
+ eventConfig->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
332
+ let item = rawJson->(Utils.magic: JSON.t => Envio.svmSimulateItem)
333
+ let rawItem = rawJson->(Utils.magic: JSON.t => {..})
334
+ let blockJson: option<JSON.t> =
335
+ rawItem["block"]->(Utils.magic: 'a => Nullable.t<JSON.t>)->Nullable.toOption
336
+ let slot = switch item.slot {
337
+ | Some(s) => s
338
+ | None =>
339
+ switch blockJson {
340
+ | Some(bj) =>
341
+ switch (bj->(Utils.magic: JSON.t => dict<JSON.t>))->Dict.get("slot") {
342
+ | Some(v) =>
343
+ v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption->Option.getOr(
344
+ currentBlock.contents,
345
+ )
346
+ | None => currentBlock.contents
347
+ }
348
+ | None => currentBlock.contents
349
+ }
350
+ }
351
+ currentBlock := slot
352
+ let transaction = switch item.transaction {
353
+ | Some(tx) => tx->(Utils.magic: unknown => svmSimTransaction)
354
+ | None => {}
355
+ }
356
+ let transactionIndex = transaction.transactionIndex->Option.getOr(0)
357
+ let path = item.path->Option.getOr([0])
358
+ let programId =
359
+ item.programId->Option.getOr(svmEventConfig.programId->SvmTypes.Pubkey.toString)
360
+ let accountArguments = switch item.accountArguments {
361
+ | Some(args) => args
362
+ | None =>
363
+ switch item.accounts {
364
+ | Some(named) =>
365
+ svmEventConfig.accounts->Array.map(name =>
366
+ switch named->Dict.get(name) {
367
+ | Some({address}) => address
368
+ | None => ""
369
+ }
370
+ )
371
+ | None => []
372
+ }
373
+ }
374
+ let data = item.data->Option.getOr(svmEventConfig.discriminator->Option.getOr("0x"))
375
+ let decoded = item.args->Option.map(args => (
376
+ {
377
+ SvmHyperSyncClient.ResponseTypes.name: instructionName,
378
+ argsJson: args->JSON.stringify,
379
+ accountsJson: "{}",
380
+ extraAccounts: [],
381
+ }: SvmHyperSyncClient.ResponseTypes.decodedInstruction
382
+ ))
383
+ let logs = item.logs->Option.map(logs =>
384
+ logs->Array.map((log): SvmHyperSyncClient.EventItems.log => {
385
+ kind: ?log.kind,
386
+ message: ?log.message,
387
+ })
388
+ )
389
+
390
+ let liveRegistrations = liveRegistrationsFor(~config, ~chainId, ~eventConfig)
391
+ if liveRegistrations->Utils.Array.isEmpty {
392
+ JsError.throwWithMessage(
393
+ `simulate: no handler runs for instruction "${instructionName}" on program "${programName}". Register a handler with indexer.onInstruction before simulating it.`,
394
+ )
395
+ }
396
+
397
+ svmTxs
398
+ ->Array.push({
399
+ slot,
400
+ transactionIndex,
401
+ signature: ?transaction.signature,
402
+ allSignatures: ?transaction.allSignatures,
403
+ feePayer: ?transaction.feePayer,
404
+ success: ?transaction.success,
405
+ err: ?transaction.err,
406
+ fee: ?transaction.fee,
407
+ computeUnitsConsumed: ?transaction.computeUnitsConsumed,
408
+ accountKeys: ?transaction.accountKeys,
409
+ recentBlockhash: ?transaction.recentBlockhash,
410
+ version: ?transaction.version,
411
+ })
412
+ ->ignore
413
+ switch transaction.accountActivities {
414
+ | Some(rows) =>
415
+ rows->Array.forEach(activity =>
416
+ svmActivities
417
+ ->Array.push({
418
+ TransactionStore.slot,
419
+ transactionIndex,
420
+ account: activity.address,
421
+ accountIndex: ?activity.transactionAccountIndex,
422
+ isSigner: ?activity.isSigner,
423
+ isWritable: ?activity.isWritable,
424
+ preBalance: ?activity.lamports->Option.flatMap(l => l.pre),
425
+ postBalance: ?activity.lamports->Option.flatMap(l => l.post),
426
+ mint: ?activity.token->Option.flatMap(t => t.mint),
427
+ owner: ?activity.token->Option.flatMap(t => t.owner),
428
+ decimals: ?activity.token->Option.flatMap(t => t.decimals),
429
+ preAmount: ?activity.token->Option.flatMap(t => t.preAmount),
430
+ postAmount: ?activity.token->Option.flatMap(t => t.postAmount),
431
+ })
432
+ ->ignore
433
+ )
434
+ | None => ()
435
+ }
436
+ let blockTime = switch item.block {
437
+ | Some(block) => block.time
438
+ | None => None
439
+ }
440
+ let blockHash = switch item.block {
441
+ | Some(block) => block.hash
442
+ | None => None
443
+ }
444
+ svmBlocks
445
+ ->Array.push({
446
+ blockNumber: slot,
447
+ ?blockHash,
448
+ blockTimestamp: ?blockTime,
449
+ })
450
+ ->ignore
451
+
452
+ liveRegistrations->Array.forEach(reg => {
453
+ let onEventRegistrationIndex = onEventRegistrations->Array.length
454
+ let onEventRegistration = {...reg, index: onEventRegistrationIndex}
455
+ onEventRegistrations->Array.push(onEventRegistration)->ignore
456
+ let payload = SvmHyperSyncSource.toSvmInstruction(
457
+ {
458
+ onEventRegistrationIndex,
459
+ slot,
460
+ transactionIndex,
461
+ path,
462
+ programId,
463
+ accounts: accountArguments,
464
+ data,
465
+ isInner: item.isInner->Option.getOr(false),
466
+ ?decoded,
467
+ ?logs,
468
+ },
469
+ ~programName,
470
+ ~instructionName,
471
+ ~eventConfig=svmEventConfig,
472
+ ~fieldSelection=onEventRegistration.fieldSelection,
473
+ )
474
+ let payloadDict = payload->(Utils.magic: Envio.svmInstruction => dict<unknown>)
475
+ payloadDict->Dict.set("srcAddress", programId->(Utils.magic: string => unknown))
476
+ items
477
+ ->Array.push(
478
+ Internal.Event({
479
+ onEventRegistration,
480
+ chainId,
481
+ blockNumber: slot,
482
+ logIndex: transactionIndex,
483
+ orderPath: path,
484
+ transactionIndex,
485
+ payload: payload->(Utils.magic: Envio.svmInstruction => Internal.eventPayload),
486
+ }),
487
+ )
488
+ ->ignore
489
+ })
490
+
491
+ | (Svm, _, _) =>
492
+ JsError.throwWithMessage(`simulate: Invalid item. Each item must have "program" and "instruction" fields.`)
493
+
494
+ | (_, _, _) =>
495
+ let (contractName, eventName) = switch (raw->getContract, raw->getEvent) {
496
+ | (Some(c), Some(e)) => (c, e)
497
+ | _ =>
498
+ JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`)
499
+ }
264
500
  // Event simulate item
265
501
  let eventConfig = switch findEventConfig(~config, ~contractName, ~eventName) {
266
502
  | Some(ec) => ec
@@ -399,13 +635,21 @@ let parse = (
399
635
  )
400
636
  ->ignore
401
637
  })
402
-
403
- | _ =>
404
- JsError.throwWithMessage(`simulate: Invalid item. Each item must have "contract" and "event" fields.`)
405
638
  }
406
639
  })
407
640
 
408
- items
641
+ switch config.ecosystem.name {
642
+ | Svm => {
643
+ items,
644
+ transactionStore: Some(TransactionStore.fromSvmJs(svmTxs, svmActivities)),
645
+ blockStore: Some(BlockStore.fromJs(svmBlocks, ~ecosystem=Svm, ~shouldChecksum=false)),
646
+ }
647
+ | _ => {
648
+ items,
649
+ transactionStore: None,
650
+ blockStore: None,
651
+ }
652
+ }
409
653
  }
410
654
 
411
655
  // Apply simulate source config from processConfig JSON to a Config.t
@@ -444,13 +688,21 @@ let patchConfig = (
444
688
  // Parse with the process's startBlock so items default into the range
445
689
  // the source will be queried over; the source now filters by range.
446
690
  let chainConfig = {...chainConfig, startBlock, endBlock}
447
- let items = parse(
691
+ let {items, transactionStore, blockStore} = parse(
448
692
  ~simulateItems,
449
693
  ~config,
450
694
  ~chainConfig,
451
695
  ~onEventRegistrations=chainRegistrations.onEventRegistrations,
452
696
  )
453
- {...chainConfig, sourceConfig: Config.SimulateSourceConfig({items, endBlock})}
697
+ {
698
+ ...chainConfig,
699
+ sourceConfig: Config.SimulateSourceConfig({
700
+ items,
701
+ endBlock,
702
+ ?transactionStore,
703
+ ?blockStore,
704
+ }),
705
+ }
454
706
  | None => chainConfig
455
707
  }
456
708
  | None => chainConfig