Skip to content

Commit

Permalink
Add statistics API endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Groxan committed Sep 8, 2020
1 parent 62cc7b5 commit 4b2db30
Show file tree
Hide file tree
Showing 10 changed files with 883 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Tzkt.Api/Controllers/CommitmentsController.cs
Expand Up @@ -46,7 +46,7 @@ public Task<Commitment> Get([BlindedAddress] string address)
/// <param name="activationLevel">Filters commitments by activation level</param>
/// <param name="balance">Filters commitments by activated balance</param>
/// <param name="select">Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.</param>
/// <param name="sort">Sorts delegators by specified field. Supported fields: `id` (default), `balance`, `firstActivity`, `lastActivity`, `numTransactions`, `numContracts`.</param>
/// <param name="sort">Sorts delegators by specified field. Supported fields: `id` (default), `balance`, `activationLevel`.</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
/// <param name="limit">Maximum number of items to return</param>
/// <returns></returns>
Expand Down
181 changes: 181 additions & 0 deletions Tzkt.Api/Controllers/StatisticsController.cs
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

using Tzkt.Api.Models;
using Tzkt.Api.Repositories;

namespace Tzkt.Api.Controllers
{
[ApiController]
[Route("v1/statistics")]
public class StatisticsController : ControllerBase
{
private readonly StatisticsRepository Statistics;

public StatisticsController(StatisticsRepository statistics)
{
Statistics = statistics;
}

/// <summary>
/// Get statistics
/// </summary>
/// <remarks>
/// Returns a list of end-of-block statistics.
/// </remarks>
/// <param name="level">Filters statistics by level.</param>
/// <param name="timestamp">Filters statistics by timestamp.</param>
/// <param name="select">Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.</param>
/// <param name="sort">Sorts delegators by specified field. Supported fields: `id` (default), `level`, `cycle`, `date`.</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
/// <param name="limit">Maximum number of items to return</param>
/// <param name="quote">Comma-separated list of ticker symbols to inject historical prices into response</param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<Statistics>>> Get(
Int32Parameter level,
TimestampParameter timestamp,
SelectParameter select,
SortParameter sort,
OffsetParameter offset,
[Range(0, 10000)] int limit = 100,
Symbols quote = Symbols.None)
{
#region validate
if (sort != null && !sort.Validate("id", "level", "cycle", "date"))
return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed.");
#endregion

if (select == null)
return Ok(await Statistics.Get(StatisticsPeriod.None, null, level, timestamp, null, sort, offset, limit, quote));

if (select.Values != null)
{
if (select.Values.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.None, null, level, timestamp, null, sort, offset, limit, select.Values[0], quote));
else
return Ok(await Statistics.Get(StatisticsPeriod.None, null, level, timestamp, null, sort, offset, limit, select.Values, quote));
}
else
{
if (select.Fields.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.None, null, level, timestamp, null, sort, offset, limit, select.Fields[0], quote));
else
{
return Ok(new SelectionResponse
{
Cols = select.Fields,
Rows = await Statistics.Get(StatisticsPeriod.None, null, level, timestamp, null, sort, offset, limit, select.Fields, quote)
});
}
}
}

/// <summary>
/// Get daily statistics
/// </summary>
/// <remarks>
/// Returns a list of end-of-day statistics.
/// </remarks>
/// <param name="date">Filters statistics by date.</param>
/// <param name="select">Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.</param>
/// <param name="sort">Sorts delegators by specified field. Supported fields: `id` (default), `level`, `cycle`, `date`.</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
/// <param name="limit">Maximum number of items to return</param>
/// <param name="quote">Comma-separated list of ticker symbols to inject historical prices into response</param>
/// <returns></returns>
[HttpGet("daily")]
public async Task<ActionResult<IEnumerable<Statistics>>> GetDaily(
DateTimeParameter date,
SelectParameter select,
SortParameter sort,
OffsetParameter offset,
[Range(0, 10000)] int limit = 100,
Symbols quote = Symbols.None)
{
#region validate
if (sort != null && !sort.Validate("id", "level", "cycle", "date"))
return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed.");
#endregion

if (select == null)
return Ok(await Statistics.Get(StatisticsPeriod.Daily, null, null, null, date, sort, offset, limit, quote));

if (select.Values != null)
{
if (select.Values.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.Daily, null, null, null, date, sort, offset, limit, select.Values[0], quote));
else
return Ok(await Statistics.Get(StatisticsPeriod.Daily, null, null, null, date, sort, offset, limit, select.Values, quote));
}
else
{
if (select.Fields.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.Daily, null, null, null, date, sort, offset, limit, select.Fields[0], quote));
else
{
return Ok(new SelectionResponse
{
Cols = select.Fields,
Rows = await Statistics.Get(StatisticsPeriod.Daily, null, null, null, date, sort, offset, limit, select.Fields, quote)
});
}
}
}

