diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs index 1d9773cc..04ac64e2 100644 --- a/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs +++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Controllers/FederationGatewayController.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using NBitcoin; +using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Features.PoA; using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.JsonErrors; @@ -24,6 +25,7 @@ public static class FederationGatewayRouteEndPoint public const string GetMaturedBlockDeposits = "get_matured_block_deposits"; public const string GetInfo = "info"; + public const string GetBlockHeightClosestToTimestamp = "get_block_height_closest_to_timestamp"; } /// @@ -49,6 +51,8 @@ public class FederationGatewayController : Controller private readonly FederationManager federationManager; + private readonly IConsensusManager consensusManager; + public FederationGatewayController( ILoggerFactory loggerFactory, Network network, @@ -57,6 +61,7 @@ public FederationGatewayController( ILeaderReceiver leaderReceiver, IFederationGatewaySettings federationGatewaySettings, IFederationWalletManager federationWalletManager, + IConsensusManager consensusManager, FederationManager federationManager = null) { this.logger = loggerFactory.CreateLogger(this.GetType().FullName); @@ -67,6 +72,7 @@ public FederationGatewayController( this.federationGatewaySettings = federationGatewaySettings; this.federationWalletManager = federationWalletManager; this.federationManager = federationManager; + this.consensusManager = consensusManager; } /// Pushes the current block tip to be used for updating the federated leader in a round robin fashion. @@ -165,6 +171,39 @@ public IActionResult GetInfo() } } + /// Finds first block with timestamp lower than and return's it's height. + /// model with height of the block. + [Route(FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp)] + [HttpGet] + public JsonResult GetBlockHeightClosestToTimestamp(uint timestamp) + { + try + { + ChainedHeader current = this.consensusManager.Tip; + + while (current.Height != 0) + { + if (current.Header.Time <= timestamp) + { + var model = new ClosestHeightModel() { Height = current.Height }; + + return this.Json(model); + } + + current = current.Previous; + } + + return this.Json(new ClosestHeightModel() { Height = 0 }); ; + } + catch (Exception e) + { + this.logger.LogTrace("Exception thrown calling /api/FederationGateway/{0}: {1}.", FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp, e.Message); + ErrorResult errorResult = ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString()); + + return this.Json(errorResult); + } + } + /// /// Builds an containing errors contained in the . /// diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/Models/ClosestHeightModel.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/Models/ClosestHeightModel.cs new file mode 100644 index 00000000..61a1b913 --- /dev/null +++ b/src/Stratis.FederatedPeg.Features.FederationGateway/Models/ClosestHeightModel.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Stratis.FederatedPeg.Features.FederationGateway.Models +{ + public class ClosestHeightModel + { + [JsonProperty(PropertyName = "height")] + public int Height { get; set; } + } +} diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/FederationGatewayClient.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/FederationGatewayClient.cs index c9e4626a..0de2becb 100644 --- a/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/FederationGatewayClient.cs +++ b/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/FederationGatewayClient.cs @@ -16,6 +16,9 @@ public interface IFederationGatewayClient /// Task> GetMaturedBlockDepositsAsync(MaturedBlockRequestModel model, CancellationToken cancellation = default(CancellationToken)); + + /// + Task GetBlockHeightClosestToTimestampAsync(uint timestamp, CancellationToken cancellation = default(CancellationToken)); } /// @@ -37,5 +40,13 @@ public FederationGatewayClient(ILoggerFactory loggerFactory, IFederationGatewayS { return this.SendPostRequestAsync>(model, FederationGatewayRouteEndPoint.GetMaturedBlockDeposits, cancellation); } + + /// + public Task GetBlockHeightClosestToTimestampAsync(uint timestamp, CancellationToken cancellation = default(CancellationToken)) + { + string parameters = $"{nameof(timestamp)}={timestamp}"; + + return this.SendGetRequestAsync(FederationGatewayRouteEndPoint.GetBlockHeightClosestToTimestamp, cancellation, parameters); + } } } diff --git a/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/RestApiClientBase.cs b/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/RestApiClientBase.cs index 713e1bfc..d47211da 100644 --- a/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/RestApiClientBase.cs +++ b/src/Stratis.FederatedPeg.Features.FederationGateway/RestClients/RestApiClientBase.cs @@ -48,6 +48,51 @@ public RestApiClientBase(ILoggerFactory loggerFactory, IFederationGatewaySetting }, onRetry: OnRetry); } + protected async Task SendGetRequestAsync(string apiMethodName, CancellationToken cancellation, string requestParameters = null) + { + string url = $"{this.endpointUrl}/{apiMethodName}"; + + if (requestParameters != null) + url += $"?{requestParameters}"; + + var publicationUri = new Uri(url); + + HttpResponseMessage response = null; + + using (HttpClient client = this.httpClientFactory.CreateClient()) + { + try + { + // Retry the following call according to the policy. + await this.policy.ExecuteAsync(async token => + { + this.logger.LogDebug("Sending get request to Uri '{0}'.", publicationUri); + response = await client.GetAsync(publicationUri, cancellation).ConfigureAwait(false); + }, cancellation); + } + catch (OperationCanceledException) + { + this.logger.LogTrace("(-)[CANCELLED]:null"); + return null; + } + catch (Exception ex) + { + this.logger.LogError("The counter-chain daemon is not ready to receive API calls at this time ({0})", publicationUri); + return new HttpResponseMessage() { ReasonPhrase = ex.Message, StatusCode = HttpStatusCode.InternalServerError }; + } + } + + this.logger.LogTrace("(-)[SUCCESS]"); + return response; + } + + protected async Task SendGetRequestAsync(string apiMethodName, CancellationToken cancellation, string requestParameters = null) where Response : class + { + HttpResponseMessage response = await this.SendGetRequestAsync(apiMethodName, cancellation, requestParameters).ConfigureAwait(false); + + return await this.ParseResponseAsync(response).ConfigureAwait(false); + } + protected async Task SendPostRequestAsync(Model requestModel, string apiMethodName, CancellationToken cancellation) where Model : class { var publicationUri = new Uri($"{this.endpointUrl}/{apiMethodName}"); @@ -93,10 +138,15 @@ protected async Task SendPostRequestAsync(Model reque { HttpResponseMessage response = await this.SendPostRequestAsync(requestModel, apiMethodName, cancellation).ConfigureAwait(false); + return await this.ParseResponseAsync(response).ConfigureAwait(false); + } + + private async Task ParseResponseAsync(HttpResponseMessage message) where Response : class + { // Parse response. - if ((response != null) && response.IsSuccessStatusCode && (response.Content != null)) + if ((message != null) && message.IsSuccessStatusCode && (message.Content != null)) { - string successJson = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string successJson = await message.Content.ReadAsStringAsync().ConfigureAwait(false); if (successJson != null) { Response responseModel = JsonConvert.DeserializeObject(successJson); diff --git a/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs b/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs index 2b335a8f..a7c30616 100644 --- a/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs +++ b/src/Stratis.FederatedPeg.Tests/FederationGatewayControllerTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using NBitcoin; using NBitcoin.Protocol; +using Newtonsoft.Json; using NSubstitute; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Configuration; @@ -74,6 +75,7 @@ private FederationGatewayController CreateController() this.leaderReceiver, this.federationGatewaySettings, this.federationWalletManager, + this.consensusManager, this.federationManager); return controller; @@ -236,6 +238,7 @@ public void Call_Sidechain_Gateway_Get_Info() this.leaderReceiver, settings, this.federationWalletManager, + this.consensusManager, this.federationManager); IActionResult result = controller.GetInfo(); @@ -275,6 +278,7 @@ public void Call_Mainchain_Gateway_Get_Info() this.leaderReceiver, settings, this.federationWalletManager, + this.consensusManager, this.federationManager); IActionResult result = controller.GetInfo(); @@ -293,5 +297,26 @@ public void Call_Mainchain_Gateway_Get_Info() model.MinimumDepositConfirmations.Should().Be(1); model.MultisigPublicKey.Should().Be(multisigPubKey); } + + [Fact] + public void GetBlockHeightClosestToTimestampTest() + { + FederationGatewayController controller = this.CreateController(); + + List headers = ChainedHeadersHelper.CreateConsecutiveHeaders(1000); + + for (int i = 0; i < headers.Count; i++) + headers[i].Header.Time = (uint)(i * 3); + + this.consensusManager.Tip.Returns(headers.Last()); + + ChainedHeader targetHeader = headers[500]; + + JsonResult result = controller.GetBlockHeightClosestToTimestamp(targetHeader.Header.Time); + + var responseHeight = result.Value as ClosestHeightModel; + + Assert.Equal(targetHeader.Height, responseHeight.Height); + } } }