envio 3.9.0 → 3.10.0-subgraph

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 (143) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +41 -8
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +61 -44
  10. package/src/Config.res.mjs +56 -19
  11. package/src/Core.res +19 -0
  12. package/src/Core.res.mjs +11 -0
  13. package/src/CrossChainState.res +15 -23
  14. package/src/CrossChainState.res.mjs +10 -18
  15. package/src/CrossChainState.resi +4 -1
  16. package/src/Ecosystem.res +7 -2
  17. package/src/Envio.res +6 -4
  18. package/src/EventConfigBuilder.res +169 -26
  19. package/src/EventConfigBuilder.res.mjs +124 -15
  20. package/src/EventProcessing.res +6 -6
  21. package/src/EventProcessing.res.mjs +8 -6
  22. package/src/HandlerLoader.res +20 -8
  23. package/src/HandlerLoader.res.mjs +11 -2
  24. package/src/HandlerRegister.res +21 -4
  25. package/src/HandlerRegister.res.mjs +24 -5
  26. package/src/InMemoryStore.res +9 -7
  27. package/src/InMemoryStore.res.mjs +3 -3
  28. package/src/IndexerState.res +43 -6
  29. package/src/IndexerState.res.mjs +36 -7
  30. package/src/IndexerState.resi +4 -4
  31. package/src/Internal.res +16 -17
  32. package/src/Metrics.res +72 -6
  33. package/src/Metrics.res.mjs +30 -2
  34. package/src/Persistence.res +36 -30
  35. package/src/Persistence.res.mjs +1 -20
  36. package/src/PgStorage.res +104 -56
  37. package/src/PgStorage.res.mjs +57 -35
  38. package/src/PruneStaleHistory.res +85 -52
  39. package/src/PruneStaleHistory.res.mjs +63 -37
  40. package/src/Rollback.res +68 -45
  41. package/src/Rollback.res.mjs +38 -19
  42. package/src/RollbackCommit.res +7 -9
  43. package/src/RollbackCommit.res.mjs +5 -6
  44. package/src/SafeCheckpointTracking.res +11 -11
  45. package/src/SafeCheckpointTracking.res.mjs +7 -6
  46. package/src/SimulateItems.res +33 -18
  47. package/src/SimulateItems.res.mjs +26 -22
  48. package/src/Sink.res +38 -26
  49. package/src/Sink.res.mjs +21 -16
  50. package/src/TestIndexer.res +5 -5
  51. package/src/TestIndexer.res.mjs +1 -1
  52. package/src/UserContext.res +358 -51
  53. package/src/UserContext.res.mjs +271 -35
  54. package/src/Utils.res +170 -0
  55. package/src/Utils.res.mjs +153 -0
  56. package/src/Writing.res +2 -2
  57. package/src/Writing.res.mjs +1 -1
  58. package/src/bindings/ClickHouse.res +366 -698
  59. package/src/bindings/ClickHouse.res.mjs +371 -421
  60. package/src/bindings/ClickHouseSink.res +337 -0
  61. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  62. package/src/bindings/EventSource.res +8 -2
  63. package/src/bindings/NodeJs.res +6 -0
  64. package/src/bindings/Vitest.res +23 -0
  65. package/src/bindings/Vitest.res.mjs +3 -0
  66. package/src/bindings/WebSocket.res +9 -10
  67. package/src/db/CheckpointBounds.res +53 -0
  68. package/src/db/CheckpointBounds.res.mjs +44 -0
  69. package/src/db/EntityFilter.res +36 -17
  70. package/src/db/EntityFilter.res.mjs +25 -14
  71. package/src/db/EntityHistory.res +47 -22
  72. package/src/db/EntityHistory.res.mjs +19 -12
  73. package/src/db/InternalTable.res +84 -35
  74. package/src/db/InternalTable.res.mjs +56 -23
  75. package/src/db/RollbackFloors.res +55 -0
  76. package/src/db/RollbackFloors.res.mjs +92 -0
  77. package/src/db/Table.res +22 -4
  78. package/src/db/Table.res.mjs +30 -9
  79. package/src/sources/Evm.res +2 -0
  80. package/src/sources/Evm.res.mjs +2 -0
  81. package/src/sources/EvmHyperSyncSource.res +3 -3
  82. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  83. package/src/sources/EvmRpcWs.res +124 -0
  84. package/src/sources/EvmRpcWs.res.mjs +117 -0
  85. package/src/sources/Fuel.res +2 -0
  86. package/src/sources/Fuel.res.mjs +2 -0
  87. package/src/sources/FuelHyperSyncSource.res +1 -1
  88. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  89. package/src/sources/HeightFeed.res +568 -0
  90. package/src/sources/HeightFeed.res.mjs +452 -0
  91. package/src/sources/HeightStream.res +257 -0
  92. package/src/sources/HeightStream.res.mjs +194 -0
  93. package/src/sources/HyperSync.res +5 -0
  94. package/src/sources/HyperSync.res.mjs +3 -0
  95. package/src/sources/HyperSync.resi +4 -0
  96. package/src/sources/HyperSyncSSE.res +49 -0
  97. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  98. package/src/sources/RpcSource.res +1 -1
  99. package/src/sources/RpcSource.res.mjs +2 -2
  100. package/src/sources/Source.res +54 -1
  101. package/src/sources/Source.res.mjs +25 -0
  102. package/src/sources/SourceManager.res +220 -227
  103. package/src/sources/SourceManager.res.mjs +139 -164
  104. package/src/sources/SourceManager.resi +8 -0
  105. package/src/sources/Svm.res +2 -0
  106. package/src/sources/Svm.res.mjs +2 -0
  107. package/src/sources/SvmHyperSyncClient.res +70 -127
  108. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  109. package/src/sources/SvmHyperSyncSource.res +18 -15
  110. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  111. package/src/subgraph/blocks.ts +176 -0
  112. package/src/subgraph/calls.ts +217 -0
  113. package/src/subgraph/conformance.ts +102 -0
  114. package/src/subgraph/division.ts +133 -0
  115. package/src/subgraph/errors.ts +90 -0
  116. package/src/subgraph/graph-ts-types/VERSION +2 -0
  117. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  118. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  119. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  120. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  121. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  122. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  123. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  124. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  125. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  126. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  127. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  128. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  129. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  130. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  131. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  132. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  133. package/src/subgraph/graph-ts.ts +1948 -0
  134. package/src/subgraph/hosts.ts +148 -0
  135. package/src/subgraph/runtime.ts +863 -0
  136. package/src/subgraph/scope.ts +66 -0
  137. package/svm.schema.json +6 -81
  138. package/src/MemoryStorage.res +0 -729
  139. package/src/MemoryStorage.res.mjs +0 -586
  140. package/src/sources/HyperSyncHeightStream.res +0 -129
  141. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  142. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  143. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -14,19 +14,14 @@ module Registration = {
14
14
  values: array<string>,
15
15
  }
