Skip to content

Commit 0a3e329

Browse files
committed
refactor: reward charts data access
See: BEDS-875
1 parent 3821521 commit 0a3e329

File tree

3 files changed

+106
-136
lines changed

3 files changed

+106
-136
lines changed

backend/pkg/api/data_access/vdb_rewards.go

Lines changed: 100 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -683,25 +683,15 @@ func (d *DataAccessService) GetValidatorDashboardGroupRewards(ctx context.Contex
683683
return ret, nil
684684
}
685685

686-
func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Context, dashboardId t.VDBId, groupIds []int64, protocolModes t.VDBProtocolModes, aggregation enums.ChartAggregation, afterTs uint64, beforeTs uint64) (*t.ChartData[int, decimal.Decimal], error) {
687-
// @DATA-ACCESS incorporate protocolModes
688-
// bar chart for the CL and EL rewards for each group for each epoch.
689-
// NO series for all groups combined except if AggregateGroups is true.
690-
// series id is group id, series property is 'cl' or 'el'
691-
ret := &t.ChartData[int, decimal.Decimal]{}
692-
693-
if len(groupIds) == 0 {
694-
return ret, nil
695-
}
696-
697-
var err error
698-
686+
func buildRewardChartClDs(dashboardId t.VDBId, groupIds []int64, afterTs uint64, beforeTs uint64, aggregation enums.ChartAggregation, isAllGroupsRequested bool) (*goqu.SelectDataset, error) {
699687
var dataTable exp.LiteralExpression
700688
timeColumn := goqu.C("t")
689+
epochStartCol, epochEndCol := "epoch_start", "epoch_end"
701690
switch aggregation {
702691
case enums.IntervalEpoch:
703692
dataTable = goqu.L("validator_dashboard_data_epoch AS e")
704693
timeColumn = goqu.C("epoch_timestamp")
694+
epochStartCol, epochEndCol = "e.epoch", "e.epoch"
705695
case enums.IntervalHourly:
706696
dataTable = goqu.L("validator_dashboard_data_hourly AS e FINAL")
707697
case enums.IntervalDaily:
@@ -712,22 +702,12 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
712702
return nil, fmt.Errorf("unexpected aggregation type: %v", aggregation)
713703
}
714704

715-
requestedAllGroups := dashboardId.AggregateGroups
716-
for _, groupId := range groupIds {
717-
if groupId == t.AllGroups {
718-
// note: requesting all groups is only convenience on api level, this will NOT result in a "total" series as it wouldn't make sense for this endpoint
719-
requestedAllGroups = true
720-
break
721-
}
722-
}
723-
724-
// ------------------------------------------------------------------------------------------------------------------
725-
// Build the query that serves as base for both the main and EL rewards queries
726-
// CL
727-
rewardsDs := goqu.Dialect("postgres").
705+
clDs := goqu.Dialect("postgres").
728706
Select(
729707
goqu.L(`SUM(e.attestations_reward + e.blocks_cl_reward + e.sync_reward) AS cl_rewards`),
730708
timeColumn.As("timestamp"),
709+
goqu.L(fmt.Sprintf("min(%s)", epochStartCol)).As("epoch_start"),
710+
goqu.L(fmt.Sprintf("max(%s)", epochEndCol)).As("epoch_end"),
731711
).
732712
From(dataTable).
733713
With("validators", goqu.Dialect("postgres").
@@ -740,94 +720,108 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
740720
goqu.I("dashboard_id").Eq(dashboardId.Id),
741721
goqu.Or(
742722
goqu.I("group_id").In(groupIds),
743-
goqu.V(requestedAllGroups),
723+
goqu.V(isAllGroupsRequested),
744724
),
745725
),
746726
).
747727
Where(
748728
timeColumn.Between(goqu.Range(
749729
goqu.L("fromUnixTimestamp(?)", afterTs),
750730
goqu.L("fromUnixTimestamp(?)", beforeTs))),
751-
)
731+
).
732+
GroupBy(timeColumn).
733+
Order(timeColumn.Asc())
752734

