envio 3.7.0 → 3.9.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 (79) hide show
  1. package/README.md +2 -2
  2. package/index.d.ts +399 -156
  3. package/package.json +6 -6
  4. package/src/AddressRows.res +121 -0
  5. package/src/AddressRows.res.mjs +143 -0
  6. package/src/Batch.res +2 -0
  7. package/src/Batch.res.mjs +2 -1
  8. package/src/ChainState.res +82 -68
  9. package/src/ChainState.res.mjs +81 -65
  10. package/src/ChainState.resi +14 -9
  11. package/src/Config.res +41 -91
  12. package/src/Config.res.mjs +34 -68
  13. package/src/ContractMapping.res +66 -0
  14. package/src/ContractMapping.res.mjs +73 -0
  15. package/src/Core.res +20 -0
  16. package/src/Core.res.mjs +4 -0
  17. package/src/Envio.res +54 -76
  18. package/src/EventConfigBuilder.res +157 -30
  19. package/src/EventConfigBuilder.res.mjs +118 -24
  20. package/src/FetchState.res +59 -67
  21. package/src/FetchState.res.mjs +34 -44
  22. package/src/HandlerRegister.res +20 -11
  23. package/src/HandlerRegister.res.mjs +22 -5
  24. package/src/Hasura.res +30 -22
  25. package/src/Hasura.res.mjs +11 -5
  26. package/src/InMemoryStore.res +18 -36
  27. package/src/InMemoryStore.res.mjs +12 -26
  28. package/src/IndexerState.res +21 -1
  29. package/src/IndexerState.res.mjs +9 -3
  30. package/src/IndexerState.resi +1 -0
  31. package/src/Internal.res +33 -13
  32. package/src/Internal.res.mjs +12 -16
  33. package/src/Main.res +27 -20
  34. package/src/Main.res.mjs +19 -14
  35. package/src/MemoryStorage.res +78 -73
  36. package/src/MemoryStorage.res.mjs +66 -81
  37. package/src/Metrics.res +15 -1
  38. package/src/Metrics.res.mjs +5 -1
  39. package/src/Persistence.res +34 -18
  40. package/src/Persistence.res.mjs +8 -5
  41. package/src/PgStorage.res +189 -66
  42. package/src/PgStorage.res.mjs +93 -59
  43. package/src/Rollback.res +14 -10
  44. package/src/Rollback.res.mjs +4 -2
  45. package/src/SimulateItems.res +254 -9
  46. package/src/SimulateItems.res.mjs +214 -47
  47. package/src/TestIndexer.res +119 -121
  48. package/src/TestIndexer.res.mjs +95 -100
  49. package/src/Utils.res +7 -0
  50. package/src/Utils.res.mjs +9 -2
  51. package/src/Writing.res +2 -0
  52. package/src/Writing.res.mjs +2 -1
  53. package/src/bindings/ClickHouse.res +39 -6
  54. package/src/bindings/ClickHouse.res.mjs +29 -4
  55. package/src/bindings/NodeJs.res +9 -0
  56. package/src/bindings/NodeJs.res.mjs +8 -1
  57. package/src/bindings/Postgres.res +9 -0
  58. package/src/bindings/Postgres.res.mjs +3 -0
  59. package/src/db/EntityHistory.res +12 -18
  60. package/src/db/EntityHistory.res.mjs +3 -14
  61. package/src/db/InternalTable.res +182 -65
  62. package/src/db/InternalTable.res.mjs +183 -73
  63. package/src/db/Table.res +24 -0
  64. package/src/db/Table.res.mjs +20 -1
  65. package/src/sources/AddressSet.res +0 -22
  66. package/src/sources/AddressStore.res +48 -88
  67. package/src/sources/AddressStore.res.mjs +11 -22
  68. package/src/sources/EvmHyperSyncSource.res +3 -2
  69. package/src/sources/SimulateSource.res +8 -4
  70. package/src/sources/SimulateSource.res.mjs +7 -3
  71. package/src/sources/Svm.res +34 -7
  72. package/src/sources/Svm.res.mjs +32 -4
  73. package/src/sources/SvmHyperSyncClient.res +24 -30
  74. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  75. package/src/sources/SvmHyperSyncSource.res +88 -36
  76. package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
  77. package/src/sources/TransactionStore.res +38 -0
  78. package/src/sources/TransactionStore.res.mjs +5 -0
  79. package/svm.schema.json +37 -82
