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/package.json +5 -5
- package/src/ContractAddressingMap.res +115 -0
- package/src/ErrorHandling.res +61 -0
- package/src/EventUtils.res +87 -0
- package/src/Internal.res +2 -0
- package/src/LoadManager.res +174 -0
- package/src/Logging.res +179 -0
- package/src/Time.res +41 -0
- package/src/Utils.res +15 -0
- package/src/db/EntityHistory.res +4 -4
- package/src/db/Table.res +5 -2
- package/src/sources/Fuel.res +37 -0
- package/src/sources/HyperFuel.res +260 -0
- package/src/sources/HyperFuel.resi +59 -0
- package/src/sources/HyperFuelClient.res +408 -0
- package/src/sources/HyperSync.res +349 -0
- package/src/sources/HyperSync.resi +69 -0
- package/src/sources/vendored-fuel-abi-coder.js +1847 -0
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
|
+
}
|
package/src/db/EntityHistory.res
CHANGED
|
@@ -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>
|