Skip to content

Commit c6d5fa4

Browse files
committed
feat(api): add rewards chart history based on premium perks
1 parent 75f487e commit c6d5fa4

File tree

10 files changed

+227
-109
lines changed

10 files changed

+227
-109
lines changed

backend/pkg/api/data_access/dummy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func (d *DummyService) GetValidatorDashboardGroupRewards(ctx context.Context, da
346346
return getDummyStruct[t.VDBGroupRewardsData](ctx)
347347
}
348348

349-
func (d *DummyService) GetValidatorDashboardRewardsChart(ctx context.Context, dashboardId t.VDBId, protocolModes t.VDBProtocolModes) (*t.ChartData[int, decimal.Decimal], error) {
349+
func (d *DummyService) 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) {
350350
return getDummyStruct[t.ChartData[int, decimal.Decimal]](ctx)
351351
}
352352

backend/pkg/api/data_access/vdb.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type ValidatorDashboardRepository interface {
5858

5959
GetValidatorDashboardRewards(ctx context.Context, dashboardId t.VDBId, cursor string, colSort t.Sort[enums.VDBRewardsColumn], search string, limit uint64, protocolModes t.VDBProtocolModes) ([]t.VDBRewardsTableRow, *t.Paging, error)
6060
GetValidatorDashboardGroupRewards(ctx context.Context, dashboardId t.VDBId, groupId int64, epoch uint64, protocolModes t.VDBProtocolModes) (*t.VDBGroupRewardsData, error)
61-
GetValidatorDashboardRewardsChart(ctx context.Context, dashboardId t.VDBId, protocolModes t.VDBProtocolModes) (*t.ChartData[int, decimal.Decimal], error)
61+
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)
6262

6363
GetValidatorDashboardDuties(ctx context.Context, dashboardId t.VDBId, epoch uint64, groupId int64, cursor string, colSort t.Sort[enums.VDBDutiesColumn], search string, limit uint64, protocolModes t.VDBProtocolModes) ([]t.VDBEpochDutiesTableRow, *t.Paging, error)
6464

backend/pkg/api/data_access/vdb_rewards.go

Lines changed: 148 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/doug-martin/goqu/v9"
1414
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
15+
"github.com/doug-martin/goqu/v9/exp"
1516
"github.com/gobitfly/beaconchain/pkg/api/enums"
1617
t "github.com/gobitfly/beaconchain/pkg/api/types"
1718
"github.com/gobitfly/beaconchain/pkg/commons/cache"
@@ -705,37 +706,88 @@ func (d *DataAccessService) GetValidatorDashboardGroupRewards(ctx context.Contex
705706
return ret, nil
706707
}
707708

