From 98984c98d94c2d0336b7cae5ece7d047250193a2 Mon Sep 17 00:00:00 2001 From: s2quake Date: Thu, 22 Aug 2024 18:14:15 +0900 Subject: [PATCH] feat: Add swagger and explorer(GQL) --- Libplanet.sln | 42 ++++++++++ .../Libplanet.Node.Executable.csproj | 2 + sdk/node/Libplanet.Node.Executable/Program.cs | 84 +++++++++++-------- .../appsettings-schema.json | 30 +++++++ .../BlockChainContext.cs | 39 +++++++++ .../ExplorerHostingStartup.cs | 49 +++++++++++ .../Libplanet.Node.Explorer.csproj | 11 +++ .../Options/ExplorerOptions.cs | 13 +++ .../LibplanetServicesExtensions.cs | 1 - .../Libplanet.Node.Swagger.csproj | 14 ++++ .../Options/SwaggerOptions.cs | 13 +++ .../SwaggerHostingStartup.cs | 45 ++++++++++ .../Libplanet.Node/Services/ISwarmService.cs | 4 + .../Libplanet.Node/Services/SwarmService.cs | 2 + 14 files changed, 313 insertions(+), 36 deletions(-) create mode 100644 sdk/node/Libplanet.Node.Explorer/BlockChainContext.cs create mode 100644 sdk/node/Libplanet.Node.Explorer/ExplorerHostingStartup.cs create mode 100644 sdk/node/Libplanet.Node.Explorer/Libplanet.Node.Explorer.csproj create mode 100644 sdk/node/Libplanet.Node.Explorer/Options/ExplorerOptions.cs create mode 100644 sdk/node/Libplanet.Node.Swagger/Libplanet.Node.Swagger.csproj create mode 100644 sdk/node/Libplanet.Node.Swagger/Options/SwaggerOptions.cs create mode 100644 sdk/node/Libplanet.Node.Swagger/SwaggerHostingStartup.cs diff --git a/Libplanet.sln b/Libplanet.sln index 30d960e0b0f..86c79940175 100644 --- a/Libplanet.sln +++ b/Libplanet.sln @@ -81,6 +81,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node.Executable", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node.Tests", "sdk\node\Libplanet.Node.Tests\Libplanet.Node.Tests.csproj", "{C050C5F0-8A40-4CB1-9715-A55EBF94FBF2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node.Swagger", "sdk\node\Libplanet.Node.Swagger\Libplanet.Node.Swagger.csproj", "{FC6FACBF-4529-4458-8317-FC4CB925C8A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Node.Explorer", "sdk\node\Libplanet.Node.Explorer\Libplanet.Node.Explorer.csproj", "{BA40A654-5AF6-478E-9C1A-E20E046750B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -697,6 +701,42 @@ Global {C050C5F0-8A40-4CB1-9715-A55EBF94FBF2}.ReleaseMono|x64.Build.0 = Debug|Any CPU {C050C5F0-8A40-4CB1-9715-A55EBF94FBF2}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU {C050C5F0-8A40-4CB1-9715-A55EBF94FBF2}.ReleaseMono|x86.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|x64.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Debug|x86.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|Any CPU.Build.0 = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|x64.ActiveCfg = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|x64.Build.0 = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|x86.ActiveCfg = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.Release|x86.Build.0 = Release|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|x64.Build.0 = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU + {FC6FACBF-4529-4458-8317-FC4CB925C8A5}.ReleaseMono|x86.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|x64.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Debug|x86.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|Any CPU.Build.0 = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|x64.ActiveCfg = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|x64.Build.0 = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|x86.ActiveCfg = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.Release|x86.Build.0 = Release|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|Any CPU.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|Any CPU.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|x64.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|x64.Build.0 = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|x86.ActiveCfg = Debug|Any CPU + {BA40A654-5AF6-478E-9C1A-E20E046750B1}.ReleaseMono|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -737,6 +777,8 @@ Global {704BA731-9C70-4CBE-A607-1A2E1FB73753} = {8CA69CC9-3415-4484-9342-88D495AE2FF6} {A0EAD8F0-B7A3-4112-9F3F-2D9922A500BA} = {8CA69CC9-3415-4484-9342-88D495AE2FF6} {C050C5F0-8A40-4CB1-9715-A55EBF94FBF2} = {8CA69CC9-3415-4484-9342-88D495AE2FF6} + {FC6FACBF-4529-4458-8317-FC4CB925C8A5} = {8CA69CC9-3415-4484-9342-88D495AE2FF6} + {BA40A654-5AF6-478E-9C1A-E20E046750B1} = {8CA69CC9-3415-4484-9342-88D495AE2FF6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB552D2A-94E1-4A1C-9F3E-E0097C6158CD} diff --git a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj index 111f562660f..d43112eaed4 100644 --- a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj +++ b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj @@ -23,6 +23,8 @@ + + diff --git a/sdk/node/Libplanet.Node.Executable/Program.cs b/sdk/node/Libplanet.Node.Executable/Program.cs index 2d95e1c6fb1..53ccd302d64 100644 --- a/sdk/node/Libplanet.Node.Executable/Program.cs +++ b/sdk/node/Libplanet.Node.Executable/Program.cs @@ -1,48 +1,62 @@ using Libplanet.Node.API.Services; using Libplanet.Node.Extensions; using Libplanet.Node.Options.Schema; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Server.Kestrel.Core; -SynchronizationContext.SetSynchronizationContext(new()); -var builder = WebApplication.CreateBuilder(args); - -builder.Logging.AddConsole(); - -if (builder.Environment.IsDevelopment()) +var builder = WebHost.CreateDefaultBuilder(args); +var assemblies = new string[] +{ + "Libplanet.Node.Swagger", + "Libplanet.Node.Explorer", +}; +builder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, string.Join(';', assemblies)); +builder.ConfigureLogging(logging => +{ + logging.AddConsole(); +}); +builder.ConfigureKestrel((context, options) => { - builder.WebHost.ConfigureKestrel(options => + if (context.HostingEnvironment.IsDevelopment()) { // Setup a HTTP/2 endpoint without TLS. options.ListenLocalhost(5259, o => o.Protocols = HttpProtocols.Http1AndHttp2); - }); -} - -// Additional configuration is required to successfully run gRPC on macOS. -// For instructions on how to configure Kestrel and gRPC clients on macOS, -// visit https://go.microsoft.com/fwlink/?linkid=2099682 - -// Add services to the container. -builder.Services.AddGrpc(); -builder.Services.AddGrpcReflection(); -var libplanetBuilder = builder.Services.AddLibplanetNode(builder.Configuration); - -var app = builder.Build(); -var handlerMessage = """ - Communication with gRPC endpoints must be made through a gRPC client. To learn how to - create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909 - """; -var schema = await OptionsSchemaBuilder.GetSchemaAsync(default); - -// Configure the HTTP request pipeline. -app.MapGrpcService(); -app.MapGrpcService(); -app.MapGet("/", () => handlerMessage); -app.MapGet("/schema", () => schema); - -if (builder.Environment.IsDevelopment()) + } +}); +builder.ConfigureServices((context, services) => { - app.MapGrpcReflectionService().AllowAnonymous(); -} + services.AddGrpc(); + services.AddGrpcReflection(); + services.AddLibplanetNode(context.Configuration); +}); +builder.Configure((context, app) => +{ + app.UseRouting(); + app.UseEndpoints(endPoint => + { + var handlerMessage = """ + Communication with gRPC endpoints must be made through a gRPC client. To learn how to + create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909 + """; + string? schema = null; + endPoint.MapGet(string.Empty, async context => + { + await context.Response.WriteAsync(handlerMessage); + }); + endPoint.MapGet("/schema", async context => + { + schema ??= await OptionsSchemaBuilder.GetSchemaAsync(context.RequestAborted); + await context.Response.WriteAsync(schema); + }); + endPoint.MapGrpcService(); + endPoint.MapGrpcService(); + if (context.HostingEnvironment.IsDevelopment()) + { + endPoint.MapGrpcReflectionService().AllowAnonymous(); + } + }); +}); +using var app = builder.Build(); await app.RunAsync(); diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json index e62fc67e480..8c2542fa6c8 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json @@ -1084,6 +1084,28 @@ } } }, + "Swagger": { + "title": "SwaggerOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "IsEnabled": { + "type": "boolean", + "default": true + } + } + }, + "Explorer": { + "title": "ExplorerOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "IsEnabled": { + "type": "boolean", + "default": true + } + } + }, "Genesis": { "title": "GenesisOptions", "type": "object", @@ -1221,6 +1243,14 @@ { "type": "object", "properties": { + "Swagger": { + "description": "Type 'SwaggerOptions' does not have a description.", + "$ref": "#/definitions/Swagger" + }, + "Explorer": { + "description": "Type 'ExplorerOptions' does not have a description.", + "$ref": "#/definitions/Explorer" + }, "Genesis": { "description": "Options for the genesis block.", "$ref": "#/definitions/Genesis" diff --git a/sdk/node/Libplanet.Node.Explorer/BlockChainContext.cs b/sdk/node/Libplanet.Node.Explorer/BlockChainContext.cs new file mode 100644 index 00000000000..00a7161cf33 --- /dev/null +++ b/sdk/node/Libplanet.Node.Explorer/BlockChainContext.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using Libplanet.Blockchain; +using Libplanet.Explorer.Indexing; +using Libplanet.Explorer.Interfaces; +using Libplanet.Net; +using Libplanet.Node.Services; +using Libplanet.Store; + +namespace Libplanet.Node.Explorer; + +internal sealed class BlockChainContext( + IBlockChainService blockChainService, ISwarmService swarmService) : IBlockChainContext +{ + public bool Preloaded => false; + + public BlockChain BlockChain => blockChainService.BlockChain; + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility ... + public IStore Store + { + get + { + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; + var propertyInfo = typeof(BlockChain).GetProperty("Store", bindingFlags) ?? + throw new InvalidOperationException("Store property not found."); + if (propertyInfo.GetValue(BlockChain) is IStore store) + { + return store; + } + + throw new InvalidOperationException("Store property is not IStore."); + } + } +#pragma warning restore S3011 + + public Swarm Swarm => swarmService.Swarm; + + public IBlockChainIndex Index => throw new NotSupportedException(); +} diff --git a/sdk/node/Libplanet.Node.Explorer/ExplorerHostingStartup.cs b/sdk/node/Libplanet.Node.Explorer/ExplorerHostingStartup.cs new file mode 100644 index 00000000000..85a415f6f7b --- /dev/null +++ b/sdk/node/Libplanet.Node.Explorer/ExplorerHostingStartup.cs @@ -0,0 +1,49 @@ +using Libplanet.Explorer; +using Libplanet.Node.Explorer; +using Libplanet.Node.Explorer.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +[assembly: HostingStartup(typeof(ExplorerHostingStartup))] + +namespace Libplanet.Node.Explorer; + +internal sealed class ExplorerHostingStartup : IHostingStartup, IStartupFilter +{ + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + services.AddOptions() + .Bind(context.Configuration.GetSection(ExplorerOptions.Position)); + + var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>().Value; + + if (options.IsEnabled) + { + services.AddSingleton(); + services.AddSingleton>(); + serviceProvider = services.BuildServiceProvider(); + var startUp + = serviceProvider.GetRequiredService>(); + startUp.ConfigureServices(services); + services.AddSingleton(this); + } + }); + } + + public Action Configure(Action next) + { + return builder => + { + var serviceProvider = builder.ApplicationServices; + var environment = serviceProvider.GetRequiredService(); + var startUp = serviceProvider.GetService>(); + startUp?.Configure(builder, environment); + next(builder); + }; + } +} diff --git a/sdk/node/Libplanet.Node.Explorer/Libplanet.Node.Explorer.csproj b/sdk/node/Libplanet.Node.Explorer/Libplanet.Node.Explorer.csproj new file mode 100644 index 00000000000..2a7b407a823 --- /dev/null +++ b/sdk/node/Libplanet.Node.Explorer/Libplanet.Node.Explorer.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/sdk/node/Libplanet.Node.Explorer/Options/ExplorerOptions.cs b/sdk/node/Libplanet.Node.Explorer/Options/ExplorerOptions.cs new file mode 100644 index 00000000000..95533c1bbbd --- /dev/null +++ b/sdk/node/Libplanet.Node.Explorer/Options/ExplorerOptions.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; +using Libplanet.Node.Options; + +namespace Libplanet.Node.Explorer.Options; + +[Options(Position)] +public sealed class ExplorerOptions : OptionsBase +{ + public const string Position = "Explorer"; + + [DefaultValue(true)] + public bool IsEnabled { get; set; } = true; +} diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs index e6097fd520e..6caf19c05b3 100644 --- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs +++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs @@ -1,4 +1,3 @@ -using Libplanet.Blockchain; using Libplanet.Node.Extensions.NodeBuilder; using Libplanet.Node.Options; using Libplanet.Node.Services; diff --git a/sdk/node/Libplanet.Node.Swagger/Libplanet.Node.Swagger.csproj b/sdk/node/Libplanet.Node.Swagger/Libplanet.Node.Swagger.csproj new file mode 100644 index 00000000000..a3bd076e5a6 --- /dev/null +++ b/sdk/node/Libplanet.Node.Swagger/Libplanet.Node.Swagger.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/sdk/node/Libplanet.Node.Swagger/Options/SwaggerOptions.cs b/sdk/node/Libplanet.Node.Swagger/Options/SwaggerOptions.cs new file mode 100644 index 00000000000..3e035eb708a --- /dev/null +++ b/sdk/node/Libplanet.Node.Swagger/Options/SwaggerOptions.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; +using Libplanet.Node.Options; + +namespace Libplanet.Node.Swagger.Options; + +[Options(Position)] +public sealed class SwaggerOptions : OptionsBase +{ + public const string Position = "Swagger"; + + [DefaultValue(true)] + public bool IsEnabled { get; set; } = true; +} diff --git a/sdk/node/Libplanet.Node.Swagger/SwaggerHostingStartup.cs b/sdk/node/Libplanet.Node.Swagger/SwaggerHostingStartup.cs new file mode 100644 index 00000000000..95830ebce68 --- /dev/null +++ b/sdk/node/Libplanet.Node.Swagger/SwaggerHostingStartup.cs @@ -0,0 +1,45 @@ +using Libplanet.Node.Swagger; +using Libplanet.Node.Swagger.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +[assembly: HostingStartup(typeof(SwaggerHostingStartup))] + +namespace Libplanet.Node.Swagger; + +internal sealed class SwaggerHostingStartup : IHostingStartup, IStartupFilter +{ + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + services.AddOptions() + .Bind(context.Configuration.GetSection(SwaggerOptions.Position)); + + var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>().Value; + + if (options.IsEnabled) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + services.AddAuthorization(); + services.AddAuthentication("Bearer").AddJwtBearer(); + services.AddSingleton(this); + } + }); + } + + public Action Configure(Action next) + { + return builder => + { + builder.UseSwagger(); + builder.UseSwaggerUI(); + next(builder); + }; + } +} diff --git a/sdk/node/Libplanet.Node/Services/ISwarmService.cs b/sdk/node/Libplanet.Node/Services/ISwarmService.cs index fc2fbc1a0c2..6b47eafa44a 100644 --- a/sdk/node/Libplanet.Node/Services/ISwarmService.cs +++ b/sdk/node/Libplanet.Node/Services/ISwarmService.cs @@ -1,3 +1,5 @@ +using Libplanet.Net; + namespace Libplanet.Node.Services; public interface ISwarmService @@ -5,4 +7,6 @@ public interface ISwarmService public event EventHandler? Started; public event EventHandler? Stopped; + + Swarm Swarm { get; } } diff --git a/sdk/node/Libplanet.Node/Services/SwarmService.cs b/sdk/node/Libplanet.Node/Services/SwarmService.cs index 7b44889b641..df3f8a45516 100644 --- a/sdk/node/Libplanet.Node/Services/SwarmService.cs +++ b/sdk/node/Libplanet.Node/Services/SwarmService.cs @@ -33,6 +33,8 @@ internal sealed class SwarmService( public bool IsRunning => _swarm is not null; + public Swarm Swarm => _swarm ?? throw new InvalidOperationException("Node is not running."); + public async Task StartAsync(CancellationToken cancellationToken) { if (_swarm is not null)