Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Stratis.Bitcoin.Features.Api/AlphabeticalTagOrderingFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Stratis.Bitcoin.Features.Api
{
public class AlphabeticalTagOrderingFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Tags = swaggerDoc.Tags.OrderBy(t => t.Name).ToList();
}
}
}
33 changes: 33 additions & 0 deletions src/Stratis.Bitcoin.Features.Api/CamelCaseRouteFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Stratis.Bitcoin.Features.Api
{
/// <summary>
/// Converts PascalCase route parameters to camelCase
/// </summary>
internal class CamelCaseRouteFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var paths = swaggerDoc.Paths.ToDictionary(entry =>
{
var pathParts = entry.Key.Split('/', StringSplitOptions.RemoveEmptyEntries);
var camelCasePathParts = pathParts.Select(
part => part.StartsWith('{') || part.All(char.IsUpper)
? part
: char.ToLowerInvariant(part[0]) + part[1..]);
return $"/{string.Join('/', camelCasePathParts)}";
},
entry => entry.Value);

swaggerDoc.Paths = new OpenApiPaths();
foreach ((string key, OpenApiPathItem value) in paths)
{
swaggerDoc.Paths.Add(key, value);
}
}
}
}
56 changes: 32 additions & 24 deletions src/Stratis.Bitcoin.Features.Api/ConfigureSwaggerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using NBitcoin;
using Stratis.Bitcoin.Controllers.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Script = NBitcoin.Script;

namespace Stratis.Bitcoin.Features.Api
{
Expand All @@ -18,25 +22,6 @@ namespace Stratis.Bitcoin.Features.Api
/// </remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private static readonly string[] ApiXmlDocuments = new string[]
{
"Stratis.Bitcoin.xml",
"Stratis.Bitcoin.Features.BlockStore.xml",
"Stratis.Bitcoin.Features.ColdStaking.xml",
"Stratis.Bitcoin.Features.Consensus.xml",
"Stratis.Bitcoin.Features.PoA.xml",
"Stratis.Bitcoin.Features.MemoryPool.xml",
"Stratis.Bitcoin.Features.Miner.xml",
"Stratis.Bitcoin.Features.Notifications.xml",
"Stratis.Bitcoin.Features.RPC.xml",
"Stratis.Bitcoin.Features.SignalR.xml",
"Stratis.Bitcoin.Features.SmartContracts.xml",
"Stratis.Bitcoin.Features.Wallet.xml",
"Stratis.Bitcoin.Features.WatchOnlyWallet.xml",
"Stratis.Features.Diagnostic.xml",
"Stratis.Features.FederatedPeg.xml"
};

private readonly IApiVersionDescriptionProvider provider;

/// <summary>
Expand All @@ -57,15 +42,36 @@ public void Configure(SwaggerGenOptions options)
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}

// Includes XML comments in Swagger documentation
string basePath = AppContext.BaseDirectory;
foreach (string xmlPath in ApiXmlDocuments.Select(xmlDocument => Path.Combine(basePath, xmlDocument)))
// Retrieve relevant XML documents via assembly scanning
var xmlDocuments = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetTypes().Any(t => t.IsSubclassOf(typeof(ControllerBase))))
.Select(a => Path.Combine(AppContext.BaseDirectory, $"{a.GetName().Name}.xml"));

foreach (string xmlPath in xmlDocuments)
{
if (File.Exists(xmlPath))
{
options.IncludeXmlComments(xmlPath);
options.IncludeXmlComments(xmlPath, true);
}
}

options.CustomSchemaIds(type => type switch
{
// resolve naming clash
{ } scriptType when scriptType == typeof(Stratis.Bitcoin.Controllers.Models.Script) => "HexEncodedScript",
_ => type.ToString()
});

// map custom types to openapi schema types
options.MapType<uint256>(() => new OpenApiSchema { Type = "string" });
options.MapType<Script>(() => new OpenApiSchema { Type = "string" });
options.MapType<Money>(() => new OpenApiSchema { Type = "int64" });
options.MapType<PubKey>(() => new OpenApiSchema { Type = "string" });

options.DocumentFilter<CamelCaseRouteFilter>();
options.DocumentFilter<AlphabeticalTagOrderingFilter>();

options.DescribeAllParametersInCamelCase();
}

static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
Expand All @@ -74,7 +80,9 @@ static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
Title = "Stratis Node API",
Version = description.ApiVersion.ToString(),
Description = "Access to the Stratis Node's api."
Description = "The Stratis Node API allows you to manage and monitor the node, as well as query data from the running network.",
Contact = new OpenApiContact { Name = "Stratis Platform", Url = new Uri("https://www.stratisplatform.com") },
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
};

