envio 3.14.0-subgraph → 3.14.1-resolvers.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 +8 -1
- package/fuel.schema.json +7 -0
- package/index.d.ts +258 -5
- package/index.js +10 -0
- package/package.json +6 -6
- package/src/Bin.res +48 -0
- package/src/Bin.res.mjs +34 -1
- package/src/Config.res +12 -11
- package/src/Config.res.mjs +9 -6
- package/src/Core.res +0 -21
- package/src/Core.res.mjs +0 -17
- package/src/Env.res +141 -1
- package/src/Env.res.mjs +133 -1
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +6 -8
- package/src/HandlerLoader.res +22 -48
- package/src/HandlerLoader.res.mjs +15 -41
- package/src/HandlerTypeCheck.res +207 -0
- package/src/HandlerTypeCheck.res.mjs +262 -0
- package/src/LoadLayer.res +7 -39
- package/src/LoadLayer.res.mjs +38 -65
- package/src/LoadLayer.resi +0 -28
- package/src/PgStorage.res +5 -24
- package/src/PgStorage.res.mjs +1 -18
- package/src/UserContext.res +62 -377
- package/src/UserContext.res.mjs +39 -297
- package/src/bindings/NodeJs.res +31 -0
- package/src/bindings/NodeJs.res.mjs +3 -0
- package/src/db/Table.res +0 -10
- package/src/db/Table.res.mjs +11 -22
- package/src/resolvers/ResolverProcess.res +805 -0
- package/src/resolvers/ResolverProcess.res.mjs +662 -0
- package/src/resolvers/ResolverQuery.res +313 -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 +259 -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 +333 -0
- package/src/resolvers/hasuraMetadata.js +137 -0
- package/src/resolvers/index.js +123 -0
- package/src/resolvers/manifest.js +376 -0
- package/src/resolvers/server.js +304 -0
- package/src/sources/EvmRpcClient.res +2 -0
- package/src/sources/RpcSource.res +17 -4
- package/src/sources/RpcSource.res.mjs +23 -5
- package/src/sources/SourceManager.res +63 -40
- package/src/sources/SourceManager.res.mjs +32 -20
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +1 -1
- package/svm.schema.json +7 -0
- package/src/HandlerTypeCheck.mjs +0 -14
- package/src/HandlerTypeCheckWorker.mjs +0 -68
- package/src/SyncWrites.res +0 -86
- package/src/SyncWrites.res.mjs +0 -85
- package/src/subgraph/abi-types.ts +0 -63
- package/src/subgraph/assemblyscript.ts +0 -85
- package/src/subgraph/blocks.ts +0 -183
- package/src/subgraph/calls.ts +0 -149
- package/src/subgraph/conformance.ts +0 -83
- package/src/subgraph/errors.ts +0 -96
- package/src/subgraph/graph-cli-worker.mjs +0 -26
- package/src/subgraph/graph-cli.ts +0 -190
- package/src/subgraph/graph-ts-enums.ts +0 -28
- package/src/subgraph/graph-ts-types/VERSION +0 -2
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
- package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
- package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
- package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
- package/src/subgraph/graph-ts-types/index.d.ts +0 -102
- package/src/subgraph/graph-ts.ts +0 -2285
- package/src/subgraph/hosts.ts +0 -158
- package/src/subgraph/rpc.ts +0 -65
- package/src/subgraph/runtime.ts +0 -595
- package/src/subgraph/scope.ts +0 -68
package/evm.schema.json
CHANGED
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
"null"
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
|
+
"resolvers": {
|
|
33
|
+
"description": "Optional relative path to the custom GraphQL resolvers, either a single module (e.g. 'src/Resolvers.ts') or a directory (e.g. 'src/resolvers'), in which case every .js/.mjs/.ts file beneath it is loaded, tests excluded. The resolvers they export are served as additional root fields on the indexer's GraphQL endpoint.",
|
|
34
|
+
"type": [
|
|
35
|
+
"string",
|
|
36
|
+
"null"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
32
39
|
"full_batch_size": {
|
|
33
40
|
"description": "Target number of events to be processed per batch. Set it to smaller number if you have many Effect API calls which are slow to resolve and can't be batched. (Default: 5000)",
|
|
34
41
|
"type": [
|
|
@@ -543,7 +550,7 @@
|
|
|
543
550
|
"type": "object",
|
|
544
551
|
"properties": {
|
|
545
552
|
"url": {
|
|
546
|
-
"description": "The RPC endpoint URL.",
|
|
553
|
+
"description": "The RPC endpoint URL. An entry whose url is empty, for example `${RPC_URL:-}` with the variable unset, is ignored.",
|
|
547
554
|
"type": "string"
|
|
548
555
|
},
|
|
549
556
|
"for": {
|
package/fuel.schema.json
CHANGED
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
"null"
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
|
+
"resolvers": {
|
|
33
|
+
"description": "Optional relative path to the custom GraphQL resolvers, either a single module (e.g. 'src/Resolvers.ts') or a directory (e.g. 'src/resolvers'), in which case every .js/.mjs/.ts file beneath it is loaded, tests excluded. The resolvers they export are served as additional root fields on the indexer's GraphQL endpoint.",
|
|
34
|
+
"type": [
|
|
35
|
+
"string",
|
|
36
|
+
"null"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
32
39
|
"full_batch_size": {
|
|
33
40
|
"description": "Target number of events to be processed per batch. Set it to smaller number if you have many Effect API calls which are slow to resolve and can't be batched. (Default: 5000)",
|
|
34
41
|
"type": [
|
package/index.d.ts
CHANGED
|
@@ -2027,10 +2027,10 @@ type ConfigEntities<Config extends IndexerConfigTypes = GlobalConfig> =
|
|
|
2027
2027
|
type PerChainEntityNames<Config extends IndexerConfigTypes = GlobalConfig> =
|
|
2028
2028
|
Config extends { perChainEntities: infer Names extends string } ? Names : never;
|
|
2029
2029
|
|
|
2030
|
-
/** The row shape
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
type
|
|
2030
|
+
/** The row shape chain-agnostic APIs exchange. A per-chain entity's row is
|
|
2031
|
+
* only identified together with its chain, so the chain id travels alongside
|
|
2032
|
+
* the entity fields. */
|
|
2033
|
+
type ChainScopedEntityRow<
|
|
2034
2034
|
Config extends IndexerConfigTypes,
|
|
2035
2035
|
Name,
|
|
2036
2036
|
Entity
|
|
@@ -2224,7 +2224,7 @@ export type TestIndexerFromConfig<Config extends IndexerConfigTypes = GlobalConf
|
|
|
2224
2224
|
} & SingleEcosystemTestChains<Config> & {
|
|
2225
2225
|
/** Entity operations for direct manipulation outside of handlers. */
|
|
2226
2226
|
readonly [K in keyof ConfigEntities<Config>]: TestIndexerEntityOperations<
|
|
2227
|
-
|
|
2227
|
+
ChainScopedEntityRow<Config, K, ConfigEntities<Config>[K]>
|
|
2228
2228
|
>;
|
|
2229
2229
|
};
|
|
2230
2230
|
|
|
@@ -2298,6 +2298,259 @@ export type EnumName = keyof EnumsT & string;
|
|
|
2298
2298
|
/** Lookup an enum value type by name (e.g. `Enum<"AccountType">`). */
|
|
2299
2299
|
export type Enum<TName extends EnumName> = EnumsT[TName];
|
|
2300
2300
|
|
|
2301
|
+
// ============== Custom Resolver Types ==============
|
|
2302
|
+
|
|
2303
|
+
/** Handle for a custom GraphQL resolver created via {@link createResolver}. */
|
|
2304
|
+
export declare abstract class Resolver<Args, Output> {
|
|
2305
|
+
protected opaque: Args | Output;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/** The selection the caller asked for: field names and their nesting, with
|
|
2309
|
+
* aliases and fragments already resolved away. A resolver may use it to skip
|
|
2310
|
+
* work it wasn't asked for — the response is projected against the declared
|
|
2311
|
+
* schema either way, so correctness never depends on honouring it. */
|
|
2312
|
+
export type ResolverSelection = { readonly [field: string]: ResolverSelection };
|
|
2313
|
+
|
|
2314
|
+
/** Request metadata, forwarded by envio-serve. */
|
|
2315
|
+
export type ResolverContext = {
|
|
2316
|
+
/** The role the operation was executed under. `admin: true` resolvers are
|
|
2317
|
+
* only reachable as `"admin"`. */
|
|
2318
|
+
readonly role: "public" | "admin";
|
|
2319
|
+
/** Correlates this field's work with the operation it belongs to. */
|
|
2320
|
+
readonly requestId: string;
|
|
2321
|
+
/** W3C trace context of the incoming request, when the caller sent one. */
|
|
2322
|
+
readonly traceparent?: string;
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
/** How far a chain has been indexed. Read it to refuse to answer from a stale
|
|
2326
|
+
* index — `sourceBlock - progressBlock` is how far behind the head the
|
|
2327
|
+
* indexer is. */
|
|
2328
|
+
export type ChainHeight = {
|
|
2329
|
+
readonly chainId: number;
|
|
2330
|
+
/** `"evm"`, `"fuel"` or `"svm"`. Chain ids are only unique within one. */
|
|
2331
|
+
readonly ecosystem: string;
|
|
2332
|
+
readonly startBlock: number;
|
|
2333
|
+
readonly endBlock: number | null;
|
|
2334
|
+
/** The latest block seen on the chain. */
|
|
2335
|
+
readonly sourceBlock: number;
|
|
2336
|
+
/** The latest block fetched from the source. */
|
|
2337
|
+
readonly bufferBlock: number;
|
|
2338
|
+
/** The latest block whose events have been processed and committed. */
|
|
2339
|
+
readonly progressBlock: number;
|
|
2340
|
+
/** When the chain first caught up to the head, or null while it hasn't. */
|
|
2341
|
+
readonly readyAt: Date | null;
|
|
2342
|
+
readonly isReady: boolean;
|
|
2343
|
+
};
|
|
2344
|
+
|
|
2345
|
+
/** Raw SQL, run under the resolver's `statement_timeout` and against its
|
|
2346
|
+
* bounded pool. */
|
|
2347
|
+
export type ResolverSql = {
|
|
2348
|
+
/** ``db.sql`select 1` `` */
|
|
2349
|
+
<Row = Record<string, unknown>>(
|
|
2350
|
+
strings: TemplateStringsArray,
|
|
2351
|
+
...values: readonly unknown[]
|
|
2352
|
+
): Promise<Row[]>;
|
|
2353
|
+
/** `db.sql.unsafe(text, params)` — the direct lift for a query built as a
|
|
2354
|
+
* string with positional parameters. */
|
|
2355
|
+
unsafe<Row = Record<string, unknown>>(
|
|
2356
|
+
text: string,
|
|
2357
|
+
params?: readonly unknown[],
|
|
2358
|
+
options?: { readonly prepare?: boolean }
|
|
2359
|
+
): Promise<Row[]>;
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
/** Options accepted by `db.find`. */
|
|
2363
|
+
export type ResolverFindOptions<Entity> = {
|
|
2364
|
+
/** Filter by field, with the same operators as `context.<Entity>.getWhere`.
|
|
2365
|
+
* Unlike `getWhere`, any field can be filtered on here — it is a query
|
|
2366
|
+
* against the table, so an unindexed field is a slow one, not an error. */
|
|
2367
|
+
readonly where?: GetWhereFilter<Entity>;
|
|
2368
|
+
readonly orderBy?: readonly {
|
|
2369
|
+
readonly field: keyof Entity & string;
|
|
2370
|
+
readonly direction: "asc" | "desc";
|
|
2371
|
+
}[];
|
|
2372
|
+
readonly limit?: number;
|
|
2373
|
+
readonly offset?: number;
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
/** The database a resolver reads through. Every query it runs carries the
|
|
2377
|
+
* resolver's `timeoutMs` as a `statement_timeout` and takes one of the
|
|
2378
|
+
* process's bounded connections. */
|
|
2379
|
+
export type ResolverDb<Config extends IndexerConfigTypes = GlobalConfig> = {
|
|
2380
|
+
/** Entities matching `where`, decoded exactly as a handler sees them. */
|
|
2381
|
+
readonly find: <Name extends keyof ConfigEntities<Config> & string>(
|
|
2382
|
+
entityName: Name,
|
|
2383
|
+
options?: ResolverFindOptions<
|
|
2384
|
+
ChainScopedEntityRow<Config, Name, ConfigEntities<Config>[Name]>
|
|
2385
|
+
>
|
|
2386
|
+
) => Promise<
|
|
2387
|
+
ChainScopedEntityRow<Config, Name, ConfigEntities<Config>[Name]>[]
|
|
2388
|
+
>;
|
|
2389
|
+
/** One entity by id, or null. Not available for per-chain entities, whose
|
|
2390
|
+
* key is the id together with a chain — use {@link ResolverDb.find}. */
|
|
2391
|
+
readonly get: <
|
|
2392
|
+
Name extends Exclude<
|
|
2393
|
+
keyof ConfigEntities<Config> & string,
|
|
2394
|
+
PerChainEntityNames<Config>
|
|
2395
|
+
>
|
|
2396
|
+
>(
|
|
2397
|
+
entityName: Name,
|
|
2398
|
+
id: EntityId<ConfigEntities<Config>[Name]>
|
|
2399
|
+
) => Promise<ConfigEntities<Config>[Name] | null>;
|
|
2400
|
+
/** Raw SQL, for what the loaders don't express. */
|
|
2401
|
+
readonly sql: ResolverSql;
|
|
2402
|
+
/** The Postgres schema the indexer's tables live in. Raw SQL has to qualify
|
|
2403
|
+
* table names with it — it is not `"public"` on a hosted deployment. */
|
|
2404
|
+
readonly pgSchema: string;
|
|
2405
|
+
/** Several queries on one connection, under one timeout. */
|
|
2406
|
+
readonly transaction: <T>(work: (sql: ResolverSql) => Promise<T>) => Promise<T>;
|
|
2407
|
+
/** How far each chain has been indexed, keyed by chain id. */
|
|
2408
|
+
readonly chainHeights: () => Promise<Record<string, ChainHeight>>;
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
/** Arguments passed to a {@link Resolver}'s handler. */
|
|
2412
|
+
export type ResolverHandlerArgs<
|
|
2413
|
+
Args,
|
|
2414
|
+
Config extends IndexerConfigTypes = GlobalConfig
|
|
2415
|
+
> = {
|
|
2416
|
+
/** Coerced and validated against the declared arg schemas before the
|
|
2417
|
+
* resolver is reached. */
|
|
2418
|
+
readonly args: Args;
|
|
2419
|
+
readonly db: ResolverDb<Config>;
|
|
2420
|
+
readonly selection: ResolverSelection;
|
|
2421
|
+
readonly ctx: ResolverContext;
|
|
2422
|
+
};
|
|
2423
|
+
|
|
2424
|
+
/**
|
|
2425
|
+
* Declares a custom GraphQL resolver: a root query field served alongside the
|
|
2426
|
+
* generated entity fields, backed by TypeScript in this project.
|
|
2427
|
+
*
|
|
2428
|
+
* Arguments and result are declared with the same `S` schemas effects use, and
|
|
2429
|
+
* the GraphQL types are derived from them. A field is non-null unless its
|
|
2430
|
+
* schema is optional. Object types must be named with {@link defineType} —
|
|
2431
|
+
* GraphQL has no anonymous types.
|
|
2432
|
+
*
|
|
2433
|
+
* ```ts
|
|
2434
|
+
* export const marketsAprByPeriod = createResolver({
|
|
2435
|
+
* name: "marketsAprByPeriod",
|
|
2436
|
+
* args: { periodStart: S.int32, periodEnd: S.int32 },
|
|
2437
|
+
* output: S.array(defineType("MarketApr", { marketAddress: S.string })),
|
|
2438
|
+
* timeoutMs: 30_000,
|
|
2439
|
+
* handler: async ({ args, db }) => db.find("MarketInfo"),
|
|
2440
|
+
* });
|
|
2441
|
+
* ```
|
|
2442
|
+
*/
|
|
2443
|
+
export function createResolver<
|
|
2444
|
+
// Both are schemas at runtime: `createResolver` reads `output.t` and walks
|
|
2445
|
+
// every argument with `toGraphQLType` while the module is being imported, so
|
|
2446
|
+
// anything else fails at indexer startup rather than in the editor.
|
|
2447
|
+
AS extends Record<string, Sury.Schema<unknown>>,
|
|
2448
|
+
OS extends Sury.Schema<unknown>,
|
|
2449
|
+
A = UnknownToOutput<AS>,
|
|
2450
|
+
O = UnknownToOutput<OS>,
|
|
2451
|
+
// A hack to enforce that the inferred return type matches the output schema.
|
|
2452
|
+
R extends O = O
|
|
2453
|
+
>(options: {
|
|
2454
|
+
/** The GraphQL field name. Must not collide with a generated one. */
|
|
2455
|
+
readonly name: string;
|
|
2456
|
+
/** Shown in the schema, for the people querying it. */
|
|
2457
|
+
readonly description?: string;
|
|
2458
|
+
/** Argument schemas, keyed by argument name. */
|
|
2459
|
+
readonly args?: AS;
|
|
2460
|
+
/** The result schema. Determines the field's GraphQL type. */
|
|
2461
|
+
readonly output: OS;
|
|
2462
|
+
/** Refuse the field to callers who cannot present a key from
|
|
2463
|
+
* `ENVIO_RESOLVERS_PRIVATE_KEYS` in the `x-envio-private-key` header. With
|
|
2464
|
+
* no keys configured it refuses everyone.
|
|
2465
|
+
*
|
|
2466
|
+
* This hides the answers, not the field: Hasura will not route to an action
|
|
2467
|
+
* it does not publish, so a private resolver is on the public schema and its
|
|
2468
|
+
* name, arguments and result type stay visible to anonymous introspection.
|
|
2469
|
+
* A name that would itself disclose something does not belong here. */
|
|
2470
|
+
readonly private?: boolean;
|
|
2471
|
+
/** @deprecated The former spelling of `private`, and identical to it. */
|
|
2472
|
+
readonly admin?: boolean;
|
|
2473
|
+
/** Refuse to answer with a 503 when the index is further behind head than
|
|
2474
|
+
* this. A number applies to every chain; an object of chainId to blocks
|
|
2475
|
+
* applies per chain and ignores the chains it does not name — which is
|
|
2476
|
+
* usually what you want, since a few hundred blocks is seconds on one chain
|
|
2477
|
+
* and hours on another. Omitted, the resolver answers whatever the index
|
|
2478
|
+
* currently holds. */
|
|
2479
|
+
readonly maxBlocksBehind?: number | Readonly<Record<number, number>>;
|
|
2480
|
+
/** The `statement_timeout` every query this resolver runs is bounded by.
|
|
2481
|
+
* Required: it is the only thing bounding a runaway query. */
|
|
2482
|
+
readonly timeoutMs: number;
|
|
2483
|
+
readonly handler: (args: ResolverHandlerArgs<A>) => Promise<R>;
|
|
2484
|
+
}): Resolver<A, O>;
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Thrown by a resolver to put a specific error on the client's response.
|
|
2488
|
+
*
|
|
2489
|
+
* Anything else a handler throws is reported as an internal failure with its
|
|
2490
|
+
* message withheld — a driver error can carry a connection string, and the
|
|
2491
|
+
* caller is the public internet — so this is how a resolver says more than
|
|
2492
|
+
* that. envio-serve carries the extensions through unchanged.
|
|
2493
|
+
*
|
|
2494
|
+
* ```ts
|
|
2495
|
+
* throw new ResolverError("Service is syncing", {
|
|
2496
|
+
* code: "SERVICE_UNAVAILABLE",
|
|
2497
|
+
* httpStatus: 503,
|
|
2498
|
+
* });
|
|
2499
|
+
* ```
|
|
2500
|
+
*/
|
|
2501
|
+
export declare class ResolverError extends Error {
|
|
2502
|
+
constructor(
|
|
2503
|
+
message: string,
|
|
2504
|
+
options?: {
|
|
2505
|
+
/** Becomes `extensions.code`. Defaults to `"INTERNAL_SERVER_ERROR"`. */
|
|
2506
|
+
readonly code?: string;
|
|
2507
|
+
/** Becomes `extensions.http.status`. */
|
|
2508
|
+
readonly httpStatus?: number;
|
|
2509
|
+
/** Merged into `extensions` alongside `code`. */
|
|
2510
|
+
readonly extensions?: Record<string, unknown>;
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
readonly code: string;
|
|
2514
|
+
readonly httpStatus?: number;
|
|
2515
|
+
readonly extensions?: Record<string, unknown>;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/** Names a schema so it can appear in the GraphQL schema as an object type. */
|
|
2519
|
+
export function defineType<F extends Record<string, unknown>>(
|
|
2520
|
+
name: string,
|
|
2521
|
+
fields: F
|
|
2522
|
+
): Sury.Schema<UnknownToOutput<F>>;
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Names a schema that appears in an argument.
|
|
2526
|
+
*
|
|
2527
|
+
* GraphQL keeps input and output types in a separate namespace from object
|
|
2528
|
+
* types, so a `where` argument is declared with this rather than with
|
|
2529
|
+
* {@link defineType}.
|
|
2530
|
+
*/
|
|
2531
|
+
export function defineInput<F extends Record<string, unknown>>(
|
|
2532
|
+
name: string,
|
|
2533
|
+
fields: F
|
|
2534
|
+
): Sury.Schema<UnknownToOutput<F>>;
|
|
2535
|
+
|
|
2536
|
+
/** Every resolver declared so far, in declaration order. */
|
|
2537
|
+
export function getRegisteredResolvers(): readonly Resolver<unknown, unknown>[];
|
|
2538
|
+
|
|
2539
|
+
/** The manifest and SDL for everything declared so far. */
|
|
2540
|
+
export function buildRegisteredManifest(): { manifest: unknown; sdl: string };
|
|
2541
|
+
|
|
2542
|
+
/** Names a schema representing a GraphQL enum. */
|
|
2543
|
+
export function defineEnum<const V extends readonly string[]>(
|
|
2544
|
+
name: string,
|
|
2545
|
+
values: V
|
|
2546
|
+
): Sury.Schema<V[number]>;
|
|
2547
|
+
|
|
2548
|
+
/** Names a schema representing a custom GraphQL scalar, such as `BigInt`. */
|
|
2549
|
+
export function defineScalar<T>(
|
|
2550
|
+
name: string,
|
|
2551
|
+
schema: Sury.Schema<T>
|
|
2552
|
+
): Sury.Schema<T>;
|
|
2553
|
+
|
|
2301
2554
|
// ============== Runtime values ==============
|
|
2302
2555
|
|
|
2303
2556
|
/** The indexer instance. Register handlers with `indexer.onEvent`,
|
package/index.js
CHANGED
|
@@ -9,6 +9,16 @@ import { schema as bigDecimalSchema } from "./src/bindings/BigDecimal.res.mjs";
|
|
|
9
9
|
|
|
10
10
|
export * from "./src/Envio.res.mjs";
|
|
11
11
|
export { indexer, createTestIndexer } from "./src/Api.res.mjs";
|
|
12
|
+
export {
|
|
13
|
+
createResolver,
|
|
14
|
+
defineType,
|
|
15
|
+
defineInput,
|
|
16
|
+
defineEnum,
|
|
17
|
+
defineScalar,
|
|
18
|
+
getRegisteredResolvers,
|
|
19
|
+
buildRegisteredManifest,
|
|
20
|
+
ResolverError,
|
|
21
|
+
} from "./src/resolvers/index.js";
|
|
12
22
|
export { default as BigDecimal } from "bignumber.js";
|
|
13
23
|
|
|
14
24
|
// Important! Should match the index.d.ts file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.1-resolvers.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
|
|
6
6
|
"bin": "./bin.mjs",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"postgres": "3.4.8"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"envio-linux-x64": "3.14.
|
|
58
|
-
"envio-linux-x64-musl": "3.14.
|
|
59
|
-
"envio-linux-arm64": "3.14.
|
|
60
|
-
"envio-darwin-x64": "3.14.
|
|
61
|
-
"envio-darwin-arm64": "3.14.
|
|
57
|
+
"envio-linux-x64": "3.14.1-resolvers.2",
|
|
58
|
+
"envio-linux-x64-musl": "3.14.1-resolvers.2",
|
|
59
|
+
"envio-linux-arm64": "3.14.1-resolvers.2",
|
|
60
|
+
"envio-darwin-x64": "3.14.1-resolvers.2",
|
|
61
|
+
"envio-darwin-arm64": "3.14.1-resolvers.2"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/Bin.res
CHANGED
|
@@ -18,11 +18,13 @@ type startCmd = {
|
|
|
18
18
|
}
|
|
19
19
|
type migrateCmd = {reset: bool, config: JSON.t}
|
|
20
20
|
type dropSchemaCmd = {config: JSON.t}
|
|
21
|
+
type resolversCmd = {mode: string, cwd: string, env: dict<JSON.t>, config: JSON.t}
|
|
21
22
|
|
|
22
23
|
type command =
|
|
23
24
|
| Start(startCmd)
|
|
24
25
|
| Migrate(migrateCmd)
|
|
25
26
|
| DropSchema(dropSchemaCmd)
|
|
27
|
+
| Resolvers(resolversCmd)
|
|
26
28
|
|
|
27
29
|
let decodeCommand = (json: JSON.t): command => {
|
|
28
30
|
let obj = switch json->JSON.Decode.object {
|
|
@@ -37,6 +39,7 @@ let decodeCommand = (json: JSON.t): command => {
|
|
|
37
39
|
| "start" => Start(json->(Utils.magic: JSON.t => startCmd))
|
|
38
40
|
| "migrate" => Migrate(json->(Utils.magic: JSON.t => migrateCmd))
|
|
39
41
|
| "drop-schema" => DropSchema(json->(Utils.magic: JSON.t => dropSchemaCmd))
|
|
42
|
+
| "resolvers" => Resolvers(json->(Utils.magic: JSON.t => resolversCmd))
|
|
40
43
|
| other => JsError.throwWithMessage(`Unknown command kind: ${other}`)
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -67,6 +70,12 @@ let run = async args => {
|
|
|
67
70
|
Config.prime(config)
|
|
68
71
|
processChdir(cwd)
|
|
69
72
|
applyEnv(env)
|
|
73
|
+
// `envio dev` only — see ResolverProcess.startForDev. Deployed, the
|
|
74
|
+
// resolvers are their own service and are never started from here.
|
|
75
|
+
let loaded = Config.load()
|
|
76
|
+
if loaded.isDev {
|
|
77
|
+
let _ = await ResolverProcess.startForDev(~config=loaded, ~projectRoot=cwd)
|
|
78
|
+
}
|
|
70
79
|
await Main.start(~reset)
|
|
71
80
|
| Migrate({reset, config}) =>
|
|
72
81
|
Config.prime(config)
|
|
@@ -74,6 +83,45 @@ let run = async args => {
|
|
|
74
83
|
| DropSchema({config}) =>
|
|
75
84
|
Config.prime(config)
|
|
76
85
|
await Main.dropSchema()
|
|
86
|
+
| Resolvers({mode, cwd, env, config}) =>
|
|
87
|
+
Config.prime(config)
|
|
88
|
+
processChdir(cwd)
|
|
89
|
+
applyEnv(env)
|
|
90
|
+
switch mode {
|
|
91
|
+
| "manifest" =>
|
|
92
|
+
await ResolverProcess.writeManifest(~config=Config.load(), ~projectRoot=cwd)
|
|
93
|
+
| "metadata" =>
|
|
94
|
+
let handlerUrl = switch Env.Resolvers.publicUrl() {
|
|
95
|
+
| Some(url) => url
|
|
96
|
+
| None =>
|
|
97
|
+
JsError.throwWithMessage(
|
|
98
|
+
"ENVIO_RESOLVERS_PUBLIC_URL is not set. It is the URL Hasura posts to and is baked into every action, so the metadata cannot be printed without it.",
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
let metadata = await ResolverProcess.metadataJson(
|
|
102
|
+
~config=Config.load(),
|
|
103
|
+
~projectRoot=cwd,
|
|
104
|
+
~handlerUrl,
|
|
105
|
+
)
|
|
106
|
+
// stdout, so it pipes into `hasura metadata apply` or `jq` rather
|
|
107
|
+
// than being buried in the log stream.
|
|
108
|
+
Console.log(JSON.stringify(metadata, ~space=2))
|
|
109
|
+
| "migrate" =>
|
|
110
|
+
let {applied, reasons} = await ResolverProcess.migrate(
|
|
111
|
+
~config=Config.load(),
|
|
112
|
+
~projectRoot=cwd,
|
|
113
|
+
)
|
|
114
|
+
Logging.info(
|
|
115
|
+
applied
|
|
116
|
+
? `Updated Hasura to match these resolvers: ${reasons->Array.join("; ")}`
|
|
117
|
+
: "Hasura already matches these resolvers; nothing to update",
|
|
118
|
+
)
|
|
119
|
+
// Serving keeps the event loop alive on its own, so returning here
|
|
120
|
+
// leaves the process running rather than exiting.
|
|
121
|
+
| _ =>
|
|
122
|
+
let running = await ResolverProcess.serve(~config=Config.load(), ~projectRoot=cwd)
|
|
123
|
+
running->ResolverProcess.handleSignals
|
|
124
|
+
}
|
|
77
125
|
}
|
|
78
126
|
}
|
|
79
127
|
}
|
package/src/Bin.res.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Env from "./Env.res.mjs";
|
|
3
4
|
import * as Core from "./Core.res.mjs";
|
|
4
5
|
import * as Main from "./Main.res.mjs";
|
|
5
6
|
import * as Utils from "./Utils.res.mjs";
|
|
@@ -12,6 +13,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
12
13
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
13
14
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
14
15
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
16
|
+
import * as ResolverProcess from "./resolvers/ResolverProcess.res.mjs";
|
|
15
17
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
18
|
|
|
17
19
|
let setEnvVar = ((k, v) => { process.env[k] = v; });
|
|
@@ -37,6 +39,11 @@ function decodeCommand(json) {
|
|
|
37
39
|
TAG: "Migrate",
|
|
38
40
|
_0: json
|
|
39
41
|
};
|
|
42
|
+
case "resolvers" :
|
|
43
|
+
return {
|
|
44
|
+
TAG: "Resolvers",
|
|
45
|
+
_0: json
|
|
46
|
+
};
|
|
40
47
|
case "start" :
|
|
41
48
|
return {
|
|
42
49
|
TAG: "Start",
|
|
@@ -70,9 +77,14 @@ async function run(args) {
|
|
|
70
77
|
switch (match.TAG) {
|
|
71
78
|
case "Start" :
|
|
72
79
|
let match$1 = match._0;
|
|
80
|
+
let cwd = match$1.cwd;
|
|
73
81
|
Config.prime(match$1.config);
|
|
74
|
-
process.chdir(
|
|
82
|
+
process.chdir(cwd);
|
|
75
83
|
applyEnv(match$1.env);
|
|
84
|
+
let loaded = Config.load();
|
|
85
|
+
if (loaded.isDev) {
|
|
86
|
+
await ResolverProcess.startForDev(loaded, cwd);
|
|
87
|
+
}
|
|
76
88
|
return await Main.start(undefined, match$1.reset, undefined, undefined, undefined);
|
|
77
89
|
case "Migrate" :
|
|
78
90
|
let match$2 = match._0;
|
|
@@ -81,6 +93,27 @@ async function run(args) {
|
|
|
81
93
|
case "DropSchema" :
|
|
82
94
|
Config.prime(match._0.config);
|
|
83
95
|
return await Main.dropSchema();
|
|
96
|
+
case "Resolvers" :
|
|
97
|
+
let match$3 = match._0;
|
|
98
|
+
let cwd$1 = match$3.cwd;
|
|
99
|
+
Config.prime(match$3.config);
|
|
100
|
+
process.chdir(cwd$1);
|
|
101
|
+
applyEnv(match$3.env);
|
|
102
|
+
switch (match$3.mode) {
|
|
103
|
+
case "manifest" :
|
|
104
|
+
return await ResolverProcess.writeManifest(Config.load(), cwd$1);
|
|
105
|
+
case "metadata" :
|
|
106
|
+
let url = Env.Resolvers.publicUrl();
|
|
107
|
+
let handlerUrl = url !== undefined ? url : Stdlib_JsError.throwWithMessage("ENVIO_RESOLVERS_PUBLIC_URL is not set. It is the URL Hasura posts to and is baked into every action, so the metadata cannot be printed without it.");
|
|
108
|
+
let metadata = await ResolverProcess.metadataJson(Config.load(), cwd$1, handlerUrl);
|
|
109
|
+
console.log(JSON.stringify(metadata, undefined, 2));
|
|
110
|
+
return;
|
|
111
|
+
case "migrate" :
|
|
112
|
+
let match$4 = await ResolverProcess.migrate(Config.load(), cwd$1);
|
|
113
|
+
return Logging.info(match$4.applied ? `Updated Hasura to match these resolvers: ` + match$4.reasons.join("; ") : "Hasura already matches these resolvers; nothing to update");
|
|
114
|
+
default:
|
|
115
|
+
return ResolverProcess.handleSignals(await ResolverProcess.serve(Config.load(), cwd$1, undefined, undefined, undefined, undefined));
|
|
116
|
+
}
|
|
84
117
|
}
|
|
85
118
|
} catch (raw_exn) {
|
|
86
119
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
package/src/Config.res
CHANGED
|
@@ -106,6 +106,9 @@ type t = {
|
|
|
106
106
|
name: string,
|
|
107
107
|
description: option<string>,
|
|
108
108
|
handlers: string,
|
|
109
|
+
// Relative path to the module declaring custom GraphQL resolvers, when the
|
|
110
|
+
// project ships them. None means entity fields only.
|
|
111
|
+
resolvers: option<string>,
|
|
109
112
|
contractHandlers: array<contractHandler>,
|
|
110
113
|
shouldRollbackOnReorg: bool,
|
|
111
114
|
shouldSaveFullHistory: bool,
|
|
@@ -151,9 +154,6 @@ type t = {
|
|
|
151
154
|
// reach, so its checkpoints have to be comparable across chains.
|
|
152
155
|
checkpointSequence: CheckpointSequence.t,
|
|
153
156
|
envioInfo: JSON.t,
|
|
154
|
-
// Set only in subgraph mode: the translated manifest, which is what makes
|
|
155
|
-
// the subgraph runtime take over handler registration.
|
|
156
|
-
subgraph: option<JSON.t>,
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
type rpcSourceFor = | @as("sync") Sync | @as("fallback") Fallback | @as("realtime") Realtime
|
|
@@ -600,6 +600,7 @@ let publicConfigSchema = S.schema(s =>
|
|
|
600
600
|
"name": s.matches(S.string),
|
|
601
601
|
"description": s.matches(S.option(S.string)),
|
|
602
602
|
"handlers": s.matches(S.option(S.string)),
|
|
603
|
+
"resolvers": s.matches(S.option(S.string)),
|
|
603
604
|
"isDev": s.matches(S.option(S.bool)),
|
|
604
605
|
"isolatedChains": s.matches(S.option(S.array(ChainId.schema))),
|
|
605
606
|
"projectFlags": s.matches(S.option(S.string)),
|
|
@@ -615,7 +616,6 @@ let publicConfigSchema = S.schema(s =>
|
|
|
615
616
|
"svm": s.matches(S.option(publicConfigEcosystemSchema)),
|
|
616
617
|
"enums": s.matches(S.option(S.dict(S.array(S.string)))),
|
|
617
618
|
"entities": s.matches(S.option(S.array(entityJsonSchema))),
|
|
618
|
-
"subgraph": s.matches(S.option(S.json(~validate=false))),
|
|
619
619
|
}
|
|
620
620
|
)
|
|
621
621
|
|
|
@@ -688,8 +688,12 @@ let withProjectFlags = (command, ~config: t) =>
|
|
|
688
688
|
|
|
689
689
|
let ecosystemFields = ["evm", "fuel", "svm"]
|
|
690
690
|
|
|
691
|
-
//
|
|
692
|
-
//
|
|
691
|
+
// Where the custom GraphQL resolvers live. Nothing about what is indexed or
|
|
692
|
+
// how it is stored, so adopting resolvers must not read as a config change.
|
|
693
|
+
let servingFields = ["resolvers"]
|
|
694
|
+
|
|
695
|
+
// What `envio_info` records: the public config without the command's fields,
|
|
696
|
+
// the serving fields, or its chains. A chain is recorded by its own `envio_chains` row and its
|
|
693
697
|
// config's `envio_addresses` rows instead, so adding one never rewrites this
|
|
694
698
|
// record. The rest of a chain's config — its sources, its block lag, a
|
|
695
699
|
// contract's start block — is read from config.yaml on every start.
|
|
@@ -698,10 +702,7 @@ let toEnvioInfo = (publicConfigJson: JSON.t): JSON.t => {
|
|
|
698
702
|
switch envioInfo {
|
|
699
703
|
| Object(obj) => {
|
|
700
704
|
commandFields->Array.forEach(field => obj->Utils.Dict.deleteInPlace(field))
|
|
701
|
-
|
|
702
|
-
// describes stored data — that's already in `evm` / `entities` — so a
|
|
703
|
-
// specVersion bump or a mapping-path edit must not force a reset.
|
|
704
|
-
obj->Utils.Dict.deleteInPlace("subgraph")
|
|
705
|
+
servingFields->Array.forEach(field => obj->Utils.Dict.deleteInPlace(field))
|
|
705
706
|
ecosystemFields->Array.forEach(ecosystem =>
|
|
706
707
|
switch obj->Dict.get(ecosystem) {
|
|
707
708
|
| Some(Object(ecosystemDict)) => ecosystemDict->Utils.Dict.deleteInPlace("chains")
|
|
@@ -1167,6 +1168,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
1167
1168
|
name: publicConfig["name"],
|
|
1168
1169
|
description: publicConfig["description"],
|
|
1169
1170
|
handlers: publicConfig["handlers"]->Option.getOr("src/handlers"),
|
|
1171
|
+
resolvers: publicConfig["resolvers"],
|
|
1170
1172
|
contractHandlers,
|
|
1171
1173
|
shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
|
|
1172
1174
|
shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
|
|
@@ -1191,7 +1193,6 @@ let fromPublic = (publicConfigJson: JSON.t) => {
|
|
|
1191
1193
|
allEnums,
|
|
1192
1194
|
checkpointSequence: CheckpointSequence.fromEntities(userEntities),
|
|
1193
1195
|
envioInfo: publicConfigJson->toEnvioInfo,
|
|
1194
|
-
subgraph: publicConfig["subgraph"],
|
|
1195
1196
|
}
|
|
1196
1197
|
|
|
1197
1198
|
switch publicConfig["isolatedChains"] {
|
package/src/Config.res.mjs
CHANGED
|
@@ -436,6 +436,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
|
|
|
436
436
|
name: s.m(S$RescriptSchema.string),
|
|
437
437
|
description: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
|
|
438
438
|
handlers: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
|
|
439
|
+
resolvers: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
|
|
439
440
|
isDev: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
|
|
440
441
|
isolatedChains: s.m(S$RescriptSchema.option(S$RescriptSchema.array(ChainId.schema))),
|
|
441
442
|
projectFlags: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
|
|
@@ -450,8 +451,7 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
|
|
|
450
451
|
fuel: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
|
|
451
452
|
svm: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
|
|
452
453
|
enums: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(S$RescriptSchema.array(S$RescriptSchema.string)))),
|
|
453
|
-
entities: s.m(S$RescriptSchema.option(S$RescriptSchema.array(entityJsonSchema)))
|
|
454
|
-
subgraph: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
|
|
454
|
+
entities: s.m(S$RescriptSchema.option(S$RescriptSchema.array(entityJsonSchema)))
|
|
455
455
|
}));
|
|
456
456
|
|
|
457
457
|
function contractMappingOf(chainConfigs) {
|
|
@@ -517,11 +517,13 @@ let ecosystemFields = [
|
|
|
517
517
|
"svm"
|
|
518
518
|
];
|
|
519
519
|
|
|
520
|
+
let servingFields = ["resolvers"];
|
|
521
|
+
|
|
520
522
|
function toEnvioInfo(publicConfigJson) {
|
|
521
523
|
let envioInfo = JSON.parse(JSON.stringify(publicConfigJson));
|
|
522
524
|
if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
|
|
523
525
|
commandFields.forEach(field => Utils.Dict.deleteInPlace(envioInfo, field));
|
|
524
|
-
Utils.Dict.deleteInPlace(envioInfo,
|
|
526
|
+
servingFields.forEach(field => Utils.Dict.deleteInPlace(envioInfo, field));
|
|
525
527
|
ecosystemFields.forEach(ecosystem => {
|
|
526
528
|
let match = envioInfo[ecosystem];
|
|
527
529
|
if (typeof match === "object" && match !== null && !Array.isArray(match)) {
|
|
@@ -830,6 +832,7 @@ function fromPublic(publicConfigJson) {
|
|
|
830
832
|
let config_name = publicConfig.name;
|
|
831
833
|
let config_description = publicConfig.description;
|
|
832
834
|
let config_handlers = Stdlib_Option.getOr(publicConfig.handlers, "src/handlers");
|
|
835
|
+
let config_resolvers = publicConfig.resolvers;
|
|
833
836
|
let config_shouldRollbackOnReorg = Stdlib_Option.getOr(publicConfig.rollbackOnReorg, true);
|
|
834
837
|
let config_shouldSaveFullHistory = Stdlib_Option.getOr(publicConfig.saveFullHistory, false);
|
|
835
838
|
let config_chainIdMode = Stdlib_Option.getOr(publicConfig.chainIdMode, "int32");
|
|
@@ -841,11 +844,11 @@ function fromPublic(publicConfigJson) {
|
|
|
841
844
|
let config_projectFlags = Stdlib_Option.getOr(publicConfig.projectFlags, "");
|
|
842
845
|
let config_checkpointSequence = CheckpointSequence.fromEntities(userEntities);
|
|
843
846
|
let config_envioInfo = toEnvioInfo(publicConfigJson);
|
|
844
|
-
let config_subgraph = publicConfig.subgraph;
|
|
845
847
|
let config = {
|
|
846
848
|
name: config_name,
|
|
847
849
|
description: config_description,
|
|
848
850
|
handlers: config_handlers,
|
|
851
|
+
resolvers: config_resolvers,
|
|
849
852
|
contractHandlers: contractHandlers,
|
|
850
853
|
shouldRollbackOnReorg: config_shouldRollbackOnReorg,
|
|
851
854
|
shouldSaveFullHistory: config_shouldSaveFullHistory,
|
|
@@ -869,8 +872,7 @@ function fromPublic(publicConfigJson) {
|
|
|
869
872
|
userEntities: userEntities,
|
|
870
873
|
allEnums: allEnums,
|
|
871
874
|
checkpointSequence: config_checkpointSequence,
|
|
872
|
-
envioInfo: config_envioInfo
|
|
873
|
-
subgraph: config_subgraph
|
|
875
|
+
envioInfo: config_envioInfo
|
|
874
876
|
};
|
|
875
877
|
let chainIds = publicConfig.isolatedChains;
|
|
876
878
|
if (chainIds !== undefined) {
|
|
@@ -1188,6 +1190,7 @@ export {
|
|
|
1188
1190
|
commandFields,
|
|
1189
1191
|
withProjectFlags,
|
|
1190
1192
|
ecosystemFields,
|
|
1193
|
+
servingFields,
|
|
1191
1194
|
toEnvioInfo,
|
|
1192
1195
|
fromPublic,
|
|
1193
1196
|
normalizeUserAddress,
|