diff --git a/src/Stratis.Bitcoin.Features.Api/ConfigureSwaggerOptions.cs b/src/Stratis.Bitcoin.Features.Api/ConfigureSwaggerOptions.cs index 02073a8514..f26ee6394c 100644 --- a/src/Stratis.Bitcoin.Features.Api/ConfigureSwaggerOptions.cs +++ b/src/Stratis.Bitcoin.Features.Api/ConfigureSwaggerOptions.cs @@ -74,7 +74,7 @@ static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) { Title = "Stratis Node API", Version = description.ApiVersion.ToString(), - Description = "Access to the Stratis Node's core features." + Description = "Access to the Stratis Node's api." }; if (info.Version.Contains("dev")) diff --git a/src/Stratis.Bitcoin.Features.Api/Startup.cs b/src/Stratis.Bitcoin.Features.Api/Startup.cs index 2148ad6127..7136012856 100644 --- a/src/Stratis.Bitcoin.Features.Api/Startup.cs +++ b/src/Stratis.Bitcoin.Features.Api/Startup.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Builder; +using System.Linq; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -83,7 +85,17 @@ public void ConfigureServices(IServiceCollection services) .AddNewtonsoftJson(options => { Utilities.JsonConverters.Serializer.RegisterFrontConverters(options.SerializerSettings); }) - .AddControllers(this.fullNode.Services.Features, services); + .AddControllers(this.fullNode.Services.Features, services) + .ConfigureApplicationPartManager(a => + { + foreach (ApplicationPart appPart in a.ApplicationParts.ToList()) + { + if (appPart.Name != "Stratis.Features.Unity3dApi") + continue; + + a.ApplicationParts.Remove(appPart); + } + }); // Enable API versioning. // Note much of this is borrowed from https://github.com/microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/Startup.cs @@ -114,7 +126,6 @@ public void ConfigureServices(IServiceCollection services) services.AddSwaggerGen(c => { c.SwaggerDoc("contracts", new OpenApiInfo { Title = "Contract API", Version = "1" }); - }); services.AddSwaggerGenNewtonsoftSupport(); // Use Newtonsoft JSON serializer with swagger. Needs to be placed after AddSwaggerGen() diff --git a/src/Stratis.CirrusD/Program.cs b/src/Stratis.CirrusD/Program.cs index f37beb0e4b..f445ca65b7 100644 --- a/src/Stratis.CirrusD/Program.cs +++ b/src/Stratis.CirrusD/Program.cs @@ -19,6 +19,7 @@ using Stratis.Features.Collateral.CounterChain; using Stratis.Features.Diagnostic; using Stratis.Features.SQLiteWalletRepository; +using Stratis.Features.Unity3dApi; using Stratis.Sidechains.Networks; namespace Stratis.CirrusD @@ -77,6 +78,7 @@ private static IFullNode GetSideChainFullNode(NodeSettings nodeSettings) .UseSmartContractWallet() .AddSQLiteWalletRepository() .UseApi() + .UseUnity3dApi() .AddRPC() .AddSignalR(options => { diff --git a/src/Stratis.CirrusD/Stratis.CirrusD.csproj b/src/Stratis.CirrusD/Stratis.CirrusD.csproj index f1801121f6..3f635ff87c 100644 --- a/src/Stratis.CirrusD/Stratis.CirrusD.csproj +++ b/src/Stratis.CirrusD/Stratis.CirrusD.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs b/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs new file mode 100644 index 0000000000..e304a8f9a8 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NBitcoin; +using Stratis.Bitcoin.Base; +using Stratis.Bitcoin.Controllers.Models; +using Stratis.Bitcoin.Features.BlockStore.AddressIndexing; +using Stratis.Bitcoin.Features.BlockStore.Controllers; +using Stratis.Bitcoin.Features.BlockStore.Models; +using Stratis.Bitcoin.Features.Consensus; +using Stratis.Bitcoin.Features.Consensus.CoinViews; +using Stratis.Bitcoin.Features.Wallet.Controllers; +using Stratis.Bitcoin.Features.Wallet.Models; +using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Utilities; +using Stratis.Bitcoin.Utilities.JsonErrors; + +namespace Stratis.Features.Unity3dApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class Unity3dController : Controller + { + private readonly IAddressIndexer addressIndexer; + + private readonly IBlockStore blockStore; + + private readonly IChainState chainState; + + private readonly Network network; + + private readonly ICoinView coinView; + + private readonly WalletController walletController; + + private readonly ChainIndexer chainIndexer; + + private readonly IStakeChain stakeChain; + + /// Instance logger. + private readonly ILogger logger; + + public Unity3dController(ILoggerFactory loggerFactory, IAddressIndexer addressIndexer, + IBlockStore blockStore, IChainState chainState, Network network, ICoinView coinView, WalletController walletController, ChainIndexer chainIndexer, IStakeChain stakeChain) + { + Guard.NotNull(loggerFactory, nameof(loggerFactory)); + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + this.addressIndexer = Guard.NotNull(addressIndexer, nameof(addressIndexer)); + this.blockStore = Guard.NotNull(blockStore, nameof(blockStore)); + this.chainState = Guard.NotNull(chainState, nameof(chainState)); + this.network = Guard.NotNull(network, nameof(network)); + this.coinView = Guard.NotNull(coinView, nameof(coinView)); + this.walletController = Guard.NotNull(walletController, nameof(walletController)); + this.chainIndexer = Guard.NotNull(chainIndexer, nameof(chainIndexer)); + this.stakeChain = Guard.NotNull(stakeChain, nameof(stakeChain)); + } + + /// + /// Gets UTXOs for specified address. + /// + /// Address to get UTXOs for. + [Route("getutxosforaddress")] + [HttpGet] + public GetUTXOsResponseModel GetUTXOsForAddress([FromQuery] string address) + { + VerboseAddressBalancesResult balancesResult = this.addressIndexer.GetAddressIndexerState(new[] {address}); + + if (balancesResult.BalancesData == null || balancesResult.BalancesData.Count != 1) + { + this.logger.LogWarning("No balances found for address {0}, Reason: {1}", address, balancesResult.Reason); + return new GetUTXOsResponseModel() {Reason = balancesResult.Reason}; + } + + BitcoinAddress bitcoinAddress = this.network.CreateBitcoinAddress(address); + + AddressIndexerData addressBalances = balancesResult.BalancesData.First(); + + List deposits = addressBalances.BalanceChanges.Where(x => x.Deposited).ToList(); + long totalDeposited = deposits.Sum(x => x.Satoshi); + long totalWithdrawn = addressBalances.BalanceChanges.Where(x => !x.Deposited).Sum(x => x.Satoshi); + + long balanceSat = totalDeposited - totalWithdrawn; + + List heights = deposits.Select(x => x.BalanceChangedHeight).Distinct().ToList(); + HashSet blocksToRequest = new HashSet(heights.Count); + + foreach (int height in heights) + { + uint256 blockHash = this.chainState.ConsensusTip.GetAncestor(height).Header.GetHash(); + blocksToRequest.Add(blockHash); + } + + List blocks = this.blockStore.GetBlocks(blocksToRequest.ToList()); + List collectedOutPoints = new List(deposits.Count); + + foreach (List txList in blocks.Select(x => x.Transactions)) + { + foreach (Transaction transaction in txList.Where(x => !x.IsCoinBase && !x.IsCoinStake)) + { + for (int i = 0; i < transaction.Outputs.Count; i++) + { + if (!transaction.Outputs[i].IsTo(bitcoinAddress)) + continue; + + collectedOutPoints.Add(new OutPoint(transaction, i)); + } + } + } + + FetchCoinsResponse fetchCoinsResponse = this.coinView.FetchCoins(collectedOutPoints.ToArray()); + + GetUTXOsResponseModel response = new GetUTXOsResponseModel() + { + BalanceSat = balanceSat, + UTXOs = new List() + }; + + foreach (KeyValuePair unspentOutput in fetchCoinsResponse.UnspentOutputs) + { + if (unspentOutput.Value.Coins == null) + continue; // spent + + OutPoint outPoint = unspentOutput.Key; + Money value = unspentOutput.Value.Coins.TxOut.Value; + + response.UTXOs.Add(new UTXOModel(outPoint, value)); + } + + return response; + } + + /// Provides balance of the given address confirmed with at least 1 confirmation. + /// Address that will be queried. + /// A result object containing the balance for each requested address and if so, a message stating why the indexer is not queryable. + /// Returns balances for the requested addresses + /// Unexpected exception occurred + [Route("getaddressbalance")] + [HttpGet] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + public long GetAddressBalance(string address) + { + try + { + AddressBalancesResult result = this.addressIndexer.GetAddressBalances(new []{address}, 1); + + return result.Balances.First().Balance.Satoshi; + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return -1; + } + } + + /// + /// Gets the block header of a block identified by a block hash. + /// + /// The hash of the block to retrieve. + /// Json formatted . null if block not found. Returns formatted error if fails. + /// Thrown if isJsonFormat = false" + /// Thrown if hash is empty. + /// Thrown if logger is not provided. + /// Binary serialization is not supported with this method. + [Route("getblockheader")] + [HttpGet] + public BlockHeaderModel GetBlockHeader([FromQuery] string hash) + { + try + { + Guard.NotEmpty(hash, nameof(hash)); + + this.logger.LogDebug("GetBlockHeader {0}", hash); + + BlockHeaderModel model = null; + BlockHeader blockHeader = this.chainIndexer?.GetHeader(uint256.Parse(hash))?.Header; + if (blockHeader != null) + { + model = new BlockHeaderModel(blockHeader); + } + + return model; + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return null; + } + } + + /// + /// Gets a raw transaction that is present on this full node. + /// This method gets transaction using block store. + /// + /// The transaction ID (a hash of the transaction). + /// Json formatted or . null if transaction not found. Returns formatted error if otherwise fails. + /// Thrown if fullNode, network, or chain are not available. + /// Thrown if trxid is empty or not a valid. + /// Requires txindex=1, otherwise only txes that spend or create UTXOs for a wallet can be returned. + [Route("getrawtransaction")] + [HttpGet] + public RawTxModel GetRawTransaction([FromQuery] string trxid) + { + try + { + Guard.NotEmpty(trxid, nameof(trxid)); + + uint256 txid; + if (!uint256.TryParse(trxid, out txid)) + { + throw new ArgumentException(nameof(trxid)); + } + + Transaction trx = this.blockStore?.GetTransactionById(txid); + + if (trx == null) + { + return null; + } + + return new RawTxModel() { Hex = trx.ToHex() }; + + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return null; + } + } + + /// + /// Sends a transaction that has already been built. + /// Use the /api/Wallet/build-transaction call to create transactions. + /// + /// An object containing the necessary parameters used to a send transaction request. + /// The Cancellation Token + /// A JSON object containing information about the sent transaction. + /// Returns transaction details + /// Invalid request, cannot broadcast transaction, or unexpected exception occurred + /// No connected peers + /// Request is null + [Route("send-transaction")] + [HttpPost] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.Forbidden)] + [ProducesResponseType((int)HttpStatusCode.InternalServerError)] + public async Task SendTransaction([FromBody] SendTransactionRequest request, + CancellationToken cancellationToken = default(CancellationToken)) + { + return await this.walletController.SendTransaction(request, cancellationToken).ConfigureAwait(false); + } + + /// + /// Validates a bech32 or base58 bitcoin address. + /// + /// A Bitcoin address to validate in a string format. + /// Json formatted containing a boolean indicating address validity. Returns formatted error if fails. + /// Thrown if address provided is empty. + /// Thrown if network is not provided. + [Route("validateaddress")] + [HttpGet] + public ValidatedAddress ValidateAddress([FromQuery] string address) + { + Guard.NotEmpty(address, nameof(address)); + + var result = new ValidatedAddress + { + IsValid = false, + Address = address, + }; + + try + { + // P2WPKH + if (BitcoinWitPubKeyAddress.IsValid(address, this.network, out Exception _)) + { + result.IsValid = true; + } + // P2WSH + else if (BitcoinWitScriptAddress.IsValid(address, this.network, out Exception _)) + { + result.IsValid = true; + } + // P2PKH + else if (BitcoinPubKeyAddress.IsValid(address, this.network)) + { + result.IsValid = true; + } + // P2SH + else if (BitcoinScriptAddress.IsValid(address, this.network)) + { + result.IsValid = true; + result.IsScript = true; + } + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return null; + } + + if (result.IsValid) + { + var scriptPubKey = BitcoinAddress.Create(address, this.network).ScriptPubKey; + result.ScriptPubKey = scriptPubKey.ToHex(); + result.IsWitness = scriptPubKey.IsWitness(this.network); + } + + return result; + } + + + /// + /// Retrieves the block which matches the supplied block hash. + /// + /// An object containing the necessary parameters to search for a block. + /// if block is found, if not found. Returns with error information if exception thrown. + /// Returns data about the block or block not found message + /// Block hash invalid, or an unexpected exception occurred + [Route(BlockStoreRouteEndPoint.GetBlock)] + [HttpGet] + [ProducesResponseType((int) HttpStatusCode.OK)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + public BlockModel GetBlock([FromQuery] SearchByHashRequest query) + { + if (!this.ModelState.IsValid) + return null; + + try + { + uint256 blockId = uint256.Parse(query.Hash); + + ChainedHeader chainedHeader = this.chainIndexer.GetHeader(blockId); + + if (chainedHeader == null) + return null; + + Block block = chainedHeader.Block ?? this.blockStore.GetBlock(blockId); + + // In rare occasions a block that is found in the + // indexer may not have been pushed to the store yet. + if (block == null) + return null; + + BlockModel blockModel = query.ShowTransactionDetails + ? new BlockTransactionDetailsModel(block, chainedHeader, this.chainIndexer.Tip, this.network) + : new BlockModel(block, chainedHeader, this.chainIndexer.Tip, this.network); + + if (this.network.Consensus.IsProofOfStake) + { + var posBlock = block as PosBlock; + + blockModel.PosBlockSignature = posBlock.BlockSignature.ToHex(this.network); + blockModel.PosBlockTrust = new Target(chainedHeader.GetBlockTarget()).ToUInt256().ToString(); + blockModel.PosChainTrust = chainedHeader.ChainWork.ToString(); // this should be similar to ChainWork + + if (this.stakeChain != null) + { + BlockStake blockStake = this.stakeChain.Get(blockId); + + blockModel.PosModifierv2 = blockStake?.StakeModifierV2.ToString(); + blockModel.PosFlags = blockStake?.Flags == BlockFlag.BLOCK_PROOF_OF_STAKE ? "proof-of-stake" : "proof-of-work"; + blockModel.PosHashProof = blockStake?.HashProof?.ToString(); + } + } + + return blockModel; + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return null; + } + } + + /// + /// Retrieves the 's tip. + /// + /// An instance of containing the tip's hash and height. + /// Returns the address indexer tip + /// Unexpected exception occurred + [Route("tip")] + [HttpGet] + [ProducesResponseType((int) HttpStatusCode.OK)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + public TipModel GetTip() + { + try + { + ChainedHeader addressIndexerTip = this.addressIndexer.IndexerTip; + + if (addressIndexerTip == null) + return null; + + return new TipModel() { TipHash = addressIndexerTip.HashBlock.ToString(), TipHeight = addressIndexerTip.Height }; + } + catch (Exception e) + { + this.logger.LogError("Exception occurred: {0}", e.ToString()); + return null; + } + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/Program.cs b/src/Stratis.Features.Unity3dApi/Program.cs new file mode 100644 index 0000000000..3a1de5d662 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Program.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Stratis.Bitcoin; +using Stratis.Bitcoin.Features.Api; +using Stratis.Bitcoin.Utilities; + +namespace Stratis.Features.Unity3dApi +{ + public class Program + { + public static IWebHost Initialize(IEnumerable services, FullNode fullNode, + Unity3dApiSettings apiSettings, ICertificateStore store, IWebHostBuilder webHostBuilder) + { + Guard.NotNull(fullNode, nameof(fullNode)); + Guard.NotNull(webHostBuilder, nameof(webHostBuilder)); + + Uri apiUri = apiSettings.ApiUri; + + X509Certificate2 certificate = apiSettings.UseHttps + ? GetHttpsCertificate(apiSettings.HttpsCertificateFilePath, store) + : null; + + webHostBuilder + .UseKestrel(options => + { + if (!apiSettings.UseHttps) + return; + + options.AllowSynchronousIO = true; + Action configureListener = listenOptions => { listenOptions.UseHttps(certificate); }; + var ipAddresses = Dns.GetHostAddresses(apiSettings.ApiUri.DnsSafeHost); + foreach (var ipAddress in ipAddresses) + { + options.Listen(ipAddress, apiSettings.ApiPort, configureListener); + } + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(apiUri.ToString()) + .ConfigureServices(collection => + { + if (services == null) + { + return; + } + + // copies all the services defined for the full node to the Api. + // also copies over singleton instances already defined + foreach (ServiceDescriptor service in services) + { + // open types can't be singletons + if (service.ServiceType.IsGenericType || service.Lifetime == ServiceLifetime.Scoped) + { + collection.Add(service); + continue; + } + + object obj = fullNode.Services.ServiceProvider.GetService(service.ServiceType); + if (obj != null && service.Lifetime == ServiceLifetime.Singleton && service.ImplementationInstance == null) + { + collection.AddSingleton(service.ServiceType, obj); + } + else + { + collection.Add(service); + } + } + }) + .UseStartup(); + + IWebHost host = webHostBuilder.Build(); + + host.Start(); + + return host; + } + + private static X509Certificate2 GetHttpsCertificate(string certificateFilePath, ICertificateStore store) + { + if (store.TryGet(certificateFilePath, out var certificate)) + return certificate; + + throw new FileLoadException($"Failed to load certificate from path {certificateFilePath}"); + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/Properties/launchSettings.json b/src/Stratis.Features.Unity3dApi/Properties/launchSettings.json new file mode 100644 index 0000000000..2cf5fa8071 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:58798", + "sslPort": 44366 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Stratis.Features.Unity3dApi": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/ResponseModels.cs b/src/Stratis.Features.Unity3dApi/ResponseModels.cs new file mode 100644 index 0000000000..3666597c3b --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/ResponseModels.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using NBitcoin; + +namespace Stratis.Features.Unity3dApi +{ + public class GetUTXOsResponseModel + { + public long BalanceSat; + + public List UTXOs; + + public string Reason; + } + + public class UTXOModel + { + public UTXOModel() + { + } + + public UTXOModel(OutPoint outPoint, Money value) + { + this.Hash = outPoint.Hash.ToString(); + this.N = outPoint.N; + this.Satoshis = value.Satoshi; + } + + public string Hash; + + public uint N; + + public long Satoshis; + } + + public sealed class TipModel + { + public string TipHash { get; set; } + + public int TipHeight { get; set; } + } + + public sealed class RawTxModel + { + public string Hex { get; set; } + } +} diff --git a/src/Stratis.Features.Unity3dApi/Startup.cs b/src/Stratis.Features.Unity3dApi/Startup.cs new file mode 100644 index 0000000000..bdb4d2fdc8 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Startup.cs @@ -0,0 +1,167 @@ +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Stratis.Bitcoin; +using Stratis.Bitcoin.Features.Api; +using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerUI; + +namespace Stratis.Features.Unity3dApi +{ + public class Startup + { + public Startup(IWebHostEnvironment env, IFullNode fullNode) + { + this.fullNode = fullNode; + + IConfigurationBuilder builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + this.Configuration = builder.Build(); + } + + private IFullNode fullNode; + private SwaggerUIOptions uiOptions; + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddLogging( + loggingBuilder => + { + loggingBuilder.AddConfiguration(this.Configuration.GetSection("Logging")); + loggingBuilder.AddConsole(); + loggingBuilder.AddDebug(); + }); + + // Add service and create Policy to allow Cross-Origin Requests + services.AddCors + ( + options => + { + options.AddPolicy + ( + "CorsPolicy", + + builder => + { + var allowedDomains = new[] { "http://localhost", "http://localhost:4200" }; + + builder + .WithOrigins(allowedDomains) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + ); + }); + + // Add framework services. + services + .AddMvc(options => + { + options.Filters.Add(typeof(LoggingActionFilter)); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + var apiSettings = (ApiSettings)serviceProvider.GetRequiredService(typeof(ApiSettings)); + if (apiSettings.KeepaliveTimer != null) + { + options.Filters.Add(typeof(KeepaliveActionFilter)); + } + }) + // add serializers for NBitcoin objects + .AddNewtonsoftJson(options => { + Stratis.Bitcoin.Utilities.JsonConverters.Serializer.RegisterFrontConverters(options.SerializerSettings); + }) + .AddControllers(this.fullNode.Services.Features, services) + .ConfigureApplicationPartManager(a => + { + foreach (ApplicationPart appPart in a.ApplicationParts.ToList()) + { + if (appPart.Name == typeof(Startup).Namespace) + continue; + + a.ApplicationParts.Remove(appPart); + } + }); + + // Enable API versioning. + // Note much of this is borrowed from https://github.com/microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/Startup.cs + services.AddApiVersioning(options => + { + // Our versions are configured to be set via URL path, no need to read from querystring etc. + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + + // When no API version is specified, redirect to version 1. + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + // Add the versioned API explorer, which adds the IApiVersionDescriptionProvider service and allows Swagger integration. + services.AddVersionedApiExplorer( + options => + { + // Format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; + + // Substitute the version into the URLs in the swagger interface where we would otherwise see {version:apiVersion} + options.SubstituteApiVersionInUrl = true; + }); + + // Add custom Options injectable for Swagger. This is injected with the IApiVersionDescriptionProvider service from above. + services.AddTransient, ConfigureSwaggerOptions>(); + + // Register the Swagger generator. This will use the options we injected just above. + services.AddSwaggerGen(); + services.AddSwaggerGenNewtonsoftSupport(); // Use Newtonsoft JSON serializer with swagger. Needs to be placed after AddSwaggerGen() + + // Hack to be able to access and modify the options object + services.AddSingleton(_ => this.uiOptions); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IApiVersionDescriptionProvider provider) + { + app.UseStaticFiles(); + app.UseRouting(); + + app.UseCors("CorsPolicy"); + + // Register this before MVC and Swagger. + app.UseMiddleware(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + + // Enable middleware to serve generated Swagger as a JSON endpoint. + app.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.) + app.UseSwaggerUI(c => + { + c.DefaultModelRendering(ModelRendering.Model); + + // Build a swagger endpoint for each discovered API version + foreach (ApiVersionDescription description in provider.ApiVersionDescriptions) + { + c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + + // Hack to be able to access and modify the options object configured here + this.uiOptions = c; + }); + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj b/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj new file mode 100644 index 0000000000..50438c1f88 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Stratis.Features.Unity3dApi.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + Library + + + + + + + + + + + diff --git a/src/Stratis.Features.Unity3dApi/Unity3dApiFeature.cs b/src/Stratis.Features.Unity3dApi/Unity3dApiFeature.cs new file mode 100644 index 0000000000..3d1ae63128 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Unity3dApiFeature.cs @@ -0,0 +1,152 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NBitcoin; +using Stratis.Bitcoin; +using Stratis.Bitcoin.Builder; +using Stratis.Bitcoin.Builder.Feature; +using Stratis.Bitcoin.Features.Api; +using Stratis.Bitcoin.Features.Wallet.Controllers; +using Stratis.Features.Unity3dApi.Controllers; + +namespace Stratis.Features.Unity3dApi +{ + /// + /// Provides an Api to the full node + /// + public sealed class Unity3dApiFeature : FullNodeFeature + { + /// How long we are willing to wait for the API to stop. + private const int ApiStopTimeoutSeconds = 10; + + private readonly IFullNodeBuilder fullNodeBuilder; + + private readonly FullNode fullNode; + + private readonly Unity3dApiSettings apiSettings; + + private readonly ILogger logger; + + private IWebHost webHost; + + private readonly ICertificateStore certificateStore; + + public Unity3dApiFeature( + IFullNodeBuilder fullNodeBuilder, + FullNode fullNode, + Unity3dApiSettings apiSettings, + ILoggerFactory loggerFactory, + ICertificateStore certificateStore) + { + this.fullNodeBuilder = fullNodeBuilder; + this.fullNode = fullNode; + this.apiSettings = apiSettings; + this.certificateStore = certificateStore; + this.logger = loggerFactory.CreateLogger(this.GetType().FullName); + + this.InitializeBeforeBase = true; + } + + public override Task InitializeAsync() + { + if (!this.apiSettings.EnableUnityAPI) + { + this.logger.LogInformation("Unity3d api disabled."); + return Task.CompletedTask; + } + + this.logger.LogInformation("Unity API starting on URL '{0}'.", this.apiSettings.ApiUri); + this.webHost = Program.Initialize(this.fullNodeBuilder.Services, this.fullNode, this.apiSettings, this.certificateStore, new WebHostBuilder()); + + if (this.apiSettings.KeepaliveTimer == null) + { + this.logger.LogTrace("(-)[KEEPALIVE_DISABLED]"); + return Task.CompletedTask; + } + + // Start the keepalive timer, if set. + // If the timer expires, the node will shut down. + this.apiSettings.KeepaliveTimer.Elapsed += (sender, args) => + { + this.logger.LogInformation($"Unity Api: The application will shut down because the keepalive timer has elapsed."); + + this.apiSettings.KeepaliveTimer.Stop(); + this.apiSettings.KeepaliveTimer.Enabled = false; + this.fullNode.NodeLifetime.StopApplication(); + }; + + this.apiSettings.KeepaliveTimer.Start(); + + return Task.CompletedTask; + } + + /// + /// Prints command-line help. + /// + /// The network to extract values from. + public static void PrintHelp(Network network) + { + ApiSettings.PrintHelp(network); + } + + /// + /// Get the default configuration. + /// + /// The string builder to add the settings to. + /// The network to base the defaults off. + public static void BuildDefaultConfigurationFile(StringBuilder builder, Network network) + { + ApiSettings.BuildDefaultConfigurationFile(builder, network); + } + + /// + public override void Dispose() + { + // Make sure the timer is stopped and disposed. + if (this.apiSettings.KeepaliveTimer != null) + { + this.apiSettings.KeepaliveTimer.Stop(); + this.apiSettings.KeepaliveTimer.Enabled = false; + this.apiSettings.KeepaliveTimer.Dispose(); + } + + // Make sure we are releasing the listening ip address / port. + if (this.webHost != null) + { + this.logger.LogInformation("Unity API stopping on URL '{0}'.", this.apiSettings.ApiUri); + this.webHost.StopAsync(TimeSpan.FromSeconds(ApiStopTimeoutSeconds)).Wait(); + this.webHost = null; + } + } + } + + /// + /// A class providing extension methods for . + /// + public static class Unity3dApiFeatureExtension + { + public static IFullNodeBuilder UseUnity3dApi(this IFullNodeBuilder fullNodeBuilder) + { + fullNodeBuilder.ConfigureFeature(features => + { + features + .AddFeature() + .FeatureServices(services => + { + services.AddSingleton(fullNodeBuilder); + services.AddSingleton(); + services.AddSingleton(); + + // Controller + services.AddTransient(); + services.AddTransient(); + }); + }); + + return fullNodeBuilder; + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs b/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs new file mode 100644 index 0000000000..8b0fc74f45 --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/Unity3dApiSettings.cs @@ -0,0 +1,146 @@ +using System; +using System.Text; +using System.Timers; +using Microsoft.Extensions.Logging; +using NBitcoin; +using Stratis.Bitcoin.Configuration; +using Stratis.Bitcoin.Utilities; + +namespace Stratis.Features.Unity3dApi +{ + public class Unity3dApiSettings + { + /// The default port used by the API when the node runs on the Stratis network. + public const string DefaultApiHost = "http://localhost"; + + /// Instance logger. + private readonly ILogger logger; + + public bool EnableUnityAPI { get; set; } + + /// URI to node's API interface. + public Uri ApiUri { get; set; } + + /// Port of node's API interface. + public int ApiPort { get; set; } + + /// URI to node's API interface. + public Timer KeepaliveTimer { get; private set; } + + /// + /// Port on which to listen for incoming API connections. + /// + public int DefaultAPIPort { get; protected set; } = 44336; + + /// + /// The HTTPS certificate file path. + /// + /// + /// Password protected certificates are not supported. On MacOs, only p12 certificates can be used without password. + /// Please refer to .Net Core documentation for usage: . + /// + public string HttpsCertificateFilePath { get; set; } + + /// Use HTTPS or not. + public bool UseHttps { get; set; } + + /// + /// Initializes an instance of the object from the node configuration. + /// + /// The node configuration. + public Unity3dApiSettings(NodeSettings nodeSettings) + { + Guard.NotNull(nodeSettings, nameof(nodeSettings)); + + this.logger = nodeSettings.LoggerFactory.CreateLogger(typeof(Unity3dApiSettings).FullName); + TextFileConfiguration config = nodeSettings.ConfigReader; + + this.EnableUnityAPI = config.GetOrDefault("unityapi_enable", false); + + if (!this.EnableUnityAPI) + { + this.logger.LogDebug("Unity API disabled."); + return; + } + + this.UseHttps = config.GetOrDefault("unityapi_usehttps", false); + this.HttpsCertificateFilePath = config.GetOrDefault("unityapi_certificatefilepath", (string)null); + + if (this.UseHttps && string.IsNullOrWhiteSpace(this.HttpsCertificateFilePath)) + throw new ConfigurationException("The path to a certificate needs to be provided when using https. Please use the argument 'certificatefilepath' to provide it."); + + var defaultApiHost = this.UseHttps + ? DefaultApiHost.Replace(@"http://", @"https://") + : DefaultApiHost; + + string apiHost = config.GetOrDefault("unityapi_apiuri", defaultApiHost, this.logger); + var apiUri = new Uri(apiHost); + + // Find out which port should be used for the API. + int apiPort = config.GetOrDefault("unityapi_apiport", DefaultAPIPort, this.logger); + + // If no port is set in the API URI. + if (apiUri.IsDefaultPort) + { + this.ApiUri = new Uri($"{apiHost}:{apiPort}"); + this.ApiPort = apiPort; + } + // If a port is set in the -apiuri, it takes precedence over the default port or the port passed in -apiport. + else + { + this.ApiUri = apiUri; + this.ApiPort = apiUri.Port; + } + + // Set the keepalive interval (set in seconds). + int keepAlive = config.GetOrDefault("unityapi_keepalive", 0, this.logger); + if (keepAlive > 0) + { + this.KeepaliveTimer = new Timer + { + AutoReset = false, + Interval = keepAlive * 1000 + }; + } + } + + /// Prints the help information on how to configure the API settings to the logger. + /// The network to use. + public static void PrintHelp(Network network) + { + var builder = new StringBuilder(); + + builder.AppendLine($"-unityapi_enable= Use unity3d API. Defaults to false."); + builder.AppendLine($"-unityapi_apiuri= URI to node's API interface. Defaults to '{ DefaultApiHost }'."); + builder.AppendLine($"-unityapi_apiport=<0-65535> Port of node's API interface. Defaults to { network.DefaultAPIPort }."); + builder.AppendLine($"-unityapi_keepalive= Keep Alive interval (set in seconds). Default: 0 (no keep alive)."); + builder.AppendLine($"-unityapi_usehttps= Use https protocol on the API. Defaults to false."); + builder.AppendLine($"-unityapi_certificatefilepath= Path to the certificate used for https traffic encryption. Defaults to . Password protected files are not supported. On MacOs, only p12 certificates can be used without password."); + + NodeSettings.Default(network).Logger.LogInformation(builder.ToString()); + } + + /// + /// Get the default configuration. + /// + /// The string builder to add the settings to. + /// The network to base the defaults off. + public static void BuildDefaultConfigurationFile(StringBuilder builder, Network network) + { + builder.AppendLine("####Unity3d API Settings####"); + builder.AppendLine($"#Enable unity3d api support"); + builder.AppendLine($"#unityapi_enable="); + builder.AppendLine($"#URI to node's API interface. Defaults to '{ DefaultApiHost }'."); + builder.AppendLine($"#unityapi_apiuri={ DefaultApiHost }"); + builder.AppendLine($"#Port of node's API interface. Defaults to { network.DefaultAPIPort }."); + builder.AppendLine($"#unityapi_apiport={ network.DefaultAPIPort }"); + builder.AppendLine($"#Keep Alive interval (set in seconds). Default: 0 (no keep alive)."); + builder.AppendLine($"#unityapi_keepalive=0"); + builder.AppendLine($"#Use HTTPS protocol on the API. Default is false."); + builder.AppendLine($"#unityapi_usehttps=false"); + builder.AppendLine($"#Path to the file containing the certificate to use for https traffic encryption. Password protected files are not supported. On MacOs, only p12 certificates can be used without password."); + builder.AppendLine(@"#Please refer to .Net Core documentation for usage: 'https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2.-ctor?view=netcore-2.1#System_Security_Cryptography_X509Certificates_X509Certificate2__ctor_System_Byte___'."); + builder.AppendLine($"#unityapi_certificatefilepath="); + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/appsettings.Development.json b/src/Stratis.Features.Unity3dApi/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Stratis.Features.Unity3dApi/appsettings.json b/src/Stratis.Features.Unity3dApi/appsettings.json new file mode 100644 index 0000000000..4e0e03cdca --- /dev/null +++ b/src/Stratis.Features.Unity3dApi/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/src/Stratis.FullNode.sln b/src/Stratis.FullNode.sln index 290c2ab3db..299d4e095c 100644 --- a/src/Stratis.FullNode.sln +++ b/src/Stratis.FullNode.sln @@ -183,7 +183,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Patricia.Tests", "S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Bitcoin.Features.Interop", "Stratis.Bitcoin.Features.Interop\Stratis.Bitcoin.Features.Interop.csproj", "{3DCC6195-1271-4A12-8B94-E821925D98DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stratis.External.Masternodes", "Stratis.External.Masternodes\Stratis.External.Masternodes.csproj", "{F04464B5-9D56-4C9A-9778-27B9D314A296}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.External.Masternodes", "Stratis.External.Masternodes\Stratis.External.Masternodes.csproj", "{F04464B5-9D56-4C9A-9778-27B9D314A296}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.Features.Unity3dApi", "Stratis.Features.Unity3dApi\Stratis.Features.Unity3dApi.csproj", "{B08D2057-F48D-4E72-99F4-95A35E6E0DFD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -487,6 +489,10 @@ Global {F04464B5-9D56-4C9A-9778-27B9D314A296}.Debug|Any CPU.Build.0 = Debug|Any CPU {F04464B5-9D56-4C9A-9778-27B9D314A296}.Release|Any CPU.ActiveCfg = Release|Any CPU {F04464B5-9D56-4C9A-9778-27B9D314A296}.Release|Any CPU.Build.0 = Release|Any CPU + {B08D2057-F48D-4E72-99F4-95A35E6E0DFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B08D2057-F48D-4E72-99F4-95A35E6E0DFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B08D2057-F48D-4E72-99F4-95A35E6E0DFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B08D2057-F48D-4E72-99F4-95A35E6E0DFD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -556,6 +562,7 @@ Global {EE71EBA7-5515-4F1E-B0E0-6C32BAAD6B35} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} {3DCC6195-1271-4A12-8B94-E821925D98DC} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} {F04464B5-9D56-4C9A-9778-27B9D314A296} = {1B9A916F-DDAC-4675-B424-EDEDC1A58231} + {B08D2057-F48D-4E72-99F4-95A35E6E0DFD} = {15D29FFD-6142-4DC5-AFFD-10BA0CA55C45} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6C780ABA-5872-4B83-AD3F-A5BD423AD907} diff --git a/src/Stratis.StraxD/Program.cs b/src/Stratis.StraxD/Program.cs index fa263e6363..794f484d13 100644 --- a/src/Stratis.StraxD/Program.cs +++ b/src/Stratis.StraxD/Program.cs @@ -17,6 +17,7 @@ using Stratis.Bitcoin.Utilities; using Stratis.Features.Diagnostic; using Stratis.Features.SQLiteWalletRepository; +using Stratis.Features.Unity3dApi; namespace Stratis.StraxD { @@ -45,6 +46,7 @@ public static async Task Main(string[] args) .AddSQLiteWalletRepository() .AddPowPosMining(true) .UseApi() + .UseUnity3dApi() .AddRPC() .AddSignalR(options => { diff --git a/src/Stratis.StraxD/Stratis.StraxD.csproj b/src/Stratis.StraxD/Stratis.StraxD.csproj index f61eb9e137..73f9d1e1ec 100644 --- a/src/Stratis.StraxD/Stratis.StraxD.csproj +++ b/src/Stratis.StraxD/Stratis.StraxD.csproj @@ -34,7 +34,8 @@ - + +