16
16
 
17
- // The full per-(instruction, chain) registration passed to the Rust client
18
- // at construction: routing identity, the fetch state queries are built
19
- // from, and the Borsh schema pieces the client builds decoders from.
17
+ // One `onInstruction`/`contractRegister` binding of an instruction on a
18
+ // chain: the handler-specific routing state and the fetch state queries are
19
+ // built from. Chain-scoped `index` is echoed back on routed items.
20
20
  type input = {
21
- // Chain-scoped sequential registration index, echoed back on routed items.
22
21
  index: int,
23
- instructionName: string,
24
- contractName: string,
25
- programId: string,
26
22
  isWildcard: bool,
27
23
  // Earliest slot this registration accepts; `None` is unrestricted.
28
24
  startBlock: option<int>,
29
- discriminator?: string,
30
25
  isInner?: bool,
31
26
  // DNF: outer array is OR of AND-groups.
32
27
  accountFilters: array<array<accountFilter>>,
@@ -36,28 +31,73 @@ module Registration = {
36
31
  accountActivityFields: array<string>,
37
32
  logFields: array<string>,
38
33
  instructionFields: array<string>,
39
- // Borsh schema pieces; empty accounts + absent argsJson = no schema.
40
- accounts: array<string>,
34
+ }
35
+
36
+ // One config instruction: its identity (data prefix of any length; absent
37
+ // matches every instruction of the program) and Borsh layout, shared by
38
+ // every registration bound to it.
39
+ type instruction = {
40
+ name: string,
41
+ discriminator?: string,
41
42
  argsJson?: string,
43
+ registrations: array<input>,
44
+ }
45
+
46
+ type program = {
47
+ // The config's program name.
48
+ name: string,
49
+ programId: string,
42
50
  definedTypesJson?: string,
51
+ instructions: array<instruction>,
43
52
  }
44
53
 
54
+ // Groups a chain's registrations under the config instruction each was
55
+ // built from, so the Rust client holds one layout per instruction rather
56
+ // than a copy per registration.
45
57
  let fromOnEventRegistrations = (
46
58
  onEventRegistrations: array<Internal.svmOnEventRegistration>,
47
- ): array<input> =>
48
- onEventRegistrations->Array.map(reg => {
59
+ ): array<program> => {
60
+ let programs: array<program> = []
61
+ onEventRegistrations->Array.forEach(reg => {
49
62
  let eventConfig =
50
63
  reg.eventConfig->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
51
- {
64
+ let program = switch programs->Array.find(p => p.name === eventConfig.contractName) {
65
+ | Some(program) => program
66
+ | None =>
67
+ let program = {
68
+ name: eventConfig.contractName,
69
+ programId: eventConfig.programId->SvmTypes.Pubkey.toString,
70
+ definedTypesJson: ?switch eventConfig.definedTypes {
71
+ | JSON.Null => None
72
+ | definedTypes => Some(definedTypes->JSON.stringify)
73
+ },
74
+ instructions: [],
75
+ }
76
+ programs->Array.push(program)->ignore
77
+ program
78
+ }
79
+ let instruction = switch program.instructions->Array.find(i => i.name === eventConfig.name) {
80
+ | Some(instruction) => instruction
81
+ | None =>
82
+ let instruction = {
83
+ name: eventConfig.name,
84
+ discriminator: ?eventConfig.discriminator,
85
+ argsJson: ?switch eventConfig.args {
86
+ | JSON.Null => None
87
+ | args => Some(args->JSON.stringify)
88
+ },
89
+ registrations: [],
90
+ }
91
+ program.instructions->Array.push(instruction)->ignore
92
+ instruction
93
+ }
94
+ instruction.registrations
95
+ ->Array.push({
52
96
  index: reg.index,
53
- instructionName: eventConfig.name,
54
- contractName: eventConfig.contractName,
55
- programId: eventConfig.programId->SvmTypes.Pubkey.toString,
56
97
  isWildcard: reg.isWildcard,
57
98
  startBlock: reg.startBlock,
58
- discriminator: ?eventConfig.discriminator,
59
- isInner: ?eventConfig.isInner,
60
- accountFilters: eventConfig.accountFilters->Array.map(group =>
99
+ isInner: ?reg.isInner,
100
+ accountFilters: reg.accountFilters->Array.map(group =>
61
101
  group->Array.map(
62
102
  (filter): accountFilter => {
63
103
  position: filter.position,
@@ -70,71 +110,10 @@ module Registration = {
70
110
  accountActivityFields: reg.fieldSelection.accountActivityFields->Utils.Set.toArray,
71
111
  logFields: reg.fieldSelection.logFields->Utils.Set.toArray,
72
112
  instructionFields: reg.fieldSelection.instructionFields->Utils.Set.toArray,
73
- accounts: eventConfig.accounts,
74
- argsJson: ?switch eventConfig.args {
75
- | JSON.Null => None
76
- | args => Some(args->JSON.stringify)
77
- },
78
- definedTypesJson: ?switch eventConfig.definedTypes {
79
- | JSON.Null => None
80
- | definedTypes => Some(definedTypes->JSON.stringify)
81
- },
82
- }
113
+ })
114
+ ->ignore
83
115
  })
84
- }
85
-
86
- module QueryTypes = {
87
- type blockField =
88
- | @as("slot") Slot
89
- | @as("blockhash") Blockhash
90
- | @as("parent_slot") ParentSlot
91
- | @as("parent_blockhash") ParentBlockhash
92
- | @as("block_time") BlockTime
93
- | @as("block_height") BlockHeight
94
-
95
- type transactionField =
96
- | @as("slot") Slot
97
- | @as("transaction_index") TransactionIndex
98
- | @as("signatures") Signatures
99
- | @as("fee_payer") FeePayer
100
- | @as("success") Success
101
- | @as("err") Err
102
- | @as("fee") Fee
103
- | @as("compute_units_consumed") ComputeUnitsConsumed
104
- | @as("account_keys") AccountKeys
105
- | @as("recent_blockhash") RecentBlockhash
106
- | @as("version") Version
107
- | @as("loaded_addresses_writable") LoadedAddressesWritable
108
- | @as("loaded_addresses_readonly") LoadedAddressesReadonly
109
-
110
- type fieldSelection = {block?: array<blockField>, transaction?: array<transactionField>}
111
-
112
- /** Filter for selecting instruction calls. All non-empty fields are AND-ed:
113
- an instruction must match at least one value in every non-empty field.
114
-
115
- Discriminator filters (d1..d8) take hex-encoded byte prefixes ("0x" optional).
116
- Account filters (a0..a9) take base58 pubkey strings. */
117
- type instructionSelection = {
118
- executingAccount?: array<string>,
119
- d1?: array<string>,
120
- d2?: array<string>,
121
- d4?: array<string>,
122
- d8?: array<string>,
123
- isInner?: bool,
124
- /** Success of the parent transaction; absent matches both. */
125
- txSuccess?: bool,
126
- }
127
-
128
- // The `get` query surface, used only for block-data range queries; event
129
- // fetching goes through `getEventItems`, which builds its query in Rust.
130
- type query = {
131
- fromSlot: int,
132
- toSlot?: int,
133
- instructionCalls?: array<instructionSelection>,
134
- includeAllBlocks?: bool,
135
- fields?: fieldSelection,
136
- maxNumBlocks?: int,
137
- maxNumInstructions?: int,
116
+ programs
138
117
  }
139
118
  }
140
119
 
@@ -146,37 +125,6 @@ module ResponseTypes = {
146
125
  blockhash: string,
147
126
  blockTime: Null.t<int>,
148
127
  }
149
-
150
- type instructionCall = {
151
- slot: int,
152
- transactionIndex: int,
153
- instructionAddress: array<int>,
154
- /** The invoked program's account. */
155
- executingAccount: string,
156
- accountArguments: array<string>,
157
- data: string,
158
- d1: Null.t<string>,
159
- d2: Null.t<string>,
160
- d4: Null.t<string>,
161
- d8: Null.t<string>,
162
- isInner: bool,
163
- /** Success of the parent transaction, not of this invocation. */
164
- txSuccess: bool,
165
- /** Per-invocation failure reason (e.g. "custom program error: 0x1"). */
166
- error: Null.t<string>,
167
- computeUnitsConsumed: Null.t<bigint>,
168
- }
169
-
170
- type queryResponseData = {
171
- blocks: array<block>,
172
- instructionCalls: array<instructionCall>,
173
- }
174
-
175
- type queryResponse = {
176
- nextSlot: int,
177
- responseBytes: int,
178
- data: queryResponseData,
179
- }
180
128
  }
181
129
 
182
130
  module EventItems = {
@@ -213,12 +161,12 @@ module EventItems = {
213
161
  path: array<int>,
214
162
  programId: string,
215
163
  accounts: array<string>,
216
- data: string,
164
+ data: Uint8Array.t,
217
165
  isInner: bool,
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,
166
+ // Borsh-decoded args as a JS value tree (wide integers as bigint).
167
+ // Non-null exactly when the routed registration selected `args`: an
168
+ // instruction its layout rejects is dropped in Rust.
169
+ args: Null.t<unknown>,
222
170
  // Non-null only when the routed registration selected `fields.log`.
223
171
  logs: Null.t<array<log>>,
224
172
  }
@@ -234,16 +182,11 @@ module EventItems = {
234
182
  }
235
183
  }
236
184
 
237
- type query = QueryTypes.query
238
- type queryResponse = ResponseTypes.queryResponse
239
-
240
185
  type t = {
241
186
  getHeight: unit => promise<int>,
242
187
  // Block-hash query construction, pagination, and cursor-backed skipped-slot
243
188
  // coverage live in Rust.
244
189
  getBlockHashes: (~blockNumbers: array<int>) => promise<(BlockStore.t, array<RequestStat.t>)>,
245
- // Block-data range queries only; the store pages it returns are empty.
246
- get: (~query: query) => promise<(queryResponse, TransactionStore.t, BlockStore.t)>,
247
190
  // Returns the routed items plus pages of raw transactions and blocks (kept
248
191
  // in Rust), keyed by (slot, transactionIndex) / slot, materialised at batch
249
192
  // prep.
@@ -258,7 +201,7 @@ external classFromConfig: (
258
201
  Core.svmHyperSyncClientCtor,
259
202
  cfg,
260
203
  string,
261
- array<Registration.input>,
204
+ array<Registration.program>,
262
205
  AddressStore.t,
263
206
  ) => t = "fromConfig"
264
207
 
@@ -268,7 +211,7 @@ let make = (
268
211
  ~httpReqTimeoutMillis=?,
269
212
  ~retryBaseMs=?,
270
213
  ~retryCeilingMs=?,
271
- ~eventRegistrations=[],
214
+ ~programs=[],
272
215
  ~addressStore,
273
216
  ) => {
274
217
  let envioVersion = Utils.EnvioPackage.value.version
@@ -281,7 +224,7 @@ let make = (
281
224
  ?retryCeilingMs,
282
225
  },
283
226
  `hyperindex/${envioVersion}`,
284
- eventRegistrations,
227
+ programs,
285
228
  addressStore,
286
229
  )
287
230
  }
@@ -4,24 +4,55 @@ import * as Core from "../Core.res.mjs";
4
4
  import * as Utils from "../Utils.res.mjs";
5
5
 
6
6
  function fromOnEventRegistrations(onEventRegistrations) {
7
- return onEventRegistrations.map(reg => {
7
+ let programs = [];
8
+ onEventRegistrations.forEach(reg => {
8
9
  let eventConfig = reg.eventConfig;
9
- let args = eventConfig.args;
10
- let tmp;
11
- tmp = args === null ? undefined : JSON.stringify(args);
12
- let definedTypes = eventConfig.definedTypes;
13
- let tmp$1;
14
- tmp$1 = definedTypes === null ? undefined : JSON.stringify(definedTypes);
15
- return {
10
+ let program = programs.find(p => p.name === eventConfig.contractName);
11
+ let program$1;
12
+ if (program !== undefined) {
13
+ program$1 = program;
14
+ } else {
15
+ let definedTypes = eventConfig.definedTypes;
16
+ let tmp;
17
+ tmp = definedTypes === null ? undefined : JSON.stringify(definedTypes);
18
+ let program_name = eventConfig.contractName;
19
+ let program_programId = eventConfig.programId;
20
+ let program_instructions = [];
21
+ let program$2 = {
22
+ name: program_name,
23
+ programId: program_programId,
24
+ definedTypesJson: tmp,
25
+ instructions: program_instructions
26
+ };
27
+ programs.push(program$2);
28
+ program$1 = program$2;
29
+ }
30
+ let instruction = program$1.instructions.find(i => i.name === eventConfig.name);
31
+ let instruction$1;
32
+ if (instruction !== undefined) {
33
+ instruction$1 = instruction;
34
+ } else {
35
+ let args = eventConfig.args;
36
+ let tmp$1;
37
+ tmp$1 = args === null ? undefined : JSON.stringify(args);
38
+ let instruction_name = eventConfig.name;
39
+ let instruction_discriminator = eventConfig.discriminator;
40
+ let instruction_registrations = [];
41
+ let instruction$2 = {
42
+ name: instruction_name,
43
+ discriminator: instruction_discriminator,
44
+ argsJson: tmp$1,
45
+ registrations: instruction_registrations
46
+ };
47
+ program$1.instructions.push(instruction$2);
48
+ instruction$1 = instruction$2;
49
+ }
50
+ instruction$1.registrations.push({
16
51
  index: reg.index,
17
- instructionName: eventConfig.name,
18
- contractName: eventConfig.contractName,
19
- programId: eventConfig.programId,
20
52
  isWildcard: reg.isWildcard,
21
53
  startBlock: reg.startBlock,
22
- discriminator: eventConfig.discriminator,
23
- isInner: eventConfig.isInner,
24
- accountFilters: eventConfig.accountFilters.map(group => group.map(filter => ({
54
+ isInner: reg.isInner,
55
+ accountFilters: reg.accountFilters.map(group => group.map(filter => ({
25
56
  position: filter.position,
26
57
  values: filter.values
27
58
  }))),
@@ -29,26 +60,22 @@ function fromOnEventRegistrations(onEventRegistrations) {
29
60
  blockFields: Array.from(reg.fieldSelection.blockFields),
30
61
  accountActivityFields: Array.from(reg.fieldSelection.accountActivityFields),
31
62
  logFields: Array.from(reg.fieldSelection.logFields),
32
- instructionFields: Array.from(reg.fieldSelection.instructionFields),
33
- accounts: eventConfig.accounts,
34
- argsJson: tmp,
35
- definedTypesJson: tmp$1
36
- };
63
+ instructionFields: Array.from(reg.fieldSelection.instructionFields)
64
+ });
37
65
  });
66
+ return programs;
38
67
  }
39
68
 
40
69
  let Registration = {
41
70
  fromOnEventRegistrations: fromOnEventRegistrations
42
71
  };
43
72
 
44
- let QueryTypes = {};
45
-
46
73
  let ResponseTypes = {};
47
74
 
48
75
  let EventItems = {};
49
76
 
50
- function make(url, apiToken, httpReqTimeoutMillis, retryBaseMs, retryCeilingMs, eventRegistrationsOpt, addressStore) {
51
- let eventRegistrations = eventRegistrationsOpt !== undefined ? eventRegistrationsOpt : [];
77
+ function make(url, apiToken, httpReqTimeoutMillis, retryBaseMs, retryCeilingMs, programsOpt, addressStore) {
78
+ let programs = programsOpt !== undefined ? programsOpt : [];
52
79
  let envioVersion = Utils.EnvioPackage.value.version;
53
80
  return Core.getAddon().SvmHyperSyncClient.fromConfig({
54
81
  url: url,
@@ -56,12 +83,11 @@ function make(url, apiToken, httpReqTimeoutMillis, retryBaseMs, retryCeilingMs,
56
83
  httpReqTimeoutMillis: httpReqTimeoutMillis,
57
84
  retryBaseMs: retryBaseMs,
58
85
  retryCeilingMs: retryCeilingMs
59
- }, `hyperindex/` + envioVersion, eventRegistrations, addressStore);
86
+ }, `hyperindex/` + envioVersion, programs, addressStore);
60
87
  }
61
88
 
62
89
  export {
63
90
  Registration,
64
- QueryTypes,
65
91
  ResponseTypes,
66
92
  EventItems,
67
93
  make,
@@ -10,10 +10,9 @@ type options = {
10
10
  addressStore: AddressStore.t,
11
11
  }
12
12
 
13
- let namedAccounts = (
14
- ~idlNames: array<string>,
15
- ~accountArguments: array<string>,
16
- ): dict<Envio.svmInstructionAccount> => {
13
+ let namedAccounts = (~idlNames: array<string>, ~accountArguments: array<string>): dict<
14
+ Envio.svmInstructionAccount,
15
+ > => {
17
16
  let out = Dict.make()
18
17
  idlNames->Array.forEachWithIndex((name, i) =>
19
18
  switch accountArguments->Array.get(i) {
@@ -32,7 +31,10 @@ let namedAccounts = (
32
31
  out
33
32
  }
34
33
 
35
- let selectedLog = (log: SvmHyperSyncClient.EventItems.log, ~logFields: Utils.Set.t<string>): Envio.svmLog => {
34
+ let selectedLog = (
35
+ log: SvmHyperSyncClient.EventItems.log,
36
+ ~logFields: Utils.Set.t<string>,
37
+ ): Envio.svmLog => {
36
38
  let out = Dict.make()
37
39
  if logFields->Utils.Set.has("kind") {
38
40
  switch log.kind->Null.toOption {
@@ -63,9 +65,11 @@ let toSvmInstruction = (
63
65
  ~fieldSelection: Internal.fieldSelection,
64
66
  ): Envio.svmInstruction => {
65
67
  let hasSelection = name => fieldSelection.instructionFields->Utils.Set.has(name)
68
+ // A program-wide registration has no configured discriminator, so the whole
69
+ // data stands in, hex-encoded like a configured one would be.
66
70
  let discriminator = switch eventConfig.discriminator {
67
71
  | Some(d) => d
68
- | None => item.data
72
+ | None => "0x" ++ item.data->NodeJs.Buffer.fromUint8Array->NodeJs.Buffer.toHex
69
73
  }
70
74
  let out = Dict.make()
71
75
  out->setField("programName", programName)
@@ -83,8 +87,9 @@ let toSvmInstruction = (
83
87
  if hasSelection("isInner") {
84
88
  out->setField("isInner", item.isInner)
85
89
  }
86
- if hasSelection("args") {
87
- out->setField("args", item.argsJson->JSON.parseOrThrow)
90
+ switch item.args->Null.toOption {
91
+ | Some(args) => out->setField("args", args)
92
+ | None => ()
88
93
  }
89
94
  if hasSelection("accounts") {
90
95
  out->setField(
@@ -125,9 +130,7 @@ let make = (
125
130
  ~url=endpointUrl,
126
131
  ~apiToken?,
127
132
  ~httpReqTimeoutMillis=clientTimeoutMillis,
128
- ~eventRegistrations=SvmHyperSyncClient.Registration.fromOnEventRegistrations(
129
- onEventRegistrations,
130
- ),
133
+ ~programs=SvmHyperSyncClient.Registration.fromOnEventRegistrations(onEventRegistrations),
131
134
  ~addressStore,
132
135
  )
133
136
 
@@ -198,7 +201,7 @@ let make = (
198
201
  ~fieldSelection=onEventRegistration.fieldSelection,
199
202
  )
200
203
  Internal.Event({
201
- onEventRegistration,
204
+ onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
202
205
  chainId,
203
206
  blockNumber: item.slot,
204
207
  // A slot orders by `(transactionIndex, path)` — the
@@ -236,15 +239,15 @@ let make = (
236
239
 
237
240
  // Called through the client rather than passed as a value: the client is a
238
241
  // napi class, so a detached method reference loses the instance it belongs to.
239
- let getBlockHashes = HyperSync.makeGetBlockHashes(
240
- ~query=(~blockNumbers) => client.getBlockHashes(~blockNumbers),
242
+ let getBlockHashes = HyperSync.makeGetBlockHashes(~query=(~blockNumbers) =>
243
+ client.getBlockHashes(~blockNumbers)
241
244
  )
242
245
 
243
246
  {
244
247
  name,
245
248
  sourceFor: Sync,
246
249
  chainId,
247
- pollingInterval: 1000,
250
+ pollingInterval: HyperSync.pollingInterval,
248
251
  poweredByHyperSync: true,
249
252
  getBlockHashes,
250
253
  getHeightOrThrow: async () => {
@@ -48,7 +48,7 @@ function setField(out, name, value) {
48
48
 
49
49
  function toSvmInstruction(item, programName, instructionName, eventConfig, fieldSelection) {
50
50
  let d = eventConfig.discriminator;
51
- let discriminator = d !== undefined ? d : item.data;
51
+ let discriminator = d !== undefined ? d : "0x" + Buffer.from(item.data).toString("hex");
52
52
  let out = {};
53
53
  setField(out, "programName", programName);
54
54
  setField(out, "instructionName", instructionName);
@@ -65,8 +65,9 @@ function toSvmInstruction(item, programName, instructionName, eventConfig, field
65
65
  if (fieldSelection.instructionFields.has("isInner")) {
66
66
  setField(out, "isInner", item.isInner);
67
67
  }
68
- if (fieldSelection.instructionFields.has("args")) {
69
- setField(out, "args", JSON.parse(item.argsJson));
68
+ let args = item.args;
69
+ if (args !== null) {
70
+ setField(out, "args", args);
70
71
  }
71
72
  if (fieldSelection.instructionFields.has("accounts")) {
72
73
  setField(out, "accounts", namedAccounts(eventConfig.accounts, item.accounts));
@@ -163,7 +164,7 @@ function make(param) {
163
164
  sourceFor: "Sync",
164
165
  chainId: chainId,
165
166
  poweredByHyperSync: true,
166
- pollingInterval: 1000,
167
+ pollingInterval: HyperSync.pollingInterval,
167
168
  getBlockHashes: getBlockHashes,
168
169
  getHeightOrThrow: async () => {
169
170
  let timer = Performance.now();
@@ -0,0 +1,176 @@
1
+ /**
2
+ * A block handler's `block.timestamp`.
3
+ *
4
+ * envio hands a block handler only the block number, so the timestamp has to be
5
+ * fetched. Every handler invocation in a batch asks within the same microtask,
6
+ * so the requests are collected and answered by a single HyperSync range query
7
+ * rather than one round trip per block. An RPC endpoint, if one is configured,
8
+ * is the fallback for whatever HyperSync couldn't answer.
9
+ */
10
+
11
+ import { createPublicClient, fallback, http } from "viem";
12
+
13
+ const API_TOKEN_ENV_VAR = "ENVIO_API_TOKEN";
14
+
15
+ const hypersyncUrl = (chainId: number) => `https://${chainId}.hypersync.xyz/query`;
16
+
17
+ type Waiter = {
18
+ resolve: (timestamp: bigint) => void;
19
+ reject: (error: unknown) => void;
20
+ };
21
+
22
+ /** Blocks asked for since the last flush, by chain then block number. */
23
+ let pending = new Map<number, Map<number, Waiter[]>>();
24
+ let flushScheduled = false;
25
+
26
+ let rpcUrls: string[] = [];
27
+ let client: ReturnType<typeof createPublicClient> | null = null;
28
+
29
+ export function configureBlockTimestamps(urls: string[]) {
30
+ rpcUrls = urls;
31
+ client = null;
32
+ pending = new Map();
33
+ flushScheduled = false;
34
+ }
35
+
36
+ export function requestBlockTimestamp(chainId: number, blockNumber: number): Promise<bigint> {
37
+ return new Promise((resolve, reject) => {
38
+ let blocks = pending.get(chainId);
39
+ if (!blocks) {
40
+ blocks = new Map();
41
+ pending.set(chainId, blocks);
42
+ }
43
+ const waiters = blocks.get(blockNumber);
44
+ if (waiters) {
45
+ waiters.push({ resolve, reject });
46
+ } else {
47
+ blocks.set(blockNumber, [{ resolve, reject }]);
48
+ }
49
+
50
+ if (!flushScheduled) {
51
+ flushScheduled = true;
52
+ queueMicrotask(() => {
53
+ flushScheduled = false;
54
+ const collected = pending;
55
+ pending = new Map();
56
+ for (const [chain, blocksForChain] of collected) {
57
+ void answer(chain, blocksForChain);
58
+ }
59
+ });
60
+ }
61
+ });
62
+ }
63
+
64
+ async function answer(chainId: number, blocks: Map<number, Waiter[]>) {
65
+ let timestamps = new Map<number, bigint>();
66
+
67
+ // A batch can hold thousands of blocks, and one argument per block would
68
+ // overflow the call stack inside the try, where it would read as a HyperSync
69
+ // failure and fall back to one round trip per block.
70
+ let lowest = Infinity;
71
+ let highest = -Infinity;
72
+ for (const blockNumber of blocks.keys()) {
73
+ if (blockNumber < lowest) lowest = blockNumber;
74
+ if (blockNumber > highest) highest = blockNumber;
75
+ }
76
+
77
+ try {
78
+ timestamps = await fromHyperSync(chainId, lowest, highest);
79
+ } catch (error) {
80
+ if (rpcUrls.length === 0) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ for (const waiters of blocks.values()) {
83
+ for (const waiter of waiters) {
84
+ waiter.reject(
85
+ new Error(
86
+ `Envio Subgraph couldn't read a block timestamp from HyperSync: ${message}\n` +
87
+ `Set ${API_TOKEN_ENV_VAR} in .env or the environment — create one at\n` +
88
+ `https://envio.dev/app/api-tokens — or set ENVIO_SUBGRAPH_RPC to fall back to RPC.`,
89
+ ),
90
+ );
91
+ }
92
+ }
93
+ return;
94
+ }
95
+ }
96
+
97
+ for (const [blockNumber, waiters] of blocks) {
98
+ const known = timestamps.get(blockNumber);
99
+ if (known !== undefined) {
100
+ for (const waiter of waiters) waiter.resolve(known);
101
+ continue;
102
+ }
103
+ try {
104
+ const timestamp = await fromRpc(blockNumber);
105
+ for (const waiter of waiters) waiter.resolve(timestamp);
106
+ } catch (error) {
107
+ for (const waiter of waiters) waiter.reject(error);
108
+ }
109
+ }
110
+ }
111
+
112
+ type HyperSyncResponse = {
113
+ data: { blocks?: { number: number; timestamp: string }[] }[];
114
+ next_block: number;
115
+ };
116
+
117
+ async function fromHyperSync(
118
+ chainId: number,
119
+ fromBlock: number,
120
+ toBlock: number,
121
+ ): Promise<Map<number, bigint>> {
122
+ const token = process.env[API_TOKEN_ENV_VAR];
123
+ const timestamps = new Map<number, bigint>();
124
+
125
+ let cursor = fromBlock;
126
+ // HyperSync answers as much of the range as one response holds and points at
127
+ // where to resume, so a wide range takes more than one round trip.
128
+ while (cursor <= toBlock) {
129
+ const response = await fetch(hypersyncUrl(chainId), {
130
+ method: "POST",
131
+ headers: {
132
+ "content-type": "application/json",
133
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
134
+ },
135
+ body: JSON.stringify({
136
+ from_block: cursor,
137
+ to_block: toBlock + 1,
138
+ include_all_blocks: true,
139
+ field_selection: { block: ["number", "timestamp"] },
140
+ }),
141
+ });
142
+
143
+ if (!response.ok) {
144
+ throw new Error(`HyperSync returned ${response.status} ${await response.text()}`);
145
+ }
146
+
147
+ const body = (await response.json()) as HyperSyncResponse;
148
+ for (const page of body.data ?? []) {
149
+ for (const block of page.blocks ?? []) {
150
+ timestamps.set(block.number, BigInt(block.timestamp));
151
+ }
152
+ }
153
+
154
+ if (!body.next_block || body.next_block <= cursor) {
155
+ break;
156
+ }
157
+ cursor = body.next_block;
158
+ }
159
+
160
+ return timestamps;
161
+ }
162
+
163
+ async function fromRpc(blockNumber: number): Promise<bigint> {
164
+ if (rpcUrls.length === 0) {
165
+ throw new Error(
166
+ `Envio Subgraph couldn't read the timestamp of block ${blockNumber}: HyperSync didn't\n` +
167
+ `return it, and no RPC fallback is configured. Set ENVIO_SUBGRAPH_RPC in .env or the\n` +
168
+ `environment.`,
169
+ );
170
+ }
171
+ client ??= createPublicClient({
172
+ transport: fallback(rpcUrls.map((url) => http(url, { retryCount: 0 }))),
173
+ });
174
+ const block = await client.getBlock({ blockNumber: BigInt(blockNumber) });
175
+ return block.timestamp;
176
+ }