envio 3.0.1 → 3.0.2-svm-alpha.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/evm.schema.json +8 -8
- package/fuel.schema.json +12 -12
- package/index.d.ts +167 -1
- package/package.json +6 -7
- package/src/ChainFetcher.res +25 -1
- package/src/ChainFetcher.res.mjs +19 -1
- package/src/Config.res +158 -94
- package/src/Config.res.mjs +60 -97
- package/src/Core.res +32 -0
- package/src/Env.res.mjs +1 -2
- package/src/Envio.res +103 -0
- package/src/EventConfigBuilder.res +52 -0
- package/src/EventConfigBuilder.res.mjs +32 -0
- package/src/HandlerLoader.res +12 -1
- package/src/HandlerLoader.res.mjs +6 -1
- package/src/Hasura.res +43 -0
- package/src/Hasura.res.mjs +38 -0
- package/src/Internal.res +39 -0
- package/src/Main.res +53 -3
- package/src/Main.res.mjs +34 -2
- package/src/Persistence.res +2 -17
- package/src/Persistence.res.mjs +2 -14
- package/src/SimulateItems.res +23 -10
- package/src/SimulateItems.res.mjs +21 -6
- package/src/SvmTypes.res +9 -0
- package/src/SvmTypes.res.mjs +14 -0
- package/src/sources/EventRouter.res +65 -0
- package/src/sources/EventRouter.res.mjs +43 -0
- package/src/sources/HyperSyncClient.res +30 -157
- package/src/sources/HyperSyncClient.res.mjs +20 -6
- package/src/sources/HyperSyncSolanaClient.res +249 -0
- package/src/sources/HyperSyncSolanaClient.res.mjs +25 -0
- package/src/sources/HyperSyncSolanaSource.res +566 -0
- package/src/sources/HyperSyncSolanaSource.res.mjs +488 -0
- package/src/sources/HyperSyncSource.res +5 -8
- package/src/sources/HyperSyncSource.res.mjs +1 -8
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/Svm.res +2 -2
- package/src/sources/Svm.res.mjs +3 -2
- package/src/tui/Tui.res +9 -2
- package/src/tui/Tui.res.mjs +19 -4
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +352 -4
package/src/Core.res
CHANGED
|
@@ -4,9 +4,41 @@
|
|
|
4
4
|
|
|
5
5
|
// NAPI encodes Rust `Option<T>` as `null | T` (never `undefined`), so the
|
|
6
6
|
// tighter `Null.t` captures the exact boundary shape.
|
|
7
|
+
//
|
|
8
|
+
// Opaque carriers for the NAPI class constructors. Static factories
|
|
9
|
+
// (`newWithAgent`, `fromConfig`, `fromSignatures`) hang off these via `@send`
|
|
10
|
+
// in `HyperSyncClient.res` / `HyperSyncSolanaClient.res`.
|
|
11
|
+
type hypersyncClientCtor
|
|
12
|
+
type hypersyncSolanaClientCtor
|
|
13
|
+
type decoderCtor
|
|
14
|
+
|
|
15
|
+
/// JS shape of one decoded instruction. Mirrors `DecodedInstructionJson` in
|
|
16
|
+
/// `packages/cli/src/hypersync_source_svm/decoder.rs`. The `argsJson` /
|
|
17
|
+
/// `accountsJson` fields are stringified to side-step napi-rs's lack of
|
|
18
|
+
/// native `serde_json::Value` passthrough; callers `JSON.parse` once.
|
|
19
|
+
type svmDecodedInstruction = {
|
|
20
|
+
name: string,
|
|
21
|
+
argsJson: string,
|
|
22
|
+
accountsJson: string,
|
|
23
|
+
extraAccounts: array<string>,
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
type addon = {
|
|
8
27
|
getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
|
|
9
28
|
runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
|
|
29
|
+
@as("HypersyncClient")
|
|
30
|
+
hypersyncClient: hypersyncClientCtor,
|
|
31
|
+
@as("HypersyncSolanaClient")
|
|
32
|
+
hypersyncSolanaClient: hypersyncSolanaClientCtor,
|
|
33
|
+
@as("Decoder")
|
|
34
|
+
decoder: decoderCtor,
|
|
35
|
+
setLogLevel: string => unit,
|
|
36
|
+
registerProgramSchema: (~descriptorJson: string) => int,
|
|
37
|
+
decodeInstruction: (
|
|
38
|
+
~schemaHandle: int,
|
|
39
|
+
~dataHex: string,
|
|
40
|
+
~accounts: array<string>,
|
|
41
|
+
) => Null.t<svmDecodedInstruction>,
|
|
10
42
|
}
|
|
11
43
|
|
|
12
44
|
@module("node:module") external createRequire: string => {..} = "createRequire"
|
package/src/Env.res.mjs
CHANGED
|
@@ -7,7 +7,6 @@ import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js";
|
|
|
7
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
8
|
import * as HyperSyncClient from "./sources/HyperSyncClient.res.mjs";
|
|
9
9
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
10
|
-
import * as HypersyncClient from "@envio-dev/hypersync-client";
|
|
11
10
|
|
|
12
11
|
import 'dotenv/config'
|
|
13
12
|
;
|
|
@@ -62,7 +61,7 @@ let hypersyncClientEnableQueryCaching = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_CL
|
|
|
62
61
|
|
|
63
62
|
let hypersyncLogLevel = EnvSafe.get(envSafe, "ENVIO_HYPERSYNC_LOG_LEVEL", HyperSyncClient.logLevelSchema, undefined, "info", undefined, undefined);
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
HyperSyncClient.setLogLevel(hypersyncLogLevel);
|
|
66
65
|
|
|
67
66
|
let logStrategy = EnvSafe.get(envSafe, "LOG_STRATEGY", S$RescriptSchema.$$enum([
|
|
68
67
|
"ecs-file",
|
package/src/Envio.res
CHANGED
|
@@ -20,6 +20,109 @@ type svmOnSlotArgs<'context> = {
|
|
|
20
20
|
context: 'context,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Borsh-decoded instruction view. Present whenever a `ProgramSchema` was
|
|
24
|
+
attached to the program (bundled schema, Anchor IDL, or hand-written YAML
|
|
25
|
+
`accounts`/`args`). Absent (`None`) when no schema applied or the
|
|
26
|
+
discriminator didn't match any registered instruction. */
|
|
27
|
+
type svmDecodedInstruction = {
|
|
28
|
+
/** Schema-declared instruction name (matches the codegen module suffix). */
|
|
29
|
+
name: string,
|
|
30
|
+
/** Borsh-decoded args. `JSON.Object({})` for no-arg instructions
|
|
31
|
+
(e.g. `VerifyCollection`). POC types this as raw `JSON.t`; cast at the
|
|
32
|
+
handler with `(json :> MyArgsType)` until typed codegen lands. */
|
|
33
|
+
args: JSON.t,
|
|
34
|
+
/** Named accounts in schema order. Keys are exactly the schema-declared
|
|
35
|
+
names; values are base58 pubkey strings. */
|
|
36
|
+
accounts: dict<string>,
|
|
37
|
+
/** Accounts beyond the schema's named list (Anchor `remaining_accounts`,
|
|
38
|
+
IDL drift). `[]` when counts match. */
|
|
39
|
+
extraAccounts: array<string>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type svmInstruction = {
|
|
43
|
+
programId: SvmTypes.Pubkey.t,
|
|
44
|
+
/** Raw instruction bytes as `0x`-prefixed hex. */
|
|
45
|
+
data: string,
|
|
46
|
+
accounts: array<SvmTypes.Pubkey.t>,
|
|
47
|
+
/** Path through the call tree: `[outerIndex]` for top-level instructions,
|
|
48
|
+
appended child indices for inner CPI calls. */
|
|
49
|
+
instructionAddress: array<int>,
|
|
50
|
+
isInner: bool,
|
|
51
|
+
/** Discriminator prefixes pre-extracted by HyperSync. Each is `Some` only
|
|
52
|
+
when the underlying instruction is at least that long. */
|
|
53
|
+
d1?: string,
|
|
54
|
+
d2?: string,
|
|
55
|
+
d4?: string,
|
|
56
|
+
d8?: string,
|
|
57
|
+
/** Borsh-decoded view. See [[svmDecodedInstruction]]. */
|
|
58
|
+
decoded?: svmDecodedInstruction,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type svmTokenBalance = {
|
|
62
|
+
account?: SvmTypes.Pubkey.t,
|
|
63
|
+
mint?: SvmTypes.Pubkey.t,
|
|
64
|
+
owner?: SvmTypes.Pubkey.t,
|
|
65
|
+
preAmount?: string,
|
|
66
|
+
postAmount?: string,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type svmTransaction = {
|
|
70
|
+
signatures: array<string>,
|
|
71
|
+
feePayer?: SvmTypes.Pubkey.t,
|
|
72
|
+
success?: bool,
|
|
73
|
+
err?: string,
|
|
74
|
+
fee?: bigint,
|
|
75
|
+
computeUnitsConsumed?: bigint,
|
|
76
|
+
accountKeys: array<SvmTypes.Pubkey.t>,
|
|
77
|
+
recentBlockhash?: string,
|
|
78
|
+
version?: string,
|
|
79
|
+
tokenBalances?: array<svmTokenBalance>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type svmLog = {
|
|
83
|
+
kind: string,
|
|
84
|
+
message: string,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Inner block record on `svmInstructionEvent`. Field names follow EVM/Fuel
|
|
88
|
+
(`height`, `time`, `hash`) so the shared `Ecosystem.t` getters in
|
|
89
|
+
`Svm.res` work uniformly across ecosystems — `height` carries the slot. */
|
|
90
|
+
type svmInstructionEventBlock = {
|
|
91
|
+
/** Slot number. Named `height` so the shared ecosystem getter reads it. */
|
|
92
|
+
height: int,
|
|
93
|
+
/** Unix block time (seconds). `0` when HyperSync didn't return a block
|
|
94
|
+
for this instruction's slot. */
|
|
95
|
+
time: int,
|
|
96
|
+
/** Block hash. Currently always empty — populated by the future
|
|
97
|
+
reorg-guard `queryBlockHash(slot)` route. */
|
|
98
|
+
hash: string,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The per-instruction payload handlers receive on `.event`. Mirrors the
|
|
102
|
+
EVM `type event` shape inside generated per-event modules. */
|
|
103
|
+
type svmInstructionEvent = {
|
|
104
|
+
contractName: string,
|
|
105
|
+
eventName: string,
|
|
106
|
+
instruction: svmInstruction,
|
|
107
|
+
/** Parent transaction. `None` when the per-instruction
|
|
108
|
+
`include_transaction` flag is `false`. */
|
|
109
|
+
transaction: option<svmTransaction>,
|
|
110
|
+
/** Program log entries scoped to this instruction. `None` when the
|
|
111
|
+
per-instruction `include_logs` flag is `false`. */
|
|
112
|
+
logs: option<array<svmLog>>,
|
|
113
|
+
/** Convenience alias for `block.height`. */
|
|
114
|
+
slot: int,
|
|
115
|
+
/** Convenience alias for `block.time`. */
|
|
116
|
+
blockTime: option<int>,
|
|
117
|
+
block: svmInstructionEventBlock,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Arguments passed to handlers registered via `indexer.onInstruction`. */
|
|
121
|
+
type svmOnInstructionArgs<'context> = {
|
|
122
|
+
event: svmInstructionEvent,
|
|
123
|
+
context: 'context,
|
|
124
|
+
}
|
|
125
|
+
|
|
23
126
|
// Internal-only type for the `indexer.onBlock` (and SVM `onSlot`) `where`
|
|
24
127
|
// callback argument. The canonical TypeScript shape lives in
|
|
25
128
|
// `packages/envio/index.d.ts`; the ReScript declaration here is free to
|
|
@@ -484,6 +484,58 @@ let buildEvmEventConfig = (
|
|
|
484
484
|
|
|
485
485
|
// ============== Build Fuel event config ==============
|
|
486
486
|
|
|
487
|
+
let buildSvmInstructionEventConfig = (
|
|
488
|
+
~contractName: string,
|
|
489
|
+
~instructionName: string,
|
|
490
|
+
~programId: SvmTypes.Pubkey.t,
|
|
491
|
+
~discriminator: option<string>,
|
|
492
|
+
~discriminatorByteLen: int,
|
|
493
|
+
~includeTransaction: bool,
|
|
494
|
+
~includeLogs: bool,
|
|
495
|
+
~includeTokenBalances: bool,
|
|
496
|
+
~accountFilters: array<Internal.svmAccountFilter>,
|
|
497
|
+
~isInner: option<bool>,
|
|
498
|
+
~isWildcard: bool,
|
|
499
|
+
~handler: option<Internal.handler>,
|
|
500
|
+
~contractRegister: option<Internal.contractRegister>,
|
|
501
|
+
~accounts: array<string>=[],
|
|
502
|
+
~args: JSON.t=JSON.Null,
|
|
503
|
+
~definedTypes: JSON.t=JSON.Null,
|
|
504
|
+
~startBlock: option<int>=?,
|
|
505
|
+
): Internal.svmInstructionEventConfig => {
|
|
506
|
+
let paramsSchema =
|
|
507
|
+
S.json(~validate=false)
|
|
508
|
+
->Utils.Schema.coerceToJsonPgType
|
|
509
|
+
->(Utils.magic: S.t<JSON.t> => S.t<Internal.eventParams>)
|
|
510
|
+
{
|
|
511
|
+
id: switch discriminator {
|
|
512
|
+
| Some(d) => d
|
|
513
|
+
| None => "none"
|
|
514
|
+
},
|
|
515
|
+
name: instructionName,
|
|
516
|
+
contractName,
|
|
517
|
+
isWildcard,
|
|
518
|
+
handler,
|
|
519
|
+
contractRegister,
|
|
520
|
+
paramsRawEventSchema: paramsSchema,
|
|
521
|
+
simulateParamsSchema: paramsSchema,
|
|
522
|
+
filterByAddresses: false,
|
|
523
|
+
dependsOnAddresses: !isWildcard,
|
|
524
|
+
startBlock,
|
|
525
|
+
programId,
|
|
526
|
+
discriminator,
|
|
527
|
+
discriminatorByteLen,
|
|
528
|
+
includeTransaction,
|
|
529
|
+
includeLogs,
|
|
530
|
+
includeTokenBalances,
|
|
531
|
+
accountFilters,
|
|
532
|
+
isInner,
|
|
533
|
+
accounts,
|
|
534
|
+
args,
|
|
535
|
+
definedTypes,
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
487
539
|
let buildFuelEventConfig = (
|
|
488
540
|
~contractName: string,
|
|
489
541
|
~eventName: string,
|
|
@@ -350,6 +350,37 @@ function buildEvmEventConfig(contractName, eventName, sighash, params, isWildcar
|
|
|
350
350
|
};
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, discriminatorByteLen, includeTransaction, includeLogs, includeTokenBalances, accountFilters, isInner, isWildcard, handler, contractRegister, accountsOpt, argsOpt, definedTypesOpt, startBlock) {
|
|
354
|
+
let accounts = accountsOpt !== undefined ? accountsOpt : [];
|
|
355
|
+
let args = argsOpt !== undefined ? argsOpt : null;
|
|
356
|
+
let definedTypes = definedTypesOpt !== undefined ? definedTypesOpt : null;
|
|
357
|
+
let paramsSchema = Utils.Schema.coerceToJsonPgType(S$RescriptSchema.json(false));
|
|
358
|
+
return {
|
|
359
|
+
id: discriminator !== undefined ? discriminator : "none",
|
|
360
|
+
name: instructionName,
|
|
361
|
+
contractName: contractName,
|
|
362
|
+
isWildcard: isWildcard,
|
|
363
|
+
filterByAddresses: false,
|
|
364
|
+
dependsOnAddresses: !isWildcard,
|
|
365
|
+
handler: handler,
|
|
366
|
+
contractRegister: contractRegister,
|
|
367
|
+
paramsRawEventSchema: paramsSchema,
|
|
368
|
+
simulateParamsSchema: paramsSchema,
|
|
369
|
+
startBlock: startBlock,
|
|
370
|
+
programId: programId,
|
|
371
|
+
discriminator: discriminator,
|
|
372
|
+
discriminatorByteLen: discriminatorByteLen,
|
|
373
|
+
includeTransaction: includeTransaction,
|
|
374
|
+
includeLogs: includeLogs,
|
|
375
|
+
includeTokenBalances: includeTokenBalances,
|
|
376
|
+
accountFilters: accountFilters,
|
|
377
|
+
isInner: isInner,
|
|
378
|
+
accounts: accounts,
|
|
379
|
+
args: args,
|
|
380
|
+
definedTypes: definedTypes
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
353
384
|
function buildFuelEventConfig(contractName, eventName, kind, sighash, rawAbi, isWildcard, handler, contractRegister, startBlock) {
|
|
354
385
|
let fuelKind;
|
|
355
386
|
switch (kind) {
|
|
@@ -428,6 +459,7 @@ export {
|
|
|
428
459
|
alwaysIncludedBlockFields,
|
|
429
460
|
resolveFieldSelection,
|
|
430
461
|
buildEvmEventConfig,
|
|
462
|
+
buildSvmInstructionEventConfig,
|
|
431
463
|
buildFuelEventConfig,
|
|
432
464
|
}
|
|
433
465
|
/* eventParamComponentSchema Not a pure module */
|
package/src/HandlerLoader.res
CHANGED
|
@@ -142,7 +142,18 @@ let applyRegistrations = (~config: Config.t): Config.t => {
|
|
|
142
142
|
dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses),
|
|
143
143
|
} :> Internal.eventConfig)
|
|
144
144
|
| Svm =>
|
|
145
|
-
|
|
145
|
+
let svmEv =
|
|
146
|
+
ev->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
|
|
147
|
+
({
|
|
148
|
+
...svmEv,
|
|
149
|
+
isWildcard,
|
|
150
|
+
handler,
|
|
151
|
+
contractRegister,
|
|
152
|
+
dependsOnAddresses: Internal.dependsOnAddresses(
|
|
153
|
+
~isWildcard,
|
|
154
|
+
~filterByAddresses=false,
|
|
155
|
+
),
|
|
156
|
+
} :> Internal.eventConfig)
|
|
146
157
|
}
|
|
147
158
|
},
|
|
148
159
|
)
|
|
@@ -109,7 +109,12 @@ function applyRegistrations(config) {
|
|
|
109
109
|
kind: ev.kind
|
|
110
110
|
};
|
|
111
111
|
case "svm" :
|
|
112
|
-
|
|
112
|
+
let newrecord = {...ev};
|
|
113
|
+
newrecord.contractRegister = contractRegister;
|
|
114
|
+
newrecord.handler = handler;
|
|
115
|
+
newrecord.dependsOnAddresses = Internal.dependsOnAddresses(isWildcard, false);
|
|
116
|
+
newrecord.isWildcard = isWildcard;
|
|
117
|
+
return newrecord;
|
|
113
118
|
}
|
|
114
119
|
});
|
|
115
120
|
return {
|
package/src/Hasura.res
CHANGED
|
@@ -40,6 +40,19 @@ let clearMetadataRoute = Rest.route(() => {
|
|
|
40
40
|
responses,
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
let reloadMetadataRoute = Rest.route(() => {
|
|
44
|
+
method: Post,
|
|
45
|
+
path: "",
|
|
46
|
+
input: s => {
|
|
47
|
+
let _ = s.field("type", S.literal("reload_metadata"))
|
|
48
|
+
{
|
|
49
|
+
"args": s.field("args", S.json(~validate=false)),
|
|
50
|
+
"auth": s->auth,
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
responses,
|
|
54
|
+
})
|
|
55
|
+
|
|
43
56
|
let trackTablesRoute = Rest.route(() => {
|
|
44
57
|
method: Post,
|
|
45
58
|
path: "",
|
|
@@ -110,6 +123,31 @@ let clearHasuraMetadata = async (~endpoint, ~auth) => {
|
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
let reloadHasuraMetadata = async (~endpoint, ~auth) => {
|
|
127
|
+
try {
|
|
128
|
+
let result = await reloadMetadataRoute->Rest.fetch(
|
|
129
|
+
{
|
|
130
|
+
"auth": auth,
|
|
131
|
+
"args": {
|
|
132
|
+
"reload_sources": ["default"],
|
|
133
|
+
}->(Utils.magic: 'a => JSON.t),
|
|
134
|
+
},
|
|
135
|
+
~client=Rest.client(endpoint),
|
|
136
|
+
)
|
|
137
|
+
let msg = switch result {
|
|
138
|
+
| QuerySucceeded => "Hasura metadata reloaded"
|
|
139
|
+
| AlreadyDone => "Hasura metadata reload acknowledged"
|
|
140
|
+
}
|
|
141
|
+
Logging.trace(msg)
|
|
142
|
+
} catch {
|
|
143
|
+
| exn =>
|
|
144
|
+
Logging.error({
|
|
145
|
+
"msg": `There was an issue reloading hasura metadata - table tracking may race with schema creation.`,
|
|
146
|
+
"err": exn->Utils.prettifyExn,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableNames: array<string>) => {
|
|
114
152
|
try {
|
|
115
153
|
let result = await trackTablesRoute->Rest.fetch(
|
|
@@ -242,6 +280,11 @@ let trackDatabase = async (
|
|
|
242
280
|
|
|
243
281
|
let _ = await clearHasuraMetadata(~endpoint, ~auth)
|
|
244
282
|
|
|
283
|
+
// Force Hasura to re-introspect the source schema before tracking, otherwise
|
|
284
|
+
// freshly-created user tables may be invisible to pg_track_tables and the call
|
|
285
|
+
// returns `metadata-warnings` (HTTP 400), leaving tracking permanently broken.
|
|
286
|
+
await reloadHasuraMetadata(~endpoint, ~auth)
|
|
287
|
+
|
|
245
288
|
await trackTables(~endpoint, ~auth, ~pgSchema, ~tableNames)
|
|
246
289
|
|
|
247
290
|
for i in 0 to tableNames->Array.length - 1 {
|
package/src/Hasura.res.mjs
CHANGED
|
@@ -46,6 +46,21 @@ function clearMetadataRoute() {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function reloadMetadataRoute() {
|
|
50
|
+
return {
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "",
|
|
53
|
+
input: s => {
|
|
54
|
+
s.field("type", S$RescriptSchema.literal("reload_metadata"));
|
|
55
|
+
return {
|
|
56
|
+
args: s.field("args", S$RescriptSchema.json(false)),
|
|
57
|
+
auth: auth(s)
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
responses: responses
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
function trackTablesRoute() {
|
|
50
65
|
return {
|
|
51
66
|
method: "POST",
|
|
@@ -112,6 +127,26 @@ async function clearHasuraMetadata(endpoint, auth) {
|
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
async function reloadHasuraMetadata(endpoint, auth) {
|
|
131
|
+
try {
|
|
132
|
+
let result = await Rest.fetch(reloadMetadataRoute, {
|
|
133
|
+
auth: auth,
|
|
134
|
+
args: {
|
|
135
|
+
reload_sources: ["default"]
|
|
136
|
+
}
|
|
137
|
+
}, Rest.client(endpoint, undefined));
|
|
138
|
+
let tmp;
|
|
139
|
+
tmp = result === "QuerySucceeded" ? "Hasura metadata reloaded" : "Hasura metadata reload acknowledged";
|
|
140
|
+
return Logging.trace(tmp);
|
|
141
|
+
} catch (raw_exn) {
|
|
142
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
143
|
+
return Logging.error({
|
|
144
|
+
msg: `There was an issue reloading hasura metadata - table tracking may race with schema creation.`,
|
|
145
|
+
err: Utils.prettifyExn(exn)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
115
150
|
async function trackTables(endpoint, auth, pgSchema, tableNames) {
|
|
116
151
|
try {
|
|
117
152
|
let result = await Rest.fetch(trackTablesRoute, {
|
|
@@ -202,6 +237,7 @@ async function trackDatabase(endpoint, auth, pgSchema, userEntities, aggregateEn
|
|
|
202
237
|
]);
|
|
203
238
|
Logging.info("Tracking tables in Hasura");
|
|
204
239
|
await clearHasuraMetadata(endpoint, auth);
|
|
240
|
+
await reloadHasuraMetadata(endpoint, auth);
|
|
205
241
|
await trackTables(endpoint, auth, pgSchema, tableNames);
|
|
206
242
|
for (let i = 0, i_finish = tableNames.length; i < i_finish; ++i) {
|
|
207
243
|
let tableName = tableNames[i];
|
|
@@ -231,10 +267,12 @@ export {
|
|
|
231
267
|
auth,
|
|
232
268
|
responses,
|
|
233
269
|
clearMetadataRoute,
|
|
270
|
+
reloadMetadataRoute,
|
|
234
271
|
trackTablesRoute,
|
|
235
272
|
rawBodyRoute,
|
|
236
273
|
sendOperation,
|
|
237
274
|
clearHasuraMetadata,
|
|
275
|
+
reloadHasuraMetadata,
|
|
238
276
|
trackTables,
|
|
239
277
|
createSelectPermission,
|
|
240
278
|
createEntityRelationship,
|
package/src/Internal.res
CHANGED
|
@@ -422,6 +422,45 @@ type evmContractConfig = {
|
|
|
422
422
|
events: array<evmEventConfig>,
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
type svmAccountFilter = {
|
|
426
|
+
position: int,
|
|
427
|
+
values: array<SvmTypes.Pubkey.t>,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type svmInstructionEventConfig = {
|
|
431
|
+
...eventConfig,
|
|
432
|
+
/** Base58 Solana program id this instruction belongs to. */
|
|
433
|
+
programId: SvmTypes.Pubkey.t,
|
|
434
|
+
/** Hex-encoded discriminator. `None` matches every instruction in the program. */
|
|
435
|
+
discriminator: option<string>,
|
|
436
|
+
/** Length of the discriminator in bytes (0 / 1 / 2 / 4 / 8). Drives the
|
|
437
|
+
`dN` selector at query time and the dispatch-key precomputation in the
|
|
438
|
+
router. */
|
|
439
|
+
discriminatorByteLen: int,
|
|
440
|
+
includeTransaction: bool,
|
|
441
|
+
includeLogs: bool,
|
|
442
|
+
includeTokenBalances: bool,
|
|
443
|
+
accountFilters: array<svmAccountFilter>,
|
|
444
|
+
/** `None` matches both outer and inner (CPI-invoked) instructions. */
|
|
445
|
+
isInner: option<bool>,
|
|
446
|
+
/** Positional account names from the Borsh schema, in declared order.
|
|
447
|
+
`[]` means no schema is attached for this instruction. */
|
|
448
|
+
accounts: array<string>,
|
|
449
|
+
/** Borsh args layout as `Vec<ArgDef>` JSON (see `human_config::svm::ArgDef`
|
|
450
|
+
on the Rust side). `JSON.Null` means no schema is attached. */
|
|
451
|
+
args: JSON.t,
|
|
452
|
+
/** Program-level nominal-type registry (`BTreeMap<String, ArgType>` JSON).
|
|
453
|
+
Duplicated on every event of the same program — the runtime dedups by
|
|
454
|
+
`programId` when registering. `JSON.Null` when empty. */
|
|
455
|
+
definedTypes: JSON.t,
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type svmProgramConfig = {
|
|
459
|
+
name: string,
|
|
460
|
+
programId: SvmTypes.Pubkey.t,
|
|
461
|
+
instructions: array<svmInstructionEventConfig>,
|
|
462
|
+
}
|
|
463
|
+
|
|
425
464
|
type indexingAddress = {
|
|
426
465
|
address: Address.t,
|
|
427
466
|
contractName: string,
|
package/src/Main.res
CHANGED
|
@@ -298,6 +298,57 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
298
298
|
)
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
// SVM identity: `{program, instruction}` from TS or
|
|
302
|
+
// `{instruction: GADT{contract, _0}}` from ReScript. Same two-format dance
|
|
303
|
+
// as the EVM `parseIdentityConfig`, but reading the SVM-native field names.
|
|
304
|
+
let parseSvmIdentityConfig = (identityConfig: 'a) => {
|
|
305
|
+
let raw =
|
|
306
|
+
identityConfig->(
|
|
307
|
+
Utils.magic: 'a => {
|
|
308
|
+
"program": unknown,
|
|
309
|
+
"instruction": unknown,
|
|
310
|
+
"where": option<JSON.t>,
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
let (programName, instructionName) = if typeof(raw["program"]) === #string {
|
|
314
|
+
(
|
|
315
|
+
raw["program"]->(Utils.magic: unknown => string),
|
|
316
|
+
raw["instruction"]->(Utils.magic: unknown => string),
|
|
317
|
+
)
|
|
318
|
+
} else {
|
|
319
|
+
let inst =
|
|
320
|
+
raw["instruction"]->(Utils.magic: unknown => {"contract": string, "_0": string})
|
|
321
|
+
(inst["contract"], inst["_0"])
|
|
322
|
+
}
|
|
323
|
+
let where = raw["where"]
|
|
324
|
+
let eventOptions: option<Internal.eventOptions<_>> = switch where {
|
|
325
|
+
| None => None
|
|
326
|
+
| Some(_) =>
|
|
327
|
+
Some({
|
|
328
|
+
where: ?(where->(Utils.magic: option<JSON.t> => option<_>)),
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
(programName, instructionName, eventOptions)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// onInstruction: delegates to HandlerRegister.setHandler. The SVM analog of
|
|
335
|
+
// onEvent; the registration store keys on `(contractName, eventName)` which
|
|
336
|
+
// for SVM is `(programName, instructionName)`.
|
|
337
|
+
let onInstructionFn = (identityConfig: 'a, handler: 'b) => {
|
|
338
|
+
HandlerRegister.throwIfFinishedRegistration(~methodName="onInstruction")
|
|
339
|
+
let (programName, instructionName, eventOptions) = parseSvmIdentityConfig(identityConfig)
|
|
340
|
+
HandlerRegister.setHandler(
|
|
341
|
+
~contractName=programName,
|
|
342
|
+
~eventName=instructionName,
|
|
343
|
+
handler->(
|
|
344
|
+
Utils.magic: 'b => Internal.genericHandler<
|
|
345
|
+
Internal.genericHandlerArgs<Internal.event, Internal.handlerContext>,
|
|
346
|
+
>
|
|
347
|
+
),
|
|
348
|
+
~eventOptions,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
301
352
|
// contractRegister: delegates to HandlerRegister.setContractRegister
|
|
302
353
|
let contractRegisterFn = (identityConfig: 'a, handler: 'b) => {
|
|
303
354
|
HandlerRegister.throwIfFinishedRegistration(~methodName="contractRegister")
|
|
@@ -456,7 +507,7 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
456
507
|
"contractRegister",
|
|
457
508
|
"onBlock",
|
|
458
509
|
]
|
|
459
|
-
| Svm => ["name", "description", "chainIds", "chains", "onSlot"]
|
|
510
|
+
| Svm => ["name", "description", "chainIds", "chains", "onInstruction", "onSlot"]
|
|
460
511
|
}
|
|
461
512
|
keysMemo := Some(keys)
|
|
462
513
|
keys
|
|
@@ -477,6 +528,7 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
477
528
|
chains->(Utils.magic: {..} => unknown)
|
|
478
529
|
}
|
|
479
530
|
| "onEvent" => onEventFn->Utils.magic
|
|
531
|
+
| "onInstruction" => onInstructionFn->Utils.magic
|
|
480
532
|
| "contractRegister" => contractRegisterFn->Utils.magic
|
|
481
533
|
| "onBlock" | "onSlot" => onBlockFn->Utils.magic
|
|
482
534
|
| _ =>
|
|
@@ -622,7 +674,6 @@ let migrate = async (~reset) => {
|
|
|
622
674
|
~chainConfigs=config.chainMap->ChainMap.values,
|
|
623
675
|
~envioInfo=getEnvioInfo(),
|
|
624
676
|
~resetCommand="envio local db-migrate setup",
|
|
625
|
-
~runCommand=None,
|
|
626
677
|
)
|
|
627
678
|
await persistence.storage.close()
|
|
628
679
|
}
|
|
@@ -669,7 +720,6 @@ let start = async (
|
|
|
669
720
|
~chainConfigs=configWithoutRegistrations.chainMap->ChainMap.values,
|
|
670
721
|
~envioInfo=getEnvioInfo(),
|
|
671
722
|
~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
|
|
672
|
-
~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
|
|
673
723
|
)
|
|
674
724
|
|
|
675
725
|
// `Config.loadWithoutRegistrations` never sees registration state; handler,
|
package/src/Main.res.mjs
CHANGED
|
@@ -249,6 +249,35 @@ function getGlobalIndexer() {
|
|
|
249
249
|
let match = parseIdentityConfig(identityConfig);
|
|
250
250
|
HandlerRegister.setHandler(match[0], match[1], handler, match[2], undefined);
|
|
251
251
|
};
|
|
252
|
+
let parseSvmIdentityConfig = identityConfig => {
|
|
253
|
+
let match;
|
|
254
|
+
if (typeof identityConfig.program === "string") {
|
|
255
|
+
match = [
|
|
256
|
+
identityConfig.program,
|
|
257
|
+
identityConfig.instruction
|
|
258
|
+
];
|
|
259
|
+
} else {
|
|
260
|
+
let inst = identityConfig.instruction;
|
|
261
|
+
match = [
|
|
262
|
+
inst.contract,
|
|
263
|
+
inst._0
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
let where = identityConfig.where;
|
|
267
|
+
let eventOptions = where !== undefined ? ({
|
|
268
|
+
where: where
|
|
269
|
+
}) : undefined;
|
|
270
|
+
return [
|
|
271
|
+
match[0],
|
|
272
|
+
match[1],
|
|
273
|
+
eventOptions
|
|
274
|
+
];
|
|
275
|
+
};
|
|
276
|
+
let onInstructionFn = (identityConfig, handler) => {
|
|
277
|
+
HandlerRegister.throwIfFinishedRegistration("onInstruction");
|
|
278
|
+
let match = parseSvmIdentityConfig(identityConfig);
|
|
279
|
+
HandlerRegister.setHandler(match[0], match[1], handler, match[2], undefined);
|
|
280
|
+
};
|
|
252
281
|
let contractRegisterFn = (identityConfig, handler) => {
|
|
253
282
|
HandlerRegister.throwIfFinishedRegistration("contractRegister");
|
|
254
283
|
let match = parseIdentityConfig(identityConfig);
|
|
@@ -340,6 +369,7 @@ function getGlobalIndexer() {
|
|
|
340
369
|
"description",
|
|
341
370
|
"chainIds",
|
|
342
371
|
"chains",
|
|
372
|
+
"onInstruction",
|
|
343
373
|
"onSlot"
|
|
344
374
|
];
|
|
345
375
|
break;
|
|
@@ -372,6 +402,8 @@ function getGlobalIndexer() {
|
|
|
372
402
|
return Config.loadWithoutRegistrations().name;
|
|
373
403
|
case "onEvent" :
|
|
374
404
|
return onEventFn;
|
|
405
|
+
case "onInstruction" :
|
|
406
|
+
return onInstructionFn;
|
|
375
407
|
case "onBlock" :
|
|
376
408
|
case "onSlot" :
|
|
377
409
|
return onBlockFn;
|
|
@@ -472,7 +504,7 @@ function getEnvioInfo() {
|
|
|
472
504
|
async function migrate(reset) {
|
|
473
505
|
let config = Config.loadWithoutRegistrations();
|
|
474
506
|
let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
|
|
475
|
-
await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup",
|
|
507
|
+
await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", reset);
|
|
476
508
|
return await persistence.storage.close();
|
|
477
509
|
}
|
|
478
510
|
|
|
@@ -497,7 +529,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
497
529
|
let isDevelopmentMode = !isTest && configWithoutRegistrations.isDev;
|
|
498
530
|
let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(configWithoutRegistrations, undefined);
|
|
499
531
|
globalPersistenceRef.contents = persistence$1;
|
|
500
|
-
await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r",
|
|
532
|
+
await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", reset);
|
|
501
533
|
let match = await HandlerLoader.registerAllHandlers(configWithoutRegistrations);
|
|
502
534
|
let registrations = match[1];
|
|
503
535
|
let config = match[0];
|
package/src/Persistence.res
CHANGED
|
@@ -168,7 +168,7 @@ let make = (
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
let init = {
|
|
171
|
-
async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~
|
|
171
|
+
async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~reset=false) => {
|
|
172
172
|
try {
|
|
173
173
|
let shouldRun = switch persistence.storageStatus {
|
|
174
174
|
| Unknown => true
|
|
@@ -212,22 +212,7 @@ let init = {
|
|
|
212
212
|
| None => ["envio info is missing — storage initialized by an older envio"]
|
|
213
213
|
| Some(stored) => Config.diffPaths(~stored, ~current=envioInfo)
|
|
214
214
|
}
|
|
215
|
-
|
|
216
|
-
// public config (see Rust `StorageConfig`), so probe for
|
|
217
|
-
// `Boolean(true)`, not an object.
|
|
218
|
-
let hasClickhouse = switch envioInfo {
|
|
219
|
-
| Object(d) =>
|
|
220
|
-
switch d->Dict.get("storage") {
|
|
221
|
-
| Some(Object(s)) =>
|
|
222
|
-
switch s->Dict.get("clickhouse") {
|
|
223
|
-
| Some(Boolean(true)) => true
|
|
224
|
-
| _ => false
|
|
225
|
-
}
|
|
226
|
-
| _ => false
|
|
227
|
-
}
|
|
228
|
-
| _ => false
|
|
229
|
-
}
|
|
230
|
-
Config.throwIfIncompatible(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
|
|
215
|
+
Config.throwIfIncompatible(changedPaths, ~resetCommand)
|
|
231
216
|
persistence.storageStatus = Ready(initialState)
|
|
232
217
|
let progress = Dict.make()
|
|
233
218
|
initialState.chains->Array.forEach(c => {
|