753-
if aggregation == enums.IntervalEpoch {
754-
rewardsDs = rewardsDs.
755-
SelectAppend(goqu.L("min(e.epoch)").As("epoch_start")).
756-
SelectAppend(goqu.L("max(e.epoch)").As("epoch_end"))
735+
if dashboardId.Validators == nil {
736+
clDs = clDs.
737+
InnerJoin(goqu.L("validators v"), goqu.On(goqu.L("e.validator_index = v.validator_index"))).
738+
Where(goqu.L("e.validator_index IN (SELECT validator_index FROM validators)"))
739+
740+
if dashboardId.AggregateGroups {
741+
clDs = clDs.
742+
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId))
743+
} else {
744+
clDs = clDs.
745+
SelectAppend(goqu.L("v.group_id AS result_group_id")).
746+
GroupByAppend(goqu.L("result_group_id")).
747+
OrderAppend(goqu.L("result_group_id").Asc())
748+
}
757749
} else {
758-
rewardsDs = rewardsDs.
759-
SelectAppend(goqu.L("min(epoch_start)").As("epoch_start")).
760-
SelectAppend(goqu.L("max(epoch_end)").As("epoch_end"))
750+
// In case a list of validators is provided set the group to the default id
751+
clDs = clDs.
752+
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
753+
Where(goqu.L("e.validator_index IN ?", dashboardId.Validators))
761754
}
755+
return clDs, nil
756+
}
762757

763-
// EL
758+
func buildRewardChartElDs(dashboardId t.VDBId, epochStarts, epochEnds []uint64) *goqu.SelectDataset {
764759
elDs := goqu.Dialect("postgres").
765760
Select(
766761
goqu.L("epoch_start"),
767762
// goqu.L("epoch_end"), not needed
768763
goqu.COALESCE(goqu.SUM(goqu.I("value")), 0).As("el_rewards")).
769764
From(goqu.L("users_val_dashboards_validators v")).
770-
LeftJoin(goqu.I("execution_rewards_finalized").As("b"), goqu.On(goqu.L("v.validator_index = b.proposer")))
771-
772-
// grouping, ordering
773-
rewardsDs = rewardsDs.
774-
GroupBy(timeColumn).
775-
Order(timeColumn.Asc())
776-
777-
elDs = elDs.
765+
LeftJoin(goqu.I("execution_rewards_finalized").As("b"), goqu.On(goqu.L("v.validator_index = b.proposer"))).
778766
GroupBy(goqu.L("epoch_start, epoch_end")).
779767
Order(goqu.L("epoch_start").Asc())
780768

781769
if dashboardId.Validators == nil {
782-
rewardsDs = rewardsDs.
783-
InnerJoin(goqu.L("validators v"), goqu.On(goqu.L("e.validator_index = v.validator_index"))).
784-
Where(goqu.L("e.validator_index IN (SELECT validator_index FROM validators)"))
785770
elDs = elDs.
786771
Where(goqu.L("v.dashboard_id = ?", dashboardId.Id))
787772

788773
if dashboardId.AggregateGroups {
789-
rewardsDs = rewardsDs.
790-
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId))
791774
elDs = elDs.
792775
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId))
793776
} else {
794-
rewardsDs = rewardsDs.
795-
SelectAppend(goqu.L("v.group_id AS result_group_id")).
796-
GroupByAppend(goqu.L("result_group_id")).
797-
OrderAppend(goqu.L("result_group_id").Asc())
798777
elDs = elDs.
799778
SelectAppend(goqu.L("v.group_id AS result_group_id")).
800779
GroupByAppend(goqu.L("result_group_id")).
801780
OrderAppend(goqu.L("result_group_id").Asc())
802781
}
803782
} else {
804783
// In case a list of validators is provided set the group to the default id
805-
rewardsDs = rewardsDs.
806-
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
807-
Where(goqu.L("e.validator_index IN ?", dashboardId.Validators))
808784
elDs = elDs.
809785
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
810786
Where(goqu.L("b.proposer = ANY(?)", pq.Array(dashboardId.Validators)))
811787
}
788+
elDs = elDs.
789+
With("epoch_ranges(epoch_start, epoch_end)", goqu.L("(SELECT * FROM unnest(?::int[], ?::int[]))", pq.Array(epochStarts), pq.Array(epochEnds))).
790+
InnerJoin(goqu.L("epoch_ranges"), goqu.On(goqu.L("b.epoch BETWEEN epoch_ranges.epoch_start AND epoch_ranges.epoch_end"))).
791+
Where(goqu.L("b.epoch BETWEEN ? AND ?", epochStarts[0], epochEnds[len(epochEnds)-1]))
792+
return elDs
793+
}
812794

