envio 2.14.3 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,17 +14,17 @@ HyperIndex is a fast, developer-friendly multichain indexer, optimized for both
14
14
  - **[Indexer auto-generation](https://docs.envio.dev/docs/HyperIndex/contract-import)** – Generate Indexers directly from smart contract addresses
15
15
  - **High performance** – Historical backfills at over 5,000+ events per second ([fastest in market](https://docs.envio.dev/blog/indexer-benchmarking-results))
16
16
  - **Local development** – Full-featured local environment with Docker
17
- - **Multichain indexing** – Index any EVM-compatible blockchain and Fuel (simultaneously)
17
+ - **[Multichain indexing](https://docs.envio.dev/docs/HyperIndex/multichain-indexing)** – Index any EVM-compatible blockchain and Fuel (simultaneously)
18
18
  - **Real-time indexing** – Instantly track blockchain events
19
- - **Reorg support** – Graceful handling of blockchain reorganizations
19
+ - **[Reorg support](https://docs.envio.dev/docs/HyperIndex/reorgs-support)** – Graceful handling of blockchain reorganizations
20
20
  - **GraphQL API** – Easy-to-query indexed data
21
21
  - **Flexible language support** – JavaScript, TypeScript, and ReScript
22
22
  - **Factory contract support** – Index data from 100,000+ factory contracts seamlessly
23
23
  - **On-chain & off-chain data integration** – Easily combine multiple data sources
24
- - **Self-hosted & managed options** – Run your own setup or use HyperIndex hosted services
24
+ - **[Self-hosted & managed options](https://docs.envio.dev/docs/HyperIndex/hosted-service)** – Run your own setup or use HyperIndex hosted services
25
25
  - **Detailed logging & error reporting** – Debug and optimize with clarity
26
26
  - **External API actions** – Trigger external services based on blockchain events
27
- - **Wildcard topic indexing** – Flexible indexing based on event topics
27
+ - **[Wildcard topic indexing](https://docs.envio.dev/docs/HyperIndex/wildcard-indexing)** – Flexible indexing based on event topics
28
28
  - **Fallback RPC data sources** – Enhanced reliability with RPC connections
29
29
 
30
30
  ## Getting Started
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "v2.14.3",
3
+ "version": "v2.16.0",
4
4
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
5
5
  "bin": "./bin.js",
6
6
  "repository": {
@@ -23,10 +23,10 @@
23
23
  },
24
24
  "homepage": "https://envio.dev",
25
25
  "optionalDependencies": {
26
- "envio-linux-x64": "v2.14.3",
27
- "envio-linux-arm64": "v2.14.3",
28
- "envio-darwin-x64": "v2.14.3",
29
- "envio-darwin-arm64": "v2.14.3"
26
+ "envio-linux-x64": "v2.16.0",
27
+ "envio-linux-arm64": "v2.16.0",
28
+ "envio-darwin-x64": "v2.16.0",
29
+ "envio-darwin-arm64": "v2.16.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@envio-dev/hypersync-client": "0.6.3",
@@ -3,6 +3,8 @@
3
3
  /* eslint-disable */
4
4
  /* tslint:disable */
5
5
 
6
+ import type {invalid as $$noEventFilters} from './bindings/OpaqueTypes.ts';
7
+
6
8
  import type {t as Address_t} from './Address.gen';
7
9
 
8
10
  export type genericEvent<params,block,transaction> = {
@@ -45,3 +47,5 @@ export type fuelTransferParams = {
45
47
  readonly assetId: string;
46
48
  readonly amount: bigint
47
49
  };
50
+
51
+ export type noEventFilters = $$noEventFilters;
package/src/Internal.res CHANGED
@@ -64,22 +64,23 @@ type genericHandlerWithLoader<'loader, 'handler, 'eventFilters> = {
64
64
  preRegisterDynamicContracts?: bool,
65
65
  }
66
66
 
67
- type eventItem = {
68
- eventName: string,
67
+ // This is private so it's not manually constructed internally
68
+ // The idea is that it can only be coerced from fuel/evmEventConfig
69
+ // and it can include their fields. We prevent manual creation,
70
+ // so the fields are not overwritten and we can safely cast the type back to fuel/evmEventConfig
71
+ type eventConfig = private {
72
+ id: string,
73
+ name: string,
69
74
  contractName: string,
75
+ isWildcard: bool,
76
+ // Usually always false for wildcard events
77
+ // But might be true for wildcard event with dynamic event filter by addresses
78
+ dependsOnAddresses: bool,
79
+ preRegisterDynamicContracts: bool,
70
80
  loader: option<loader>,
71
81
  handler: option<handler>,
72
82
  contractRegister: option<contractRegister>,
73
- timestamp: int,
74
- chain: ChainMap.Chain.t,
75
- blockNumber: int,
76
- logIndex: int,
77
- event: event,
78
83
  paramsRawEventSchema: S.schema<eventParams>,
79
- //Default to false, if an event needs to
80
- //be reprocessed after it has loaded dynamic contracts
81
- //This gets set to true and does not try and reload events
82
- hasRegisteredDynamicContracts?: bool,
83
84
  }
84
85
 
85
86
  type fuelEventKind =
@@ -89,20 +90,52 @@ type fuelEventKind =
89
90
  | Transfer
90
91
  | Call
91
92
  type fuelEventConfig = {
92
- name: string,
93
- contractName: string,
93
+ ...eventConfig,
94
94
  kind: fuelEventKind,
95
- isWildcard: bool,
96
- loader: option<loader>,
97
- handler: option<handler>,
98
- contractRegister: option<contractRegister>,
99
- paramsRawEventSchema: S.schema<eventParams>,
100
95
  }
101
96
  type fuelContractConfig = {
102
97
  name: string,
103
98
  events: array<fuelEventConfig>,
104
99
  }
105
100
 
101
+ type topicSelection = {
102
+ topic0: array<EvmTypes.Hex.t>,
103
+ topic1: array<EvmTypes.Hex.t>,
104
+ topic2: array<EvmTypes.Hex.t>,
105
+ topic3: array<EvmTypes.Hex.t>,
106
+ }
107
+
108
+ type eventFiltersArgs = {chainId: int, addresses: array<Address.t>}
109
+
110
+ type eventFilters =
111
+ Static(array<topicSelection>) | Dynamic(array<Address.t> => array<topicSelection>)
112
+
113
+ type evmEventConfig = {
114
+ ...eventConfig,
115
+ getEventFiltersOrThrow: ChainMap.Chain.t => eventFilters,
116
+ blockSchema: S.schema<eventBlock>,
117
+ transactionSchema: S.schema<eventTransaction>,
118
+ convertHyperSyncEventArgs: HyperSyncClient.Decoder.decodedEvent => eventParams,
119
+ }
120
+ type evmContractConfig = {
121
+ name: string,
122
+ abi: EvmTypes.Abi.t,
123
+ events: array<evmEventConfig>,
124
+ }
125
+
126
+ type eventItem = {
127
+ eventConfig: eventConfig,
128
+ timestamp: int,
129
+ chain: ChainMap.Chain.t,
130
+ blockNumber: int,
131
+ logIndex: int,
132
+ event: event,
133
+ //Default to false, if an event needs to
134
+ //be reprocessed after it has loaded dynamic contracts
135
+ //This gets set to true and does not try and reload events
136
+ hasRegisteredDynamicContracts?: bool,
137
+ }
138
+
106
139
  @genType
107
140
  type fuelSupplyParams = {
108
141
  subId: string,
@@ -125,3 +158,6 @@ let fuelTransferParamsSchema = S.schema(s => {
125
158
  })
126
159
 
127
160
  type entity = private {id: string}
161
+
162
+ @genType.import(("./bindings/OpaqueTypes.ts", "invalid"))
163
+ type noEventFilters
@@ -1,24 +1,17 @@
1
- type topicSelection = {
2
- topic0: array<EvmTypes.Hex.t>,
3
- topic1: array<EvmTypes.Hex.t>,
4
- topic2: array<EvmTypes.Hex.t>,
5
- topic3: array<EvmTypes.Hex.t>,
6
- }
7
-
8
1
  exception MissingRequiredTopic0
9
2
  let makeTopicSelection = (~topic0, ~topic1=[], ~topic2=[], ~topic3=[]) =>
10
3
  if topic0->Utils.Array.isEmpty {
11
4
  Error(MissingRequiredTopic0)
12
5
  } else {
13
6
  {
14
- topic0,
7
+ Internal.topic0,
15
8
  topic1,
16
9
  topic2,
17
10
  topic3,
18
11
  }->Ok
19
12
  }
20
13
 
21
- let hasFilters = ({topic1, topic2, topic3}: topicSelection) => {
14
+ let hasFilters = ({topic1, topic2, topic3}: Internal.topicSelection) => {
22
15
  [topic1, topic2, topic3]->Js.Array2.find(topic => !Utils.Array.isEmpty(topic))->Belt.Option.isSome
23
16
  }
24
17
 
@@ -26,7 +19,7 @@ let hasFilters = ({topic1, topic2, topic3}: topicSelection) => {
26
19
  For a group of topic selections, if multiple only use topic0, then they can be compressed into one
27
20
  selection combining the topic0s
28
21
  */
29
- let compressTopicSelections = (topicSelections: array<topicSelection>) => {
22
+ let compressTopicSelections = (topicSelections: array<Internal.topicSelection>) => {
30
23
  let topic0sOfSelectionsWithoutFilters = []
31
24
 
32
25
  let selectionsWithFilters = []
@@ -45,7 +38,7 @@ let compressTopicSelections = (topicSelections: array<topicSelection>) => {
45
38
  | [] => selectionsWithFilters
46
39
  | topic0 =>
47
40
  let selectionWithoutFilters = {
48
- topic0,
41
+ Internal.topic0,
49
42
  topic1: [],
50
43
  topic2: [],
51
44
  topic3: [],
@@ -56,10 +49,131 @@ let compressTopicSelections = (topicSelections: array<topicSelection>) => {
56
49
 
57
50
  type t = {
58
51
  addresses: array<Address.t>,
59
- topicSelections: array<topicSelection>,
52
+ topicSelections: array<Internal.topicSelection>,
60
53
  }
61
54
 
62
55
  let make = (~addresses, ~topicSelections) => {
63
56
  let topicSelections = compressTopicSelections(topicSelections)
64
57
  {addresses, topicSelections}
65
58
  }
59
+
60
+ type parsedEventFilters = {
61
+ getEventFiltersOrThrow: ChainMap.Chain.t => Internal.eventFilters,
62
+ dependsOnAddresses: bool,
63
+ }
64
+
65
+ let parseEventFiltersOrThrow = {
66
+ let emptyTopics = []
67
+ let noopGetter = _ => emptyTopics
68
+
69
+ (
70
+ ~eventFilters: option<Js.Json.t>,
71
+ ~sighash,
72
+ ~params,
73
+ ~topic1=noopGetter,
74
+ ~topic2=noopGetter,
75
+ ~topic3=noopGetter,
76
+ ): parsedEventFilters => {
77
+ let dependsOnAddresses = ref(false)
78
+ let topic0 = [sighash->EvmTypes.Hex.fromStringUnsafe]
79
+ let default = {
80
+ Internal.topic0,
81
+ topic1: emptyTopics,
82
+ topic2: emptyTopics,
83
+ topic3: emptyTopics,
84
+ }
85
+
86
+ let parse = (eventFilters: Js.Json.t): array<Internal.topicSelection> => {
87
+ switch eventFilters {
88
+ | Array([]) => [%raw(`{}`)]
89
+ | Array(a) => a
90
+ | _ => [eventFilters]
91
+ }->Js.Array2.map(eventFilter => {
92
+ switch eventFilter {
93
+ | Object(eventFilter) => {
94
+ let filterKeys = eventFilter->Js.Dict.keys
95
+ switch filterKeys {
96
+ | [] => default
97
+ | _ => {
98
+ filterKeys->Js.Array2.forEach(key => {
99
+ if params->Js.Array2.includes(key)->not {
100
+ // In TS type validation doesn't catch this
101
+ // when we have eventFilters as a callback
102
+ Js.Exn.raiseError(
103
+ `Invalid event filters configuration. The event doesn't have an indexed parameter "${key}" and can't use it for filtering`,
104
+ )
105
+ }
106
+ })
107
+ {
108
+ Internal.topic0,
109
+ topic1: topic1(eventFilter),
110
+ topic2: topic2(eventFilter),
111
+ topic3: topic3(eventFilter),
112
+ }
113
+ }
114
+ }
115
+ }
116
+ | _ => Js.Exn.raiseError("Invalid event filters configuration. Expected an object")
117
+ }
118
+ })
119
+ }
120
+
121
+ let getEventFiltersOrThrow = switch eventFilters {
122
+ | None => {
123
+ let static: Internal.eventFilters = Static([default])
124
+ _ => static
125
+ }
126
+ | Some(eventFilters) =>
127
+ if Js.typeof(eventFilters) === "function" {
128
+ let fn = eventFilters->(Utils.magic: Js.Json.t => Internal.eventFiltersArgs => Js.Json.t)
129
+ // When user passess a function to event filters we need to
130
+ // first determine whether it uses addresses or not
131
+ // Because the fetching logic will be different for wildcard events
132
+ // 1. If wildcard event doesn't use addresses,
133
+ // it should start fetching even without static addresses in the config
134
+ // 2. If wildcard event uses addresses in event filters,
135
+ // it should first wait for dynamic contract registration
136
+ // So to deterimine which case we run the function with dummy args
137
+ // and check if it uses addresses by using the getter.
138
+ try {
139
+ let args = (
140
+ {
141
+ chainId: 0,
142
+ addresses: [],
143
+ }: Internal.eventFiltersArgs
144
+ )->Utils.Object.defineProperty(
145
+ "addresses",
146
+ {
147
+ get: () => {
148
+ dependsOnAddresses := true
149
+ []
150
+ },
151
+ },
152
+ )
153
+ let _ = fn(args)
154
+ } catch {
155
+ | _ => ()
156
+ }
157
+ if dependsOnAddresses.contents {
158
+ chain => Internal.Dynamic(
159
+ addresses => fn({chainId: chain->ChainMap.Chain.toChainId, addresses})->parse,
160
+ )
161
+ } else {
162
+ // When we don't depend on addresses, can mark the event filter
163
+ // as static and avoid recalculating on every batch
164
+ chain => Internal.Static(
165
+ fn({chainId: chain->ChainMap.Chain.toChainId, addresses: []})->parse,
166
+ )
167
+ }
168
+ } else {
169
+ let static: Internal.eventFilters = Static(eventFilters->parse)
170
+ _ => static
171
+ }
172
+ }
173
+
174
+ {
175
+ getEventFiltersOrThrow,
176
+ dependsOnAddresses: dependsOnAddresses.contents,
177
+ }
178
+ }
179
+ }
package/src/Utils.res CHANGED
@@ -7,6 +7,21 @@ let delay = milliseconds =>
7
7
  }, milliseconds)
8
8
  })
9
9
 
10
+ module Object = {
11
+ // Define a type for the property descriptor
12
+ type propertyDescriptor<'a> = {
13
+ configurable?: bool,
14
+ enumerable?: bool,
15
+ writable?: bool,
16
+ value?: 'a,
17
+ get?: unit => 'a,
18
+ set?: 'a => unit,
19
+ }
20
+
21
+ @val @scope("Object")
22
+ external defineProperty: ('obj, string, propertyDescriptor<'a>) => 'obj = "defineProperty"
23
+ }
24
+
10
25
  module Option = {
11
26
  let mapNone = (opt: option<'a>, val: 'b): option<'b> => {
12
27
  switch opt {
@@ -50,6 +65,20 @@ module Dict = {
50
65
  */
51
66
  external dangerouslyGetNonOption: (dict<'a>, string) => option<'a> = ""
52
67
 
68
+ let push = (dict, key, value) => {
69
+ switch dict->dangerouslyGetNonOption(key) {
70
+ | Some(arr) => arr->Js.Array2.push(value)->ignore
71
+ | None => dict->Js.Dict.set(key, [value])
72
+ }
73
+ }
74
+
75
+ let pushMany = (dict, key, values) => {
76
+ switch dict->dangerouslyGetNonOption(key) {
77
+ | Some(arr) => arr->Js.Array2.pushMany(values)->ignore
78
+ | None => dict->Js.Dict.set(key, values)
79
+ }
80
+ }
81
+
53
82
  let merge: (dict<'a>, dict<'a>) => dict<'a> = %raw(`(dictA, dictB) => ({...dictA, ...dictB})`)
54
83
 
55
84
  let map = (dict, fn) => {
@@ -267,8 +296,6 @@ let unwrapResultExn = res =>
267
296
  external queueMicrotask: (unit => unit) => unit = "queueMicrotask"
268
297
 
269
298
  module Schema = {
270
- let enum = S.enum
271
-
272
299
  let getNonOptionalFieldNames = schema => {
273
300
  let acc = []
274
301
  switch schema->S.classify {
@@ -55,7 +55,7 @@ module CombinedFilter = {
55
55
  type combinedFilterRecord = {
56
56
  address?: array<Address.t>,
57
57
  //The second element of the tuple is the
58
- topics: array<array<EvmTypes.Hex.t>>,
58
+ topics: Rpc.GetLogs.topicQuery,
59
59
  fromBlock: int,
60
60
  toBlock: int,
61
61
  }
@@ -147,17 +147,18 @@ module JsonRpcProvider = {
147
147
  @send
148
148
  external getTransaction: (t, ~transactionHash: string) => promise<transaction> = "getTransaction"
149
149
 
150
- let makeGetTransactionFields = (~getTransactionByHash) => async (log: log): promise<unknown> => {
151
- let transaction = await getTransactionByHash(log.transactionHash)
152
- // Mutating should be fine, since the transaction isn't used anywhere else outside the function
153
- let fields: {..} = transaction->Obj.magic
150
+ let makeGetTransactionFields = (~getTransactionByHash) =>
151
+ async (log: log): promise<unknown> => {
152
+ let transaction = await getTransactionByHash(log.transactionHash)
153
+ // Mutating should be fine, since the transaction isn't used anywhere else outside the function
154
+ let fields: {..} = transaction->Obj.magic
154
155
 
155
- // Make it compatible with HyperSync transaction fields
156
- fields["transactionIndex"] = log.transactionIndex
157
- fields["input"] = fields["data"]
156
+ // Make it compatible with HyperSync transaction fields
157
+ fields["transactionIndex"] = log.transactionIndex
158
+ fields["input"] = fields["data"]
158
159
 
159
- fields->Obj.magic
160
- }
160
+ fields->Obj.magic
161
+ }
161
162
 
162
163
  type block = {
163
164
  _difficulty: bigint,
@@ -1 +1,2 @@
1
1
  export type Address = string;
2
+ export type invalid = never;
@@ -422,13 +422,11 @@ type t = {
422
422
 
423
423
  @module("@envio-dev/hypersync-client") @scope("HypersyncClient") external new: cfg => t = "new"
424
424
 
425
- let defaultToken = "3dc856dd-b0ea-494f-b27e-017b8b6b7e07"
426
-
427
- let make = (~url, ~bearerToken: option<string>, ~httpReqTimeoutMillis, ~maxNumRetries) =>
425
+ let make = (~url, ~apiToken, ~httpReqTimeoutMillis, ~maxNumRetries) =>
428
426
  new({
429
427
  url,
430
428
  enableChecksumAddresses: true,
431
- bearerToken: bearerToken->Belt.Option.getWithDefault(defaultToken),
429
+ bearerToken: apiToken,
432
430
  httpReqTimeoutMillis,
433
431
  maxNumRetries,
434
432
  })
@@ -360,13 +360,17 @@ module ResponseTypes = {
360
360
  let queryRoute = Rest.route(() => {
361
361
  path: "/query",
362
362
  method: Post,
363
- input: s => s.body(QueryTypes.postQueryBodySchema),
363
+ input: s =>
364
+ {
365
+ "query": s.body(QueryTypes.postQueryBodySchema),
366
+ "token": s.auth(Bearer),
367
+ },
364
368
  responses: [s => s.data(ResponseTypes.queryResponseSchema)],
365
369
  })
366
370
 
367
371
  let heightRoute = Rest.route(() => {
368
372
  path: "/height",
369
373
  method: Get,
370
- input: _ => (),
374
+ input: s => s.auth(Bearer),
371
375
  responses: [s => s.field("height", S.int)],
372
376
  })
@@ -71,7 +71,7 @@ module GetLogs = {
71
71
  topics->Belt.Array.map(toTopicFilter)
72
72
  }
73
73
 
74
- let mapTopicQuery = ({topic0, topic1, topic2, topic3}: LogSelection.topicSelection): topicQuery =>
74
+ let mapTopicQuery = ({topic0, topic1, topic2, topic3}: Internal.topicSelection): topicQuery =>
75
75
  makeTopicQuery(~topic0, ~topic1, ~topic2, ~topic3)
76
76
 
77
77
  type param = {