Skip to content

Commit

Permalink
Extend unstake request API model with couple views, close #173
Browse files Browse the repository at this point in the history
  • Loading branch information
Groxan committed Jun 11, 2024
1 parent 064f573 commit d98ee5e
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 10 deletions.
23 changes: 23 additions & 0 deletions Tzkt.Api/Extensions/ModelBindingContextExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,29 @@ public static bool TryGetMigrationKindList(this ModelBindingContext bindingConte
return true;
}

public static bool TryGetUnstakeRequestStatus(this ModelBindingContext bindingContext, string name, ref bool hasValue, out string result)
{
result = null;
var valueObject = bindingContext.ValueProvider.GetValue(name);

if (valueObject != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(name, valueObject);
if (!string.IsNullOrEmpty(valueObject.FirstValue))
{
if (!UnstakeRequestStatuses.TryParse(valueObject.FirstValue, out var status))
{
bindingContext.ModelState.TryAddModelError(name, "Invalid unstake request status.");
return false;
}
hasValue = true;
result = status;
}
}

return true;
}

public static bool TryGetSrCommitmentStatus(this ModelBindingContext bindingContext, string name, ref bool hasValue, out int? result)
{
result = null;
Expand Down
10 changes: 10 additions & 0 deletions Tzkt.Api/Models/Baking/UnstakeRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public class UnstakeRequest
/// </summary>
public long? RoundingError { get; set; }

/// <summary>
/// Actual amount that was/is/will be available for finalizing.
/// </summary>
public long ActualAmount { get; set; }

/// <summary>
/// Status of the unstake request (`pending`, `finalizable`, `finalized`).
/// </summary>
public string Status { get; set; }

/// <summary>
/// Number of staking updates related to the unstake request.
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions Tzkt.Api/Parameters/Binders/UnstakeRequestStatusBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Tzkt.Api
{
public class UnstakeRequestStatusBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = bindingContext.ModelName;
var hasValue = false;

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

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

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

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

bindingContext.Result = ModelBindingResult.Success(new UnstakeRequestStatusParameter
{
Eq = value ?? eq,
Ne = ne,
});

return Task.CompletedTask;
}
}
}
18 changes: 16 additions & 2 deletions Tzkt.Api/Parameters/UnstakeRequestFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public class UnstakeRequestFilter : INormalizable
/// </summary>
public Int64NullParameter roundingError { get; set; }

/// <summary>
/// Filter by actual amount.
/// Click on the parameter to expand more details.
/// </summary>
public Int64Parameter actualAmount { get; set; }

/// <summary>
/// Filter by status.
/// Click on the parameter to expand more details.
/// </summary>
public UnstakeRequestStatusParameter status { get; set; }

/// <summary>
/// Filter by staking updates count.
/// Click on the parameter to expand more details.
Expand Down Expand Up @@ -102,6 +114,8 @@ public class UnstakeRequestFilter : INormalizable
slashedAmount == null &&
roundingError == null &&
updatesCount == null &&
actualAmount == null &&
status == null &&
firstLevel == null &&
firstTime == null &&
lastLevel == null &&
Expand All @@ -112,8 +126,8 @@ public string Normalize(string name)
return ResponseCacheService.BuildKey("",
("id", id), ("cycle", cycle), ("baker", baker), ("staker", staker), ("requestedAmount", requestedAmount),
("restakedAmount", restakedAmount), ("finalizedAmount", finalizedAmount), ("slashedAmount", slashedAmount),
("roundingError", roundingError), ("updatesCount", updatesCount), ("firstLevel", firstLevel),
("firstTime", firstTime), ("lastLevel", lastLevel), ("lastTime", lastTime));
("roundingError", roundingError), ("actualAmount", actualAmount), ("status", status), ("updatesCount", updatesCount),
("firstLevel", firstLevel), ("firstTime", firstTime), ("lastLevel", lastLevel), ("lastTime", lastTime));
}
}
}
45 changes: 45 additions & 0 deletions Tzkt.Api/Parameters/UnstakeRequestStatusParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Annotations;

