Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BEDS 1065/apps christmas miracle #1239

Merged
merged 6 commits into from
Jan 14, 2025
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
4 changes: 2 additions & 2 deletions backend/pkg/api/data_access/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func (d *DataAccessService) GetValidatorDashboardMobileWidget(ctx context.Contex

retrieveApr := func(hours int, apr *float64) {
eg.Go(func() error {
_, elApr, _, clApr, err := d.internal_getElClAPR(ctx, wrappedDashboardId, -1, hours)
_, elApr, _, clApr, err := d.getElClAPR(ctx, wrappedDashboardId, -1, hours)
if err != nil {
return err
}
Expand All @@ -274,7 +274,7 @@ func (d *DataAccessService) GetValidatorDashboardMobileWidget(ctx context.Contex

retrieveRewards := func(hours int, rewards *t.ClElValue[decimal.Decimal]) {
eg.Go(func() error {
elRewards, _, clRewards, _, err := d.internal_getElClAPR(ctx, wrappedDashboardId, -1, hours)
elRewards, _, clRewards, _, err := d.getElClAPR(ctx, wrappedDashboardId, -1, hours)
if err != nil {
return err
}
Expand Down
253 changes: 253 additions & 0 deletions backend/pkg/api/data_access/vdb_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import (
"context"
"database/sql"
"fmt"
"math"
"math/big"
"time"

"github.com/doug-martin/goqu/v9"
"github.com/gobitfly/beaconchain/pkg/api/enums"
"github.com/gobitfly/beaconchain/pkg/api/services"
t "github.com/gobitfly/beaconchain/pkg/api/types"
"github.com/gobitfly/beaconchain/pkg/commons/cache"
"github.com/gobitfly/beaconchain/pkg/commons/utils"
"github.com/lib/pq"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
)

//////////////////// Helper functions (must be used by more than one VDB endpoint!)
Expand Down Expand Up @@ -130,3 +134,252 @@ func (d *DataAccessService) getTimeToNextWithdrawal(distance uint64) time.Time {

return timeToWithdrawal
}

func (d *DataAccessService) getElClAPR(ctx context.Context, dashboardId t.VDBId, groupId int64, hours int) (elIncome decimal.Decimal, elAPR float64, clIncome decimal.Decimal, clAPR float64, err error) {
table := ""

switch hours {
case 1:
table = "validator_dashboard_data_rolling_1h"
case 24:
table = "validator_dashboard_data_rolling_24h"
case 7 * 24:
table = "validator_dashboard_data_rolling_7d"
case 30 * 24:
table = "validator_dashboard_data_rolling_30d"
case -1:
table = "validator_dashboard_data_rolling_90d"
default:
return decimal.Zero, 0, decimal.Zero, 0, fmt.Errorf("invalid hours value: %v", hours)
}

type RewardsResult struct {
EpochStart uint64 `db:"epoch_start"`
EpochEnd uint64 `db:"epoch_end"`
ValidatorCount uint64 `db:"validator_count"`
Reward sql.NullInt64 `db:"reward"`
}

var rewardsResultTable RewardsResult
var rewardsResultTotal RewardsResult

rewardsDs := goqu.Dialect("postgres").
From(goqu.L(fmt.Sprintf("%s AS r FINAL", table))).
With("validators", goqu.L("(SELECT group_id, validator_index FROM users_val_dashboards_validators WHERE dashboard_id = ?)", dashboardId.Id)).
Select(
goqu.L("MIN(epoch_start) AS epoch_start"),
goqu.L("MAX(epoch_end) AS epoch_end"),
goqu.L("COUNT(*) AS validator_count"),
goqu.L(`
(
SUM(COALESCE(finalizeAggregation(r.balance_end), 0)) +
SUM(COALESCE(r.withdrawals_amount, 0)) -
SUM(COALESCE(r.deposits_amount, 0)) -
SUM(COALESCE(finalizeAggregation(r.balance_start), 0))
) AS reward
`))
if len(dashboardId.Validators) > 0 {
rewardsDs = rewardsDs.
Where(goqu.L("validator_index IN ?", dashboardId.Validators))
} else {
rewardsDs = rewardsDs.
InnerJoin(goqu.L("validators v"), goqu.On(goqu.L("r.validator_index = v.validator_index"))).
Where(goqu.L("r.validator_index IN (SELECT validator_index FROM validators)"))

if groupId != -1 {
rewardsDs = rewardsDs.
Where(goqu.L("v.group_id = ?", groupId))
}
}

query, args, err := rewardsDs.Prepared(true).ToSQL()
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, fmt.Errorf("error preparing query: %w", err)
}

err = d.clickhouseReader.GetContext(ctx, &rewardsResultTable, query, args...)
if err != nil || !rewardsResultTable.Reward.Valid {
return decimal.Zero, 0, decimal.Zero, 0, err
}

if rewardsResultTable.ValidatorCount == 0 {
return decimal.Zero, 0, decimal.Zero, 0, nil
}

aprDivisor := hours
if hours == -1 { // for all time APR
aprDivisor = 90 * 24
}
clAPR = ((float64(rewardsResultTable.Reward.Int64) / float64(aprDivisor)) / (float64(32e9) * float64(rewardsResultTable.ValidatorCount))) * 24.0 * 365.0 * 100.0
if math.IsNaN(clAPR) {
clAPR = 0
}

clIncome = decimal.NewFromInt(rewardsResultTable.Reward.Int64).Mul(decimal.NewFromInt(1e9))

if hours == -1 {
rewardsDs = rewardsDs.
From(goqu.L("validator_dashboard_data_rolling_total AS r FINAL"))

query, args, err = rewardsDs.Prepared(true).ToSQL()
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, fmt.Errorf("error preparing query: %w", err)
}

err = d.clickhouseReader.GetContext(ctx, &rewardsResultTotal, query, args...)
if err != nil || !rewardsResultTotal.Reward.Valid {
return decimal.Zero, 0, decimal.Zero, 0, err
}

clIncome = decimal.NewFromInt(rewardsResultTotal.Reward.Int64).Mul(decimal.NewFromInt(1e9))
}

elDs := goqu.Dialect("postgres").
Select(goqu.COALESCE(goqu.SUM(goqu.L("value / 1e18")), 0)).
From(goqu.I("execution_rewards_finalized").As("b"))

if len(dashboardId.Validators) > 0 {
elDs = elDs.
Where(goqu.L("b.proposer = ANY(?)", pq.Array(dashboardId.Validators)))
} else {
elDs = elDs.
InnerJoin(goqu.L("users_val_dashboards_validators v"), goqu.On(goqu.L("b.proposer = v.validator_index"))).
Where(goqu.L("v.dashboard_id = ?", dashboardId.Id))

if groupId != -1 {
elDs = elDs.
Where(goqu.L("v.group_id = ?", groupId))
}
}

elTableDs := elDs.
Where(goqu.L("b.epoch >= ? AND b.epoch <= ?", rewardsResultTable.EpochStart, rewardsResultTable.EpochEnd))

query, args, err = elTableDs.Prepared(true).ToSQL()
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, fmt.Errorf("error preparing query: %w", err)
}

err = d.alloyReader.GetContext(ctx, &elIncome, query, args...)
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, err
}
elIncomeFloat, _ := elIncome.Float64() // EL income is in ETH
elAPR = ((elIncomeFloat / float64(aprDivisor)) / (float64(32) * float64(rewardsResultTable.ValidatorCount))) * 24.0 * 365.0 * 100.0
if math.IsNaN(elAPR) {
elAPR = 0
}

if hours == -1 {
elTotalDs := elDs.
Where(goqu.L("b.epoch >= ? AND b.epoch <= ?", rewardsResultTotal.EpochStart, rewardsResultTotal.EpochEnd))

query, args, err = elTotalDs.Prepared(true).ToSQL()
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, fmt.Errorf("error preparing query: %w", err)
}

err = d.alloyReader.GetContext(ctx, &elIncome, query, args...)
if err != nil {
return decimal.Zero, 0, decimal.Zero, 0, err
}
}
elIncome = elIncome.Mul(decimal.NewFromInt(1e18))

return elIncome, elAPR, clIncome, clAPR, nil
}

type RpOperatorInfo struct {
ValidatorIndex uint64 `db:"validatorindex"`
NodeFee float64 `db:"node_fee"`
NodeDepositBalance decimal.Decimal `db:"node_deposit_balance"`
UserDepositBalance decimal.Decimal `db:"user_deposit_balance"`
}

func (d *DataAccessService) getValidatorDashboardRpOperatorInfo(ctx context.Context, dashboardId t.VDBId) ([]RpOperatorInfo, error) {
var rpOperatorInfo []RpOperatorInfo

ds := goqu.Dialect("postgres").
Select(
goqu.L("v.validatorindex"),
goqu.L("rplm.node_fee"),
goqu.L("rplm.node_deposit_balance"),
goqu.L("rplm.user_deposit_balance")).
From(goqu.L("rocketpool_minipools AS rplm")).
LeftJoin(goqu.L("validators AS v"), goqu.On(goqu.L("rplm.pubkey = v.pubkey"))).
Where(goqu.L("node_deposit_balance IS NOT NULL")).
Where(goqu.L("user_deposit_balance IS NOT NULL"))

if len(dashboardId.Validators) == 0 {
ds = ds.
LeftJoin(goqu.L("users_val_dashboards_validators uvdv"), goqu.On(goqu.L("uvdv.validator_index = v.validatorindex"))).
Where(goqu.L("uvdv.dashboard_id = ?", dashboardId.Id))
} else {
ds = ds.
Where(goqu.L("v.validatorindex = ANY(?)", pq.Array(dashboardId.Validators)))
}

query, args, err := ds.Prepared(true).ToSQL()
if err != nil {
return nil, fmt.Errorf("error preparing query: %w", err)
}

err = d.alloyReader.SelectContext(ctx, &rpOperatorInfo, query, args...)
if err != nil {
return nil, fmt.Errorf("error retrieving rocketpool validators data: %w", err)
}
return rpOperatorInfo, nil
}

func (d *DataAccessService) calculateValidatorDashboardBalance(ctx context.Context, rpOperatorInfo []RpOperatorInfo, validators []t.VDBValidator, validatorMapping *services.ValidatorMapping, protocolModes t.VDBProtocolModes) (t.ValidatorBalances, error) {
balances := t.ValidatorBalances{}

rpValidators := make(map[uint64]RpOperatorInfo)
for _, res := range rpOperatorInfo {
rpValidators[res.ValidatorIndex] = res
}

// Create a new sub-dashboard to get the total cl deposits for non-rocketpool validators
var nonRpDashboardId t.VDBId

for _, validator := range validators {
metadata := validatorMapping.ValidatorMetadata[validator]
validatorBalance := utils.GWeiToWei(big.NewInt(int64(metadata.Balance)))
effectiveBalance := utils.GWeiToWei(big.NewInt(int64(metadata.EffectiveBalance)))

if rpValidator, ok := rpValidators[validator]; ok {
if protocolModes.RocketPool {
// Calculate the balance of the operator
fullDeposit := rpValidator.UserDepositBalance.Add(rpValidator.NodeDepositBalance)
operatorShare := rpValidator.NodeDepositBalance.Div(fullDeposit)
invOperatorShare := decimal.NewFromInt(1).Sub(operatorShare)

base := decimal.Min(decimal.Max(decimal.Zero, validatorBalance.Sub(rpValidator.UserDepositBalance)), rpValidator.NodeDepositBalance)
commission := decimal.Max(decimal.Zero, validatorBalance.Sub(fullDeposit).Mul(invOperatorShare).Mul(decimal.NewFromFloat(rpValidator.NodeFee)))
reward := decimal.Max(decimal.Zero, validatorBalance.Sub(fullDeposit).Mul(operatorShare).Add(commission))

operatorBalance := base.Add(reward)

balances.Total = balances.Total.Add(operatorBalance)
} else {
balances.Total = balances.Total.Add(validatorBalance)
}
balances.StakedEth = balances.StakedEth.Add(rpValidator.NodeDepositBalance)
} else {
balances.Total = balances.Total.Add(validatorBalance)

nonRpDashboardId.Validators = append(nonRpDashboardId.Validators, validator)
}
balances.Effective = balances.Effective.Add(effectiveBalance)
}

// Get the total cl deposits for non-rocketpool validators
if len(nonRpDashboardId.Validators) > 0 {
totalNonRpDeposits, err := d.GetValidatorDashboardTotalClDeposits(ctx, nonRpDashboardId)
if err != nil {
return balances, fmt.Errorf("error retrieving total cl deposits for non-rocketpool validators: %w", err)
}
balances.StakedEth = balances.StakedEth.Add(totalNonRpDeposits.TotalAmount)
}
return balances, nil
}
88 changes: 6 additions & 82 deletions backend/pkg/api/data_access/vdb_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,100 +364,24 @@ func (d *DataAccessService) GetValidatorDashboardOverview(ctx context.Context, d
}
}

// Find rocketpool validators
type RpOperatorInfo struct {
ValidatorIndex uint64 `db:"validatorindex"`
NodeFee float64 `db:"node_fee"`
NodeDepositBalance decimal.Decimal `db:"node_deposit_balance"`
UserDepositBalance decimal.Decimal `db:"user_deposit_balance"`
}
var queryResult []RpOperatorInfo

ds := goqu.Dialect("postgres").
Select(
goqu.L("v.validatorindex"),
goqu.L("rplm.node_fee"),
goqu.L("rplm.node_deposit_balance"),
goqu.L("rplm.user_deposit_balance")).
From(goqu.L("rocketpool_minipools AS rplm")).
LeftJoin(goqu.L("validators AS v"), goqu.On(goqu.L("rplm.pubkey = v.pubkey"))).
Where(goqu.L("node_deposit_balance IS NOT NULL")).
Where(goqu.L("user_deposit_balance IS NOT NULL"))

if len(dashboardId.Validators) == 0 {
ds = ds.
LeftJoin(goqu.L("users_val_dashboards_validators uvdv"), goqu.On(goqu.L("uvdv.validator_index = v.validatorindex"))).
Where(goqu.L("uvdv.dashboard_id = ?", dashboardId.Id))
} else {
ds = ds.
Where(goqu.L("v.validatorindex = ANY(?)", pq.Array(dashboardId.Validators)))
}

query, args, err := ds.Prepared(true).ToSQL()
rpOperatorInfo, err := d.getValidatorDashboardRpOperatorInfo(ctx, dashboardId)
if err != nil {
return fmt.Errorf("error preparing query: %w", err)
return err
}

err = d.alloyReader.SelectContext(ctx, &queryResult, query, args...)
balances, err := d.calculateValidatorDashboardBalance(ctx, rpOperatorInfo, validators, validatorMapping, protocolModes)
if err != nil {
return fmt.Errorf("error retrieving rocketpool validators data: %w", err)
}

rpValidators := make(map[uint64]RpOperatorInfo)
for _, res := range queryResult {
rpValidators[res.ValidatorIndex] = res
}

// Create a new sub-dashboard to get the total cl deposits for non-rocketpool validators
var nonRpDashboardId t.VDBId

for _, validator := range validators {
metadata := validatorMapping.ValidatorMetadata[validator]
validatorBalance := utils.GWeiToWei(big.NewInt(int64(metadata.Balance)))
effectiveBalance := utils.GWeiToWei(big.NewInt(int64(metadata.EffectiveBalance)))

if rpValidator, ok := rpValidators[validator]; ok {
if protocolModes.RocketPool {
// Calculate the balance of the operator
fullDeposit := rpValidator.UserDepositBalance.Add(rpValidator.NodeDepositBalance)
operatorShare := rpValidator.NodeDepositBalance.Div(fullDeposit)
invOperatorShare := decimal.NewFromInt(1).Sub(operatorShare)

base := decimal.Min(decimal.Max(decimal.Zero, validatorBalance.Sub(rpValidator.UserDepositBalance)), rpValidator.NodeDepositBalance)
commission := decimal.Max(decimal.Zero, validatorBalance.Sub(fullDeposit).Mul(invOperatorShare).Mul(decimal.NewFromFloat(rpValidator.NodeFee)))
reward := decimal.Max(decimal.Zero, validatorBalance.Sub(fullDeposit).Mul(operatorShare).Add(commission))

operatorBalance := base.Add(reward)

data.Balances.Total = data.Balances.Total.Add(operatorBalance)
} else {
data.Balances.Total = data.Balances.Total.Add(validatorBalance)
}
data.Balances.StakedEth = data.Balances.StakedEth.Add(rpValidator.NodeDepositBalance)
} else {
data.Balances.Total = data.Balances.Total.Add(validatorBalance)

nonRpDashboardId.Validators = append(nonRpDashboardId.Validators, validator)
}
data.Balances.Effective = data.Balances.Effective.Add(effectiveBalance)
}

// Get the total cl deposits for non-rocketpool validators
if len(nonRpDashboardId.Validators) > 0 {
totalNonRpDeposits, err := d.GetValidatorDashboardTotalClDeposits(ctx, nonRpDashboardId)
if err != nil {
return fmt.Errorf("error retrieving total cl deposits for non-rocketpool validators: %w", err)
}
data.Balances.StakedEth = data.Balances.StakedEth.Add(totalNonRpDeposits.TotalAmount)
return err
}
data.Balances = balances

return nil
})

retrieveRewardsAndEfficiency := func(table string, hours int, rewards *t.ClElValue[decimal.Decimal], apr *t.ClElValue[float64], efficiency *float64) {
// Rewards + APR
eg.Go(func() error {
(*rewards).El, (*apr).El, (*rewards).Cl, (*apr).Cl, err = d.internal_getElClAPR(ctx, dashboardId, -1, hours)
(*rewards).El, (*apr).El, (*rewards).Cl, (*apr).Cl, err = d.getElClAPR(ctx, dashboardId, -1, hours)
if err != nil {
return err
}
Expand Down
Loading
Loading