envio 3.5.0 → 3.5.1-svm-alpha.2
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/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +33 -10
- package/package.json +6 -6
- package/src/ChainFetching.res +11 -21
- package/src/ChainFetching.res.mjs +12 -20
- package/src/ChainState.res +41 -4
- package/src/ChainState.res.mjs +39 -5
- package/src/ChainState.resi +10 -1
- package/src/Config.res +62 -3
- package/src/Config.res.mjs +49 -13
- package/src/EffectState.res +2 -2
- package/src/EffectState.res.mjs +2 -2
- package/src/EntityTables.res +32 -0
- package/src/EntityTables.res.mjs +44 -0
- package/src/Envio.res +7 -7
- package/src/Envio.res.mjs +5 -6
- package/src/FetchState.res +38 -75
- package/src/FetchState.res.mjs +20 -62
- package/src/Hasura.res +37 -3
- package/src/Hasura.res.mjs +23 -5
- package/src/InMemoryStore.res +95 -43
- package/src/InMemoryStore.res.mjs +69 -38
- package/src/IndexerState.res +30 -24
- package/src/IndexerState.res.mjs +20 -35
- package/src/IndexerState.resi +8 -6
- package/src/Internal.res +29 -18
- package/src/Internal.res.mjs +27 -10
- package/src/LoadLayer.res +39 -8
- package/src/LoadLayer.res.mjs +36 -9
- package/src/LoadLayer.resi +2 -0
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +183 -37
- package/src/PgStorage.res.mjs +149 -34
- package/src/PruneStaleHistory.res +1 -0
- package/src/PruneStaleHistory.res.mjs +2 -1
- package/src/Sink.res +3 -3
- package/src/Sink.res.mjs +2 -2
- package/src/TestIndexer.res +128 -17
- package/src/TestIndexer.res.mjs +75 -9
- package/src/UserContext.res +19 -4
- package/src/UserContext.res.mjs +15 -9
- package/src/Writing.res +8 -16
- package/src/Writing.res.mjs +10 -10
- package/src/bindings/ClickHouse.res +40 -4
- package/src/bindings/ClickHouse.res.mjs +37 -5
- package/src/bindings/Vitest.res +0 -3
- package/src/bindings/Vitest.res.mjs +2 -12
- package/src/db/EntityHistory.res +64 -21
- package/src/db/EntityHistory.res.mjs +43 -22
- package/src/db/InternalTable.res.mjs +31 -31
- package/src/db/Table.res +24 -1
- package/src/db/Table.res.mjs +26 -4
- package/src/sources/AddressStore.res +77 -26
- package/src/sources/AddressStore.res.mjs +21 -10
- package/src/sources/SvmHyperSyncClient.res +8 -7
- package/src/sources/SvmHyperSyncSource.res +38 -1
- package/src/sources/SvmHyperSyncSource.res.mjs +24 -0
- package/svm.schema.json +17 -4
package/src/db/Table.res.mjs
CHANGED
|
@@ -16,11 +16,12 @@ function makeEnumConfig(name, variants) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function mkField(fieldName, fieldType, fieldSchema, $$default, isArrayOpt, isNullableOpt, isPrimaryKeyOpt, isIndexOpt, linkedEntity, description, postgresDbName, clickhouseDbName) {
|
|
19
|
+
function mkField(fieldName, fieldType, fieldSchema, $$default, isArrayOpt, isNullableOpt, isPrimaryKeyOpt, isIndexOpt, isChainIdOpt, linkedEntity, description, postgresDbName, clickhouseDbName) {
|
|
20
20
|
let isArray = isArrayOpt !== undefined ? isArrayOpt : false;
|
|
21
21
|
let isNullable = isNullableOpt !== undefined ? isNullableOpt : false;
|
|
22
22
|
let isPrimaryKey = isPrimaryKeyOpt !== undefined ? isPrimaryKeyOpt : false;
|
|
23
23
|
let isIndex = isIndexOpt !== undefined ? isIndexOpt : false;
|
|
24
|
+
let isChainId = isChainIdOpt !== undefined ? isChainIdOpt : false;
|
|
24
25
|
return {
|
|
25
26
|
TAG: "Field",
|
|
26
27
|
_0: {
|
|
@@ -31,6 +32,7 @@ function mkField(fieldName, fieldType, fieldSchema, $$default, isArrayOpt, isNul
|
|
|
31
32
|
isNullable: isNullable,
|
|
32
33
|
isPrimaryKey: isPrimaryKey,
|
|
33
34
|
isIndex: isIndex,
|
|
35
|
+
isChainId: isChainId,
|
|
34
36
|
linkedEntity: linkedEntity,
|
|
35
37
|
defaultValue: $$default,
|
|
36
38
|
description: description,
|
|
@@ -225,6 +227,18 @@ function getDerivedFromFields(table) {
|
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
function getChainIdField(table) {
|
|
231
|
+
return Stdlib_Array.findMap(table.fields, field => {
|
|
232
|
+
if (field.TAG !== "Field") {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let field$1 = field._0;
|
|
236
|
+
if (field$1.isChainId) {
|
|
237
|
+
return field$1;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
228
242
|
function getFieldByName(table, fieldName) {
|
|
229
243
|
return table.fields.find(field => getUserDefinedFieldName(field) === fieldName);
|
|
230
244
|
}
|
|
@@ -288,7 +302,8 @@ let queryFields = Utils.$$WeakMap.memoize(table => {
|
|
|
288
302
|
return dict;
|
|
289
303
|
});
|
|
290
304
|
|
|
291
|
-
function makeRowsSchema(table, rowFieldName) {
|
|
305
|
+
function makeRowsSchema(table, rowFieldName, skipChainIdOpt) {
|
|
306
|
+
let skipChainId = skipChainIdOpt !== undefined ? skipChainIdOpt : false;
|
|
292
307
|
return S$RescriptSchema.array(S$RescriptSchema.object(s => {
|
|
293
308
|
let dict = {};
|
|
294
309
|
table.fields.forEach(field => {
|
|
@@ -296,15 +311,20 @@ function makeRowsSchema(table, rowFieldName) {
|
|
|
296
311
|
return;
|
|
297
312
|
}
|
|
298
313
|
let field$1 = field._0;
|
|
314
|
+
if (field$1.isChainId && skipChainId) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
299
317
|
dict[getApiFieldName(field$1)] = s.f(rowFieldName(field$1), field$1.fieldSchema);
|
|
300
318
|
});
|
|
301
319
|
return dict;
|
|
302
320
|
}));
|
|
303
321
|
}
|
|
304
322
|
|
|
305
|
-
let rowsSchema = Utils.$$WeakMap.memoize(table => makeRowsSchema(table, getApiFieldName));
|
|
323
|
+
let rowsSchema = Utils.$$WeakMap.memoize(table => makeRowsSchema(table, getApiFieldName, undefined));
|
|
324
|
+
|
|
325
|
+
let pgRowsSchema = Utils.$$WeakMap.memoize(table => makeRowsSchema(table, getPgDbFieldName, undefined));
|
|
306
326
|
|
|
307
|
-
let
|
|
327
|
+
let pgEntityRowsSchema = Utils.$$WeakMap.memoize(table => makeRowsSchema(table, getPgDbFieldName, true));
|
|
308
328
|
|
|
309
329
|
let NonExistingTableField = /* @__PURE__ */Primitive_exceptions.create("Table.NonExistingTableField");
|
|
310
330
|
|
|
@@ -465,6 +485,7 @@ export {
|
|
|
465
485
|
getNonDefaultFields,
|
|
466
486
|
getLinkedEntityFields,
|
|
467
487
|
getDerivedFromFields,
|
|
488
|
+
getChainIdField,
|
|
468
489
|
getFieldByName,
|
|
469
490
|
NoIdField,
|
|
470
491
|
getIdFieldOrThrow,
|
|
@@ -477,6 +498,7 @@ export {
|
|
|
477
498
|
makeRowsSchema,
|
|
478
499
|
rowsSchema,
|
|
479
500
|
pgRowsSchema,
|
|
501
|
+
pgEntityRowsSchema,
|
|
480
502
|
NonExistingTableField,
|
|
481
503
|
getUnfilteredCompositeIndexesUnsafe,
|
|
482
504
|
toSqlParams,
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
// (or address-valued param) isn't registered at the item's block.
|
|
8
8
|
type t
|
|
9
9
|
|
|
10
|
-
// A contract
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
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
|
+
type contract = {name: string, startBlock: option<int>, dependsOnAddresses: bool}
|
|
14
16
|
|
|
15
17
|
type registration = {
|
|
16
18
|
address: Address.t,
|
|
@@ -20,9 +22,10 @@ type registration = {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
type verdict =
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
25
|
+
// `fetchable` is false when nothing on the chain is fetched by address for the
|
|
26
|
+
// contract: the address is still stored and persisted, so a config that later
|
|
27
|
+
// adds events picks it up on restart, but no partition is built from it.
|
|
28
|
+
| Added({effectiveStartBlock: int, fetchable: bool})
|
|
26
29
|
// Already registered for the same contract.
|
|
27
30
|
| Duplicate({effectiveStartBlock: int, existingEffectiveStartBlock: int})
|
|
28
31
|
// Already registered for a different contract.
|
|
@@ -32,11 +35,22 @@ type verdict =
|
|
|
32
35
|
|
|
33
36
|
type rawVerdict = {
|
|
34
37
|
kind: string,
|
|
38
|
+
fetchable: bool,
|
|
35
39
|
effectiveStartBlock: int,
|
|
36
40
|
existingContractName: Null.t<string>,
|
|
37
41
|
existingEffectiveStartBlock: Null.t<int>,
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
// A registration the store handed over for persistence, paired with the
|
|
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 = {
|
|
48
|
+
address: Address.t,
|
|
49
|
+
contractName: string,
|
|
50
|
+
registrationBlock: int,
|
|
51
|
+
checkpointIdx: int,
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
type makeSetOptions = {
|
|
41
55
|
// Only addresses whose id is at or above this. Ids are handed out in
|
|
42
56
|
// registration order, so a cursor read before a batch (`nextId`) selects
|
|
@@ -66,22 +80,37 @@ let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool, ~contracts: array
|
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// `Some`: one registration without a start block makes the whole
|
|
72
|
-
// address gate open from the chain's start, and the per-registration
|
|
73
|
-
// block still holds back the registrations that declared one.
|
|
74
|
-
|
|
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
|
+
let contractsOf = (
|
|
95
|
+
~onEventRegistrations: array<Internal.onEventRegistration>,
|
|
96
|
+
~configContractNames: array<string>,
|
|
97
|
+
): array<contract> => {
|
|
75
98
|
// Only ever holds a declared start block, so a missing key is unambiguous —
|
|
76
99
|
// storing `None` in a dict would be indistinguishable from "not seen yet".
|
|
77
100
|
let startBlocks: dict<int> = Dict.make()
|
|
78
101
|
let unrestricted = Utils.Set.make()
|
|
102
|
+
let addressDependent = Utils.Set.make()
|
|
79
103
|
let names = []
|
|
80
|
-
|
|
81
|
-
let name = reg.eventConfig.contractName
|
|
104
|
+
let addName = name =>
|
|
82
105
|
if !(names->Array.includes(name)) {
|
|
83
106
|
names->Array.push(name)->ignore
|
|
84
107
|
}
|
|
108
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
109
|
+
let name = reg.eventConfig.contractName
|
|
110
|
+
addName(name)
|
|
111
|
+
if reg.dependsOnAddresses {
|
|
112
|
+
addressDependent->Utils.Set.add(name)->ignore
|
|
113
|
+
}
|
|
85
114
|
switch reg.startBlock {
|
|
86
115
|
| None => unrestricted->Utils.Set.add(name)->ignore
|
|
87
116
|
| Some(startBlock) =>
|
|
@@ -94,11 +123,13 @@ let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>):
|
|
|
94
123
|
)
|
|
95
124
|
}
|
|
96
125
|
})
|
|
126
|
+
configContractNames->Array.forEach(addName)
|
|
97
127
|
names->Array.map(name => {
|
|
98
128
|
name,
|
|
99
129
|
startBlock: unrestricted->Utils.Set.has(name)
|
|
100
130
|
? None
|
|
101
131
|
: startBlocks->Utils.Dict.dangerouslyGetNonOption(name),
|
|
132
|
+
dependsOnAddresses: addressDependent->Utils.Set.has(name),
|
|
102
133
|
})
|
|
103
134
|
}
|
|
104
135
|
|
|
@@ -112,10 +143,24 @@ let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>):
|
|
|
112
143
|
// come from that chain's one store — ids are store-scoped, so sets from
|
|
113
144
|
// different stores can't be merged.
|
|
114
145
|
@send external makeSetOf: (t, array<Address.t>) => AddressSet.t = "makeSetOf"
|
|
115
|
-
@send external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
|
|
116
|
-
@send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
|
|
117
146
|
@send
|
|
118
|
-
external
|
|
147
|
+
external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
|
|
148
|
+
@send external seedBatchRaw: (t, array<registration>) => array<rawVerdict> = "seedBatch"
|
|
149
|
+
|
|
150
|
+
// Drains the registrations awaiting persistence at or below the given block —
|
|
151
|
+
// what the batch being written covers — pairing each with the checkpoint at its
|
|
152
|
+
// registration block. Later registrations stay pending. Throws, with the queue
|
|
153
|
+
// untouched, when a drained registration's block has no checkpoint in the batch.
|
|
154
|
+
@send
|
|
155
|
+
external drainForWrite: (t, int, array<int>) => array<drainedAddress> = "drainForWrite"
|
|
156
|
+
|
|
157
|
+
// How many registrations await persistence — lets a caller skip the work of
|
|
158
|
+
// assembling what `drainForWrite` needs.
|
|
159
|
+
@send external pendingCount: t => int = "pendingCount"
|
|
160
|
+
|
|
161
|
+
// The registrations still awaiting persistence. For assertions.
|
|
162
|
+
@send external pendingEntries: t => array<Internal.indexingContract> = "pendingEntries"
|
|
163
|
+
@send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
|
|
119
164
|
@send external contractCount: (t, string) => int = "contractCount"
|
|
120
165
|
@send external size: t => int = "size"
|
|
121
166
|
|
|
@@ -132,12 +177,9 @@ external startBlockGroups: (t, string) => array<AddressSet.startBlockGroup> = "s
|
|
|
132
177
|
|
|
133
178
|
@send external contractAddresses: (t, string) => array<Address.t> = "contractAddresses"
|
|
134
179
|
|
|
135
|
-
@send external entries: t => array<Internal.indexingContract> = "entries"
|
|
136
|
-
|
|
137
180
|
let toVerdict = (raw: rawVerdict): verdict =>
|
|
138
181
|
switch raw.kind {
|
|
139
|
-
| "added" => Added({effectiveStartBlock: raw.effectiveStartBlock})
|
|
140
|
-
| "noEvents" => NoEvents({effectiveStartBlock: raw.effectiveStartBlock})
|
|
182
|
+
| "added" => Added({effectiveStartBlock: raw.effectiveStartBlock, fetchable: raw.fetchable})
|
|
141
183
|
| "duplicate" =>
|
|
142
184
|
Duplicate({
|
|
143
185
|
effectiveStartBlock: raw.effectiveStartBlock,
|
|
@@ -148,13 +190,22 @@ let toVerdict = (raw: rawVerdict): verdict =>
|
|
|
148
190
|
| kind => JsError.throwWithMessage(`Unexpected address registration verdict "${kind}"`)
|
|
149
191
|
}
|
|
150
192
|
|
|
151
|
-
// Registers
|
|
152
|
-
// batch's own earlier entries — so two contracts claiming one
|
|
153
|
-
// single batch conflict just as they would across batches.
|
|
154
|
-
// in the batch's order.
|
|
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
|
+
// An unknown contract name throws with none of the batch applied.
|
|
155
200
|
let registerBatch = (store: t, registrations: array<registration>): array<verdict> =>
|
|
156
201
|
store->registerBatchRaw(registrations)->Array.map(toVerdict)
|
|
157
202
|
|
|
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
|
+
let seedBatch = (store: t, registrations: array<registration>): array<verdict> =>
|
|
207
|
+
store->seedBatchRaw(registrations)->Array.map(toVerdict)
|
|
208
|
+
|
|
158
209
|
let makeSet = (store: t, ~contractName, ~options={}: makeSetOptions) =>
|
|
159
210
|
store->makeSetRaw(contractName, options)
|
|
160
211
|
|
|
@@ -17,14 +17,22 @@ function make(ecosystem, shouldChecksum, contracts) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function contractsOf(onEventRegistrations) {
|
|
20
|
+
function contractsOf(onEventRegistrations, configContractNames) {
|
|
21
21
|
let startBlocks = {};
|
|
22
22
|
let unrestricted = new Set();
|
|
23
|
+
let addressDependent = new Set();
|
|
23
24
|
let names = [];
|
|
24
|
-
|
|
25
|
-
let name = reg.eventConfig.contractName;
|
|
25
|
+
let addName = name => {
|
|
26
26
|
if (!names.includes(name)) {
|
|
27
27
|
names.push(name);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
onEventRegistrations.forEach(reg => {
|
|
32
|
+
let name = reg.eventConfig.contractName;
|
|
33
|
+
addName(name);
|
|
34
|
+
if (reg.dependsOnAddresses) {
|
|
35
|
+
addressDependent.add(name);
|
|
28
36
|
}
|
|
29
37
|
let startBlock = reg.startBlock;
|
|
30
38
|
if (startBlock !== undefined) {
|
|
@@ -34,9 +42,11 @@ function contractsOf(onEventRegistrations) {
|
|
|
34
42
|
}
|
|
35
43
|
unrestricted.add(name);
|
|
36
44
|
});
|
|
45
|
+
configContractNames.forEach(addName);
|
|
37
46
|
return names.map(name => ({
|
|
38
47
|
name: name,
|
|
39
|
-
startBlock: unrestricted.has(name) ? undefined : startBlocks[name]
|
|
48
|
+
startBlock: unrestricted.has(name) ? undefined : startBlocks[name],
|
|
49
|
+
dependsOnAddresses: addressDependent.has(name)
|
|
40
50
|
}));
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -46,7 +56,8 @@ function toVerdict(raw) {
|
|
|
46
56
|
case "added" :
|
|
47
57
|
return {
|
|
48
58
|
TAG: "Added",
|
|
49
|
-
effectiveStartBlock: raw.effectiveStartBlock
|
|
59
|
+
effectiveStartBlock: raw.effectiveStartBlock,
|
|
60
|
+
fetchable: raw.fetchable
|
|
50
61
|
};
|
|
51
62
|
case "conflict" :
|
|
52
63
|
return {
|
|
@@ -61,11 +72,6 @@ function toVerdict(raw) {
|
|
|
61
72
|
};
|
|
62
73
|
case "invalid" :
|
|
63
74
|
return "Invalid";
|
|
64
|
-
case "noEvents" :
|
|
65
|
-
return {
|
|
66
|
-
TAG: "NoEvents",
|
|
67
|
-
effectiveStartBlock: raw.effectiveStartBlock
|
|
68
|
-
};
|
|
69
75
|
default:
|
|
70
76
|
return Stdlib_JsError.throwWithMessage(`Unexpected address registration verdict "` + kind + `"`);
|
|
71
77
|
}
|
|
@@ -75,6 +81,10 @@ function registerBatch(store, registrations) {
|
|
|
75
81
|
return store.registerBatch(registrations).map(toVerdict);
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
function seedBatch(store, registrations) {
|
|
85
|
+
return store.seedBatch(registrations).map(toVerdict);
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
function makeSet(store, contractName, optionsOpt) {
|
|
79
89
|
let options = optionsOpt !== undefined ? optionsOpt : ({});
|
|
80
90
|
return store.makeSet(contractName, options);
|
|
@@ -89,6 +99,7 @@ export {
|
|
|
89
99
|
contractsOf,
|
|
90
100
|
toVerdict,
|
|
91
101
|
registerBatch,
|
|
102
|
+
seedBatch,
|
|
92
103
|
makeSet,
|
|
93
104
|
get,
|
|
94
105
|
}
|
|
@@ -110,13 +110,13 @@ module QueryTypes = {
|
|
|
110
110
|
|
|
111
111
|
type fieldSelection = {block?: array<blockField>, transaction?: array<transactionField>}
|
|
112
112
|
|
|
113
|
-
/** Filter for selecting
|
|
114
|
-
instruction must match at least one value in every non-empty field.
|
|
113
|
+
/** Filter for selecting instruction calls. All non-empty fields are AND-ed:
|
|
114
|
+
an instruction must match at least one value in every non-empty field.
|
|
115
115
|
|
|
116
116
|
Discriminator filters (d1..d8) take hex-encoded byte prefixes ("0x" optional).
|
|
117
117
|
Account filters (a0..a9) take base58 pubkey strings. */
|
|
118
118
|
type instructionSelection = {
|
|
119
|
-
|
|
119
|
+
executingAccount?: array<string>,
|
|
120
120
|
d1?: array<string>,
|
|
121
121
|
d2?: array<string>,
|
|
122
122
|
d4?: array<string>,
|
|
@@ -129,7 +129,7 @@ module QueryTypes = {
|
|
|
129
129
|
type query = {
|
|
130
130
|
fromSlot: int,
|
|
131
131
|
toSlot?: int,
|
|
132
|
-
|
|
132
|
+
instructionCalls?: array<instructionSelection>,
|
|
133
133
|
includeAllBlocks?: bool,
|
|
134
134
|
fields?: fieldSelection,
|
|
135
135
|
maxNumBlocks?: int,
|
|
@@ -159,15 +159,16 @@ module ResponseTypes = {
|
|
|
159
159
|
slot: int,
|
|
160
160
|
transactionIndex: int,
|
|
161
161
|
instructionAddress: array<int>,
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
executingAccount: string,
|
|
163
|
+
accountArguments: array<string>,
|
|
164
164
|
data: string,
|
|
165
165
|
d1?: string,
|
|
166
166
|
d2?: string,
|
|
167
167
|
d4?: string,
|
|
168
168
|
d8?: string,
|
|
169
169
|
isInner: bool,
|
|
170
|
-
|
|
170
|
+
// True when the parent transaction succeeded.
|
|
171
|
+
txSuccess: bool,
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
type queryResponseData = {
|
|
@@ -67,8 +67,20 @@ let toSvmInstruction = (
|
|
|
67
67
|
),
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// The server responds with next_slot == from_slot when the requested range is
|
|
71
|
+
// beyond its queryable tip (its /height can run ahead of query availability,
|
|
72
|
+
// routinely by a slot at the head and by much more during an ingest incident).
|
|
73
|
+
exception NonAdvancingResponse({fromSlot: int, nextSlot: int})
|
|
74
|
+
|
|
70
75
|
let make = (
|
|
71
|
-
{
|
|
76
|
+
{
|
|
77
|
+
chainId,
|
|
78
|
+
endpointUrl,
|
|
79
|
+
apiToken,
|
|
80
|
+
onEventRegistrations,
|
|
81
|
+
clientTimeoutMillis,
|
|
82
|
+
addressStore,
|
|
83
|
+
}: options,
|
|
72
84
|
): t => {
|
|
73
85
|
let name = "SvmHyperSync"
|
|
74
86
|
|
|
@@ -131,6 +143,31 @@ let make = (
|
|
|
131
143
|
let pageFetchTime = pageFetchRef->Performance.secondsSince
|
|
132
144
|
let requestStats = [{Source.method: "getInstructions", seconds: pageFetchTime}]
|
|
133
145
|
|
|
146
|
+
// Accepting a non-advancing page would re-issue the identical query in a
|
|
147
|
+
// hot loop with no backoff (and its zero-width range would poison the
|
|
148
|
+
// partition's event-density average), so retry it with a bounded backoff
|
|
149
|
+
// until the server tip moves past the requested slot.
|
|
150
|
+
if resp.nextSlot <= fromBlock {
|
|
151
|
+
throw(
|
|
152
|
+
Source.GetItemsError(
|
|
153
|
+
Source.FailedGettingItems({
|
|
154
|
+
exn: NonAdvancingResponse({fromSlot: fromBlock, nextSlot: resp.nextSlot}),
|
|
155
|
+
attemptedToBlock: toBlock->Option.getOr(knownHeight),
|
|
156
|
+
retry: WithBackoff({
|
|
157
|
+
message: `SVM HyperSync has no data for slot ${fromBlock->Int.toString} yet (server tip is behind the reported height). Retrying until the server catches up.`,
|
|
158
|
+
backoffMillis: Pervasives.min(
|
|
159
|
+
switch retry {
|
|
160
|
+
| 0 => 500
|
|
161
|
+
| _ => 1000 * retry
|
|
162
|
+
},
|
|
163
|
+
5_000,
|
|
164
|
+
),
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
134
171
|
let parsingRef = Performance.now()
|
|
135
172
|
|
|
136
173
|
// Per-slot blockTime lookup from the response's `blocks` table, for the
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Source from "./Source.res.mjs";
|
|
4
4
|
import * as Performance from "../bindings/Performance.res.mjs";
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
9
|
import * as SvmHyperSyncClient from "./SvmHyperSyncClient.res.mjs";
|
|
@@ -55,6 +56,8 @@ function toSvmInstruction(item, programName, instructionName) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
let NonAdvancingResponse = /* @__PURE__ */Primitive_exceptions.create("SvmHyperSyncSource.NonAdvancingResponse");
|
|
60
|
+
|
|
58
61
|
function make(param) {
|
|
59
62
|
let onEventRegistrations = param.onEventRegistrations;
|
|
60
63
|
let chainId = param.chainId;
|
|
@@ -97,6 +100,26 @@ function make(param) {
|
|
|
97
100
|
method: "getInstructions",
|
|
98
101
|
seconds: pageFetchTime
|
|
99
102
|
}];
|
|
103
|
+
if (resp.nextSlot <= fromBlock) {
|
|
104
|
+
throw {
|
|
105
|
+
RE_EXN_ID: Source.GetItemsError,
|
|
106
|
+
_1: {
|
|
107
|
+
TAG: "FailedGettingItems",
|
|
108
|
+
exn: {
|
|
109
|
+
RE_EXN_ID: NonAdvancingResponse,
|
|
110
|
+
fromSlot: fromBlock,
|
|
111
|
+
nextSlot: resp.nextSlot
|
|
112
|
+
},
|
|
113
|
+
attemptedToBlock: Stdlib_Option.getOr(toBlock, knownHeight),
|
|
114
|
+
retry: {
|
|
115
|
+
TAG: "WithBackoff",
|
|
116
|
+
message: `SVM HyperSync has no data for slot ` + fromBlock.toString() + ` yet (server tip is behind the reported height). Retrying until the server catches up.`,
|
|
117
|
+
backoffMillis: Primitive_int.min(retry !== 0 ? 1000 * retry | 0 : 500, 5000)
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
Error: new Error()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
100
123
|
let parsingRef = Performance.now();
|
|
101
124
|
let blockTimeBySlot = {};
|
|
102
125
|
resp.blocks.forEach(b => {
|
|
@@ -267,6 +290,7 @@ export {
|
|
|
267
290
|
synthLogIndex,
|
|
268
291
|
parseDecoded,
|
|
269
292
|
toSvmInstruction,
|
|
293
|
+
NonAdvancingResponse,
|
|
270
294
|
make,
|
|
271
295
|
}
|
|
272
296
|
/* SvmHyperSyncClient Not a pure module */
|
package/svm.schema.json
CHANGED
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
|
+
"disable_default_cross_chain": {
|
|
53
|
+
"description": "Make entities and effect caches per-chain instead of shared across every chain (recommended). Sharing then becomes explicit — add `@crossChain` to an entity in schema.graphql or `crossChain: true` to an effect. (default: false)",
|
|
54
|
+
"type": [
|
|
55
|
+
"boolean",
|
|
56
|
+
"null"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
52
59
|
"ecosystem": {
|
|
53
60
|
"description": "Ecosystem of the project.",
|
|
54
61
|
"$ref": "#/$defs/EcosystemTag"
|
|
@@ -264,8 +271,15 @@
|
|
|
264
271
|
"type": "object",
|
|
265
272
|
"properties": {
|
|
266
273
|
"hypersync_config": {
|
|
267
|
-
"description": "HyperSync Config for fetching historical instructions on this chain.",
|
|
268
|
-
"
|
|
274
|
+
"description": "HyperSync Config for fetching historical instructions on this chain. Defaults to the public Solana mainnet HyperSync endpoint.",
|
|
275
|
+
"anyOf": [
|
|
276
|
+
{
|
|
277
|
+
"$ref": "#/$defs/HypersyncConfig"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"type": "null"
|
|
281
|
+
}
|
|
282
|
+
]
|
|
269
283
|
},
|
|
270
284
|
"programs": {
|
|
271
285
|
"description": "Solana programs to index on this chain.",
|
|
@@ -277,7 +291,6 @@
|
|
|
277
291
|
},
|
|
278
292
|
"additionalProperties": false,
|
|
279
293
|
"required": [
|
|
280
|
-
"hypersync_config",
|
|
281
294
|
"programs"
|
|
282
295
|
]
|
|
283
296
|
},
|
|
@@ -285,7 +298,7 @@
|
|
|
285
298
|
"type": "object",
|
|
286
299
|
"properties": {
|
|
287
300
|
"url": {
|
|
288
|
-
"description": "URL of the HyperSync endpoint
|
|
301
|
+
"description": "URL of the HyperSync endpoint. Defaults to the public Solana mainnet HyperSync endpoint when the hypersync_config block is omitted.",
|
|
289
302
|
"type": "string"
|
|
290
303
|
}
|
|
291
304
|
},
|