@@ -1,62 +1,44 @@
1
- // Binding to the Rust `AddressStore` napi class: the chain-wide index of every
2
- // address being indexed, config-declared or dynamically registered. It owns
3
- // registration bookkeeping (conflict detection, `effectiveStartBlock`
4
- // derivation, reorg rollback) and hands out `AddressSet` snapshots for the
5
- // fetch state's partitions. One store lives per chain on `ChainState`, shared
6
- // with that chain's source clients — which read it to drop items whose emitter
7
- // (or address-valued param) isn't registered at the item's block.
8
1
  type t
9
2
 
10
- // A contract an address may be registered for. Every config contract of every
11
- // chain, since `context.chain.<Contract>.add` validates against that whole set
12
- // — a name outside it makes the store throw. `dependsOnAddresses` is whether
13
- // this chain fetches for the contract by address, which is what makes the store
14
- // able to answer `fetchable` on a verdict.
15
3
  type contract = {name: string, startBlock: option<int>, dependsOnAddresses: bool}
16
4
 
17
5
  type registration = {
18
6
  address: Address.t,
19
7
  contractName: string,
20
- // -1 for a config address (not dynamically registered).
8
+ // -1 for a config address.
21
9
  registrationBlock: int,
22
10
  }
23
11
 
24
12
  type verdict =
25
- // `fetchable` is false when nothing on the chain is fetched by address for the
26
- // contract: the address is still stored and persisted, so a config that later
27
- // adds events picks it up on restart, but no partition is built from it.
13
+ // False when nothing on this chain is fetched by address for the contract.
14
+ // The address is still stored and persisted.
28
15
  | Added({effectiveStartBlock: int, fetchable: bool})
29
- // Already registered for the same contract.
30
16
  | Duplicate({effectiveStartBlock: int, existingEffectiveStartBlock: int})
31
- // Already registered for a different contract.
32
- | Conflict({existingContractName: string})
33
- // Not a well-formed address for the ecosystem.
34
17
  | Invalid
35
18
 
36
19
  type rawVerdict = {
37
20
  kind: string,
38
21
  fetchable: bool,
39
22
  effectiveStartBlock: int,
40
- existingContractName: Null.t<string>,
41
23
  existingEffectiveStartBlock: Null.t<int>,
42
24
  }
43
25
 
