Skip to content
Merged
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
42 changes: 42 additions & 0 deletions Mvkt.Api.Tests/Api/TestHomeQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,55 @@ public async Task TestHomeStats()
{
var res = await Client.GetJsonAsync("/v1/home");
Assert.True(res is DJsonObject);
if (res.networkRewardsData == null) return;

var nr = res.networkRewardsData;
Assert.Null(nr.cycleRewardSummaries);

long totalRewardsAllTime = (long)nr.totalRewardsAllTime;
long totalBlockRewards = (long)nr.totalBlockRewards;
long totalEndorsementRewards = (long)nr.totalEndorsementRewards;
long totalBlockFees = (long)nr.totalBlockFees;
Assert.Equal(totalRewardsAllTime, totalBlockRewards + totalEndorsementRewards + totalBlockFees);

Assert.True((double)nr.averageRewardsPerCycle >= 0);
}

[Fact]
public async Task TestHomeNetworkRewardsDataWithCycleRewardSummaries()
{
var res = await Client.GetJsonAsync("/v1/home?cycleRewardSummaries=true");
if (res is not DJsonObject || res.networkRewardsData == null)
return;

var nr = res.networkRewardsData;
var cycleRewardSummaries = nr.cycleRewardSummaries as DJsonArray;
Assert.NotNull(cycleRewardSummaries);
if (cycleRewardSummaries.Count > 0)
Assert.Equal((double)nr.totalRewardsAllTime / cycleRewardSummaries.Count, (double)nr.averageRewardsPerCycle, 0);

if (cycleRewardSummaries.Count > 0)
{
dynamic first = (cycleRewardSummaries as dynamic)[0];
Assert.NotNull(first);
Assert.True(first.cycle is not null);
Assert.True(first.totalBlockRewards is not null);
Assert.True(first.totalEndorsementRewards is not null);
Assert.True(first.totalBlockFees is not null);
Assert.True(first.totalRewards is not null);
Assert.True(first.activeBakers is not null);
}
}

[Fact]
public async Task TestHomeStatsWithQuote()
{
var res = await Client.GetJsonAsync("/v1/home?quote=usd");
Assert.True(res is DJsonObject);
Assert.NotNull(res.priceChart);
var priceChart = res.priceChart as DJsonArray;
Assert.NotNull(priceChart);
Assert.True(priceChart.Count > 0);
}

[Fact]
Expand Down
6 changes: 3 additions & 3 deletions Mvkt.Api/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Mvkt.Api.Models;
using Mvkt.Api.Services;
Expand All @@ -11,9 +11,9 @@ namespace Mvkt.Api.Controllers
public class HomeController : ControllerBase
{
[HttpGet]
public HomeStats Get(Symbols quote = Symbols.Usd)
public HomeStats Get([FromQuery] Symbols quote = Symbols.Usd, [FromQuery] bool cycleRewardSummaries = false)
{
return HomeService.GetCurrentStats(quote);
return HomeService.GetCurrentStats(quote, cycleRewardSummaries);
}

[HttpGet("blocks")]
Expand Down
1 change: 1 addition & 0 deletions Mvkt.Api/Models/Home/HomeStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public class HomeStats
public TxsData TxsData { get; set; }
public MarketData MarketData { get; set; }
public List<ChartPoint<double>> PriceChart { get; set; }
public NetworkRewardsData NetworkRewardsData { get; set; }
}
}
22 changes: 22 additions & 0 deletions Mvkt.Api/Models/Home/NetworkRewardsData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Mvkt.Api.Models
{
public class NetworkRewardsData
{
public long TotalRewardsAllTime { get; set; }
public long TotalBlockRewards { get; set; }
public long TotalEndorsementRewards { get; set; }
public long TotalBlockFees { get; set; }
public double AverageRewardsPerCycle { get; set; }
public List<CycleRewardSummary> CycleRewardSummaries { get; set; } = new();
}

public class CycleRewardSummary
{
public int Cycle { get; set; }
public long TotalBlockRewards { get; set; }
public long TotalEndorsementRewards { get; set; }
public long TotalBlockFees { get; set; }
public long TotalRewards { get; set; }
public int ActiveBakers { get; set; }
}
}
76 changes: 72 additions & 4 deletions Mvkt.Api/Services/Home/HomeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class HomeService
static TxsData TxsData;
static MarketData MarketData;
static List<Quote> MarketChart;
static NetworkRewardsData NetworkRewardsData;
#endregion

readonly NpgsqlDataSource DataSource;
Expand Down Expand Up @@ -87,7 +88,7 @@ public HomeService(NpgsqlDataSource dataSource, BakingRightsRepository rights, T
_ = UpdateAsync();
}

public static HomeStats GetCurrentStats(Symbols quote)
public static HomeStats GetCurrentStats(Symbols quote, bool cycleRewardSummaries = false)
{
if (LastUpdate <= 0)
return null;
Expand Down Expand Up @@ -141,6 +142,20 @@ public static HomeStats GetCurrentStats(Symbols quote)
}).ToList()
};