namespace Tzkt.Api
{
[ModelBinder(BinderType = typeof(UnstakeRequestStatusBinder))]
[JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")]
[JsonSchemaExtensionData("x-tzkt-query-parameter", "pending,finalizable,finalized")]
public class UnstakeRequestStatusParameter : INormalizable
{
/// <summary>
/// **Equal** filter mode (`.eq` suffix can be omitted, i.e. `?param=...` is the same as `?param.eq=...`). \
/// Specify an unstake request status to get items where the specified field is equal to the specified value.
///
/// Example: `?status=pending`.
/// </summary>
public string Eq { get; set; }

/// <summary>
/// **Not equal** filter mode. \
/// Specify an unstake request status to get items where the specified field is not equal to the specified value.
///
/// Example: `?status.ne=finalized`.
/// </summary>
public string Ne { get; set; }

public string Normalize(string name)
{
var sb = new StringBuilder();

if (Eq != null)
{
sb.Append($"{name}.eq={Eq}&");
}

if (Ne != null)
{
sb.Append($"{name}.ne={Ne}&");
}

return sb.ToString();
}
}
}
26 changes: 26 additions & 0 deletions Tzkt.Api/Repositories/Enums/UnstakeRequestStatuses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Tzkt.Api
{
static class UnstakeRequestStatuses
{
public const string Pending = "pending";
public const string Finalizable = "finalizable";
public const string Finalized = "finalized";

public static bool TryParse(string value, out string res)
{
res = value switch
{
Pending => Pending,
Finalizable => Finalizable,
Finalized => Finalized,
_ => null
};
return res != null;
}

public static string ToString(int cycle, long remainingAmount, int unfrozenCycle)
{
return cycle > unfrozenCycle ? Pending : remainingAmount != 0 ? Finalizable : Finalized;
}
}
}
61 changes: 53 additions & 8 deletions Tzkt.Api/Repositories/StakingRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ public class StakingRepository
{
readonly NpgsqlDataSource DataSource;
readonly AccountsCache Accounts;
readonly ProtocolsCache Protocols;
readonly StateCache State;
readonly TimeCache Times;

public StakingRepository(NpgsqlDataSource dataSource, AccountsCache accounts, TimeCache times)
public StakingRepository(NpgsqlDataSource dataSource, AccountsCache accounts, ProtocolsCache protocols, StateCache state, TimeCache times)
{
DataSource = dataSource;
Accounts = accounts;
Protocols = protocols;
State = state;
Times = times;
}

Expand Down Expand Up @@ -233,7 +237,7 @@ public async Task<object[][]> GetStakingUpdates(StakingUpdateFilter filter, Pagi
#endregion

#region unstake requests
async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(UnstakeRequestFilter filter, Pagination pagination, List<SelectionField> fields = null)
async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(int unfrozenCycle, UnstakeRequestFilter filter, Pagination pagination, List<SelectionField> fields = null)
{
var select = "*";
if (fields != null)
Expand All @@ -257,6 +261,14 @@ async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(UnstakeRequestFilter
case "firstTime": columns.Add(@"""FirstLevel"""); break;
case "lastLevel": columns.Add(@"""LastLevel"""); break;
case "lastTime": columns.Add(@"""LastLevel"""); break;

case "actualAmount":
columns.Add(@"""ActualAmount""");
break;
case "status":
columns.Add(@"""Cycle""");
columns.Add(@"""RemainingAmount""");
break;
}
}

Expand All @@ -266,8 +278,16 @@ async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(UnstakeRequestFilter
select = string.Join(',', columns);
}

var sql = new SqlBuilder($@"
SELECT {select} FROM ""UnstakeRequests""")

var sql = new SqlBuilder($"""
WITH "UnstakeRequestsExt" AS NOT MATERIALIZED (
SELECT *,
"RequestedAmount" - "RestakedAmount" - "SlashedAmount" - COALESCE("RoundingError", 0) AS "ActualAmount",
"RequestedAmount" - "RestakedAmount" - "SlashedAmount" - COALESCE("RoundingError", 0) - "FinalizedAmount" AS "RemainingAmount"
FROM "UnstakeRequests"
)
SELECT {select} FROM "UnstakeRequestsExt"
""")
.FilterA(@"""Id""", filter.id)
.FilterA(@"""Cycle""", filter.cycle)
.FilterA(@"""BakerId""", filter.baker)
Expand All @@ -277,6 +297,8 @@ async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(UnstakeRequestFilter
.FilterA(@"""FinalizedAmount""", filter.finalizedAmount)
.FilterA(@"""SlashedAmount""", filter.slashedAmount)
.FilterA(@"""RoundingError""", filter.roundingError)
.FilterA(@"""ActualAmount""", filter.actualAmount)
.FilterA(@"""Cycle""", @"""RemainingAmount""", filter.status, unfrozenCycle)
.FilterA(@"""UpdatesCount""", filter.updatesCount)
.FilterA(@"""FirstLevel""", filter.firstLevel)
.FilterA(@"""FirstLevel""", filter.firstTime)
Expand All @@ -297,8 +319,17 @@ async Task<IEnumerable<dynamic>> QueryUnstakeRequestsAsync(UnstakeRequestFilter

public async Task<int> GetUnstakeRequestsCount(UnstakeRequestFilter filter)
{
var sql = new SqlBuilder(@"
SELECT COUNT(*) FROM ""UnstakeRequests""")
var unfrozenCycle = State.Current.Cycle - Protocols.Current.ConsensusRightsDelay - 2;

var sql = new SqlBuilder("""
WITH "UnstakeRequestsExt" AS NOT MATERIALIZED (
SELECT *,
"RequestedAmount" - "RestakedAmount" - "SlashedAmount" - COALESCE("RoundingError", 0) AS "ActualAmount",
"RequestedAmount" - "RestakedAmount" - "SlashedAmount" - COALESCE("RoundingError", 0) - "FinalizedAmount" AS "RemainingAmount"
FROM "UnstakeRequests"
)
SELECT COUNT(*) FROM "UnstakeRequestsExt"
""")
.FilterA(@"""Id""", filter.id)
.FilterA(@"""Cycle""", filter.cycle)
.FilterA(@"""BakerId""", filter.baker)
Expand All @@ -308,6 +339,8 @@ SELECT COUNT(*) FROM ""UnstakeRequests""")
.FilterA(@"""FinalizedAmount""", filter.finalizedAmount)
.FilterA(@"""SlashedAmount""", filter.slashedAmount)
.FilterA(@"""RoundingError""", filter.roundingError)
.FilterA(@"""ActualAmount""", filter.actualAmount)
.FilterA(@"""Cycle""", @"""RemainingAmount""", filter.status, unfrozenCycle)
.FilterA(@"""UpdatesCount""", filter.updatesCount)
.FilterA(@"""FirstLevel""", filter.firstLevel)
.FilterA(@"""FirstLevel""", filter.firstTime)
Expand All @@ -320,7 +353,8 @@ SELECT COUNT(*) FROM ""UnstakeRequests""")

public async Task<IEnumerable<UnstakeRequest>> GetUnstakeRequests(UnstakeRequestFilter filter, Pagination pagination)
{
var rows = await QueryUnstakeRequestsAsync(filter, pagination);
var unfrozenCycle = State.Current.Cycle - Protocols.Current.ConsensusRightsDelay - 2;
var rows = await QueryUnstakeRequestsAsync(unfrozenCycle, filter, pagination);
return rows.Select(row => new UnstakeRequest
{
Id = row.Id,
Expand All @@ -332,6 +366,8 @@ public async Task<IEnumerable<UnstakeRequest>> GetUnstakeRequests(UnstakeRequest
FinalizedAmount = row.FinalizedAmount,
SlashedAmount = row.SlashedAmount,
RoundingError = row.RoundingError,
ActualAmount = row.ActualAmount,
Status = UnstakeRequestStatuses.ToString(row.Cycle, row.RemainingAmount, unfrozenCycle),
UpdatesCount = row.UpdatesCount,
FirstLevel = row.FirstLevel,
FirstTime = Times[row.FirstLevel],
Expand All @@ -342,7 +378,8 @@ public async Task<IEnumerable<UnstakeRequest>> GetUnstakeRequests(UnstakeRequest

public async Task<object[][]> GetUnstakeRequests(UnstakeRequestFilter filter, Pagination pagination, List<SelectionField> fields)
{
var rows = await QueryUnstakeRequestsAsync(filter, pagination, fields);
var unfrozenCycle = State.Current.Cycle - Protocols.Current.ConsensusRightsDelay - 2;
var rows = await QueryUnstakeRequestsAsync(unfrozenCycle, filter, pagination, fields);

var result = new object[rows.Count()][];
for (int i = 0; i < result.Length; i++)
Expand Down Expand Up @@ -404,6 +441,14 @@ public async Task<object[][]> GetUnstakeRequests(UnstakeRequestFilter filter, Pa
foreach (var row in rows)
result[j++][i] = row.RoundingError;
break;
case "actualAmount":
foreach (var row in rows)
result[j++][i] = row.ActualAmount;
break;
case "status":
foreach (var row in rows)
result[j++][i] = UnstakeRequestStatuses.ToString(row.Cycle, row.RemainingAmount, unfrozenCycle);
break;
case "updatesCount":
foreach (var row in rows)
result[j++][i] = row.UpdatesCount;
Expand Down
39 changes: 39 additions & 0 deletions Tzkt.Api/Utils/SqlBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,45 @@ public SqlBuilder Filter(string column, VoterStatusParameter status)
return this;
}

public SqlBuilder FilterA(string cycleCol, string remainingAmountCol, UnstakeRequestStatusParameter status, int unfrozenCycle)
{
if (status == null) return this;

if (status.Eq != null)
{
switch (status.Eq)
{
case UnstakeRequestStatuses.Pending:
AppendFilter($"{cycleCol} > {unfrozenCycle}");
break;
case UnstakeRequestStatuses.Finalizable:
AppendFilter($"({cycleCol} <= {unfrozenCycle} AND {remainingAmountCol} != 0)");
break;
case UnstakeRequestStatuses.Finalized:
AppendFilter($"({cycleCol} <= {unfrozenCycle} AND {remainingAmountCol} = 0)");
break;
}
}

if (status.Ne != null)
{
switch (status.Ne)
{
case UnstakeRequestStatuses.Pending:
AppendFilter($"{cycleCol} <= {unfrozenCycle}");
break;
case UnstakeRequestStatuses.Finalizable:
AppendFilter($"({cycleCol} > {unfrozenCycle} OR {remainingAmountCol} = 0)");
break;
case UnstakeRequestStatuses.Finalized:
AppendFilter($"({cycleCol} > {unfrozenCycle} OR {remainingAmountCol} != 0)");
break;
}
}

return this;
}

public SqlBuilder FilterA(string column, RefutationGameStatusParameter status)
{
if (status == null) return this;
Expand Down

0 comments on commit d98ee5e

Please sign in to comment.