708-
func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Context, dashboardId t.VDBId, protocolModes t.VDBProtocolModes) (*t.ChartData[int, decimal.Decimal], error) {
709+
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) {
709710
// @DATA-ACCESS incorporate protocolModes
710711
// bar chart for the CL and EL rewards for each group for each epoch.
711712
// NO series for all groups combined except if AggregateGroups is true.
712713
// series id is group id, series property is 'cl' or 'el'
714+
ret := &t.ChartData[int, decimal.Decimal]{}
713715

714-
wg := errgroup.Group{}
716+
if len(groupIds) == 0 {
717+
return ret, nil
718+
}
715719

716-
latestFinalizedEpoch := cache.LatestFinalizedEpoch.Get()
717-
const epochLookBack = 224
718-
startEpoch := uint64(0)
719-
if latestFinalizedEpoch > epochLookBack {
720-
startEpoch = latestFinalizedEpoch - epochLookBack
720+
var err error
721+
722+
var dataTable exp.LiteralExpression
723+
dataColumn := goqu.C("t")
724+
switch aggregation {
725+
case enums.IntervalEpoch:
726+
dataTable = goqu.L("validator_dashboard_data_epoch AS e")
727+
dataColumn = goqu.C("epoch_timestamp")
728+
case enums.IntervalHourly:
729+
dataTable = goqu.L("validator_dashboard_data_hourly AS e FINAL")
730+
case enums.IntervalDaily:
731+
dataTable = goqu.L("validator_dashboard_data_daily AS e FINAL")
732+
case enums.IntervalWeekly:
733+
dataTable = goqu.L("validator_dashboard_data_weekly AS e FINAL")
734+
default:
735+
return nil, fmt.Errorf("unexpected aggregation type: %v", aggregation)
736+
}
737+
738+
requestedAllGroups := dashboardId.AggregateGroups
739+
for _, groupId := range groupIds {
740+
if groupId == t.AllGroups {
741+
// 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
742+
requestedAllGroups = true
743+
break
744+
}
721745
}
722746

723747
// ------------------------------------------------------------------------------------------------------------------
724748
// Build the query that serves as base for both the main and EL rewards queries
749+
// CL
725750
rewardsDs := goqu.Dialect("postgres").
726-
Select(
727-
goqu.L("e.epoch"),
728-
goqu.L(`SUM(COALESCE(e.attestations_reward, 0) + COALESCE(e.blocks_cl_reward, 0) + COALESCE(e.sync_reward, 0)) AS cl_rewards`)).
729-
From(goqu.L("validator_dashboard_data_epoch e")).
730-
With("validators", goqu.L("(SELECT validator_index as validator_index, group_id FROM users_val_dashboards_validators WHERE dashboard_id = ?)", dashboardId.Id)).
731-
Where(goqu.L("e.epoch_timestamp >= fromUnixTimestamp(?)", utils.EpochToTime(startEpoch).Unix()))
751+
Select(goqu.L(`SUM(COALESCE(e.attestations_reward, 0) + COALESCE(e.blocks_cl_reward, 0) + COALESCE(e.sync_reward, 0)) AS cl_rewards`)).
752+
From(dataTable).
753+
With("validators", goqu.Dialect("postgres").
754+
From(goqu.T("users_val_dashboards_validators")).
755+
Select(
756+
goqu.I("validator_index"),
757+
goqu.I("group_id"),
758+
).
759+
Where(
760+
goqu.I("dashboard_id").Eq(dashboardId.Id),
761+
goqu.Or(
762+
goqu.I("group_id").In(groupIds),
763+
goqu.V(requestedAllGroups),
764+
),
765+
),
766+
).
767+
Where(
768+
dataColumn.Between(goqu.Range(
769+
goqu.L("fromUnixTimestamp(?)", afterTs),
770+
goqu.L("fromUnixTimestamp(?)", beforeTs))),
771+
)
772+
773+
if aggregation == enums.IntervalEpoch {
774+
rewardsDs = rewardsDs.
775+
SelectAppend(goqu.L("e.epoch").As("epoch_start")).
776+
SelectAppend(goqu.L("e.epoch").As("epoch_end"))
777+
} else {
778+
rewardsDs = rewardsDs.
779+
SelectAppend(goqu.L("epoch_start")).
780+
SelectAppend(goqu.L("epoch_end"))
781+
}
732782

783+
// EL
733784
elDs := goqu.Dialect("postgres").
734785
Select(
735-
goqu.L("b.epoch"),
786+
goqu.L("epoch_start"),
787+
// goqu.L("epoch_end"), not needed
736788
goqu.L("SUM(COALESCE(rb.value, ep.fee_recipient_reward * 1e18, 0)) AS el_rewards")).
737789
From(goqu.L("users_val_dashboards_validators v")).
738-
LeftJoin(goqu.L("blocks b"), goqu.On(goqu.L("v.validator_index = b.proposer AND b.status = '1'"))).
790+
RightJoin(goqu.L("blocks b"), goqu.On(goqu.L("v.validator_index = b.proposer AND b.status = '1'"))).
739791
LeftJoin(goqu.L("execution_payloads ep"), goqu.On(goqu.L("ep.block_hash = b.exec_block_hash"))).
740792
LeftJoin(
741793
goqu.Lateral(goqu.Dialect("postgres").
@@ -746,8 +798,16 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
746798
Where(goqu.L("relays_blocks.exec_block_hash = b.exec_block_hash")).
747799
GroupBy("exec_block_hash")).As("rb"),
748800
goqu.On(goqu.L("rb.exec_block_hash = b.exec_block_hash")),
749-
).
750-
Where(goqu.L("b.epoch >= ?", startEpoch))
801+
)
802+
803+
// grouping, ordering
804+
rewardsDs = rewardsDs.
805+
GroupBy(goqu.L("epoch_start, epoch_end")).
806+
Order(goqu.L("epoch_start").Asc())
807+
808+
elDs = elDs.
809+
GroupBy(goqu.L("epoch_start, epoch_end")).
810+
Order(goqu.L("epoch_start").Asc())
751811

752812
if dashboardId.Validators == nil {
753813
rewardsDs = rewardsDs.
@@ -758,149 +818,147 @@ func (d *DataAccessService) GetValidatorDashboardRewardsChart(ctx context.Contex
758818

759819
if dashboardId.AggregateGroups {
760820
rewardsDs = rewardsDs.
761-
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
762-
GroupBy(goqu.L("e.epoch")).
763-
Order(goqu.L("e.epoch").Asc())
821+
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId))
764822
elDs = elDs.
765-
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
766-
GroupBy(goqu.L("b.epoch")).
767-
Order(goqu.L("b.epoch").Asc())
823+
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId))
768824
} else {
769825
rewardsDs = rewardsDs.
770826
SelectAppend(goqu.L("v.group_id AS result_group_id")).
771-
GroupBy(goqu.L("e.epoch"), goqu.L("result_group_id")).
772-
Order(goqu.L("e.epoch").Asc(), goqu.L("result_group_id").Asc())
827+
GroupByAppend(goqu.L("result_group_id")).
828+
OrderAppend(goqu.L("result_group_id").Asc())
773829
elDs = elDs.
774830
SelectAppend(goqu.L("v.group_id AS result_group_id")).
775-
GroupBy(goqu.L("b.epoch"), goqu.L("result_group_id")).
776-
Order(goqu.L("b.epoch").Asc(), goqu.L("result_group_id").Asc())
831+
GroupByAppend(goqu.L("result_group_id")).
832+
OrderAppend(goqu.L("result_group_id").Asc())
777833
}
778834
} else {
779835
// In case a list of validators is provided set the group to the default id
780836
rewardsDs = rewardsDs.
781837
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
782-
Where(goqu.L("e.validator_index IN ?", dashboardId.Validators)).
783-
GroupBy(goqu.L("e.epoch")).
784-
Order(goqu.L("e.epoch").Asc())
838+
Where(goqu.L("e.validator_index IN ?", dashboardId.Validators))
785839
elDs = elDs.
786840
SelectAppend(goqu.L("?::smallint AS result_group_id", t.DefaultGroupId)).
787-
Where(goqu.L("b.proposer = ANY(?)", pq.Array(dashboardId.Validators))).
788-
GroupBy(goqu.L("b.epoch")).
789-
Order(goqu.L("b.epoch").Asc())
841+
Where(goqu.L("b.proposer = ANY(?)", pq.Array(dashboardId.Validators)))
790842
}
791843