var networkRewardsData = NetworkRewardsData;
if (networkRewardsData != null && !cycleRewardSummaries)
{
networkRewardsData = new NetworkRewardsData
{
TotalRewardsAllTime = networkRewardsData.TotalRewardsAllTime,
TotalBlockRewards = networkRewardsData.TotalBlockRewards,
TotalEndorsementRewards = networkRewardsData.TotalEndorsementRewards,
TotalBlockFees = networkRewardsData.TotalBlockFees,
AverageRewardsPerCycle = networkRewardsData.AverageRewardsPerCycle,
CycleRewardSummaries = null
};
}

return new HomeStats
{
DailyData = DailyData,
Expand All @@ -150,7 +165,8 @@ public static HomeStats GetCurrentStats(Symbols quote)
AccountsData = AccountsData,
TxsData = TxsData,
MarketData = MarketData,
PriceChart = priceChart
PriceChart = priceChart,
NetworkRewardsData = networkRewardsData
};
}

Expand Down Expand Up @@ -191,7 +207,8 @@ public async Task UpdateAsync()
TotalBurned = statistics.TotalBurned + burnBalance // Complete burned amount
};
AccountsData = await GetAccountsData(db); // 320

NetworkRewardsData = await GetNetworkRewardsData(db);

LastUpdate = State.Current.Level;
}
}
Expand Down Expand Up @@ -656,7 +673,58 @@ async Task<AccountsData> GetAccountsData(IDbConnection db)
})
};
}


async Task<NetworkRewardsData> GetNetworkRewardsData(IDbConnection db)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a heavy computation that wouldn't scale with the growing amount of bakers. Did you stress test /home before and after the addition? It may cause a spike in cpu/mem so I want to be sure that it's harmless

Copy link
Copy Markdown
Collaborator Author

@0xVoronov 0xVoronov Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several points:

  1. The BakerCycles table does not contain many records, about 1,000 now, and expected to grow to 100,000+ (which is still not a large volume).
  2. This query is not executed on every /home request; it runs only during UpdateAsync() updates, once per UpdatePeriod levels. Therefore, I don’t see any issues with this query, as it places only a moderate load on the database.

I ran a quick test of the query itself using EXPLAIN, and also tested the method in a loop. We’ll need to set up an environment to perform proper load testing in a cluster on a real machine, but that can be done later.

BakerCycles GROUP BY Cycle (cycle reward summaries)
Runs: 5000
Min: 0.8 ms
Max: 6.1 ms
Avg: 1.0 ms
Median: 0.9 ms
p95: 1.5 ms
p99: 2.3 ms

{
var currentCycle = State.Current.Cycle;

const string sql = @"
SELECT
""Cycle"",
SUM(""BlockRewardsDelegated"" + ""BlockRewardsStakedOwn"" + ""BlockRewardsStakedEdge"" + ""BlockRewardsStakedShared"") AS total_block_rewards,
SUM(""EndorsementRewardsDelegated"" + ""EndorsementRewardsStakedOwn"" + ""EndorsementRewardsStakedEdge"" + ""EndorsementRewardsStakedShared"") AS total_endorsement_rewards,
SUM(""BlockFees"") AS total_block_fees,
COUNT(DISTINCT ""BakerId"") AS active_bakers
FROM ""BakerCycles""
WHERE ""Cycle"" <= @currentCycle
GROUP BY ""Cycle""
ORDER BY ""Cycle"" DESC";

var rows = (await db.QueryAsync<(int Cycle, long total_block_rewards, long total_endorsement_rewards, long total_block_fees, int active_bakers)>(sql, new { currentCycle })).ToList();

long totalBlockRewards = 0, totalEndorsementRewards = 0, totalBlockFees = 0;
var cycleRewardSummaries = new List<CycleRewardSummary>();

foreach (var r in rows)
{
totalBlockRewards += r.total_block_rewards;
totalEndorsementRewards += r.total_endorsement_rewards;
totalBlockFees += r.total_block_fees;
cycleRewardSummaries.Add(new CycleRewardSummary
{
Cycle = r.Cycle,
TotalBlockRewards = r.total_block_rewards,
TotalEndorsementRewards = r.total_endorsement_rewards,
TotalBlockFees = r.total_block_fees,
TotalRewards = r.total_block_rewards + r.total_endorsement_rewards + r.total_block_fees,
ActiveBakers = r.active_bakers
});
}

var totalRewardsAllTime = totalBlockRewards + totalEndorsementRewards + totalBlockFees;
var count = rows.Count;

return new NetworkRewardsData
{
TotalRewardsAllTime = totalRewardsAllTime,
TotalBlockRewards = totalBlockRewards,
TotalEndorsementRewards = totalEndorsementRewards,
TotalBlockFees = totalBlockFees,
AverageRewardsPerCycle = count > 0 ? (double)totalRewardsAllTime / count : 0,
CycleRewardSummaries = cycleRewardSummaries
};
}

async Task<GovernanceData> GetGovernanceData()
{
var epoch = await VotingRepo.GetEpoch(State.Current.VotingEpoch);
Expand Down
Loading