envio 2.17.0 → 2.17.1

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/src/Utils.res CHANGED
@@ -22,6 +22,11 @@ module Object = {
22
22
  external defineProperty: ('obj, string, propertyDescriptor<'a>) => 'obj = "defineProperty"
23
23
  }
24
24
 
25
+ module Error = {
26
+ @new
27
+ external make: string => exn = "Error"
28
+ }
29
+
25
30
  module Option = {
26
31
  let mapNone = (opt: option<'a>, val: 'b): option<'b> => {
27
32
  switch opt {
@@ -123,6 +128,9 @@ module Math = {
123
128
  }
124
129
 
125
130
  module Array = {
131
+ @send
132
+ external forEachAsync: (array<'a>, 'a => promise<unit>) => unit = "forEach"
133
+
126
134
  @val external jsArrayCreate: int => array<'a> = "Array"
127
135
 
128
136
  /* Given a comaprator and two sorted lists, combine them into a single sorted list */
@@ -452,3 +460,10 @@ module Map = {
452
460
  @send external set: (t<'k, 'v>, 'k, 'v) => t<'k, 'v> = "set"
453
461
  @send external delete: (t<'k, 'v>, 'k) => bool = "delete"
454
462
  }
463
+
464
+ module Proxy = {
465
+ type traps<'a> = {get?: (~target: 'a, ~prop: unknown) => unknown}
466
+
467
+ @new
468
+ external make: ('a, traps<'a>) => 'a = "Proxy"
469
+ }
@@ -186,7 +186,7 @@ let fromTable = (table: table, ~schema: S.t<'entity>): t<'entity> => {
186
186
 
187
187
  let currentHistoryFields =
188
188
  currentChangeFieldNames->Belt.Array.map(fieldName =>
189
- mkField(fieldName, Integer, ~isPrimaryKey=true)
189
+ mkField(fieldName, Integer, ~fieldSchema=S.never, ~isPrimaryKey=true)
190
190
  )
191
191
 
192
192
  let previousChangeFieldNames =
@@ -194,7 +194,7 @@ let fromTable = (table: table, ~schema: S.t<'entity>): t<'entity> => {
194
194
 
195
195
  let previousHistoryFields =
196
196
  previousChangeFieldNames->Belt.Array.map(fieldName =>
197
- mkField(fieldName, Integer, ~isNullable=true)
197
+ mkField(fieldName, Integer, ~fieldSchema=S.never, ~isNullable=true)
198
198
  )
199
199
 
200
200
  let id = "id"
@@ -224,9 +224,9 @@ let fromTable = (table: table, ~schema: S.t<'entity>): t<'entity> => {
224
224
 
225
225
  let actionFieldName = "action"
226
226
 
227
- let actionField = mkField(actionFieldName, Custom(RowAction.enum.name))
227
+ let actionField = mkField(actionFieldName, Custom(RowAction.enum.name), ~fieldSchema=S.never)
228
228
 
229
- let serialField = mkField("serial", Serial, ~isNullable=true, ~isIndex=true)
229
+ let serialField = mkField("serial", Serial, ~fieldSchema=S.never, ~isNullable=true, ~isIndex=true)
230
230
 
231
231
  let dataFieldNames = dataFields->Belt.Array.map(field => field->getFieldName)
232
232
 
package/src/db/Table.res CHANGED
@@ -19,6 +19,7 @@ type fieldType =
19
19
  type field = {
20
20
  fieldName: string,
21
21
  fieldType: fieldType,
22
+ fieldSchema: S.t<unknown>,
22
23
  isArray: bool,
23
24
  isNullable: bool,
24
25
  isPrimaryKey: bool,
@@ -36,18 +37,20 @@ type derivedFromField = {
36
37
  type fieldOrDerived = Field(field) | DerivedFrom(derivedFromField)
37
38
 
38
39
  let mkField = (
40
+ fieldName,
41
+ fieldType,
42
+ ~fieldSchema,
39
43
  ~default=?,
40
44
  ~isArray=false,
41
45
  ~isNullable=false,
42
46
  ~isPrimaryKey=false,
43
47
  ~isIndex=false,
44
48
  ~linkedEntity=?,
45
- fieldName,
46
- fieldType,
47
49
  ) =>
48
50
  {
49
51
  fieldName,
50
52
  fieldType,
53
+ fieldSchema: fieldSchema->S.toUnknown,
51
54
  isArray,
52
55
  isNullable,
53
56
  isPrimaryKey,
@@ -0,0 +1,37 @@
1
+ type receiptType =
2
+ | @as(0) Call
3
+ | @as(1) Return
4
+ | @as(2) ReturnData
5
+ | @as(3) Panic
6
+ | @as(4) Revert
7
+ | @as(5) Log
8
+ | @as(6) LogData
9
+ // Transfer is to another contract, TransferOut is to wallet address
10
+ | @as(7) Transfer
11
+ | @as(8) TransferOut
12
+ | @as(9) ScriptResult
13
+ | @as(10) MessageOut
14
+ | @as(11) Mint
15
+ | @as(12) Burn
16
+
17
+ @module("./vendored-fuel-abi-coder.js")
18
+ external transpileAbi: Js.Json.t => Ethers.abi = "transpileAbi"
19
+
20
+ @module("./vendored-fuel-abi-coder.js") @scope("AbiCoder")
21
+ external getLogDecoder: (~abi: Ethers.abi, ~logId: string) => string => unknown = "getLogDecoder"
22
+
23
+ module Receipt = {
24
+ @tag("receiptType")
25
+ type t =
26
+ | @as(0) Call({assetId: string, amount: bigint, to: string})
27
+ | @as(6) LogData({data: string, rb: bigint})
28
+ | @as(7) Transfer({amount: bigint, assetId: string, to: string})
29
+ | @as(8) TransferOut({amount: bigint, assetId: string, toAddress: string})
30
+ | @as(11) Mint({val: bigint, subId: string})
31
+ | @as(12) Burn({val: bigint, subId: string})
32
+
33
+ let getLogDataDecoder = (~abi: Ethers.abi, ~logId: string) => {
34
+ let decode = getLogDecoder(~abi, ~logId)
35
+ data => data->decode->Utils.magic
36
+ }
37
+ }
@@ -0,0 +1,260 @@
1
+ open Belt
2
+
3
+ //Manage clients in cache so we don't need to reinstantiate each time
4
+ //Ideally client should be passed in as a param to the functions but
5
+ //we are still sharing the same signature with eth archive query builder
6
+
7
+ module CachedClients = {
8
+ let cache: Js.Dict.t<HyperFuelClient.t> = Js.Dict.empty()
9
+
10
+ let getClient = url => {
11
+ switch cache->Utils.Dict.dangerouslyGetNonOption(url) {
12
+ | Some(client) => client
13
+ | None =>
14
+ let newClient = HyperFuelClient.make({url: url})
15
+ cache->Js.Dict.set(url, newClient)
16
+ newClient
17
+ }
18
+ }
19
+ }
20
+
21
+ type hyperSyncPage<'item> = {
22
+ items: array<'item>,
23
+ nextBlock: int,
24
+ archiveHeight: int,
25
+ }
26
+
27
+ type block = {
28
+ id: string,
29
+ time: int,
30
+ height: int,
31
+ }
32
+
33
+ type item = {
34
+ transactionId: string,
35
+ contractId: Address.t,
36
+ receipt: Fuel.Receipt.t,
37
+ receiptIndex: int,
38
+ block: block,
39
+ }
40
+
41
+ type blockNumberAndHash = {
42
+ blockNumber: int,
43
+ hash: string,
44
+ }
45
+
46
+ type logsQueryPage = hyperSyncPage<item>
47
+
48
+ type missingParams = {
49
+ queryName: string,
50
+ missingParams: array<string>,
51
+ }
52
+ type queryError = UnexpectedMissingParams(missingParams)
53
+
54
+ let queryErrorToMsq = (e: queryError): string => {
55
+ switch e {
56
+ | UnexpectedMissingParams({queryName, missingParams}) =>
57
+ `${queryName} query failed due to unexpected missing params on response:
58
+ ${missingParams->Js.Array2.joinWith(", ")}`
59
+ }
60
+ }
61
+
62
+ type queryResponse<'a> = result<'a, queryError>
63
+
64
+ module GetLogs = {
65
+ type error =
66
+ | UnexpectedMissingParams({missingParams: array<string>})
67
+ | WrongInstance
68
+
69
+ exception Error(error)
70
+
71
+ let makeRequestBody = (
72
+ ~fromBlock,
73
+ ~toBlockInclusive,
74
+ ~recieptsSelection,
75
+ ): HyperFuelClient.QueryTypes.query => {
76
+ {
77
+ fromBlock,
78
+ toBlockExclusive: ?switch toBlockInclusive {
79
+ | Some(toBlockInclusive) => Some(toBlockInclusive + 1)
80
+ | None => None
81
+ },
82
+ receipts: recieptsSelection,
83
+ fieldSelection: {
84
+ receipt: [
85
+ TxId,
86
+ BlockHeight,
87
+ RootContractId,
88
+ Data,
89
+ ReceiptIndex,
90
+ ReceiptType,
91
+ Rb,
92
+ // TODO: Include them only when there's a mint/burn/transferOut receipt selection
93
+ SubId,
94
+ Val,
95
+ Amount,
96
+ ToAddress,
97
+ AssetId,
98
+ To,
99
+ ],
100
+ block: [Id, Height, Time],
101
+ },
102
+ }
103
+ }
104
+
105
+ let getParam = (param, name) => {
106
+ switch param {
107
+ | Some(v) => v
108
+ | None =>
109
+ raise(
110
+ Error(
111
+ UnexpectedMissingParams({
112
+ missingParams: [name],
113
+ }),
114
+ ),
115
+ )
116
+ }
117
+ }
118
+
119
+ //Note this function can throw an error
120
+ let decodeLogQueryPageItems = (response_data: HyperFuelClient.queryResponseDataTyped): array<
121
+ item,
122
+ > => {
123
+ let {receipts, blocks} = response_data
124
+
125
+ let blocksDict = Js.Dict.empty()
126
+ blocks
127
+ ->(Utils.magic: option<'a> => 'a)
128
+ ->Array.forEach(block => {
129
+ blocksDict->Js.Dict.set(block.height->(Utils.magic: int => string), block)
130
+ })
131
+
132
+ let items = []
133
+
134
+ receipts->Array.forEach(receipt => {
135
+ switch receipt.rootContractId {
136
+ | None => ()
137
+ | Some(contractId) => {
138
+ let block =
139
+ blocksDict
140
+ ->Utils.Dict.dangerouslyGetNonOption(receipt.blockHeight->(Utils.magic: int => string))
141
+ ->getParam("Failed to find block associated to receipt")
142
+ items
143
+ ->Array.push({
144
+ transactionId: receipt.txId,
145
+ block: {
146
+ height: block.height,
147
+ id: block.id,
148
+ time: block.time,
149
+ },
150
+ contractId,
151
+ receipt: receipt->(Utils.magic: HyperFuelClient.FuelTypes.receipt => Fuel.Receipt.t),
152
+ receiptIndex: receipt.receiptIndex,
153
+ })
154
+ ->ignore
155
+ }
156
+ }
157
+ })
158
+ items
159
+ }
160
+
161
+ let convertResponse = (res: HyperFuelClient.queryResponseTyped): logsQueryPage => {
162
+ let {nextBlock, ?archiveHeight} = res
163
+ let page: logsQueryPage = {
164
+ items: res.data->decodeLogQueryPageItems,
165
+ nextBlock,
166
+ archiveHeight: archiveHeight->Option.getWithDefault(0), // TODO: FIXME: Shouldn't have a default here
167
+ }
168
+ page
169
+ }
170
+
171
+ let query = async (~serverUrl, ~fromBlock, ~toBlock, ~recieptsSelection): logsQueryPage => {
172
+ let query: HyperFuelClient.QueryTypes.query = makeRequestBody(
173
+ ~fromBlock,
174
+ ~toBlockInclusive=toBlock,
175
+ ~recieptsSelection,
176
+ )
177
+
178
+ let hyperFuelClient = CachedClients.getClient(serverUrl)
179
+
180
+ let res = await hyperFuelClient->HyperFuelClient.getSelectedData(query)
181
+ if res.nextBlock <= fromBlock {
182
+ // Might happen when /height response was from another instance of HyperSync
183
+ raise(Error(WrongInstance))
184
+ }
185
+ res->convertResponse
186
+ }
187
+ }
188
+
189
+ module BlockData = {
190
+ let convertResponse = (res: HyperFuelClient.queryResponseTyped): option<
191
+ ReorgDetection.blockDataWithTimestamp,
192
+ > => {
193
+ res.data.blocks->Option.flatMap(blocks => {
194
+ blocks
195
+ ->Array.get(0)
196
+ ->Option.map(block => {
197
+ switch block {
198
+ | {height: blockNumber, time: timestamp, id: blockHash} =>
199
+ (
200
+ {
201
+ blockTimestamp: timestamp,
202
+ blockNumber,
203
+ blockHash,
204
+ }: ReorgDetection.blockDataWithTimestamp
205
+ )
206
+ }
207
+ })
208
+ })
209
+ }
210
+
211
+ let rec queryBlockData = async (~serverUrl, ~blockNumber, ~logger): option<
212
+ ReorgDetection.blockDataWithTimestamp,
213
+ > => {
214
+ let query: HyperFuelClient.QueryTypes.query = {
215
+ fromBlock: blockNumber,
216
+ toBlockExclusive: blockNumber + 1,
217
+ // FIXME: Theoretically it should work without the outputs filter, but it doesn't for some reason
218
+ outputs: [%raw(`{}`)],
219
+ // FIXME: Had to add inputs {} as well, since it failed on block 1211599 during wildcard Call indexing
220
+ inputs: [%raw(`{}`)],
221
+ fieldSelection: {
222
+ block: [Height, Id, Time],
223
+ },
224
+ includeAllBlocks: true,
225
+ }
226
+
227
+ let hyperFuelClient = CachedClients.getClient(serverUrl)
228
+
229
+ let logger = Logging.createChildFrom(
230
+ ~logger,
231
+ ~params={"logType": "hypersync get blockhash query", "blockNumber": blockNumber},
232
+ )
233
+
234
+ let executeQuery = () => hyperFuelClient->HyperFuelClient.getSelectedData(query)
235
+
236
+ let res = await executeQuery->Time.retryAsyncWithExponentialBackOff(~logger)
237
+
238
+ // If the block is not found, retry the query. This can occur since replicas of hypersync might not hack caught up yet
239
+ if res.nextBlock <= blockNumber {
240
+ let logger = Logging.createChild(~params={"url": serverUrl})
241
+ let delayMilliseconds = 100
242
+ logger->Logging.childInfo(
243
+ `Block #${blockNumber->Int.toString} not found in HyperFuel. HyperFuel has multiple instances and it's possible that they drift independently slightly from the head. Indexing should continue correctly after retrying the query in ${delayMilliseconds->Int.toString}ms.`,
244
+ )
245
+ await Time.resolvePromiseAfterDelay(~delayMilliseconds)
246
+ await queryBlockData(~serverUrl, ~blockNumber, ~logger)
247
+ } else {
248
+ res->convertResponse
249
+ }
250
+ }
251
+ }
252
+
253
+ let queryBlockData = BlockData.queryBlockData
254
+
255
+ let heightRoute = Rest.route(() => {
256
+ path: "/height",
257
+ method: Get,
258
+ input: _ => (),
259
+ responses: [s => s.field("height", S.int)],
260
+ })
@@ -0,0 +1,59 @@
1
+ type hyperSyncPage<'item> = {
2
+ items: array<'item>,
3
+ nextBlock: int,
4
+ archiveHeight: int,
5
+ }
6
+
7
+ type block = {
8
+ id: string,
9
+ time: int,
10
+ height: int,
11
+ }
12
+
13
+ type item = {
14
+ transactionId: string,
15
+ contractId: Address.t,
16
+ receipt: Fuel.Receipt.t,
17
+ receiptIndex: int,
18
+ block: block,
19
+ }
20
+
21
+ type blockNumberAndHash = {
22
+ blockNumber: int,
23
+ hash: string,
24
+ }
25
+
26
+ type logsQueryPage = hyperSyncPage<item>
27
+
28
+ type missingParams = {
29
+ queryName: string,
30
+ missingParams: array<string>,
31
+ }
32
+ type queryError = UnexpectedMissingParams(missingParams)
33
+
34
+ let queryErrorToMsq: queryError => string
35
+
36
+ type queryResponse<'a> = result<'a, queryError>
37
+
38
+ module GetLogs: {
39
+ type error =
40
+ | UnexpectedMissingParams({missingParams: array<string>})
41
+ | WrongInstance
42
+
43
+ exception Error(error)
44
+
45
+ let query: (
46
+ ~serverUrl: string,
47
+ ~fromBlock: int,
48
+ ~toBlock: option<int>,
49
+ ~recieptsSelection: array<HyperFuelClient.QueryTypes.receiptSelection>,
50
+ ) => promise<logsQueryPage>
51
+ }
52
+
53
+ let queryBlockData: (
54
+ ~serverUrl: string,
55
+ ~blockNumber: int,
56
+ ~logger: Pino.t,
57
+ ) => promise<option<ReorgDetection.blockDataWithTimestamp>>
58
+
59
+ let heightRoute: Rest.route<unit, int>