/// <summary>
/// Get cyclic statistics
/// </summary>
/// <remarks>
/// Returns a list of end-of-cycle statistics.
/// </remarks>
/// <param name="cycle">Filters statistics by cycle.</param>
/// <param name="select">Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.</param>
/// <param name="sort">Sorts delegators by specified field. Supported fields: `id` (default), `level`, `cycle`, `date`.</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
/// <param name="limit">Maximum number of items to return</param>
/// <param name="quote">Comma-separated list of ticker symbols to inject historical prices into response</param>
/// <returns></returns>
[HttpGet("cyclic")]
public async Task<ActionResult<IEnumerable<Statistics>>> GetCycles(
Int32Parameter cycle,
SelectParameter select,
SortParameter sort,
OffsetParameter offset,
[Range(0, 10000)] int limit = 100,
Symbols quote = Symbols.None)
{
#region validate
if (sort != null && !sort.Validate("id", "level", "cycle", "date"))
return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed.");
#endregion

if (select == null)
return Ok(await Statistics.Get(StatisticsPeriod.Cyclic, cycle, null, null, null, sort, offset, limit, quote));

if (select.Values != null)
{
if (select.Values.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.Cyclic, cycle, null, null, null, sort, offset, limit, select.Values[0], quote));
else
return Ok(await Statistics.Get(StatisticsPeriod.Cyclic, cycle, null, null, null, sort, offset, limit, select.Values, quote));
}
else
{
if (select.Fields.Length == 1)
return Ok(await Statistics.Get(StatisticsPeriod.Cyclic, cycle, null, null, null, sort, offset, limit, select.Fields[0], quote));
else
{
return Ok(new SelectionResponse
{
Cols = select.Fields,
Rows = await Statistics.Get(StatisticsPeriod.Cyclic, cycle, null, null, null, sort, offset, limit, select.Fields, quote)
});
}
}
}
}
}
79 changes: 79 additions & 0 deletions Tzkt.Api/Models/Statistics/Statistics.cs
@@ -0,0 +1,79 @@
using System;

namespace Tzkt.Api.Models
{
public class Statistics
{
/// <summary>
/// Cycle at the end of which the statistics has been calculated. This field is only present in cyclic statistics.
/// </summary>
public int? Cycle { get; set; }

/// <summary>
/// Day at the end of which the statistics has been calculated. This field is only present in daily statistics.
/// </summary>
public DateTime? Date { get; set; }

/// <summary>
/// Level of the block at which the statistics has been calculated
/// </summary>
public int Level { get; set; }

/// <summary>
/// Timestamp of the block at which the statistics has been calculated (ISO 8601, e.g. `2020-02-20T02:40:57Z`)
/// </summary>
public DateTime Timestamp { get; set; }

/// <summary>
/// Total supply - all existing tokens (including locked vested funds and frozen funds) plus not yet activated fundraiser tokens
/// </summary>
public long TotalSupply { get; set; }

/// <summary>
/// Circulating supply - all active tokens which can affect supply and demand (can be spent/transferred)
/// </summary>
public long CirculatingSupply { get; set; }

/// <summary>
/// Total amount of tokens initially created when starting the blockchain
/// </summary>
public long TotalBootstrapped { get; set; }

/// <summary>
/// Total commitment amount (tokens to be activated by fundraisers)
/// </summary>
public long TotalCommitments { get; set; }

/// <summary>
/// Total amount of tokens activated by fundraisers
/// </summary>
public long TotalActivated { get; set; }

/// <summary>
/// Total amount of created/issued tokens
/// </summary>
public long TotalCreated { get; set; }

/// <summary>
/// Total amount of burned tokens
/// </summary>
public long TotalBurned { get; set; }

/// <summary>
/// Total amount of tokens locked on vested contracts
/// </summary>
public long TotalVested { get; set; }

/// <summary>
/// Total amount of frozen tokens (frozen security deposits, frozen rewards and frozen fees)
/// </summary>
public long TotalFrozen { get; set; }

#region injecting
/// <summary>
/// Injected historical quote at the time of the block at which the statistics has been calculated
/// </summary>
public QuoteShort Quote { get; set; }
#endregion
}
}
71 changes: 71 additions & 0 deletions Tzkt.Api/Parameters/Binders/TimestampBinder.cs
@@ -0,0 +1,71 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Tzkt.Api.Services.Cache;

namespace Tzkt.Api
{
public class TimestampBinder : IModelBinder
{
readonly TimeCache Time;

public TimestampBinder(TimeCache time)
{
Time = time;
}

public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = bindingContext.ModelName;
var hasValue = false;

if (!bindingContext.TryGetDateTime($"{model}", ref hasValue, out var value))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.eq", ref hasValue, out var eq))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.ne", ref hasValue, out var ne))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.gt", ref hasValue, out var gt))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.ge", ref hasValue, out var ge))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.lt", ref hasValue, out var lt))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTime($"{model}.le", ref hasValue, out var le))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTimeList($"{model}.in", ref hasValue, out var @in))
return Task.CompletedTask;

if (!bindingContext.TryGetDateTimeList($"{model}.ni", ref hasValue, out var ni))
return Task.CompletedTask;

if (!hasValue)
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}

bindingContext.Result = ModelBindingResult.Success(new TimestampParameter
{
Eq = (value ?? eq) == null ? null : (int?)Time.FindLevel((DateTime)(value ?? eq), SearchMode.Exact),
Ne = ne == null ? null : (int?)Time.FindLevel((DateTime)ne, SearchMode.Exact),
Gt = gt == null ? null : (int?)Time.FindLevel((DateTime)gt, SearchMode.ExactOrLower),
Ge = ge == null ? null : (int?)Time.FindLevel((DateTime)ge, SearchMode.ExactOrHigher),
Lt = lt == null ? null : (int?)Time.FindLevel((DateTime)lt, SearchMode.ExactOrHigher),
Le = le == null ? null : (int?)Time.FindLevel((DateTime)le, SearchMode.ExactOrLower),
In = @in?.Select(x => Time.FindLevel(x, SearchMode.Exact)).ToList(),
Ni = ni?.Select(x => Time.FindLevel(x, SearchMode.Exact)).ToList(),
});

return Task.CompletedTask;
}
}
}

0 comments on commit 4b2db30

Please sign in to comment.