Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions Mvkt.Api.Tests/Api/TestRewardsQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,48 @@ public async Task TestBakerStats()
var res = await Client.GetJsonAsync($"/v1/rewards/bakers/{Settings.Baker}/stats");

Assert.True(res is DJsonObject);

Assert.True((double)res.luck >= 0);
Assert.True((double)res.performance >= 0 && (double)res.performance <= 100);
Assert.True((double)res.reliability >= 0 && (double)res.reliability <= 100);
Assert.True((long)res.totalExpectedRewards >= 0);
Assert.True((long)res.totalActualRewards >= 0);

if (res.apy != null)
{
Assert.True(res.apy is DJsonObject);

Assert.True((double)res.apy.ownStakeApy >= 0);
Assert.True((double)res.apy.externalStakeApy >= 0);
Assert.True((double)res.apy.delegationApy >= 0);

Assert.True((double)res.apy.ownStakeApy < 1000);
Assert.True((double)res.apy.externalStakeApy < 1000);
Assert.True((double)res.apy.delegationApy < 1000);
}

Assert.True(res.apy is DJsonObject);
Assert.True(((double)res.apy.ownStakeApy >= 0 && (double)res.apy.externalStakeApy >= 0 && (double)res.apy.delegationApy >= 0));
Assert.True(((double)res.apy.ownStakeApy < 1000 && (double)res.apy.externalStakeApy < 1000 && (double)res.apy.delegationApy < 1000));

Assert.True((int)res.cyclesUsed > 0);
Assert.True(res.kpis is DJsonObject);
}

[Fact]
public async Task TestBakerStatsWithCycle()
{
var res = await Client.GetJsonAsync($"/v1/rewards/bakers/{Settings.Baker}/stats?cycle={Settings.Cycle}");

Assert.True(res is DJsonObject);
var cyclesUsed = res.cyclesUsed != null ? (int)res.cyclesUsed : 0;
Assert.True(cyclesUsed == 1);
Assert.True((int)res.cycle == Settings.Cycle);
Assert.True(res.kpis is DJsonObject);
}

[Fact]
public async Task TestBakerStatsWithCyclesLimit()
{
var res = await Client.GetJsonAsync($"/v1/rewards/bakers/{Settings.Baker}/stats?cyclesLimit=5");

Assert.True(res is DJsonObject);
Assert.True((int)res.cyclesUsed == 5);
Assert.Null(res.cycle);
}

[Fact]
public async Task TestBakerStatsInvalidBakerAddress()
{
var response = await Client.GetAsync("/v1/rewards/bakers/mv111111111111111111111111111111111111/stats");
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
}

