envio 3.7.0 → 3.9.0-resolvers.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 +2 -2
- package/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +674 -161
- package/index.js +10 -0
- package/package.json +6 -7
- package/src/AddressRows.res +121 -0
- package/src/AddressRows.res.mjs +143 -0
- package/src/Batch.res +11 -0
- package/src/Batch.res.mjs +2 -1
- package/src/Bin.res +47 -0
- package/src/Bin.res.mjs +34 -1
- package/src/ChainState.res +108 -98
- package/src/ChainState.res.mjs +86 -67
- package/src/ChainState.resi +28 -15
- package/src/Config.res +61 -131
- package/src/Config.res.mjs +47 -79
- package/src/ContractMapping.res +66 -0
- package/src/ContractMapping.res.mjs +73 -0
- package/src/Core.res +24 -0
- package/src/Core.res.mjs +4 -0
- package/src/Ecosystem.res +7 -2
- package/src/Env.res +128 -1
- package/src/Env.res.mjs +123 -1
- package/src/Envio.res +54 -76
- package/src/EventConfigBuilder.res +316 -46
- package/src/EventConfigBuilder.res.mjs +231 -28
- package/src/FetchState.res +59 -67
- package/src/FetchState.res.mjs +34 -44
- package/src/HandlerRegister.res +26 -15
- package/src/HandlerRegister.res.mjs +27 -7
- package/src/Hasura.res +30 -22
- package/src/Hasura.res.mjs +11 -5
- package/src/InMemoryStore.res +23 -38
- package/src/InMemoryStore.res.mjs +13 -27
- package/src/IndexerState.res +40 -3
- package/src/IndexerState.res.mjs +30 -4
- package/src/IndexerState.resi +4 -1
- package/src/Internal.res +49 -30
- package/src/Internal.res.mjs +12 -16
- package/src/Main.res +27 -20
- package/src/Main.res.mjs +19 -14
- package/src/Metrics.res +15 -1
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +46 -21
- package/src/Persistence.res.mjs +8 -5
- package/src/PgStorage.res +238 -79
- package/src/PgStorage.res.mjs +115 -73
- package/src/PruneStaleHistory.res +1 -1
- package/src/PruneStaleHistory.res.mjs +1 -1
- package/src/Rollback.res +62 -41
- package/src/Rollback.res.mjs +61 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SimulateItems.res +254 -9
- package/src/SimulateItems.res.mjs +214 -47
- package/src/Sink.res +35 -26
- package/src/Sink.res.mjs +20 -16
- package/src/TestIndexer.res +121 -123
- package/src/TestIndexer.res.mjs +97 -102
- package/src/Utils.res +38 -0
- package/src/Utils.res.mjs +39 -2
- package/src/Writing.res +4 -2
- package/src/Writing.res.mjs +3 -2
- package/src/bindings/ClickHouse.res +311 -656
- package/src/bindings/ClickHouse.res.mjs +294 -395
- package/src/bindings/ClickHouseSink.res +303 -0
- package/src/bindings/ClickHouseSink.res.mjs +216 -0
- package/src/bindings/NodeJs.res +9 -0
- package/src/bindings/NodeJs.res.mjs +8 -1
- package/src/bindings/Postgres.res +9 -0
- package/src/bindings/Postgres.res.mjs +3 -0
- package/src/db/EntityHistory.res +29 -24
- package/src/db/EntityHistory.res.mjs +11 -19
- package/src/db/InternalTable.res +258 -83
- package/src/db/InternalTable.res.mjs +223 -85
- package/src/db/RollbackScope.res +29 -0
- package/src/db/RollbackScope.res.mjs +31 -0
- package/src/db/Table.res +28 -1
- package/src/db/Table.res.mjs +26 -2
- package/src/resolvers/ResolverProcess.res +809 -0
- package/src/resolvers/ResolverProcess.res.mjs +667 -0
- package/src/resolvers/ResolverQuery.res +317 -0
- package/src/resolvers/ResolverQuery.res.mjs +220 -0
- package/src/resolvers/collisions.js +108 -0
- package/src/resolvers/db.js +363 -0
- package/src/resolvers/dispatch.js +165 -0
- package/src/resolvers/errors.js +30 -0
- package/src/resolvers/graphqlSelection.js +292 -0
- package/src/resolvers/hasuraAction.js +84 -0
- package/src/resolvers/hasuraApply.js +323 -0
- package/src/resolvers/hasuraMetadata.js +119 -0
- package/src/resolvers/index.js +95 -0
- package/src/resolvers/manifest.js +370 -0
- package/src/resolvers/server.js +270 -0
- package/src/sources/AddressSet.res +0 -22
- package/src/sources/AddressStore.res +48 -88
- package/src/sources/AddressStore.res.mjs +11 -22
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -2
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/SimulateSource.res +8 -4
- package/src/sources/SimulateSource.res.mjs +7 -3
- package/src/sources/Svm.res +36 -7
- package/src/sources/Svm.res.mjs +34 -4
- package/src/sources/SvmHyperSyncClient.res +20 -116
- package/src/sources/SvmHyperSyncClient.res.mjs +5 -7
- package/src/sources/SvmHyperSyncSource.res +89 -37
- package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
- package/src/sources/TransactionStore.res +38 -0
- package/src/sources/TransactionStore.res.mjs +5 -0
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +1 -1
- package/svm.schema.json +44 -156
- package/src/MemoryStorage.res +0 -724
- package/src/MemoryStorage.res.mjs +0 -601
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// The HTTP surface of the resolver process: one POST per caller -- Hasura's
|
|
2
|
+
// action contract and envio-serve's -- and the two probes an orchestrator
|
|
3
|
+
// restarts and routes on.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately not a framework. The whole surface is three routes and a JSON
|
|
6
|
+
// body with a size cap, and the one thing that must not drift is the wire
|
|
7
|
+
// contract, which is easier to read as bytes in and bytes out.
|
|
8
|
+
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { RESOLVER_SECRET_HEADER } from "./hasuraMetadata.js";
|
|
12
|
+
import { createDispatcher } from "./dispatch.js";
|
|
13
|
+
import {
|
|
14
|
+
actionErrorBody,
|
|
15
|
+
badActionRequest,
|
|
16
|
+
toActionResponse,
|
|
17
|
+
toResolveRequest,
|
|
18
|
+
} from "./hasuraAction.js";
|
|
19
|
+
import { error as logError, warn as logWarn } from "../Logging.res.mjs";
|
|
20
|
+
import { Resolvers as ResolversEnv } from "../Env.res.mjs";
|
|
21
|
+
|
|
22
|
+
// Serve coerces arguments before dispatching, so a request is small by
|
|
23
|
+
// construction. The cap is here because the socket is reachable by whatever
|
|
24
|
+
// shares its network, not because a legitimate request could approach it.
|
|
25
|
+
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
function readBody(request, limit) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
request.on("data", (chunk) => {
|
|
32
|
+
size += chunk.length;
|
|
33
|
+
if (size > limit) {
|
|
34
|
+
reject(new Error(`Request body exceeds ${limit} bytes`));
|
|
35
|
+
request.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
});
|
|
40
|
+
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
41
|
+
request.on("error", reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function send(response, status, body) {
|
|
46
|
+
const payload = JSON.stringify(body);
|
|
47
|
+
response.writeHead(status, {
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
"content-length": Buffer.byteLength(payload),
|
|
50
|
+
});
|
|
51
|
+
response.end(payload);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Constant time, so a wrong guess cannot be narrowed by how long the compare
|
|
55
|
+
// took. Lengths are compared first because timingSafeEqual throws on a mismatch.
|
|
56
|
+
function presentedSecret(request, expected) {
|
|
57
|
+
const presented = request.headers[RESOLVER_SECRET_HEADER];
|
|
58
|
+
if (typeof presented !== "string") return false;
|
|
59
|
+
const a = Buffer.from(presented);
|
|
60
|
+
const b = Buffer.from(expected);
|
|
61
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const wireError = (message, code) => ({
|
|
65
|
+
errors: [{ message, extensions: { code } }],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// What a readiness probe is prepared to wait for, and so what the check has to
|
|
69
|
+
// answer within whether or not the database is the thing that is slow.
|
|
70
|
+
const READYZ_BUDGET_MS = 2_000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The bound on each pooled operation the probe runs -- its `statement_timeout`
|
|
74
|
+
* and, because `forResolver` waits `min(poolWait, timeoutMs)` for a slot, its
|
|
75
|
+
* share of the pool queue too.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately well under the wall-clock budget, and the probe runs two of
|
|
78
|
+
* them. A busy pool and a silent database are different faults: leaving room
|
|
79
|
+
* here is what lets the pool's own timeout surface first and name itself,
|
|
80
|
+
* instead of the budget firing and reporting a database that answered fine.
|
|
81
|
+
*/
|
|
82
|
+
export const READYZ_PROBE_MS = 500;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Starts the resolver process's HTTP server.
|
|
86
|
+
*
|
|
87
|
+
* Resolves once the socket is listening, with the port it bound — pass
|
|
88
|
+
* `port: 0` to let the OS choose one.
|
|
89
|
+
*/
|
|
90
|
+
export async function startResolverServer(options) {
|
|
91
|
+
const { resolvers, pool, exposeErrors = false, checkCompatible, actionSecret } = options;
|
|
92
|
+
const port = options.port ?? ResolversEnv.port();
|
|
93
|
+
const host = options.host ?? "0.0.0.0";
|
|
94
|
+
|
|
95
|
+
// Otherwise the resolver is simply absent and nothing says why: the
|
|
96
|
+
// declaration asked for admin-only, and admin-only is unreachable until
|
|
97
|
+
// something can tell an admin caller from any other.
|
|
98
|
+
const unreachable = (resolvers ?? []).filter((resolver) => resolver?.admin === true);
|
|
99
|
+
if (actionSecret === undefined && unreachable.length > 0) {
|
|
100
|
+
logWarn(
|
|
101
|
+
`ENVIO_RESOLVERS_ACTION_SECRET is not set, so an 'admin' role in a request is only the caller's own claim and is not believed. ${unreachable
|
|
102
|
+
.map((resolver) => `'${resolver.name}'`)
|
|
103
|
+
.join(", ")} declared admin, so nothing can reach ${
|
|
104
|
+
unreachable.length > 1 ? "them" : "it"
|
|
105
|
+
} until the secret is set.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dispatch = createDispatcher({
|
|
110
|
+
resolvers,
|
|
111
|
+
pool,
|
|
112
|
+
exposeErrors,
|
|
113
|
+
onError:
|
|
114
|
+
options.onError ??
|
|
115
|
+
((error, context) =>
|
|
116
|
+
logError(
|
|
117
|
+
`Resolver '${context.field}' failed (requestId ${context.requestId}): ${
|
|
118
|
+
error?.stack ?? error
|
|
119
|
+
}`
|
|
120
|
+
)),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const server = createServer((request, response) => {
|
|
124
|
+
const path = (request.url ?? "").split("?")[0];
|
|
125
|
+
|
|
126
|
+
if (request.method === "GET" && path === "/healthz") {
|
|
127
|
+
send(response, 200, { status: "ok" });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (request.method === "GET" && path === "/readyz") {
|
|
132
|
+
// Ready means the database answers and is the one this build indexes,
|
|
133
|
+
// not merely that the process is up: a resolver process that cannot
|
|
134
|
+
// reach Postgres has nothing to serve, and one pointed at a database
|
|
135
|
+
// indexed by a different build would answer with plausible wrong
|
|
136
|
+
// numbers.
|
|
137
|
+
//
|
|
138
|
+
// The budget is wall-clock, not just the query's `statement_timeout`.
|
|
139
|
+
// That only starts once `SET LOCAL` has been sent, so a Postgres that
|
|
140
|
+
// accepts the socket and never finishes the handshake is bounded by the
|
|
141
|
+
// pool's `connect_timeout` instead -- sized for the pool wait, and far
|
|
142
|
+
// past what a probe waits for. An orchestrator that gives up first marks
|
|
143
|
+
// the pod unready without the reason ever reaching it.
|
|
144
|
+
let answered = false;
|
|
145
|
+
let budget = null;
|
|
146
|
+
const answer = (status, payload) => {
|
|
147
|
+
if (answered) return;
|
|
148
|
+
answered = true;
|
|
149
|
+
clearTimeout(budget);
|
|
150
|
+
send(response, status, payload);
|
|
151
|
+
};
|
|
152
|
+
budget = setTimeout(
|
|
153
|
+
() =>
|
|
154
|
+
answer(503, {
|
|
155
|
+
status: "unavailable",
|
|
156
|
+
reason: `The database did not answer within ${READYZ_BUDGET_MS}ms.`,
|
|
157
|
+
}),
|
|
158
|
+
READYZ_BUDGET_MS
|
|
159
|
+
);
|
|
160
|
+
pool
|
|
161
|
+
.forResolver({ name: "readyz", timeoutMs: READYZ_PROBE_MS })
|
|
162
|
+
.sql.unsafe("SELECT 1;")
|
|
163
|
+
.then(async () => (checkCompatible ? await checkCompatible() : null))
|
|
164
|
+
.then((reason) =>
|
|
165
|
+
reason == null
|
|
166
|
+
? answer(200, { status: "ok" })
|
|
167
|
+
: answer(503, { status: "incompatible", reason })
|
|
168
|
+
)
|
|
169
|
+
.catch((error) => {
|
|
170
|
+
answer(503, { status: "unavailable", reason: error.message });
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const isServe = request.method === "POST" && path === "/resolve";
|
|
176
|
+
const isAction = request.method === "POST" && path === "/hasura-action";
|
|
177
|
+
|
|
178
|
+
if (!isServe && !isAction) {
|
|
179
|
+
send(response, 404, wireError(`No route for ${request.method} ${path}`, "NOT_FOUND"));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Both routes take the caller's role from the request body -- `/resolve` as
|
|
184
|
+
// a field, `/hasura-action` in `session_variables` -- so on an open socket
|
|
185
|
+
// either is a way to assert `admin`. The gate belongs in front of both, and
|
|
186
|
+
// ahead of reading the body at all.
|
|
187
|
+
//
|
|
188
|
+
// With no secret configured there is no gate, and then an `admin` claim is
|
|
189
|
+
// self-certified: `admin: true` keeps a resolver off the public schema, so
|
|
190
|
+
// honouring the claim would leave it reachable by anything that can dial
|
|
191
|
+
// this process. Unauthenticated callers are therefore public, whatever they
|
|
192
|
+
// say they are -- the resolver is unreachable rather than unguarded.
|
|
193
|
+
if (actionSecret !== undefined && !presentedSecret(request, actionSecret)) {
|
|
194
|
+
const refusal = "This resolver service requires its shared secret on every request.";
|
|
195
|
+
send(
|
|
196
|
+
response,
|
|
197
|
+
403,
|
|
198
|
+
isAction ? actionErrorBody(refusal, "FORBIDDEN") : wireError(refusal, "FORBIDDEN")
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const asCaller = (parsed) =>
|
|
204
|
+
actionSecret !== undefined || parsed === null || typeof parsed !== "object"
|
|
205
|
+
? parsed
|
|
206
|
+
: { ...parsed, role: "public" };
|
|
207
|
+
|
|
208
|
+
readBody(request, MAX_REQUEST_BYTES)
|
|
209
|
+
.then(async (raw) => {
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(raw);
|
|
213
|
+
} catch {
|
|
214
|
+
if (isAction) {
|
|
215
|
+
send(response, 400, actionErrorBody("Request body is not valid JSON", "BAD_REQUEST"));
|
|
216
|
+
} else {
|
|
217
|
+
send(response, 400, wireError("Request body is not valid JSON", "BAD_REQUEST"));
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (isAction) {
|
|
223
|
+
const invalid = badActionRequest(parsed);
|
|
224
|
+
if (invalid !== null) {
|
|
225
|
+
send(
|
|
226
|
+
response,
|
|
227
|
+
400,
|
|
228
|
+
actionErrorBody(`Malformed action request: ${invalid}`, "BAD_REQUEST")
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const answer = await dispatch(asCaller(toResolveRequest(parsed, randomUUID())));
|
|
233
|
+
const { status, body } = toActionResponse(answer);
|
|
234
|
+
send(response, status, body);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const answer = await dispatch(asCaller(parsed));
|
|
239
|
+
// A malformed request is the caller's error and says so in the status;
|
|
240
|
+
// everything a resolver itself produces is a 200 with a GraphQL-shaped
|
|
241
|
+
// body, because the operation as a whole still succeeded.
|
|
242
|
+
const isBadRequest =
|
|
243
|
+
answer.errors?.[0]?.extensions?.code === "BAD_REQUEST";
|
|
244
|
+
send(response, isBadRequest ? 400 : 200, answer);
|
|
245
|
+
})
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
if (isAction) {
|
|
248
|
+
send(response, 400, actionErrorBody(error.message, "BAD_REQUEST"));
|
|
249
|
+
} else {
|
|
250
|
+
send(response, 400, wireError(error.message, "BAD_REQUEST"));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await new Promise((resolve, reject) => {
|
|
256
|
+
server.once("error", reject);
|
|
257
|
+
server.listen(port, host, () => {
|
|
258
|
+
server.removeListener("error", reject);
|
|
259
|
+
resolve();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
port: server.address().port,
|
|
265
|
+
close: () =>
|
|
266
|
+
new Promise((resolve, reject) =>
|
|
267
|
+
server.close((error) => (error ? reject(error) : resolve()))
|
|
268
|
+
),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -1,47 +1,25 @@
|
|
|
1
|
-
// Binding to the Rust `AddressSet` napi class: an immutable, ordered snapshot
|
|
2
|
-
// of some of a chain's indexed addresses, handed to a fetch-state partition
|
|
3
|
-
// instead of a JS address array. Sets are ordered by
|
|
4
|
-
// `(effectiveStartBlock, address)`, so the same addresses always produce the
|
|
5
|
-
// same set however they were registered or restored. Everything a query derives
|
|
6
|
-
// from one (padded topics, the routing owner index, per-contract counts) is
|
|
7
|
-
// built once inside Rust and shared by every query the partition makes.
|
|
8
1
|
type t
|
|
9
2
|
|
|
10
3
|
type startBlockGroup = {startBlock: int, count: int}
|
|
11
4
|
|
|
12
5
|
@send external size: t => int = "size"
|
|
13
6
|
|
|
14
|
-
// Live addresses of one contract, derived once per set inside Rust — so the
|
|
15
|
-
// fetch state can size and split partitions without ever walking the addresses.
|
|
16
7
|
@send external countFor: (t, string) => int = "countFor"
|
|
17
8
|
|
|
18
|
-
// This set holds the address under the contract and it has already started at
|
|
19
|
-
// the block — the gate a real source's router applies to a server-side
|
|
20
|
-
// address-filtered query. The temporal half still comes from the store: a
|
|
21
|
-
// merged partition's addresses don't all start at the same block.
|
|
22
9
|
@send external containsAt: (t, Address.t, string, int) => bool = "containsAt"
|
|
23
10
|
|
|
24
11
|
@send external contractNames: t => array<string> = "contractNames"
|
|
25
12
|
|
|
26
|
-
// Distinct effective start blocks in this set, ascending — how partition
|
|
27
|
-
// creation decides where one partition ends and the next begins.
|
|
28
13
|
@send external startBlockGroups: t => array<startBlockGroup> = "startBlockGroups"
|
|
29
14
|
|
|
30
15
|
@send external slice: (t, ~offset: int, ~limit: option<int>) => t = "slice"
|
|
31
16
|
|
|
32
|
-
// Keeps only the named contracts' addresses, in set order.
|
|
33
17
|
@send external filterByContracts: (t, array<string>) => t = "filterByContracts"
|
|
34
18
|
|
|
35
|
-
// Drops addresses registered after the target block, and any the store has
|
|
36
|
-
// tombstoned since this set was built.
|
|
37
19
|
@send external filterByRegistrationBlock: (t, int) => t = "filterByRegistrationBlock"
|
|
38
20
|
|
|
39
|
-
// Union with another set of the same store, keeping set order; duplicates
|
|
40
|
-
// collapse.
|
|
41
21
|
@send external merge: (t, t) => t = "merge"
|
|
42
22
|
|
|
43
|
-
// Canonical address strings in set order. Not on a query path — queries read
|
|
44
|
-
// the set's cached slices inside Rust.
|
|
45
23
|
@send external addresses: t => array<Address.t> = "addresses"
|
|
46
24
|
|
|
47
25
|
@send external entries: t => array<Internal.indexingContract> = "entries"
|
|
@@ -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
|
|
8
|
+
// -1 for a config address.
|
|
21
9
|
registrationBlock: int,
|
|
22
10
|
}
|
|
23
11
|
|
|
24
12
|
type verdict =
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
-
|
|
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
|
-
~
|
|
64
|
+
~contractMapping: ContractMapping.t,
|
|
97
65
|
): array<contract> => {
|
|
98
|
-
// Only ever holds a declared start block, so a missing key is unambiguous
|
|
99
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 */
|
package/src/sources/Evm.res
CHANGED
|
@@ -77,6 +77,8 @@ let make = (~logger: Pino.t): Ecosystem.t => {
|
|
|
77
77
|
blockTimestampName: "timestamp",
|
|
78
78
|
blockHashName: "hash",
|
|
79
79
|
onBlockMethodName: "onBlock",
|
|
80
|
+
contractNoun: "contract",
|
|
81
|
+
eventNoun: "event",
|
|
80
82
|
// EVM filter shape: `{block: {number: {_gte?, _lte?, _every?}}}`.
|
|
81
83
|
// The inner range chunk is returned as raw `S.unknown` and parsed a
|
|
82
84
|
// second time in `Main.res` by the shared `blockRangeSchema`.
|
package/src/sources/Evm.res.mjs
CHANGED
|
@@ -56,6 +56,8 @@ function make(logger) {
|
|
|
56
56
|
blockTimestampName: "timestamp",
|
|
57
57
|
blockHashName: "hash",
|
|
58
58
|
onBlockMethodName: "onBlock",
|
|
59
|
+
contractNoun: "contract",
|
|
60
|
+
eventNoun: "event",
|
|
59
61
|
onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
|
|
60
62
|
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
|
|
61
63
|
logger: logger,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
open Source
|
|
2
2
|
|
|
3
|
-
// Surfaced by
|
|
4
|
-
// token. The corrupted-token test feeds the real server error
|
|
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
|
|