792844
// ------------------------------------------------------------------------------------------------------------------
793845
// Build the main query and get the data
794846
queryResult := []struct {
795-
Epoch uint64 `db:"epoch"`
796-
GroupId uint64 `db:"result_group_id"`
797-
ClRewards int64 `db:"cl_rewards"`
847+
EpochStart uint64 `db:"epoch_start"`
848+
EpochEnd uint64 `db:"epoch_end"`
849+
GroupId uint64 `db:"result_group_id"`
850+
ClRewards int64 `db:"cl_rewards"`
798851
}{}
799852

800-
wg.Go(func() error {
801-
query, args, err := rewardsDs.Prepared(true).ToSQL()
802-
if err != nil {
803-
return fmt.Errorf("error preparing query: %w", err)
804-
}
853+
query, args, err := rewardsDs.Prepared(true).ToSQL()
854+
if err != nil {
855+
return nil, fmt.Errorf("error preparing query: %w", err)
856+
}
805857

806-
err = d.clickhouseReader.SelectContext(ctx, &queryResult, query, args...)
807-
if err != nil {
808-
return fmt.Errorf("error retrieving rewards chart data: %w", err)
809-
}
810-
return nil
811-
})
858+
err = d.clickhouseReader.SelectContext(ctx, &queryResult, query, args...)
859+
if err != nil {
860+
return nil, fmt.Errorf("error retrieving rewards chart data: %w", err)
861+
}
862+
863+
if len(queryResult) == 0 {
864+
return ret, nil
865+
}
866+
867+
var epochStarts, epochEnds []uint64
868+
for _, res := range queryResult {
869+
epochStarts = append(epochStarts, res.EpochStart)
870+
epochEnds = append(epochEnds, res.EpochEnd)
871+
}
872+
elDs = elDs.
873+
With("epoch_ranges(epoch_start, epoch_end)", goqu.L("(SELECT * FROM unnest(?::int[], ?::int[]))", pq.Array(epochStarts), pq.Array(epochEnds))).
874+
InnerJoin(goqu.L("epoch_ranges"), goqu.On(goqu.L("b.epoch BETWEEN epoch_ranges.epoch_start AND epoch_ranges.epoch_end"))).
875+
Where(goqu.L("b.epoch BETWEEN ? AND ?", epochStarts[0], epochEnds[len(epochEnds)-1]))
812876