if (info.Version.Contains("dev"))
Expand Down
86 changes: 71 additions & 15 deletions src/Stratis.Bitcoin.Features.Api/NodeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
Expand Down Expand Up @@ -33,7 +35,7 @@
namespace Stratis.Bitcoin.Features.Api
{
/// <summary>
/// Provides methods that interact with the full node.
/// Manage and monitor the node
/// </summary>
[ApiVersion("1")]
[Route("api/[controller]")]
Expand Down Expand Up @@ -136,8 +138,13 @@ public NodeController(
/// </summary>
/// <param name="publish">If true, publish a full node event with the status.</param>
/// <returns>A <see cref="StatusModel"/> with information about the node.</returns>
/// <response type="200">Full node information returned</response>
/// <response type="500">Unexpected error occurred</response>
[HttpGet]
[Route("status")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(StatusModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Status([FromQuery] bool publish)
{
// Output has been merged with RPC's GetInfo() since they provided similar functionality.
Expand Down Expand Up @@ -222,10 +229,12 @@ public IActionResult Status([FromQuery] bool publish)
/// <returns>Json formatted <see cref="BlockHeaderModel"/>. Returns <see cref="HexModel"/> if Json format is not requested. <c>null</c> if block not found. Returns <see cref="Microsoft.AspNetCore.Mvc.IActionResult"/> formatted error if fails.</returns>
/// <response code="200">Returns the block header if found.</response>
/// <response code="400">Null hash provided, or block header does not exist.</response>
[Route("getblockheader")]
/// <response code="404">Block header not found.</response>
[Route("getBlockHeader")]
[HttpGet]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(BlockHeaderModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponse), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public IActionResult GetBlockHeader([FromQuery] string hash, bool isJsonFormat = true)
{
Expand Down Expand Up @@ -263,8 +272,10 @@ public IActionResult GetBlockHeader([FromQuery] string hash, bool isJsonFormat =
/// <exception cref="ArgumentNullException">Thrown if fullNode, network, or chain are not available.</exception>
/// <exception cref="ArgumentException">Thrown if trxid is empty or not a valid<see cref="uint256"/>.</exception>
/// <remarks>Requires txindex=1, otherwise only txes that spend or create UTXOs for a wallet can be returned.</remarks>
[Route("getrawtransaction")]
[Route("getRawTransaction")]
[HttpGet]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetRawTransactionAsync([FromQuery] string trxid, bool verbose = false)
{
try
Expand Down Expand Up @@ -311,8 +322,13 @@ public async Task<IActionResult> GetRawTransactionAsync([FromQuery] string trxid
/// </summary>
/// <param name="request">A class containing the necessary parameters for a block search request.</param>
/// <returns>The JSON representation of the transaction.</returns>
/// <response code="200">Raw transaction decoded</response>
/// <response code="400">Validation error or unexpected error occurred</response>
[HttpPost]
[Route("decoderawtransaction")]
[Route("decodeRawTransaction")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(TransactionVerboseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public IActionResult DecodeRawTransaction([FromBody] DecodeRawTransactionModel request)
{
try
Expand All @@ -338,8 +354,13 @@ public IActionResult DecodeRawTransaction([FromBody] DecodeRawTransactionModel r
/// <returns>Json formatted <see cref="ValidatedAddress"/> containing a boolean indicating address validity. Returns <see cref="Microsoft.AspNetCore.Mvc.IActionResult"/> formatted error if fails.</returns>
/// <exception cref="ArgumentException">Thrown if address provided is empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if network is not provided.</exception>
[Route("validateaddress")]
/// <response code="200">Returns validation result</response>
/// <response code="400">Unexpected error occurred</response>
[Route("validateAddress")]
[HttpGet]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(ValidatedAddress), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public IActionResult ValidateAddress([FromQuery] string address)
{
Guard.NotEmpty(address, nameof(address));
Expand Down Expand Up @@ -400,8 +421,13 @@ public IActionResult ValidateAddress([FromQuery] string address)
/// <returns>Json formatted <see cref="GetTxOutModel"/>. <c>null</c> if no unspent outputs given parameters. Returns <see cref="Microsoft.AspNetCore.Mvc.IActionResult"/> formatted error if fails.</returns>
/// <exception cref="ArgumentNullException">Thrown if network or chain not provided.</exception>
/// <exception cref="ArgumentException">Thrown if trxid is empty or not a valid <see cref="uint256"/></exception>
[Route("gettxout")]
/// <response code="200">Unspent outputs returned</response>
/// <response code="400">Unexpected error occurred</response>
[Route("getTxOut")]
[HttpGet]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(GetTxOutModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetTxOutAsync([FromQuery] string trxid, uint vout = 0, bool includeMemPool = true)
{
try
Expand Down Expand Up @@ -448,8 +474,13 @@ public async Task<IActionResult> GetTxOutAsync([FromQuery] string trxid, uint vo
/// <param name="txids">The txids to filter</param>
/// <param name="blockhash">If specified, looks for txid in the block with this hash</param>
/// <returns>The hex-encoded merkle proof.</returns>
[Route("gettxoutproof")]
/// <response code="200">Proof returned</response>
/// <response code="500">Block or transactions not found, or unexpected error occurred</response>
[Route("getTxOutProof")]
[HttpGet]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(MerkleBlock), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public Task<IActionResult> GetTxOutProofAsync([FromQuery] string[] txids, string blockhash = "")
{
List<uint256> transactionIds = txids.Select(txString => uint256.Parse(txString)).ToList();
Expand Down Expand Up @@ -510,9 +541,13 @@ public Task<IActionResult> GetTxOutProofAsync([FromQuery] string[] txids, string
/// <seealso cref="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests"/>
/// </remarks>
/// <returns><see cref="OkResult"/></returns>
/// <response type="200">Shut down the node successfully</response>
/// <response type="500">Unexpected error occurred</response>
[HttpPost]
[Route("shutdown")]
[Route("stop")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Shutdown([FromBody] bool corsProtection = true)
{
// Start the node shutdown process, by calling StopApplication, which will signal to
Expand All @@ -528,10 +563,13 @@ public IActionResult Shutdown([FromBody] bool corsProtection = true)
/// </summary>
/// <param name="height">The rewind height.</param>
/// <returns>A json text result indicating success or an <see cref="ErrorResult"/> indicating failure.</returns>
/// <response type="200">Rewind flag set</response>
/// <response type="400">Unexpected error occurred</response>
[Route("rewind")]
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponse), (int)HttpStatusCode.BadRequest)]
public IActionResult Rewind([FromQuery] int height)
{
try
Expand All @@ -552,8 +590,12 @@ public IActionResult Rewind([FromQuery] int height)
/// </summary>
/// <param name="request">The request containing the loggers to modify.</param>
/// <returns><see cref="Microsoft.AspNetCore.Mvc.OkResult"/></returns>
/// <response type="200">Log rules updated</response>
/// <response type="400">Invalid request, rule name doesn't exist, or unexpected error occurred</response>
[HttpPut]
[Route("loglevels")]
[Route("logLevels")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public IActionResult UpdateLogLevel([FromBody] LogRulesRequest request)
{
Guard.NotNull(request, nameof(request));
Expand Down Expand Up @@ -606,8 +648,13 @@ public IActionResult UpdateLogLevel([FromBody] LogRulesRequest request)
/// Get the enabled log rules.
/// </summary>
/// <returns>A list of log rules.</returns>
/// <response type="200">Log rules retrieved</response>
/// <response type="400">Invalid request, or unexpected error occurred</response>
[HttpGet]
[Route("logrules")]
[Route("logRules")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(IEnumerable<LogRuleModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public IActionResult GetLogRules()
{
// Checks the request is valid.
Expand Down Expand Up @@ -665,8 +712,13 @@ public IActionResult GetLogRules()
/// Get the currently running async loops/delegates/tasks for diagnostic purposes.
/// </summary>
/// <returns>A list of running async loops/delegates/tasks.</returns>
/// <response type="200">Async loops retrieved</response>
/// <response type="400">Invalid request, or unexpected error occurred</response>
[HttpGet]
[Route("asyncloops")]
[Route("asyncLoops")]
[Produces(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(IEnumerable<AsyncLoopModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)]
public IActionResult GetAsyncLoops()
{
// Checks the request is valid.
Expand Down Expand Up @@ -697,8 +749,12 @@ public IActionResult GetAsyncLoops()
/// Schedules data folder storing chain state in the <see cref="DataFolder"/> for deletion on the next graceful shutdown.
/// </summary>
/// <returns>Returns an <see cref="OkResult"/>.</returns>
/// <response type="200">Chain state scheduled for deletion</response>
/// <response type="500">Unexpected error occurred</response>
[HttpDelete]
[Route("datafolder/chain")]
[Route("dataFolder/chain")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteChain()
{
this.nodeSettings.DataFolder.ScheduleChainDeletion();
Expand Down
8 changes: 7 additions & 1 deletion src/Stratis.Bitcoin.Features.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -66,7 +67,7 @@ public void ConfigureServices(IServiceCollection services)
}
);
});

// Add framework services.
services
.AddMvc(options =>
Expand Down Expand Up @@ -142,6 +143,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF

app.UseCors("CorsPolicy");

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

// Register this before MVC and Swagger.
app.UseMiddleware<NoCacheMiddleware>();

Expand Down
Loading