diff --git a/package-lock.json b/package-lock.json index cda0020f..f53b9faa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6759,6 +6759,11 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", "integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==" }, + "node_modules/@types/humanize-duration": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.2.tgz", + "integrity": "sha512-KOGjfVAD8CHMgXL6z96f7eCNRFUENKa2BG87l7JsSg9ZA6lRFsipZ0faF4kKFqnzxirVgXmOnWqTLAKUog1h/g==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -14776,6 +14781,11 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-duration": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.30.0.tgz", + "integrity": "sha512-NxpT0fhQTFuMTLnuu1Xp+ozNpYirQnbV3NlOjEKBYlE3uvMRu3LDuq8EPc3gVXxVYnchQfqVM4/+T9iwHPLLeA==" + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -27420,9 +27430,11 @@ "license": "MIT", "dependencies": { "@graphql-tools/stitch": "^9.0.3", + "@types/humanize-duration": "^3.27.2", "class-validator": "^0.14.0", "graphql": "16.6.0", "graphql-scalars": "^1.22.4", + "humanize-duration": "^3.30.0", "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "type-graphql": "2.0.0-beta.1" diff --git a/packages/api/package.json b/packages/api/package.json index 0495055f..e262be4d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -18,11 +18,13 @@ }, "dependencies": { "@graphql-tools/stitch": "^9.0.3", - "lodash": "^4.17.21", - "reflect-metadata": "^0.1.13", + "@types/humanize-duration": "^3.27.2", "class-validator": "^0.14.0", "graphql": "16.6.0", "graphql-scalars": "^1.22.4", + "humanize-duration": "^3.30.0", + "lodash": "^4.17.21", + "reflect-metadata": "^0.1.13", "type-graphql": "2.0.0-beta.1" }, "peerDependencies": { diff --git a/packages/api/src/graphql/GraphqlModule.ts b/packages/api/src/graphql/GraphqlModule.ts index 94c4f0da..4bc56551 100644 --- a/packages/api/src/graphql/GraphqlModule.ts +++ b/packages/api/src/graphql/GraphqlModule.ts @@ -1,9 +1,25 @@ -import type { UnTypedClass } from "@proto-kit/protocol"; -import { ConfigurableModule } from "@proto-kit/common"; +import { + ConfigurableModule, + TypedClass, +} from "@proto-kit/common"; import { GraphQLSchema } from "graphql/type"; +import { injectable, Lifecycle, scoped } from "tsyringe"; +import { Resolver } from "type-graphql"; + +const graphqlModuleMetadataKey = "graphqlModule"; export abstract class GraphqlModule extends ConfigurableModule { - public abstract resolverType: UnTypedClass; + public constructor() { + super(); + + const isDecoratedProperly = + Reflect.getMetadata(graphqlModuleMetadataKey, this.constructor) === true; + if (!isDecoratedProperly) { + throw new Error( + `Module ${this.constructor.name} not decorated property. Make sure to use @graphqlModule() on all GraphqlModules` + ); + } + } } export abstract class SchemaGeneratingGraphqlModule< @@ -11,3 +27,20 @@ export abstract class SchemaGeneratingGraphqlModule< > extends GraphqlModule { public abstract generateSchema(): GraphQLSchema; } + +export function graphqlModule() { + return ( + /** + * Check if the target class extends RuntimeModule, while + * also providing static config presets + */ + target: TypedClass> + ) => { + injectable()(target); + scoped(Lifecycle.ContainerScoped)(target); + // eslint-disable-next-line new-cap + Resolver()(target); + + Reflect.defineMetadata(graphqlModuleMetadataKey, true, target); + }; +} diff --git a/packages/api/src/graphql/GraphqlSequencerModule.ts b/packages/api/src/graphql/GraphqlSequencerModule.ts index 06b27af1..a88bd821 100644 --- a/packages/api/src/graphql/GraphqlSequencerModule.ts +++ b/packages/api/src/graphql/GraphqlSequencerModule.ts @@ -12,7 +12,7 @@ import { } from "@proto-kit/common"; import { GraphqlServer } from "./GraphqlServer"; -import { GraphqlModule, SchemaGeneratingGraphqlModule } from "./GraphqlModule"; +import { SchemaGeneratingGraphqlModule } from "./GraphqlModule"; export type GraphqlModulesRecord = ModulesRecord; @@ -52,11 +52,18 @@ export class GraphqlSequencerModule // eslint-disable-next-line guard-for-in for (const moduleName in this.definition.modules) { - const module: GraphqlModule = this.resolve(moduleName); - this.graphqlServer.registerModule(module); + const moduleClass = this.definition.modules[moduleName]; + this.graphqlServer.registerModule(moduleClass); - if (module instanceof SchemaGeneratingGraphqlModule) { + if ( + Object.prototype.isPrototypeOf.call( + SchemaGeneratingGraphqlModule, + moduleClass + ) + ) { log.debug(`Registering manual schema for ${moduleName}`); + const module: SchemaGeneratingGraphqlModule = + this.resolve(moduleName); this.graphqlServer.registerSchema(module.generateSchema()); } } diff --git a/packages/api/src/graphql/GraphqlServer.ts b/packages/api/src/graphql/GraphqlServer.ts index 60882805..7cdb9bcb 100644 --- a/packages/api/src/graphql/GraphqlServer.ts +++ b/packages/api/src/graphql/GraphqlServer.ts @@ -1,7 +1,7 @@ -import { buildSchemaSync } from "type-graphql"; +import { buildSchemaSync, NonEmptyArray } from "type-graphql"; import { DependencyContainer, injectable } from "tsyringe"; import { SequencerModule } from "@proto-kit/sequencer"; -import { log, noop } from "@proto-kit/common"; +import { log, noop, TypedClass } from "@proto-kit/common"; import { GraphQLSchema } from "graphql/type"; import { stitchSchemas } from "@graphql-tools/stitch"; import { createYoga } from "graphql-yoga"; @@ -14,9 +14,18 @@ interface GraphqlServerOptions { port: number; } +function assertArrayIsNotEmpty( + array: readonly T[], + errorMessage: string +): asserts array is NonEmptyArray { + if (array.length === 0) { + throw new Error(errorMessage); + } +} + @injectable() export class GraphqlServer extends SequencerModule { - private readonly modules: GraphqlModule[] = []; + private readonly modules: TypedClass>[] = []; private readonly schemas: GraphQLSchema[] = []; @@ -34,7 +43,7 @@ export class GraphqlServer extends SequencerModule { } } - public registerModule(module: GraphqlModule) { + public registerModule(module: TypedClass>) { this.modules.push(module); } @@ -50,12 +59,14 @@ export class GraphqlServer extends SequencerModule { const { dependencyContainer, modules } = this; this.assertDependencyContainerSet(dependencyContainer); + assertArrayIsNotEmpty( + modules, + "At least one module has to be provided to GraphqlServer" + ); + // Building schema const resolverSchema = buildSchemaSync({ - resolvers: [ - modules[0].resolverType, - ...modules.slice(1).map((x) => x.resolverType), - ], + resolvers: modules, // resolvers: [MempoolResolver as Function], // eslint-disable-next-line max-len diff --git a/packages/api/src/graphql/modules/BlockStorageResolver.ts b/packages/api/src/graphql/modules/BlockStorageResolver.ts index 87318e89..eb3e0921 100644 --- a/packages/api/src/graphql/modules/BlockStorageResolver.ts +++ b/packages/api/src/graphql/modules/BlockStorageResolver.ts @@ -15,7 +15,7 @@ import { ComputedBlockTransaction, } from "@proto-kit/sequencer"; -import { GraphqlModule } from "../GraphqlModule"; +import { graphqlModule, GraphqlModule } from "../GraphqlModule"; import { TransactionObject } from "./MempoolResolver"; @@ -72,11 +72,8 @@ export class ComputedBlockModel { } } -@injectable() -@Resolver(ComputedBlockModel) +@graphqlModule() export class BlockStorageResolver extends GraphqlModule { - public resolverType = BlockStorageResolver; - // TODO seperate these two block interfaces public constructor( @inject("BlockStorage") diff --git a/packages/api/src/graphql/modules/MempoolResolver.ts b/packages/api/src/graphql/modules/MempoolResolver.ts index c0249b59..ec07e9f9 100644 --- a/packages/api/src/graphql/modules/MempoolResolver.ts +++ b/packages/api/src/graphql/modules/MempoolResolver.ts @@ -12,7 +12,7 @@ import { inject, injectable } from "tsyringe"; import { IsNumberString } from "class-validator"; import { Mempool, PendingTransaction } from "@proto-kit/sequencer"; -import { GraphqlModule } from "../GraphqlModule.js"; +import { graphqlModule, GraphqlModule } from "../GraphqlModule.js"; @ObjectType() @InputType("SignatureInput") @@ -71,11 +71,8 @@ export class TransactionObject { } } -@injectable() -@Resolver(TransactionObject) +@graphqlModule() export class MempoolResolver extends GraphqlModule { - public resolverType = MempoolResolver; - private readonly mempool: Mempool; public constructor(@inject("Mempool") mempool: Mempool) { diff --git a/packages/api/src/graphql/modules/NodeStatusResolver.ts b/packages/api/src/graphql/modules/NodeStatusResolver.ts new file mode 100644 index 00000000..647eb90f --- /dev/null +++ b/packages/api/src/graphql/modules/NodeStatusResolver.ts @@ -0,0 +1,55 @@ +import { Field, ObjectType, Query } from "type-graphql"; +import { ChildContainerProvider } from "@proto-kit/common"; + +import { graphqlModule, GraphqlModule } from "../GraphqlModule"; +import { NodeStatus, NodeStatusService } from "../services/NodeStatusService"; + +@ObjectType() +export class NodeStatusObject { + public static fromServiceLayerModel(status: NodeStatus) { + return new NodeStatusObject( + status.uptime, + status.uptimeHumanReadable, + status.height + ); + } + + @Field() + public uptime: number; + + @Field() + public height: number; + + @Field() + public uptimeHumanReadable: string; + + public constructor( + uptime: number, + uptimeHumanReadable: string, + height: number + ) { + this.uptime = uptime; + this.uptimeHumanReadable = uptimeHumanReadable; + this.height = height; + } +} + +@graphqlModule() +export class NodeStatusResolver extends GraphqlModule { + public constructor(private readonly nodeStatusService: NodeStatusService) { + super(); + } + + public create(childContainerProvider: ChildContainerProvider) { + super.create(childContainerProvider); + + // Workaround to initialize uptime + void this.nodeStatusService.getNodeStatus(); + } + + @Query(() => NodeStatusObject) + public async nodeStatus(): Promise { + const status = await this.nodeStatusService.getNodeStatus(); + return NodeStatusObject.fromServiceLayerModel(status); + } +} diff --git a/packages/api/src/graphql/modules/QueryGraphqlModule.ts b/packages/api/src/graphql/modules/QueryGraphqlModule.ts index 6eec4ae7..4328ebce 100644 --- a/packages/api/src/graphql/modules/QueryGraphqlModule.ts +++ b/packages/api/src/graphql/modules/QueryGraphqlModule.ts @@ -37,7 +37,7 @@ import { NetworkStateQuery, BlockStorage } from "@proto-kit/sequencer"; -import { SchemaGeneratingGraphqlModule } from "../GraphqlModule"; +import { graphqlModule, SchemaGeneratingGraphqlModule } from "../GraphqlModule"; import { BaseModuleType, log, @@ -62,13 +62,10 @@ interface AnyJson { [key: string]: any; } -@injectable() -@Resolver() +@graphqlModule() export class QueryGraphqlModule< RuntimeModules extends RuntimeModulesRecord > extends SchemaGeneratingGraphqlModule { - public resolverType = QueryGraphqlModule; - public constructor( @inject("QueryTransportModule") private readonly queryTransportModule: QueryTransportModule, @@ -265,8 +262,6 @@ export class QueryGraphqlModule< if (stateProperty instanceof StateMap) { // StateMap - console.log("StateMap"); - moduleTypes[fieldKey] = this.generateStateMapResolver( `${namePrefix}${fieldKey}`, query[fieldKey], diff --git a/packages/api/src/graphql/services/NodeStatusService.ts b/packages/api/src/graphql/services/NodeStatusService.ts new file mode 100644 index 00000000..550772e0 --- /dev/null +++ b/packages/api/src/graphql/services/NodeStatusService.ts @@ -0,0 +1,31 @@ +import { inject, injectable } from "tsyringe"; +import { BlockStorage } from "@proto-kit/sequencer"; +import humanizeDuration from "humanize-duration"; + +export interface NodeStatus { + uptime: number; + uptimeHumanReadable: string; + height: number; +} + +@injectable() +export class NodeStatusService { + private readonly startupTime = Date.now(); + + public constructor( + @inject("BlockStorage") private readonly blockStorage: BlockStorage + ) { + } + + public async getNodeStatus(): Promise { + const uptime = Date.now() - this.startupTime; + const uptimeHumanReadable = humanizeDuration(uptime); + const height = await this.blockStorage.getCurrentBlockHeight(); + + return { + uptime, + uptimeHumanReadable, + height, + }; + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 64391f72..b91505a4 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -4,3 +4,5 @@ export * from "./graphql/modules/BlockStorageResolver"; export * from "./graphql/GraphqlModule"; export * from "./graphql/GraphqlServer"; export * from "./graphql/GraphqlSequencerModule"; +export * from "./graphql/modules/NodeStatusResolver"; +export * from "./graphql/services/NodeStatusService"; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 1af41578..f7d85734 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,6 +1,8 @@ // allows to reference interfaces as 'classes' rather than instances export type TypedClass = new (...args: any[]) => Class; +export type UnTypedClass = new (...args: any[]) => any; + /** * Using simple `keyof Target` would result into the key * being `string | number | symbol`, but we want just a `string` diff --git a/packages/protocol/src/utils/utils.ts b/packages/protocol/src/utils/utils.ts index 13b8fe8f..88eecd50 100644 --- a/packages/protocol/src/utils/utils.ts +++ b/packages/protocol/src/utils/utils.ts @@ -10,8 +10,6 @@ export type ReturnType = FunctionType extends ( ? Return : any; -export type UnTypedClass = new (...args: any[]) => any; - export type TypedClass = new (...args: any[]) => Class; export type Subclass any> = (new ( diff --git a/packages/sdk/src/graphql.ts b/packages/sdk/src/graphql.ts index 1196c2e4..8eab9968 100644 --- a/packages/sdk/src/graphql.ts +++ b/packages/sdk/src/graphql.ts @@ -16,17 +16,18 @@ import { } from "@proto-kit/protocol"; import { Presets, log } from "@proto-kit/common"; import { - AsyncStateService, + AsyncStateService, BlockProducerModule, LocalTaskQueue, LocalTaskWorkerModule, NoopBaseLayer, PrivateMempool, - Sequencer, - UnsignedTransaction, + Sequencer, TimedBlockTrigger, + UnsignedTransaction } from "@proto-kit/sequencer"; import { BlockStorageResolver, GraphqlSequencerModule, GraphqlServer, MempoolResolver, - QueryGraphqlModule, + NodeStatusResolver, + QueryGraphqlModule } from "@proto-kit/api"; import { AppChain } from "./appChain/AppChain"; @@ -96,18 +97,25 @@ const appChain = AppChain.from({ modules: { Mempool: PrivateMempool, GraphqlServer, + LocalTaskWorkerModule, + BaseLayer: NoopBaseLayer, + BlockProducerModule, + BlockTrigger: TimedBlockTrigger, + TaskQueue: LocalTaskQueue, Graphql: GraphqlSequencerModule.from({ modules: { MempoolResolver, QueryGraphqlModule, BlockStorageResolver, + NodeStatusResolver, }, config: { MempoolResolver: {}, QueryGraphqlModule: {}, BlockStorageResolver: {}, + NodeStatusResolver: {}, }, }), }, @@ -141,9 +149,18 @@ appChain.configure({ QueryGraphqlModule: {}, MempoolResolver: {}, BlockStorageResolver: {}, + NodeStatusResolver: {} }, Mempool: {}, + BlockProducerModule: {}, + LocalTaskWorkerModule: {}, + BaseLayer: {}, + TaskQueue: {}, + + BlockTrigger: { + blocktime: 5000 + }, }, TransactionSender: {},