795+
func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Context, dashboardId t.VDBId, groupIds []int64, protocolModes t.VDBProtocolModes, aggregation enums.ChartAggregation, afterTs uint64, beforeTs uint64) (*t.ChartData[int, decimal.Decimal], error) {
796+
// @DATA-ACCESS incorporate protocolModes
797+
// bar chart for the CL and EL rewards for each group for each epoch.
798+
// NO series for all groups combined except if AggregateGroups is true.
799+
// series id is group id, series property is 'cl' or 'el'
800+
ret := &t.ChartData[int, decimal.Decimal]{}
801+
802+
if len(groupIds) == 0 {
803+
return ret, nil
804+
}
805+
806+
requestedAllGroups := dashboardId.AggregateGroups || slices.Contains(groupIds, t.AllGroups)
813807
// ------------------------------------------------------------------------------------------------------------------
814-
// Build the main query and get the data
815-
queryResult := []struct {
808+
// CL
809+
810+
clDs, err := buildRewardChartClDs(dashboardId, groupIds, afterTs, beforeTs, aggregation, requestedAllGroups)
811+
if err != nil {
812+
return nil, fmt.Errorf("error building CL rewards query: %w", err)
813+
}
814+
type clResult struct {
816815
Timestamp time.Time `db:"timestamp"`
817816
EpochStart uint64 `db:"epoch_start"`
818817
EpochEnd uint64 `db:"epoch_end"`
819818
GroupId uint64 `db:"result_group_id"`
820819
ClRewards int64 `db:"cl_rewards"`
821-
}{}
822-
823-
query, args, err := rewardsDs.Prepared(true).ToSQL()
824-
if err != nil {
825-
return nil, fmt.Errorf("error preparing query: %w", err)
826820
}
827821

828-
err = d.clickhouseReader.SelectContext(ctx, &queryResult, query, args...)
822+
queryResult, err := runQueryRows[[]clResult](ctx, d.clickhouseReader, clDs)
829823
if err != nil {
830-
return nil, fmt.Errorf("error retrieving rewards chart data: %w", err)
824+
return nil, fmt.Errorf("error retrieving rewards chart cl data: %w", err)
831825
}
832826

833827
if len(queryResult) == 0 {
@@ -842,12 +836,10 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
842836
epochBoundariesMap := make(map[time.Time]epochBoundaries)
843837
for _, res := range queryResult {
844838
curBoundary := epochBoundariesMap[res.Timestamp]
845-
if epochBoundariesMap[res.Timestamp].Start == 0 || epochBoundariesMap[res.Timestamp].Start > res.EpochStart {
839+
if curBoundary.Start == 0 || curBoundary.Start > res.EpochStart {
846840
curBoundary.Start = res.EpochStart
847841
}
848-
if epochBoundariesMap[res.Timestamp].End < res.EpochEnd {
849-
curBoundary.End = res.EpochEnd
850-
}
842+
curBoundary.End = max(curBoundary.End, res.EpochEnd)
851843
epochBoundariesMap[res.Timestamp] = curBoundary
852844
}
853845
var epochStarts, epochEnds []uint64
@@ -857,89 +849,67 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
857849
}
858850
slices.Sort(epochStarts)
859851
slices.Sort(epochEnds)
860-
elDs = elDs.
861-
With("epoch_ranges(epoch_start, epoch_end)", goqu.L("(SELECT * FROM unnest(?::int[], ?::int[]))", pq.Array(epochStarts), pq.Array(epochEnds))).
862-
InnerJoin(goqu.L("epoch_ranges"), goqu.On(goqu.L("b.epoch BETWEEN epoch_ranges.epoch_start AND epoch_ranges.epoch_end"))).
863-
Where(goqu.L("b.epoch BETWEEN ? AND ?", epochStarts[0], epochEnds[len(epochEnds)-1]))
864852

865853
// ------------------------------------------------------------------------------------------------------------------
866-
// Get the EL rewards
867-
elRewards := make(map[uint64]map[uint64]decimal.Decimal)
854+
// EL
868855

869-
elQueryResult := []struct {
856+
type elResult struct {
870857
EpochStart uint64 `db:"epoch_start"`
871858
GroupId uint64 `db:"result_group_id"`
872859
ElRewards decimal.Decimal `db:"el_rewards"`
873-
}{}
874-
875-
query, args, err = elDs.Prepared(true).ToSQL()
876-
if err != nil {
877-
return nil, fmt.Errorf("error preparing query: %w", err)
878860
}
879-
880-
err = d.readerDb.SelectContext(ctx, &elQueryResult, query, args...)
861+
elQueryResult, err := runQueryRows[[]elResult](ctx, d.readerDb, buildRewardChartElDs(dashboardId, epochStarts, epochEnds))
881862
if err != nil {
882-
return nil, fmt.Errorf("error retrieving el rewards data for rewards chart: %w", err)
863+
return nil, fmt.Errorf("error retrieving rewards chart el data: %w", err)
883864
}
884865

885-
for _, entry := range elQueryResult {
886-
if _, ok := elRewards[entry.EpochStart]; !ok {
887-
elRewards[entry.EpochStart] = make(map[uint64]decimal.Decimal)
888-
}
889-
elRewards[entry.EpochStart][entry.GroupId] = entry.ElRewards
866+
type elRewardsKey struct {
867+
epochStart uint64
868+
groupId uint64
890869
}
891-
892-
if err != nil {
893-
return nil, fmt.Errorf("error retrieving validator dashboard rewards chart data: %w", err)
870+
elRewards := make(map[elRewardsKey]decimal.Decimal)
871+
for _, entry := range elQueryResult {
872+
elRewards[elRewardsKey{entry.EpochStart, entry.GroupId}] = entry.ElRewards
894873
}
895874

896875
// ------------------------------------------------------------------------------------------------------------------
897876
// Create a map structure to store the data
898-
epochStartData := make(map[uint64]map[int]t.ClElValue[decimal.Decimal])
899-
epochStartList := make([]uint64, 0)
900-
groupMap := make(map[int]bool)
901877

902-
for _, res := range queryResult {
903-
if _, ok := epochStartData[res.EpochStart]; !ok {
904-
epochStartData[res.EpochStart] = make(map[int]t.ClElValue[decimal.Decimal])
905-
epochStartList = append(epochStartList, res.EpochStart)
906-
}
878+
epochStartData := make(map[elRewardsKey]t.ClElValue[decimal.Decimal])
879+
epochStartMap := make(map[uint64]struct{})
880+
groupMap := make(map[uint64]struct{})
907881

908-
epochStartData[res.EpochStart][int(res.GroupId)] = t.ClElValue[decimal.Decimal]{
909-
El: elRewards[res.EpochStart][res.GroupId],
882+
for _, res := range queryResult {
883+
groupMap[res.GroupId] = struct{}{}
884+
epochStartMap[res.EpochStart] = struct{}{}
885+
epochStartData[elRewardsKey{res.EpochStart, res.GroupId}] = t.ClElValue[decimal.Decimal]{
886+
El: elRewards[elRewardsKey{res.EpochStart, res.GroupId}],
910887
Cl: utils.GWeiToWei(big.NewInt(res.ClRewards)),
911888
}
912-
groupMap[int(res.GroupId)] = true
913889
}
914-
915-
// Get the list of groups
916-
groupList := slices.Collect(maps.Keys(groupMap))
917-
slices.Sort(groupList)
918-
919-
// Create the series structure
920-
propertyNames := []string{"el", "cl"}
921-
for _, groupId := range groupList {
922-
for _, propertyName := range propertyNames {
923-
ret.Series = append(ret.Series, t.ChartSeries[int, decimal.Decimal]{
924-
Id: groupId,
925-
Property: propertyName,
926-
})
927-
}
928-
}
929-
930-
// Fill the epoch data
890+
epochStartList := slices.Sorted(maps.Keys(epochStartMap))
931891
for _, epoch := range epochStartList {
932892
ret.Categories = append(ret.Categories, uint64(utils.EpochToTime(epoch).Unix()))
933-
for idx, series := range ret.Series {
934-
d := epochStartData[epoch][series.Id]
935-
if series.Property == "el" {
936-
ret.Series[idx].Data = append(ret.Series[idx].Data, &d.El)
937-
} else if series.Property == "cl" {
938-
ret.Series[idx].Data = append(ret.Series[idx].Data, &d.Cl)
939-
} else {
940-
return nil, fmt.Errorf("unknown series property: %s", series.Property)
941-
}
942-
}
893+
}
894+
for _, groupId := range slices.Sorted(maps.Keys(groupMap)) {
895+
var clData, elData []*decimal.Decimal
896+
for _, epoch := range epochStartList {
897+
d := epochStartData[elRewardsKey{epoch, groupId}]
898+
clData = append(clData, &d.Cl)
899+
elData = append(elData, &d.El)
900+
}
901+
ret.Series = append(ret.Series,
902+
t.ChartSeries[int, decimal.Decimal]{
903+
Id: int(groupId),
904+
Property: "cl",
905+
Data: clData,
906+
},
907+
t.ChartSeries[int, decimal.Decimal]{
908+
Id: int(groupId),
909+
Property: "el",
910+
Data: elData,
911+
},
912+
)
943913
}
944914

945915
return ret, nil

backend/pkg/api/handlers/handler_service.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,6 @@ func (h *HandlerService) handleDashboardId(ctx context.Context, param string) (*
189189
return dashboardId, nil
190190
}
191191

192-
type ChartTimeDashboardLimits struct {
193-
MinAllowedTs uint64
194-
LatestExportedTs uint64
195-
MaxAllowedInterval uint64
196-
}
197-
198192
// getDashboardPremiumPerks gets the premium perks of the dashboard OWNER or if it's a guest dashboard, it returns free tier premium perks
199193
func (h *HandlerService) getDashboardPremiumPerks(ctx context.Context, id types.VDBId) (*types.PremiumPerks, error) {
200194
// for guest dashboards, return free tier perks

backend/pkg/commons/db/user.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,12 @@ func GetFreeTierProduct(ctx context.Context) (*t.PremiumProduct, error) {
482482
Daily: 0,
483483
Weekly: 0,
484484
},
485+
RewardsChartHistorySeconds: t.ChartHistorySeconds{
486+
Epoch: 0,
487+
Hourly: 0,
488+
Daily: 0,
489+
Weekly: maxJsInt,
490+
},
485491
EmailNotificationsPerDay: 10,
486492
ConfigureNotificationsViaApi: false,
487493
ValidatorGroupNotifications: 1,

0 commit comments

Comments
 (0)