44
- // A registration the store handed over for persistence, paired with the
45
- // checkpoint that owns its row. `checkpointIdx` indexes the block numbers
46
- // passed to `drainForWrite` — the ids stay on the JS side.
47
- type drainedAddress = {
26
+ type rejectedRow = {
48
27
  address: Address.t,
49
28
  contractName: string,
29
+ effectiveStartBlock: int,
30
+ existingEffectiveStartBlock: int,
31
+ }
32
+
33
+ type drainedAddress = {
34
+ address: NodeJs.Buffer.t,
35
+ contractId: int,
50
36
  registrationBlock: int,
51
37
  checkpointIdx: int,
52
38
  }
53
39
 
54
40
  type makeSetOptions = {
55
- // Only addresses whose id is at or above this. Ids are handed out in
56
- // registration order, so a cursor read before a batch (`nextId`) selects
57
- // exactly what that batch added.
58
41
  minId?: int,
59
- // Inclusive effectiveStartBlock bounds.
60
42
  fromStartBlock?: int,
61
43
  toStartBlock?: int,
62
44
  offset?: int,
@@ -68,9 +50,6 @@ external newEvm: (Core.addressStoreCtor, bool, array<contract>) => t = "newEvm"
68
50
  @send external newSvm: (Core.addressStoreCtor, array<contract>) => t = "newSvm"
69
51
  @send external newFuel: (Core.addressStoreCtor, array<contract>) => t = "newFuel"
70
52
 
71
- // The store's ecosystem is fixed here, from the chain's config, and decides how
72
- // address strings are parsed and rendered back. EVM carries the chain's
73
- // address-checksumming setting so entries render exactly as the sources do.
74
53
  let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool, ~contracts: array<contract>): t => {
75
54
  let ctor = Core.getAddon().addressStore
76
55
  switch ecosystem {
@@ -80,34 +59,20 @@ let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool, ~contracts: array
80
59
  }
81
60
  }
82
61
 
83
- // Every contract an address may be registered for, with the earliest block any
84
- // of this chain's events for it may fire at. `None` means unrestricted, so it
85
- // wins over any `Some`: one registration without a start block makes the whole
86
- // contract's address gate open from the chain's start, and the per-registration
87
- // start block still holds back the registrations that declared one.
88
- //
89
- // A contract whose events are all wildcard is fetched without consulting
90
- // addresses, so registering one changes nothing about what's queried — it
91
- // carries `dependsOnAddresses: false` just like a contract named only by
92
- // `configContractNames`. Either way the addresses are stored and persisted,
93
- // never fetched.
94
62
  let contractsOf = (
95
63
  ~onEventRegistrations: array<Internal.onEventRegistration>,
96
- ~configContractNames: array<string>,
64
+ ~contractMapping: ContractMapping.t,
97
65
  ): array<contract> => {
98
- // Only ever holds a declared start block, so a missing key is unambiguous
99
- // storing `None` in a dict would be indistinguishable from "not seen yet".
66
+ // Only ever holds a declared start block, so a missing key is unambiguous.
67
+ // Storing None in a dict would be indistinguishable from "not seen yet".
100
68
  let startBlocks: dict<int> = Dict.make()
101
69
  let unrestricted = Utils.Set.make()
102
70
  let addressDependent = Utils.Set.make()
103
- let names = []
104
- let addName = name =>
105
- if !(names->Array.includes(name)) {
106
- names->Array.push(name)->ignore
107
- }
108
71
  onEventRegistrations->Array.forEach(reg => {
109
72
  let name = reg.eventConfig.contractName
110
- addName(name)
73
+ contractMapping
74
+ ->ContractMapping.idOfOrThrow(name, ~context=" has event registrations but")
75
+ ->ignore
111
76
  if reg.dependsOnAddresses {
112
77
  addressDependent->Utils.Set.add(name)->ignore
113
78
  }
@@ -123,8 +88,9 @@ let contractsOf = (
123
88
  )
124
89
  }
125
90
  })
126
- configContractNames->Array.forEach(addName)
127
- names->Array.map(name => {
91
+ contractMapping
92
+ ->ContractMapping.names
93
+ ->Array.map(name => {
128
94
  name,
129
95
  startBlock: unrestricted->Utils.Set.has(name)
130
96
  ? None
@@ -135,45 +101,53 @@ let contractsOf = (
135
101
 
136
102
  @send external nextId: t => int = "nextId"
137
103
 
138
- // A set holding nothing — what an address-free (wildcard) partition carries, so
139
- // every partition is queried through the same handle.
140
104
  @send external emptySet: t => AddressSet.t = "emptySet"
141
105
 
142
- // A set over exactly these addresses, in set order. Every set of a chain must
143
- // come from that chain's one store — ids are store-scoped, so sets from
144
- // different stores can't be merged.
145
- @send external makeSetOf: (t, array<Address.t>) => AddressSet.t = "makeSetOf"
146
106
  @send
147
107
  external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
148
108
  @send external seedBatchRaw: (t, array<registration>) => array<rawVerdict> = "seedBatch"
149
109
 
150
- // Drains the registrations awaiting persistence at or below the given block —
151
- // what the batch being written covers — pairing each with the checkpoint at its
152
- // registration block. Later registrations stay pending. Throws, with the queue
153
- // untouched, when a drained registration's block has no checkpoint in the batch.
110
+ @send
111
+ external seedRowsRaw: (
112
+ t,
113
+ NodeJs.Buffer.t,
114
+ array<int>,
115
+ array<int>,
116
+ array<int>,
117
+ ) => array<rejectedRow> = "seedRows"
118
+
119
+ let seedRows = (store: t, rows: AddressRows.seedRows) => {
120
+ let (bytes, lengths) = AddressRows.packBuffers(rows.addresses)
121
+ store->seedRowsRaw(bytes, lengths, rows.contractIds, rows.registrationBlocks)
122
+ }
123
+
124
+ // Throws with the queue untouched when a drained registration's block has no
125
+ // checkpoint in the batch.
154
126
  @send
155
127
  external drainForWrite: (t, int, array<int>) => array<drainedAddress> = "drainForWrite"
156
128
 
157
- // How many registrations await persistence — lets a caller skip the work of
158
- // assembling what `drainForWrite` needs.
159
129
  @send external pendingCount: t => int = "pendingCount"
160
130
 
161
- // The registrations still awaiting persistence. For assertions.
162
131
  @send external pendingEntries: t => array<Internal.indexingContract> = "pendingEntries"
163
132
  @send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
164
133
  @send external contractCount: (t, string) => int = "contractCount"
134
+
135
+ type contractAddressCount = {contractName: string, count: int}
136
+
137
+ @send external contractCounts: t => array<contractAddressCount> = "contractCounts"
165
138
  @send external size: t => int = "size"
166
139
 
167
- // The chain-wide gate routing applies, exposed for the simulate source — it has
168
- // no real query boundary to gate at.
169
140
  @send external isIndexedAt: (t, Address.t, string, int) => bool = "isIndexedAt"
170
141
 
171
- // Drops every address registered after the target block, returning how many
172
- // were dropped. Ids are tombstoned rather than reused, so sets built before the
173
- // rollback still point at the right entries.
174
- @send external rollback: (t, int) => int = "rollback"
142
+ type rolledBackAddress = {address: NodeJs.Buffer.t, contractId: int}
175
143
 
176
- @send external getRaw: (t, Address.t) => Null.t<Internal.indexingContract> = "get"
144
+ // Ids are tombstoned rather than reused, so sets built before the rollback
145
+ // still point at the right entries.
146
+ @send external rollback: (t, int) => array<rolledBackAddress> = "rollback"
147
+
148
+ @send external getAll: (t, Address.t) => array<Internal.indexingContract> = "getAll"
149
+
150
+ @send external dynamicContractNames: t => array<string> = "dynamicContractNames"
177
151
 
178
152
  @send external contractAddresses: (t, string) => array<Address.t> = "contractAddresses"
179
153
 
@@ -185,30 +159,16 @@ let toVerdict = (raw: rawVerdict): verdict =>
185
159
  effectiveStartBlock: raw.effectiveStartBlock,
186
160
  existingEffectiveStartBlock: raw.existingEffectiveStartBlock->Null.getUnsafe,
187
161
  })
188
- | "conflict" => Conflict({existingContractName: raw.existingContractName->Null.getUnsafe})
189
162
  | "invalid" => Invalid
190
163
  | kind => JsError.throwWithMessage(`Unexpected address registration verdict "${kind}"`)
191
164
  }
192
165
 
193
- // Registers dynamic registrations, resolving each address against both the
194
- // store and the batch's own earlier entries — so two contracts claiming one
195
- // address inside a single batch conflict just as they would across batches.
196
- // Verdicts come back in the batch's order. What it adds is pending persistence
197
- // until a batch write drains it.
198
- //
199
166
  // An unknown contract name throws with none of the batch applied.
200
167
  let registerBatch = (store: t, registrations: array<registration>): array<verdict> =>
201
168
  store->registerBatchRaw(registrations)->Array.map(toVerdict)
202
169
 
203
- // `registerBatch` for addresses the database already holds — config addresses
204
- // and the dynamic ones a resume restores. Nothing is marked pending, so nothing
205
- // is ever written back.
206
170
  let seedBatch = (store: t, registrations: array<registration>): array<verdict> =>
207
171
  store->seedBatchRaw(registrations)->Array.map(toVerdict)
208
172
 
209
173
  let makeSet = (store: t, ~contractName, ~options={}: makeSetOptions) =>
210
174
  store->makeSetRaw(contractName, options)
211
-
212
- // The entry an address is registered under, whichever contract holds it —
213
- // addresses are unique chain-wide. `None` once rolled back.
214
- let get = (store: t, address) => store->getRaw(address)->Null.toOption
@@ -1,9 +1,10 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Core from "../Core.res.mjs";
4
+ import * as AddressRows from "../AddressRows.res.mjs";
4
5
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
5
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
- import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as ContractMapping from "../ContractMapping.res.mjs";
7
8
 
8
9
  function make(ecosystem, shouldChecksum, contracts) {
9
10
  let ctor = Core.getAddon().AddressStore;
@@ -17,20 +18,13 @@ function make(ecosystem, shouldChecksum, contracts) {
17
18
  }
18
19
  }
19
20
 
20
- function contractsOf(onEventRegistrations, configContractNames) {
21
+ function contractsOf(onEventRegistrations, contractMapping) {
21
22
  let startBlocks = {};
22
23
  let unrestricted = new Set();
23
24
  let addressDependent = new Set();
24
- let names = [];
25
- let addName = name => {
26
- if (!names.includes(name)) {
27
- names.push(name);
28
- return;
29
- }
30
- };
31
25
  onEventRegistrations.forEach(reg => {
32
26
  let name = reg.eventConfig.contractName;
33
- addName(name);
27
+ ContractMapping.idOfOrThrow(contractMapping, name, " has event registrations but");
34
28
  if (reg.dependsOnAddresses) {
35
29
  addressDependent.add(name);
36
30
  }
@@ -42,14 +36,18 @@ function contractsOf(onEventRegistrations, configContractNames) {
42
36
  }
43
37
  unrestricted.add(name);
44
38
  });
45
- configContractNames.forEach(addName);
46
- return names.map(name => ({
39
+ return ContractMapping.names(contractMapping).map(name => ({
47
40
  name: name,
48
41
  startBlock: unrestricted.has(name) ? undefined : startBlocks[name],
49
42
  dependsOnAddresses: addressDependent.has(name)
50
43
  }));
51
44
  }
52
45
 
46
+ function seedRows(store, rows) {
47
+ let match = AddressRows.packBuffers(rows.addresses);
48
+ return store.seedRows(match[0], match[1], rows.contractIds, rows.registrationBlocks);
49
+ }
50
+
53
51
  function toVerdict(raw) {
54
52
  let kind = raw.kind;
55
53
  switch (kind) {
@@ -59,11 +57,6 @@ function toVerdict(raw) {
59
57
  effectiveStartBlock: raw.effectiveStartBlock,
60
58
  fetchable: raw.fetchable
61
59
  };
62
- case "conflict" :
63
- return {
64
- TAG: "Conflict",
65
- existingContractName: raw.existingContractName
66
- };
67
60
  case "duplicate" :
68
61
  return {
69
62
  TAG: "Duplicate",
@@ -90,17 +83,13 @@ function makeSet(store, contractName, optionsOpt) {
90
83
  return store.makeSet(contractName, options);
91
84
  }
92
85
 
93
- function get(store, address) {
94
- return Primitive_option.fromNull(store.get(address));
95
- }
96
-
97
86
  export {
98
87
  make,
99
88
  contractsOf,
89
+ seedRows,
100
90
  toVerdict,
101
91
  registerBatch,
102
92
  seedBatch,
103
93
  makeSet,
104
- get,
105
94
  }
106
95
  /* Core Not a pure module */
@@ -1,7 +1,8 @@
1
1
  open Source
2
2
 
3
- // Surfaced by HyperSyncClient.getHeight (Rust) when HyperSync rejects the API
4
- // token. The corrupted-token test feeds the real server error through this
3
+ // Surfaced by the HyperSync client (Rust) when HyperSync rejects the API
4
+ // token. The corrupted-token test feeds the real server error (from the query
5
+ // endpoint; the edge no longer 401s malformed tokens on /height) through this
5
6
  // check so it can't silently drift away from what getHeightOrThrow guards on.
6
7
  let isUnauthorizedError = (message: string) => message->String.includes("401 Unauthorized")
7
8
 
@@ -4,6 +4,8 @@ let make = (
4
4
  ~chainId: ChainId.t,
5
5
  ~addressStore: AddressStore.t,
6
6
  ~ecosystem: Ecosystem.name=Evm,
7
+ ~transactionStore: option<TransactionStore.t>=None,
8
+ ~blockStore: option<BlockStore.t>=None,
7
9
  ): Source.t => {
8
10
  let reportedHeight = max(endBlock, 1)
9
11
 
@@ -106,10 +108,12 @@ let make = (
106
108
  Promise.resolve({
107
109
  Source.knownHeight: reportedHeight,
108
110
  parsedQueueItems,
109
- // Simulate keeps the transaction and block inline on the payload; no
110
- // transaction page and an empty block page (nothing to reorg-check).
111
- transactionStore: None,
112
- blockStore: BlockStore.fromJs([], ~ecosystem, ~shouldChecksum=false),
111
+ // EVM/Fuel keep transaction and block inline. SVM simulate returns a
112
+ // store page so materialize + activity attach run the same path as HyperSync.
113
+ transactionStore,
114
+ blockStore: blockStore->Option.getOr(
115
+ BlockStore.fromJs([], ~ecosystem, ~shouldChecksum=false),
116
+ ),
113
117
  fromBlockQueried: fromBlock,
114
118
  latestFetchedBlockNumber: toBlockQueried,
115
119
  stats: {
@@ -2,9 +2,13 @@
2
2
 
3
3
  import * as BlockStore from "./BlockStore.res.mjs";
4
4
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
7
 
6
- function make(items, endBlock, chainId, addressStore, ecosystemOpt) {
8
+ function make(items, endBlock, chainId, addressStore, ecosystemOpt, transactionStoreOpt, blockStoreOpt) {
7
9
  let ecosystem = ecosystemOpt !== undefined ? ecosystemOpt : "evm";
10
+ let transactionStore = transactionStoreOpt !== undefined ? Primitive_option.valFromOption(transactionStoreOpt) : undefined;
11
+ let blockStore = blockStoreOpt !== undefined ? Primitive_option.valFromOption(blockStoreOpt) : undefined;
8
12
  let reportedHeight = Primitive_int.max(endBlock, 1);
9
13
  return {
10
14
  name: "SimulateSource",
@@ -75,8 +79,8 @@ function make(items, endBlock, chainId, addressStore, ecosystemOpt) {
75
79
  return Promise.resolve({
76
80
  knownHeight: reportedHeight,
77
81
  parsedQueueItems: parsedQueueItems,
78
- transactionStore: undefined,
79
- blockStore: BlockStore.fromJs([], ecosystem, false),
82
+ transactionStore: transactionStore,
83
+ blockStore: Stdlib_Option.getOr(blockStore, BlockStore.fromJs([], ecosystem, false)),
80
84
  fromBlockQueried: fromBlock,
81
85
  latestFetchedBlockNumber: toBlockQueried,
82
86
  stats: {
@@ -14,13 +14,38 @@ let eventTransactionFieldMask = TransactionStore.makeMaskFn(transactionFields)
14
14
  // Rust store (`SvmBlockField`) — keep this order in sync.
15
15
  let blockFields = ["slot", "time", "hash", "height", "parentSlot", "parentHash"]
16
16
 
17
- // `slot`/`time`/`hash` are always included; every other block field is opt-in
18
- // via `field_selection.block_fields`. All are materialised from the store.
17
+ // `slot` is always included (the item's key); every other block field is
18
+ // opt-in via handler `fields.block`. All are materialised from the store.
19
19
  //
20
20
  // One instruction's selected block fields → store selection bitmask. Computed per
21
21
  // event at config build and cached on the event config.
22
22
  let eventBlockFieldMask = BlockStore.makeMaskFn(blockFields)
23
23
 
24
+ let attachAccountActivities = (payload: Internal.eventPayload, tx: Internal.eventTransaction) => {
25
+ let instruction = payload->(Utils.magic: Internal.eventPayload => dict<unknown>)
26
+ let transaction = tx->(Utils.magic: Internal.eventTransaction => dict<unknown>)
27
+ switch (instruction->Dict.get("accounts"), transaction->Dict.get("accountActivities")) {
28
+ | (Some(accounts), Some(activities)) if !(accounts->Array.isArray) =>
29
+ let byAddress = Dict.make()
30
+ (activities->(Utils.magic: unknown => array<Envio.svmAccountActivity>))->Array.forEach(
31
+ activity => byAddress->Dict.set(activity.address->SvmTypes.Pubkey.toString, activity),
32
+ )
33
+ (accounts->(Utils.magic: unknown => dict<Envio.svmInstructionAccount>))
34
+ ->Dict.valuesToArray
35
+ ->Array.forEach(account =>
36
+ switch byAddress->Dict.get(account.address->SvmTypes.Pubkey.toString) {
37
+ | Some(activity) =>
38
+ (account->(Utils.magic: Envio.svmInstructionAccount => dict<unknown>))->Dict.set(
39
+ "activity",
40
+ activity->(Utils.magic: Envio.svmAccountActivity => unknown),
41
+ )
42
+ | None => ()
43
+ }
44
+ )
45
+ | _ => ()
46
+ }
47
+ }
48
+
24
49
  let make = (~logger: Pino.t): Ecosystem.t => {
25
50
  name: Svm,
26
51
  blockNumberName: "height",
@@ -36,16 +61,18 @@ let make = (~logger: Pino.t): Ecosystem.t => {
36
61
  logger,
37
62
  toEvent: eventItem => eventItem.payload->(Utils.magic: Internal.eventPayload => Internal.event),
38
63
  toEventLogger: eventItem => {
39
- let instruction =
40
- eventItem.payload->(Utils.magic: Internal.eventPayload => Envio.svmInstruction)
64
+ let eventConfig =
65
+ eventItem.onEventRegistration.eventConfig->(
66
+ Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig
67
+ )
41
68
  Logging.createChildFrom(
42
69
  ~logger,
43
70
  ~params={
44
- "program": eventItem.onEventRegistration.eventConfig.contractName,
45
- "instruction": eventItem.onEventRegistration.eventConfig.name,
71
+ "program": eventConfig.contractName,
72
+ "instruction": eventConfig.name,
46
73
  "chainId": eventItem.chainId,
47
74
  "slot": eventItem.blockNumber,
48
- "programId": instruction.programId,
75
+ "programId": eventConfig.programId,
49
76
  },
50
77
  )
51
78
  },
@@ -9,6 +9,7 @@ import * as Internal from "../Internal.res.mjs";
9
9
  import * as BlockStore from "./BlockStore.res.mjs";
10
10
  import * as Performance from "../bindings/Performance.res.mjs";
11
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
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";
13
14
  import * as TransactionStore from "./TransactionStore.res.mjs";
14
15
 
@@ -25,6 +26,32 @@ let blockFields = [
25
26
 
26
27
  let eventBlockFieldMask = BlockStore.makeMaskFn(blockFields);
27
28
 
29
+ function attachAccountActivities(payload, tx) {
30
+ let match = payload["accounts"];
31
+ let match$1 = tx["accountActivities"];
32
+ if (match === undefined) {
33
+ return;
34
+ }
35
+ if (match$1 === undefined) {
36
+ return;
37
+ }
38
+ let accounts = Primitive_option.valFromOption(match);
39
+ if (Array.isArray(accounts)) {
40
+ return;
41
+ }
42
+ let byAddress = {};
43
+ Primitive_option.valFromOption(match$1).forEach(activity => {
44
+ byAddress[activity.address] = activity;
45
+ });
46
+ Object.values(accounts).forEach(account => {
47
+ let activity = byAddress[account.address];
48
+ if (activity !== undefined) {
49
+ account["activity"] = activity;
50
+ return;
51
+ }
52
+ });
53
+ }
54
+
28
55
  function make(logger) {
29
56
  return {
30
57
  name: "svm",
@@ -37,13 +64,13 @@ function make(logger) {
37
64
  logger: logger,
38
65
  toEvent: eventItem => eventItem.payload,
39
66
  toEventLogger: eventItem => {
40
- let instruction = eventItem.payload;
67
+ let eventConfig = eventItem.onEventRegistration.eventConfig;
41
68
  return Logging.createChildFrom(logger, {
42
- program: eventItem.onEventRegistration.eventConfig.contractName,
43
- instruction: eventItem.onEventRegistration.eventConfig.name,
69
+ program: eventConfig.contractName,
70
+ instruction: eventConfig.name,
44
71
  chainId: eventItem.chainId,
45
72
  slot: eventItem.blockNumber,
46
- programId: instruction.programId
73
+ programId: eventConfig.programId
47
74
  });
48
75
  },
49
76
  toRawEvent: param => Stdlib_JsError.throwWithMessage("Raw events are not supported for SVM")
@@ -96,6 +123,7 @@ export {
96
123
  eventTransactionFieldMask,
97
124
  blockFields,
98
125
  eventBlockFieldMask,
126
+ attachAccountActivities,
99
127
  make,
100
128
  GetFinalizedSlot,
101
129
  makeRPCSource,
@@ -27,14 +27,15 @@ module Registration = {
27
27
  // Earliest slot this registration accepts; `None` is unrestricted.
28
28
  startBlock: option<int>,
29
29
  discriminator?: string,
30
- discriminatorByteLen: int,
31
30
  isInner?: bool,
32
- includeLogs: bool,
33
31
  // DNF: outer array is OR of AND-groups.
34
32
  accountFilters: array<array<accountFilter>>,
35
33
  // camelCase Internal.svmTransactionField / svmBlockField names.
36
34
  transactionFields: array<string>,
37
35
  blockFields: array<string>,
36
+ accountActivityFields: array<string>,
37
+ logFields: array<string>,
38
+ instructionFields: array<string>,
38
39
  // Borsh schema pieces; empty accounts + absent argsJson = no schema.
39
40
  accounts: array<string>,
40
41
  argsJson?: string,
@@ -55,9 +56,7 @@ module Registration = {
55
56
  isWildcard: reg.isWildcard,
56
57
  startBlock: reg.startBlock,
57
58
  discriminator: ?eventConfig.discriminator,
58
- discriminatorByteLen: eventConfig.discriminatorByteLen,
59
59
  isInner: ?eventConfig.isInner,
60
- includeLogs: eventConfig.includeLogs,
61
60
  accountFilters: eventConfig.accountFilters->Array.map(group =>
62
61
  group->Array.map(
63
62
  (filter): accountFilter => {
@@ -68,6 +67,9 @@ module Registration = {
68
67
  ),
69
68
  transactionFields: reg.fieldSelection.transactionFields->Utils.Set.toArray,
70
69
  blockFields: reg.fieldSelection.blockFields->Utils.Set.toArray,
70
+ accountActivityFields: reg.fieldSelection.accountActivityFields->Utils.Set.toArray,
71
+ logFields: reg.fieldSelection.logFields->Utils.Set.toArray,
72
+ instructionFields: reg.fieldSelection.instructionFields->Utils.Set.toArray,
71
73
  accounts: eventConfig.accounts,
72
74
  argsJson: ?switch eventConfig.args {
73
75
  | JSON.Null => None
@@ -142,16 +144,7 @@ module ResponseTypes = {
142
144
  type block = {
143
145
  slot: int,
144
146
  blockhash: string,
145
- blockTime?: int,
146
- }
147
-
148
- /// Borsh-decoded view attached by the Rust client. `argsJson`/`accountsJson`
149
- /// are stringified to side-step napi-rs's lack of native JSON passthrough.
150
- type decodedInstruction = {
151
- name: string,
152
- argsJson: string,
153
- accountsJson: string,
154
- extraAccounts: array<string>,
147
+ blockTime: Null.t<int>,
155
148
  }
156
149
 
157
150
  type instructionCall = {
@@ -162,16 +155,16 @@ module ResponseTypes = {
162
155
  executingAccount: string,
163
156
  accountArguments: array<string>,
164
157
  data: string,
165
- d1?: string,
166
- d2?: string,
167
- d4?: string,
168
- d8?: string,
158
+ d1: Null.t<string>,
159
+ d2: Null.t<string>,
160
+ d4: Null.t<string>,
161
+ d8: Null.t<string>,
169
162
  isInner: bool,
170
163
  /** Success of the parent transaction, not of this invocation. */
171
164
  txSuccess: bool,
172
165
  /** Per-invocation failure reason (e.g. "custom program error: 0x1"). */
173
- error?: string,
174
- computeUnitsConsumed?: bigint,
166
+ error: Null.t<string>,
167
+ computeUnitsConsumed: Null.t<bigint>,
175
168
  }
176
169
 
177
170
  type queryResponseData = {
@@ -204,9 +197,11 @@ module EventItems = {
204
197
  clientFilteredContracts: option<array<string>>,
205
198
  }
206
199
 
200
+ // NAPI encodes Rust `None` as `null`, never `undefined`, so an unselected
201
+ // key arrives as an explicit null rather than a missing field.
207
202
  type log = {
208
- kind: string,
209
- message: string,
203
+ kind: Null.t<string>,
204
+ message: Null.t<string>,
210
205
  }
211
206
 
212
207
  // One routed instruction; `block` and `transaction` are materialised from
@@ -215,18 +210,17 @@ module EventItems = {
215
210
  onEventRegistrationIndex: int,
216
211
  slot: int,
217
212
  transactionIndex: int,
218
- instructionAddress: array<int>,
213
+ path: array<int>,
219
214
  programId: string,
220
215
  accounts: array<string>,
221
216
  data: string,
222
- d1?: string,
223
- d2?: string,
224
- d4?: string,
225
- d8?: string,
226
217
  isInner: bool,
227
- decoded?: ResponseTypes.decodedInstruction,
228
- // Present only when the routed registration opted in via `includeLogs`.
229
- logs?: array<log>,
218
+ // Borsh-decoded args as a JSON object literal, `{}` when the routed
219
+ // registration reads no args. An instruction the schema rejects is dropped
220
+ // in Rust, so a selected `args` is always decoded.
221
+ argsJson: string,
222
+ // Non-null only when the routed registration selected `fields.log`.
223
+ logs: Null.t<array<log>>,
230
224
  }
231
225
 
232
226
  type response = {
@@ -20,15 +20,16 @@ function fromOnEventRegistrations(onEventRegistrations) {
20
20
  isWildcard: reg.isWildcard,
21
21
  startBlock: reg.startBlock,
22
22
  discriminator: eventConfig.discriminator,
23
- discriminatorByteLen: eventConfig.discriminatorByteLen,
24
23
  isInner: eventConfig.isInner,
25
- includeLogs: eventConfig.includeLogs,
26
24
  accountFilters: eventConfig.accountFilters.map(group => group.map(filter => ({
27
25
  position: filter.position,
28
26
  values: filter.values
29
27
  }))),
30
28
  transactionFields: Array.from(reg.fieldSelection.transactionFields),
31
29
  blockFields: Array.from(reg.fieldSelection.blockFields),
30
+ accountActivityFields: Array.from(reg.fieldSelection.accountActivityFields),
31
+ logFields: Array.from(reg.fieldSelection.logFields),
32
+ instructionFields: Array.from(reg.fieldSelection.instructionFields),
32
33
  accounts: eventConfig.accounts,
33
34
  argsJson: tmp,
34
35
  definedTypesJson: tmp$1