813877
// ------------------------------------------------------------------------------------------------------------------
814878
// Get the EL rewards
815879
elRewards := make(map[uint64]map[uint64]decimal.Decimal)
816-
wg.Go(func() error {
817-
elQueryResult := []struct {
818-
Epoch uint64 `db:"epoch"`
819-
GroupId uint64 `db:"result_group_id"`
820-
ElRewards decimal.Decimal `db:"el_rewards"`
821-
}{}
822880

823-
query, args, err := elDs.Prepared(true).ToSQL()
824-
if err != nil {
825-
return fmt.Errorf("error preparing query: %w", err)
826-
}
881+
elQueryResult := []struct {
882+
EpochStart uint64 `db:"epoch_start"`
883+
GroupId uint64 `db:"result_group_id"`
884+
ElRewards decimal.Decimal `db:"el_rewards"`
885+
}{}
827886

828-
err = d.readerDb.SelectContext(ctx, &elQueryResult, query, args...)
829-
if err != nil {
830-
return fmt.Errorf("error retrieving el rewards data for rewards chart: %w", err)
831-
}
887+
query, args, err = elDs.Prepared(true).ToSQL()
888+
if err != nil {
889+
return nil, fmt.Errorf("error preparing query: %w", err)
890+
}
832891

833-
for _, entry := range elQueryResult {
834-
if _, ok := elRewards[entry.Epoch]; !ok {
835-
elRewards[entry.Epoch] = make(map[uint64]decimal.Decimal)
836-
}
837-
elRewards[entry.Epoch][entry.GroupId] = entry.ElRewards
892+
err = d.readerDb.SelectContext(ctx, &elQueryResult, query, args...)
893+
if err != nil {
894+
return nil, fmt.Errorf("error retrieving el rewards data for rewards chart: %w", err)
895+
}
896+
897+
for _, entry := range elQueryResult {
898+
if _, ok := elRewards[entry.EpochStart]; !ok {
899+
elRewards[entry.EpochStart] = make(map[uint64]decimal.Decimal)
838900
}
839-
return nil
840-
})
901+
elRewards[entry.EpochStart][entry.GroupId] = entry.ElRewards
902+
}
841903

842-
err := wg.Wait()
843904
if err != nil {
844905
return nil, fmt.Errorf("error retrieving validator dashboard rewards chart data: %w", err)
845906
}
846907

847908
// ------------------------------------------------------------------------------------------------------------------
848909
// Create a map structure to store the data
849-
epochData := make(map[uint64]map[uint64]t.ClElValue[decimal.Decimal])
850-
epochList := make([]uint64, 0)
910+
epochStartData := make(map[uint64]map[int]t.ClElValue[decimal.Decimal])
911+
epochStartList := make([]uint64, 0)
851912

852913
for _, res := range queryResult {
853-
if _, ok := epochData[res.Epoch]; !ok {
854-
epochData[res.Epoch] = make(map[uint64]t.ClElValue[decimal.Decimal])
855-
epochList = append(epochList, res.Epoch)
914+
if _, ok := epochStartData[res.EpochStart]; !ok {
915+
epochStartData[res.EpochStart] = make(map[int]t.ClElValue[decimal.Decimal])
916+
epochStartList = append(epochStartList, res.EpochStart)
856917
}
857918

858-
epochData[res.Epoch][res.GroupId] = t.ClElValue[decimal.Decimal]{
859-
El: elRewards[res.Epoch][res.GroupId],
919+
epochStartData[res.EpochStart][int(res.GroupId)] = t.ClElValue[decimal.Decimal]{
920+
El: elRewards[res.EpochStart][res.GroupId],
860921
Cl: utils.GWeiToWei(big.NewInt(res.ClRewards)),
861922
}
862923
}
863924

864925
// Get the list of groups
865-
// It should be identical for all epochs
866-
var groupList []uint64
867-
for _, groupData := range epochData {
926+
// It should be identical for all epochs (in most cases)
927+
var groupList []int
928+
for _, groupData := range epochStartData {
868929
for groupId := range groupData {
869930
groupList = append(groupList, groupId)
870931
}
871932
break
872933
}
873934
slices.Sort(groupList)
874935

875-
// Create the result
876-
var result t.ChartData[int, decimal.Decimal]
877-
878936
// Create the series structure
879937
propertyNames := []string{"el", "cl"}
880938
for _, groupId := range groupList {
881939
for _, propertyName := range propertyNames {
882-
result.Series = append(result.Series, t.ChartSeries[int, decimal.Decimal]{
883-
Id: int(groupId),
940+
ret.Series = append(ret.Series, t.ChartSeries[int, decimal.Decimal]{
941+
Id: groupId,
884942
Property: propertyName,
885943
})
886944
}
887945
}
888946

889947
// Fill the epoch data
890-
for _, epoch := range epochList {
891-
result.Categories = append(result.Categories, epoch)
892-
for idx, series := range result.Series {
948+
for _, epoch := range epochStartList {
949+
ret.Categories = append(ret.Categories, uint64(utils.EpochToTime(epoch).Unix()))
950+
for idx, series := range ret.Series {
893951
if series.Property == "el" {
894-
result.Series[idx].Data = append(result.Series[idx].Data, epochData[epoch][uint64(series.Id)].El)
952+
ret.Series[idx].Data = append(ret.Series[idx].Data, epochStartData[epoch][series.Id].El)
895953
} else if series.Property == "cl" {
896-
result.Series[idx].Data = append(result.Series[idx].Data, epochData[epoch][uint64(series.Id)].Cl)
954+
ret.Series[idx].Data = append(ret.Series[idx].Data, epochStartData[epoch][series.Id].Cl)
897955
} else {
898956
return nil, fmt.Errorf("unknown series property: %s", series.Property)
899957
}
900958
}
901959
}
902960

903-
return &result, nil
961+
return ret, nil
904962
}
905963

906964
func (d *DataAccessService) GetValidatorDashboardDuties(ctx context.Context, dashboardId t.VDBId, epoch uint64, groupId int64, cursor string, colSort t.Sort[enums.VDBDutiesColumn], search string, limit uint64, protocolModes t.VDBProtocolModes) ([]t.VDBEpochDutiesTableRow, *t.Paging, error) {

backend/pkg/api/data_access/vdb_summary.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,13 +1026,9 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
10261026

10271027
var queryResults []*t.VDBValidatorSummaryChartRow
10281028

1029-
containsGroups := false
10301029
requestedGroupsMap := make(map[int64]bool)
10311030
for _, groupId := range groupIds {
10321031
requestedGroupsMap[groupId] = true
1033-
if !containsGroups && groupId >= 0 {
1034-
containsGroups = true
1035-
}
10361032
}
10371033

10381034
totalLineRequested := requestedGroupsMap[t.AllGroups]

0 commit comments

Comments
 (0)