[Fact]
Expand Down
4 changes: 2 additions & 2 deletions Mvkt.Api/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Mvkt.Api.Models;
Expand Down Expand Up @@ -235,7 +235,7 @@ public async Task<ActionResult<IEnumerable<RelatedContract>>> GetContracts(
/// <returns></returns>
[HttpGet("{address}/delegators")]
public async Task<ActionResult<IEnumerable<Delegator>>> GetDelegators(
[Required][TzAddress] string address,
[Required][MvAddress] string address,
AccountTypeParameter type,
Int64Parameter balance,
Int32Parameter delegationLevel,
Expand Down
4 changes: 2 additions & 2 deletions Mvkt.Api/Controllers/DelegatesController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Mvkt.Api.Repositories;

Expand Down Expand Up @@ -90,7 +90,7 @@ public Task<int> GetCount(BoolParameter active)
/// <param name="address">Delegate address (starting with mv)</param>
/// <returns></returns>
[HttpGet("{address}")]
public Task<Models.Delegate> GetByAddress([Required][TzAddress] string address)
public Task<Models.Delegate> GetByAddress([Required][MvAddress] string address)
{
return Accounts.GetDelegate(address);
}
Expand Down
33 changes: 23 additions & 10 deletions Mvkt.Api/Controllers/RewardsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using NSwag.Annotations;
using Mvkt.Api.Models;
using Mvkt.Api.Repositories;
using Mvkt.Api.Services;

namespace Mvkt.Api.Controllers
{
Expand All @@ -12,11 +13,13 @@ public class RewardsController : ControllerBase
{
private readonly RewardsRepository Rewards;
private readonly StakingRepository Staking;
private readonly ResponseCacheService ResponseCache;

public RewardsController(RewardsRepository rewards, StakingRepository staking)
public RewardsController(RewardsRepository rewards, StakingRepository staking, ResponseCacheService responseCache)
{
Rewards = rewards;
Staking = staking;
ResponseCache = responseCache;
}

/// <summary>
Expand All @@ -28,7 +31,7 @@ public RewardsController(RewardsRepository rewards, StakingRepository staking)
/// <param name="address">Baker address</param>
/// <returns></returns>
[HttpGet("bakers/{address}/count")]
public Task<int> GetBakerRewardsCount([Required][TzAddress] string address)
public Task<int> GetBakerRewardsCount([Required][MvAddress] string address)
{
return Rewards.GetBakerRewardsCount(address);
}
Expand All @@ -49,7 +52,7 @@ public Task<int> GetBakerRewardsCount([Required][TzAddress] string address)
/// <returns></returns>
[HttpGet("bakers/{address}")]
public async Task<ActionResult<IEnumerable<BakerRewards>>> GetBakerRewards(
[Required][TzAddress] string address,
[Required][MvAddress] string address,
Int32Parameter cycle,
SelectParameter select,
SortParameter sort,
Expand Down Expand Up @@ -90,7 +93,7 @@ public async Task<ActionResult<IEnumerable<BakerRewards>>> GetBakerRewards(
// deprecated
[OpenApiIgnore]
[HttpGet("bakers/{address}/{cycle:int}")]
public async Task<BakerRewards> GetBakerRewardsByCycle([Required][TzAddress] string address, [Min(0)] int cycle, Symbols quote = Symbols.None)
public async Task<BakerRewards> GetBakerRewardsByCycle([Required][MvAddress] string address, [Min(0)] int cycle, Symbols quote = Symbols.None)
{
return (await Rewards.GetBakerRewards(address, cycle, null, null, 100, quote)).FirstOrDefault();
}
Expand Down Expand Up @@ -183,7 +186,7 @@ public async Task<DelegatorRewards> GetDelegatorRewardsByCycle([Required][Addres
/// <param name="limit">Maximum number of delegators to return</param>
/// <returns></returns>
[HttpGet("split/{baker}/{cycle:int}")]
public Task<RewardSplit> GetRewardSplit([Required][TzAddress] string baker, [Min(0)] int cycle, int offset = 0, [Range(0, 10000)] int limit = 100)
public Task<RewardSplit> GetRewardSplit([Required][MvAddress] string baker, [Min(0)] int cycle, int offset = 0, [Range(0, 10000)] int limit = 100)
{
return Rewards.GetRewardSplit(baker, cycle, offset, limit);
}
Expand All @@ -199,7 +202,7 @@ public Task<RewardSplit> GetRewardSplit([Required][TzAddress] string baker, [Min
/// <param name="delegator">Delegator address</param>
/// <returns></returns>
[HttpGet("split/{baker}/{cycle:int}/{delegator}")]
public Task<SplitDelegator> GetRewardSplitDelegator([Required][TzAddress] string baker, [Min(0)] int cycle, [Required][Address] string delegator)
public Task<SplitDelegator> GetRewardSplitDelegator([Required][MvAddress] string baker, [Min(0)] int cycle, [Required][Address] string delegator)
{
return Rewards.GetRewardSplitDelegator(baker, cycle, delegator);
}
Expand All @@ -209,21 +212,31 @@ public Task<SplitDelegator> GetRewardSplitDelegator([Required][TzAddress] string
/// </summary>
/// <remarks>
/// Returns aggregated statistics for a baker based on historical rewards data.
/// Includes performance metrics, reliability, luck, total income, fees, and more.
/// If cycle is set, returns stats for that cycle only. Otherwise aggregates over the last cyclesLimit cycles.
/// </remarks>
/// <param name="address">Baker address (starting with mv)</param>
/// <param name="cycle">If set, return stats for this cycle only. Otherwise use cyclesLimit.</param>
/// <param name="cyclesLimit">When cycle is not set: max number of recent cycles to aggregate. Default 10000.</param>
/// <returns></returns>
[HttpGet("bakers/{address}/stats")]
public async Task<ActionResult<BakerStats>> GetBakerStats(
[Required][TzAddress] string address)
[Required][MvAddress] string address,
[FromQuery] int? cycle = null,
[FromQuery] int cyclesLimit = 10000)
{
var stats = await Rewards.GetBakerStats(address);
var query = ResponseCacheService.BuildKey(Request.Path.Value, ("cycle", cycle), ("cyclesLimit", cyclesLimit));

if (ResponseCache.TryGet(query, out var cached))
return this.Bytes(cached);

var stats = await Rewards.GetBakerStats(address, cycle, cyclesLimit);
if (stats == null)
return NotFound();

stats.Apy = await Staking.GetBakerApy(address);

return Ok(stats);
cached = ResponseCache.Set(query, stats);
return this.Bytes(cached);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Mvkt.Api/Controllers/RightsController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Mvkt.Api.Models;
Expand Down Expand Up @@ -112,7 +112,7 @@ public async Task<ActionResult<IEnumerable<BakingRight>>> Get(
[OpenApiIgnore]
[HttpGet("schedule")]
public async Task<ActionResult<IEnumerable<BakingRight>>> GetSchedule(
[Required][TzAddress] string baker,
[Required][MvAddress] string baker,
[Required] DateTimeOffset from,
[Required] DateTimeOffset to,
[Min(0)] int maxRound = 0)
Expand Down
6 changes: 3 additions & 3 deletions Mvkt.Api/Controllers/VotingController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

using Mvkt.Api.Models;
Expand Down Expand Up @@ -219,7 +219,7 @@ public async Task<ActionResult<IEnumerable<VoterSnapshot>>> GetPeriodVoters(
/// <param name="address">Voter address</param>
/// <returns></returns>
[HttpGet("periods/{index:int}/voters/{address}")]
public Task<VoterSnapshot> GetPeriodVoter([Min(0)] int index, [Required][TzAddress] string address)
public Task<VoterSnapshot> GetPeriodVoter([Min(0)] int index, [Required][MvAddress] string address)
{
return Voting.GetVoter(index, address);
}
Expand Down Expand Up @@ -259,7 +259,7 @@ public async Task<ActionResult<IEnumerable<VoterSnapshot>>> GetPeriodVoters(
/// <param name="address">Voter address</param>
/// <returns></returns>
[HttpGet("periods/current/voters/{address}")]
public Task<VoterSnapshot> GetPeriodVoter([Required][TzAddress] string address)
public Task<VoterSnapshot> GetPeriodVoter([Required][MvAddress] string address)
{
return Voting.GetVoter(State.Current.VotingPeriod, address);
}
Expand Down
64 changes: 64 additions & 0 deletions Mvkt.Api/Models/Baking/BakerStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,70 @@ public class BakerStats
/// Total actual rewards received (micro tez)
/// </summary>
public long TotalActualRewards { get; set; }

/// <summary>
/// Cycle if request was for a single cycle; null for lifetime.
/// </summary>
public int? Cycle { get; set; }

/// <summary>
/// Number of cycles included in aggregation (1 for single-cycle, else up to cycles param).
/// </summary>
public int CyclesUsed { get; set; }

/// <summary>
/// Reward-derived KPIs and efficiency + rewards summary. Null if baker has no cycles.
/// </summary>
public BakerStatsKpis Kpis { get; set; }
}

/// <summary>
/// Aggregated KPIs and rewards summary for validator Cycle/Lifetime tabs.
/// </summary>
public class BakerStatsKpis
{
#region Reward-derived KPIs
public long TotalIncome { get; set; }
public long ExtraRewards { get; set; }
public long LostRewards { get; set; }
public long SlashedRewards { get; set; }
public long TotalSlashed { get; set; }
public long BlocksBaked { get; set; }
public long BlocksProposed { get; set; }
public long TotalBlockRewards { get; set; }
public long TotalEndorsementRewards { get; set; }
public long EndorsementsMade { get; set; }
public long EndorsementsMissed { get; set; }
/// <summary>Expected to delegators + co-stakers (client: pending = ExpectedDistribution - payouts).</summary>
public long ExpectedDistribution { get; set; }
#endregion

#region Efficiency
public double TechnicalReliability { get; set; }
public double MonetaryPerformance { get; set; }
public double FairEfficiency { get; set; }
public double LuckRatio { get; set; }
public long TotalExpectedRewards { get; set; }
public long TotalActualRewards { get; set; }
public int MissedRights { get; set; }
#endregion

#region Rewards summary
public long TotalRewards { get; set; }
public long TotalBlockFees { get; set; }
public long TotalRevelationRewards { get; set; }
public long MissedBlocks { get; set; }
public long MissedEndorsements { get; set; }
public double PerformanceRate { get; set; }
public double AvgRewardsPerCycle { get; set; }
public long TotalRewardsDelegated { get; set; }
public long TotalRewardsStakedShared { get; set; }
public long TotalRewardsOwnStake { get; set; }
public long TotalEdgeFees { get; set; }
public double DelegatorSharePercent { get; set; }
public double CoStakerSharePercent { get; set; }
public double ValidatorSharePercent { get; set; }
#endregion
}
}

Loading
Loading