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 @@
-
+
+