From b52da1ce6d9069f75ce324fd082cc92e0fcc10e6 Mon Sep 17 00:00:00 2001 From: remoterami <142154971+remoterami@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:10:18 +0100 Subject: [PATCH] refactor(api): validator addition --- backend/pkg/api/data_access/dummy.go | 8 +- backend/pkg/api/data_access/general.go | 84 +++++ backend/pkg/api/data_access/vdb.go | 4 +- backend/pkg/api/data_access/vdb_management.go | 352 ++++++++++++++---- backend/pkg/api/handlers/public.go | 25 +- backend/pkg/api/types/dashboard.go | 17 +- backend/pkg/api/types/search.go | 7 +- backend/pkg/api/types/user.go | 23 +- backend/pkg/commons/config/config.go | 3 + .../commons/config/pectra-devnet-6.chain.yml | 172 +++++++++ ...0250131124812_change_network_data_type.sql | 10 + backend/pkg/commons/db/user.go | 80 ++-- backend/pkg/commons/utils/config.go | 8 + backend/pkg/commons/utils/units.go | 4 + 14 files changed, 642 insertions(+), 155 deletions(-) create mode 100644 backend/pkg/commons/config/pectra-devnet-6.chain.yml create mode 100644 backend/pkg/commons/db/migrations/postgres/20250131124812_change_network_data_type.sql diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index d45874650..4e338cdb0 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -214,6 +214,10 @@ func (d *DummyService) GetValidatorsFromSlices(ctx context.Context, indices []ui return getDummyData[[]t.VDBValidator](ctx) } +func (d *DummyService) GetValidatorsEffectiveBalanceTotal(ctx context.Context, indices []uint64) (uint64, error) { + return getDummyData[uint64](ctx) +} + func (d *DummyService) GetUserDashboards(ctx context.Context, userId uint64) (*t.UserDashboardsData, error) { return getDummyStruct[t.UserDashboardsData](ctx) } @@ -266,7 +270,7 @@ func (d *DummyService) GetValidatorDashboardGroupExists(ctx context.Context, das return true, nil } -func (d *DummyService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) { +func (d *DummyService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator, limit uint64) ([]t.VDBPostValidatorsData, error) { return getDummyData[[]t.VDBPostValidatorsData](ctx) } @@ -542,7 +546,7 @@ func (d *DummyService) GetValidatorDashboardGroupCount(ctx context.Context, dash return getDummyData[uint64](ctx) } -func (d *DummyService) GetValidatorDashboardValidatorsCount(ctx context.Context, dashboardId t.VDBIdPrimary) (uint64, error) { +func (d *DummyService) GetValidatorDashboardEffectiveBalanceTotal(ctx context.Context, dashboardId t.VDBId) (uint64, error) { return getDummyData[uint64](ctx) } diff --git a/backend/pkg/api/data_access/general.go b/backend/pkg/api/data_access/general.go index 39046f03c..b8e71d44a 100644 --- a/backend/pkg/api/data_access/general.go +++ b/backend/pkg/api/data_access/general.go @@ -2,13 +2,17 @@ package dataaccess import ( "context" + "database/sql" "fmt" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gobitfly/beaconchain/pkg/api/types" + t "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gobitfly/beaconchain/pkg/commons/db" + "github.com/gobitfly/beaconchain/pkg/commons/utils" + constypes "github.com/gobitfly/beaconchain/pkg/consapi/types" ) // retrieve (primary) ens name and optional name (=label) maintained by beaconcha.in, if present @@ -131,3 +135,83 @@ func applySortAndPagination(defaultColumns []types.SortColumn, primary types.Sor return queryOrder, queryWhere, nil } + +// returns the effective balances of the provided validators +// executed from the vdb premium limits pov, i.e. exited validators account for the EB at exit time +func (d *DataAccessService) GetValidatorsEffectiveBalances(ctx context.Context, validators []t.VDBValidator, onlyActive bool) (map[t.VDBValidator]uint64, error) { + validatorMapping, err := d.services.GetCurrentValidatorMapping() + if err != nil { + return nil, err + } + + // active + effectiveBalances := make(map[t.VDBValidator]uint64) + var validatorExitEpochs []exp.Expression + for _, validator := range validators { + if len(validatorMapping.ValidatorMetadata) <= int(validator) { + return nil, fmt.Errorf("validator index %d not found in validator mapping", validator) + } + status := constypes.ValidatorDbStatus(validatorMapping.ValidatorMetadata[validator].Status) + if !onlyActive && + (status == constypes.DbSlashed || status == constypes.DbExited) && + validatorMapping.ValidatorMetadata[validator].EffectiveBalance == 0 { + // exited & balance withdrawn, need to query latest EB before exit + if !validatorMapping.ValidatorMetadata[validator].ExitEpoch.Valid { + return nil, fmt.Errorf("validator %d has no exit epoch", validator) + } + // goqu can't pass tuples directly + epochTs := utils.EpochToTime(uint64(validatorMapping.ValidatorMetadata[validator].ExitEpoch.Int64)).Unix() + validatorExitEpochs = append(validatorExitEpochs, goqu.L("(fromUnixTimestamp(?), ?)", epochTs, validator)) + } else { + effectiveBalances[validator] = validatorMapping.ValidatorMetadata[validator].EffectiveBalance + } + } + + // exited + if len(validatorExitEpochs) > 0 { + ds := goqu.Dialect("postgres"). + Select( + // goqu.SUM(goqu.I("balance_effective_end")).As("balance_effective_end"), + goqu.I("validator_index"), + goqu.I("balance_effective_end"), + ). + From("validator_dashboard_data_epoch"). + Where( + goqu.L("(epoch_timestamp, validator_index)").In(validatorExitEpochs), + ) + query, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return nil, err + } + + ebsBeforeExit := []struct { + ValidatorIndex uint64 `db:"validator_index"` + EffectiveBalance uint64 `db:"balance_effective_end"` + }{} + err = d.clickhouseReader.SelectContext(ctx, &ebsBeforeExit, query, args...) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + for _, eb := range ebsBeforeExit { + effectiveBalances[eb.ValidatorIndex] = eb.EffectiveBalance + } + } + return effectiveBalances, nil +} + +func (d *DataAccessService) GetValidatorsEffectiveBalanceTotal(ctx context.Context, validators []t.VDBValidator) (uint64, error) { + validatorEbs, err := d.GetValidatorsEffectiveBalances(ctx, validators, false) + if err != nil { + return 0, err + } + + var totalEb uint64 + for _, validator := range validators { + if eb, ok := validatorEbs[validator]; !ok { + return 0, fmt.Errorf("effective balance for validator %d not found", validator) + } else { + totalEb += eb + } + } + return totalEb, nil +} diff --git a/backend/pkg/api/data_access/vdb.go b/backend/pkg/api/data_access/vdb.go index 8eadc6352..8b16aa39b 100644 --- a/backend/pkg/api/data_access/vdb.go +++ b/backend/pkg/api/data_access/vdb.go @@ -29,14 +29,14 @@ type ValidatorDashboardRepository interface { GetValidatorDashboardGroupCount(ctx context.Context, dashboardId t.VDBIdPrimary) (uint64, error) GetValidatorDashboardGroupExists(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64) (bool, error) - AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) + AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator, limit uint64) ([]t.VDBPostValidatorsData, error) AddValidatorDashboardValidatorsByDepositAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) AddValidatorDashboardValidatorsByWithdrawalCredential(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, credential string, limit uint64) ([]t.VDBPostValidatorsData, error) AddValidatorDashboardValidatorsByGraffiti(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, graffiti string, limit uint64) ([]t.VDBPostValidatorsData, error) RemoveValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) error GetValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBId, groupId int64, cursor string, colSort t.Sort[enums.VDBManageValidatorsColumn], search string, limit uint64) ([]t.VDBManageValidatorsTableRow, *t.Paging, error) - GetValidatorDashboardValidatorsCount(ctx context.Context, dashboardId t.VDBIdPrimary) (uint64, error) + GetValidatorDashboardEffectiveBalanceTotal(ctx context.Context, dashboardId t.VDBId) (uint64, error) CreateValidatorDashboardPublicId(ctx context.Context, dashboardId t.VDBIdPrimary, name string, shareGroups bool) (*t.VDBPublicId, error) GetValidatorDashboardPublicId(ctx context.Context, publicDashboardId t.VDBIdPublic) (*t.VDBPublicId, error) diff --git a/backend/pkg/api/data_access/vdb_management.go b/backend/pkg/api/data_access/vdb_management.go index 92772772b..27e91cb98 100644 --- a/backend/pkg/api/data_access/vdb_management.go +++ b/backend/pkg/api/data_access/vdb_management.go @@ -758,7 +758,7 @@ func (d *DataAccessService) GetValidatorDashboardGroupExists(ctx context.Context return groupExists, err } -func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) { +func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator, limitEB uint64) ([]t.VDBPostValidatorsData, error) { result := []t.VDBPostValidatorsData{} if len(validators) == 0 { @@ -766,6 +766,49 @@ func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, return nil, nil } + // determine new validators and check for their EBs + var existingValidators []uint64 + existingValidatorsDs := goqu.Dialect("postgres"). + From(goqu.Dialect("postgres"). + From(goqu.L("unnest(?::int[])", pq.Array(validators)).As("validator_index")). + Select("*").As("req")). + SelectDistinct(goqu.I("uvdv.validator_index")). + InnerJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On(goqu.I("req.validator_index").Eq(goqu.I("uvdv.validator_index"))), + ). + Where( + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ) + existingValidatorsQuery, args, err := existingValidatorsDs.Prepared(true).ToSQL() + if err != nil { + return nil, fmt.Errorf("error preparing query: %w", err) + } + err = d.alloyReader.SelectContext(ctx, &existingValidators, existingValidatorsQuery, args...) + if err != nil { + return nil, err + } + existingValidatorsMap := utils.SliceToMap(existingValidators) + + newValidators := make([]uint64, 0, len(validators)) + for _, validator := range validators { + if _, ok := existingValidatorsMap[validator]; !ok { + newValidators = append(newValidators, validator) + } + } + + if len(newValidators) > 0 && limitEB > 0 { + // only insert new validators until the eb limit is reached + newValidatorEbs, err := d.GetValidatorsEffectiveBalances(ctx, newValidators, false) + if err != nil { + return nil, err + } + newValidators = d.applyEBFiler(newValidatorEbs, limitEB) + } + + // keep existing validators so we can update their group + validators = append(existingValidators, newValidators...) + tx, err := d.writerDb.BeginTxx(ctx, nil) if err != nil { return nil, fmt.Errorf("error starting db transactions to insert validators for a dashboard: %w", err) @@ -829,7 +872,7 @@ func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, // Updates the group for validators already in the dashboard linked to the deposit address. // Adds up to limit new validators associated with the deposit address, if not already in the dashboard. -func (d *DataAccessService) AddValidatorDashboardValidatorsByDepositAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) { +func (d *DataAccessService) AddValidatorDashboardValidatorsByDepositAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, ebLimit uint64) ([]t.VDBPostValidatorsData, error) { result := []t.VDBPostValidatorsData{} addressParsed, err := hex.DecodeString(strings.TrimPrefix(address, "0x")) @@ -837,30 +880,54 @@ func (d *DataAccessService) AddValidatorDashboardValidatorsByDepositAddress(ctx return nil, err } - uniqueValidatorIndexesQuery := ` - (SELECT - DISTINCT uvdv.validator_index - FROM validators v - JOIN eth1_deposits d ON v.pubkey = d.publickey - JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index - WHERE uvdv.dashboard_id = $1 AND d.from_address = $2) + baseDs := goqu.Dialect("postgres"). + From(goqu.T("validators").As("v")). + SelectDistinct(goqu.I("v.validatorindex")). + InnerJoin( + goqu.T("eth1_deposits").As("d"), + goqu.On(goqu.I("v.pubkey").Eq(goqu.I("d.publickey"))), + ). + Where( + goqu.I("d.from_address").Eq(addressParsed), + ) - UNION + existingValidatorsDs := baseDs. + InnerJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On(goqu.I("v.validatorindex").Eq(goqu.I("uvdv.validator_index"))), + ). + Where( + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ) - (SELECT - DISTINCT v.validatorindex AS validator_index - FROM validators v - JOIN eth1_deposits d ON v.pubkey = d.publickey - LEFT JOIN users_val_dashboards_validators uvdv - ON v.validatorindex = uvdv.validator_index AND uvdv.dashboard_id = $1 - WHERE d.from_address = $2 AND uvdv.validator_index IS NULL - ORDER BY validator_index - LIMIT $3)` + var uniqueValidatorIndexesDs *goqu.SelectDataset + if ebLimit > 0 { + newValidatorsDs := baseDs. + LeftJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On( + goqu.I("v.validatorindex").Eq(goqu.I("uvdv.validator_index")), + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ), + ). + Where( + goqu.I("uvdv.validator_index").IsNull(), + ) + err := d.applyNewValidatorsDSEBFilter(ctx, newValidatorsDs, ebLimit) + if err != nil { + return nil, err + } + uniqueValidatorIndexesDs = existingValidatorsDs.Union(newValidatorsDs) + } - addValidatorsQuery := d.getAddValidatorsQuery(uniqueValidatorIndexesQuery) + addValidatorsDs := d.getAddValidatorsQuery(uniqueValidatorIndexesDs, uint64(dashboardId), groupId) + addValidatorsQuery, args, err := addValidatorsDs.Prepared(true).ToSQL() + if err != nil { + return nil, fmt.Errorf("error preparing query: %w", err) + } var validators []uint64 - err = d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, dashboardId, addressParsed, limit, groupId) + err = d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, args...) if err != nil { return nil, err } @@ -877,7 +944,7 @@ func (d *DataAccessService) AddValidatorDashboardValidatorsByDepositAddress(ctx // Updates the group for validators already in the dashboard linked to the withdrawal address. // Adds up to limit new validators associated with the withdrawal address, if not already in the dashboard. -func (d *DataAccessService) AddValidatorDashboardValidatorsByWithdrawalCredential(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, credential string, limit uint64) ([]t.VDBPostValidatorsData, error) { +func (d *DataAccessService) AddValidatorDashboardValidatorsByWithdrawalCredential(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, credential string, ebLimit uint64) ([]t.VDBPostValidatorsData, error) { result := []t.VDBPostValidatorsData{} addressParsed, err := hex.DecodeString(strings.TrimPrefix(credential, "0x")) @@ -885,28 +952,50 @@ func (d *DataAccessService) AddValidatorDashboardValidatorsByWithdrawalCredentia return nil, err } - uniqueValidatorIndexesQuery := ` - (SELECT - DISTINCT uvdv.validator_index - FROM validators v - JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index - WHERE uvdv.dashboard_id = $1 AND v.withdrawalcredentials = $2) + baseDs := goqu.Dialect("postgres"). + From(goqu.T("validators").As("v")). + SelectDistinct(goqu.I("v.validatorindex")). + Where( + goqu.I("v.withdrawalcredentials").Eq(addressParsed), + ) - UNION + existingValidatorsDs := baseDs. + InnerJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On(goqu.I("v.validatorindex").Eq(goqu.I("uvdv.validator_index"))), + ). + Where( + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ) - (SELECT - DISTINCT v.validatorindex AS validator_index - FROM validators v - LEFT JOIN users_val_dashboards_validators uvdv - ON v.validatorindex = uvdv.validator_index AND uvdv.dashboard_id = $1 - WHERE v.withdrawalcredentials = $2 AND uvdv.validator_index IS NULL - ORDER BY v.validatorindex - LIMIT $3)` + var uniqueValidatorIndexesDs *goqu.SelectDataset + if ebLimit > 0 { + newValidatorsDs := baseDs. + LeftJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On( + goqu.I("v.validatorindex").Eq(goqu.I("uvdv.validator_index")), + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ), + ). + Where( + goqu.I("uvdv.validator_index").IsNull(), + ) + err := d.applyNewValidatorsDSEBFilter(ctx, newValidatorsDs, ebLimit) + if err != nil { + return nil, err + } + uniqueValidatorIndexesDs = existingValidatorsDs.Union(newValidatorsDs) + } - addValidatorsQuery := d.getAddValidatorsQuery(uniqueValidatorIndexesQuery) + addValidatorsDs := d.getAddValidatorsQuery(uniqueValidatorIndexesDs, uint64(dashboardId), groupId) + addValidatorsQuery, args, err := addValidatorsDs.Prepared(true).ToSQL() + if err != nil { + return nil, fmt.Errorf("error preparing query: %w", err) + } var validators []uint64 - err = d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, dashboardId, addressParsed, limit, groupId) + err = d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, args...) if err != nil { return nil, err } @@ -923,30 +1012,53 @@ func (d *DataAccessService) AddValidatorDashboardValidatorsByWithdrawalCredentia // Update the group for validators already in the dashboard linked to the graffiti (via produced block). // Add up to limit new validators associated with the graffiti, if not already in the dashboard. -func (d *DataAccessService) AddValidatorDashboardValidatorsByGraffiti(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, graffiti string, limit uint64) ([]t.VDBPostValidatorsData, error) { +func (d *DataAccessService) AddValidatorDashboardValidatorsByGraffiti(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, graffiti string, ebLimit uint64) ([]t.VDBPostValidatorsData, error) { result := []t.VDBPostValidatorsData{} - uniqueValidatorIndexesQuery := ` - (SELECT - DISTINCT uvdv.validator_index - FROM blocks b - JOIN users_val_dashboards_validators uvdv ON b.proposer = uvdv.validator_index - WHERE uvdv.dashboard_id = $1 AND b.graffiti_text = $2) - - UNION - - (SELECT DISTINCT b.proposer AS validator_index - FROM blocks b - LEFT JOIN users_val_dashboards_validators uvdv - ON b.proposer = uvdv.validator_index AND uvdv.dashboard_id = $1 - WHERE b.graffiti_text = $2 AND uvdv.validator_index IS NULL - ORDER BY b.proposer - LIMIT $3)` - - addValidatorsQuery := d.getAddValidatorsQuery(uniqueValidatorIndexesQuery) + baseDs := goqu.Dialect("postgres"). + From(goqu.T("blocks").As("b")). + SelectDistinct(goqu.I("b.proposer")). + Where( + goqu.I("b.graffiti_text").Eq(graffiti), + ) + + existingValidatorsDs := baseDs. + InnerJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On(goqu.I("b.proposer").Eq(goqu.I("uvdv.validator_index"))), + ). + Where( + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ) + + var uniqueValidatorIndexesDs *goqu.SelectDataset + if ebLimit > 0 { + newValidatorsDs := baseDs. + LeftJoin( + goqu.T("users_val_dashboards_validators").As("uvdv"), + goqu.On( + goqu.I("b.proposer").Eq(goqu.I("uvdv.validator_index")), + goqu.I("uvdv.dashboard_id").Eq(dashboardId), + ), + ). + Where( + goqu.I("uvdv.validator_index").IsNull(), + ) + err := d.applyNewValidatorsDSEBFilter(ctx, newValidatorsDs, ebLimit) + if err != nil { + return nil, err + } + uniqueValidatorIndexesDs = existingValidatorsDs.Union(newValidatorsDs) + } + + addValidatorsDs := d.getAddValidatorsQuery(uniqueValidatorIndexesDs, uint64(dashboardId), groupId) + addValidatorsQuery, args, err := addValidatorsDs.Prepared(true).ToSQL() + if err != nil { + return nil, fmt.Errorf("error preparing query: %w", err) + } var validators []uint64 - err := d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, dashboardId, graffiti, limit, groupId) + err = d.alloyWriter.SelectContext(ctx, &validators, addValidatorsQuery, args...) if err != nil { return nil, err } @@ -961,20 +1073,89 @@ func (d *DataAccessService) AddValidatorDashboardValidatorsByGraffiti(ctx contex return result, nil } -func (d *DataAccessService) getAddValidatorsQuery(uniqueValidatorIndexesQuery string) string { - return fmt.Sprintf(` - WITH unique_validator_indexes AS ( - %s - ) - INSERT INTO users_val_dashboards_validators (dashboard_id, group_id, validator_index) - SELECT $1 AS dashboard_id, $4 AS group_id, validator_index - FROM unique_validator_indexes - ON CONFLICT (dashboard_id, validator_index) DO UPDATE - SET - dashboard_id = EXCLUDED.dashboard_id, - group_id = EXCLUDED.group_id, - validator_index = EXCLUDED.validator_index - RETURNING validator_index`, uniqueValidatorIndexesQuery) +func (d *DataAccessService) applyEBFiler(validatorEbs map[t.VDBValidator]uint64, ebLimit uint64) []t.VDBValidator { + // Decide which new validators to add: + // a) insert by lowest index until ebLimit reached + // b) insert by lowest effective balance until ebLimit reached + // c) insert the combination of validators which gets closest to the ebLimit (knapsack problem) + // TODO prefer active validators & insert exited last in all 3 cases + newValidatorsList := maps.Keys(validatorEbs) + + sort.Slice(newValidatorsList, func(i, j int) bool { + // use a) as sec. sort + if validatorEbs[newValidatorsList[i]] == validatorEbs[newValidatorsList[j]] { + return newValidatorsList[i] < newValidatorsList[j] // a) + } + return validatorEbs[newValidatorsList[i]] < validatorEbs[newValidatorsList[j]] // b) + }) + + var newEbAccumulator uint64 + for i, validator := range newValidatorsList { + if newEbAccumulator+validatorEbs[validator] > ebLimit { + newValidatorsList = newValidatorsList[:i] + break + } + newEbAccumulator += validatorEbs[validator] + } + return newValidatorsList +} + +// takes a goqu ds of new validators to add to a vdb, +// determines whether they fit in the ebLimit and shrinks selection if not +func (d *DataAccessService) applyNewValidatorsDSEBFilter(ctx context.Context, newValidatorsDs *goqu.SelectDataset, ebLimit uint64) error { + var newValidators []uint64 + newValidatorsQuery, args, err := newValidatorsDs.Prepared(true).ToSQL() + if err != nil { + return fmt.Errorf("error preparing query: %w", err) + } + + err = d.alloyReader.SelectContext(ctx, &newValidators, newValidatorsQuery, args...) + if err != nil { + return err + } + validatorEbs, err := d.GetValidatorsEffectiveBalances(ctx, newValidators, false) + if err != nil { + return err + } + + filteredValidators := d.applyEBFiler(validatorEbs, ebLimit) + if len(filteredValidators) < len(newValidators) { + //nolint:staticcheck + newValidatorsDs = goqu.Dialect("postgres"). + From(goqu.L("unnest(?::int[])", pq.Array(filteredValidators)).As("validator_index")). + Select("*") + } + return nil +} + +func (d *DataAccessService) getAddValidatorsQuery(uniqueValidatorIndexesQuery *goqu.SelectDataset, dashboardId, groupId uint64) *goqu.InsertDataset { + // 0. update group for existing ones + // 1. get all that would be an option (latest EB only > 0, not present in dashboard yet) up to max available limit + // 2. add non-exited first + // 3. if max eb not reached yet and empty ones available: get exit epoch from vm + // 4. get EB at exit epoch and add until full + + return goqu.Dialect("postgres"). + Insert("users_val_dashboards_validators").Cols("dashboard_id", "group_id", "validator_index"). + With("unique_validator_indexes", uniqueValidatorIndexesQuery). + FromQuery( + goqu.Dialect("postgres"). + From(goqu.I("unique_validator_indexes")). + Select( + goqu.V(dashboardId).As("dashboard_id"), + goqu.V(groupId).As("group_id"), + goqu.L("validatorindex"), + ), + ). + OnConflict(goqu.DoUpdate( + "dashboard_id, validator_index", + goqu.Record{ + "dashboard_id": goqu.L("EXCLUDED.dashboard_id"), + "group_id": goqu.L("EXCLUDED.group_id"), + "validator_index": goqu.L("EXCLUDED.validator_index"), + }, + )). + Returning("validator_index") } func (d *DataAccessService) RemoveValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) error { @@ -999,14 +1180,23 @@ func (d *DataAccessService) RemoveValidatorDashboardValidators(ctx context.Conte return err } -func (d *DataAccessService) GetValidatorDashboardValidatorsCount(ctx context.Context, dashboardId t.VDBIdPrimary) (uint64, error) { - var count uint64 - err := d.alloyReader.GetContext(ctx, &count, ` - SELECT COUNT(*) - FROM users_val_dashboards_validators - WHERE dashboard_id = $1 - `, dashboardId) - return count, err +func (d *DataAccessService) GetValidatorDashboardEffectiveBalanceTotal(ctx context.Context, dashboardId t.VDBId) (uint64, error) { + var validators []t.VDBValidator + + if dashboardId.Validators == nil { + err := d.alloyReader.SelectContext(ctx, &validators, ` + SELECT validator_index + FROM users_val_dashboards_validators + WHERE dashboard_id = $1 + `, dashboardId.Id) + if err != nil { + return 0, err + } + } else { + validators = dashboardId.Validators + } + + return d.GetValidatorsEffectiveBalanceTotal(ctx, validators) } func (d *DataAccessService) CreateValidatorDashboardPublicId(ctx context.Context, dashboardId t.VDBIdPrimary, name string, shareGroups bool) (*t.VDBPublicId, error) { diff --git a/backend/pkg/api/handlers/public.go b/backend/pkg/api/handlers/public.go index fcf3a3883..886979f70 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -611,17 +611,17 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW returnForbidden(w, r, errors.New("bulk adding not allowed with current subscription plan")) return } - dashboardLimit := userInfo.PremiumPerks.ValidatorsPerDashboard - existingValidatorCount, err := h.getDataAccessor(ctx).GetValidatorDashboardValidatorsCount(ctx, dashboardId) + limitEB := userInfo.PremiumPerks.EffectiveBalancePerDashboard + existingEB, err := h.getDataAccessor(ctx).GetValidatorDashboardEffectiveBalanceTotal(ctx, types.VDBId{Id: dashboardId}) if err != nil { handleErr(w, r, err) return } - var limit uint64 + var spaceLeftEB uint64 if isUserAdmin(userInfo) { - limit = math.MaxUint32 // no limit for admins - } else if dashboardLimit >= existingValidatorCount { - limit = dashboardLimit - existingValidatorCount + spaceLeftEB = 105 * 1e9 // math.MaxUint64 // no limit for admins + } else if limitEB > existingEB { + spaceLeftEB = limitEB - existingEB } var data []types.VDBPostValidatorsData @@ -638,10 +638,7 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, err) return } - if len(validators) > int(limit) { - validators = validators[:limit] - } - data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidators(ctx, dashboardId, groupId, validators) + data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidators(ctx, dashboardId, groupId, validators, spaceLeftEB) case req.DepositAddress != "": depositAddress := v.checkRegex(reEthereumAddress, req.DepositAddress, "deposit_address") @@ -649,7 +646,7 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, v) return } - data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByDepositAddress(ctx, dashboardId, groupId, depositAddress, limit) + data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByDepositAddress(ctx, dashboardId, groupId, depositAddress, spaceLeftEB) case req.WithdrawalCredential != "": withdrawalCredential := v.checkRegex(reWithdrawalCredential, req.WithdrawalCredential, "withdrawal_credential") @@ -657,7 +654,7 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, v) return } - data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByWithdrawalCredential(ctx, dashboardId, groupId, withdrawalCredential, limit) + data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByWithdrawalCredential(ctx, dashboardId, groupId, withdrawalCredential, spaceLeftEB) case req.Graffiti != "": graffiti := v.checkRegex(reGraffiti, req.Graffiti, "graffiti") @@ -665,7 +662,7 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, v) return } - data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByGraffiti(ctx, dashboardId, groupId, graffiti, limit) + data, dataErr = h.getDataAccessor(ctx).AddValidatorDashboardValidatorsByGraffiti(ctx, dashboardId, groupId, graffiti, spaceLeftEB) } if dataErr != nil { @@ -1015,7 +1012,7 @@ func (h *HandlerService) PublicPutValidatorDashboardArchiving(w http.ResponseWri returnConflict(w, r, errors.New("maximum number of groups in dashboards reached")) return } - if dashboardInfo.ValidatorCount >= userInfo.PremiumPerks.ValidatorsPerDashboard { + if dashboardInfo.EffectiveBalance >= userInfo.PremiumPerks.EffectiveBalancePerDashboard { returnConflict(w, r, errors.New("maximum number of validators in dashboards reached")) return } diff --git a/backend/pkg/api/types/dashboard.go b/backend/pkg/api/types/dashboard.go index 340a21b01..8ed1568bc 100644 --- a/backend/pkg/api/types/dashboard.go +++ b/backend/pkg/api/types/dashboard.go @@ -5,14 +5,15 @@ type AccountDashboard struct { Name string `json:"name"` } type ValidatorDashboard struct { - Id uint64 `json:"id" extensions:"x-order=1"` - Name string `json:"name" extensions:"x-order=2"` - Network uint64 `json:"network" extensions:"x-order=3"` - PublicIds []VDBPublicId `json:"public_ids,omitempty" extensions:"x-order=4"` - IsArchived bool `json:"is_archived" extensions:"x-order=5"` - ArchivedReason string `json:"archived_reason,omitempty" tstype:"'user' | 'dashboard_limit' | 'validator_limit' | 'group_limit'" extensions:"x-order=6"` - ValidatorCount uint64 `json:"validator_count" extensions:"x-order=7"` - GroupCount uint64 `json:"group_count" extensions:"x-order=8"` + Id uint64 `json:"id" extensions:"x-order=1"` + Name string `json:"name" extensions:"x-order=2"` + Network uint64 `json:"network" extensions:"x-order=3"` + PublicIds []VDBPublicId `json:"public_ids,omitempty" extensions:"x-order=4"` + IsArchived bool `json:"is_archived" extensions:"x-order=5"` + ArchivedReason string `json:"archived_reason,omitempty" tstype:"'user' | 'dashboard_limit' | 'validator_limit' | 'group_limit'" extensions:"x-order=6"` + EffectiveBalance uint64 `json:"effective_balance" extensions:"x-order=7"` + ValidatorCount uint64 `json:"validator_count" extensions:"x-order=8"` + GroupCount uint64 `json:"group_count" extensions:"x-order=9"` } type UserDashboardsData struct { diff --git a/backend/pkg/api/types/search.go b/backend/pkg/api/types/search.go index c57b68cce..2c25d1c7c 100644 --- a/backend/pkg/api/types/search.go +++ b/backend/pkg/api/types/search.go @@ -30,9 +30,10 @@ type SearchValidatorsByGraffiti struct { } type SearchResult struct { - Type string `json:"type"` - ChainId uint64 `json:"chain_id"` - Value interface{} `json:"value"` + Type string `json:"type"` + ChainId uint64 `json:"chain_id"` + TotalEffectiveStake uint64 `json:"total_effective_stake"` + Value interface{} `json:"value"` } type InternalPostSearchResponse struct { diff --git a/backend/pkg/api/types/user.go b/backend/pkg/api/types/user.go index 8e4a30dc7..63ccda660 100644 --- a/backend/pkg/api/types/user.go +++ b/backend/pkg/api/types/user.go @@ -62,7 +62,8 @@ const ProductStoreEthpool ProductStore = "ethpool" const ProductStoreCustom ProductStore = "custom" type ProductSummary struct { - ValidatorsPerDashboardLimit uint64 `json:"validators_per_dashboard_limit"` + ValidatorsPerDashboardLimit uint64 `json:"validators_per_dashboard_limit"` // remove after Pectra + EffectiveBalancePerDashboardLimit uint64 `json:"effective_balance_per_dashboard_limit"` StripePublicKey string `json:"stripe_public_key"` ApiProducts []ApiProduct `json:"api_products"` PremiumProducts []PremiumProduct `json:"premium_products"` @@ -106,20 +107,22 @@ type PremiumProduct struct { } type ExtraDashboardValidatorsPremiumAddon struct { - ProductName string `json:"product_name"` - ExtraDashboardValidators uint64 `json:"extra_dashboard_validators"` - PricePerYearEur float64 `json:"price_per_year_eur"` - PricePerMonthEur float64 `json:"price_per_month_eur"` - ProductIdMonthly string `json:"product_id_monthly"` - ProductIdYearly string `json:"product_id_yearly"` - StripePriceIdMonthly string `json:"stripe_price_id_monthly"` - StripePriceIdYearly string `json:"stripe_price_id_yearly"` + ProductName string `json:"product_name"` + ExtraDashboardValidators uint64 `json:"extra_dashboard_validators"` // remove after Pectra + ExtraDashboardEffectiveBalance uint64 `json:"extra_dashboard_effective_balance"` + PricePerYearEur float64 `json:"price_per_year_eur"` + PricePerMonthEur float64 `json:"price_per_month_eur"` + ProductIdMonthly string `json:"product_id_monthly"` + ProductIdYearly string `json:"product_id_yearly"` + StripePriceIdMonthly string `json:"stripe_price_id_monthly"` + StripePriceIdYearly string `json:"stripe_price_id_yearly"` } type PremiumPerks struct { AdFree bool `json:"ad_free"` // note that this is somhow redunant, since there is already ApiPerks.NoAds ValidatorDashboards uint64 `json:"validator_dashboards"` - ValidatorsPerDashboard uint64 `json:"validators_per_dashboard"` + ValidatorsPerDashboard uint64 `json:"validators_per_dashboard"` // remove after Pectra + EffectiveBalancePerDashboard uint64 `json:"effective_balance_per_dashboard"` ValidatorGroupsPerDashboard uint64 `json:"validator_groups_per_dashboard"` ShareCustomDashboards bool `json:"share_custom_dashboards"` ManageDashboardViaApi bool `json:"manage_dashboard_via_api"` diff --git a/backend/pkg/commons/config/config.go b/backend/pkg/commons/config/config.go index fac99387c..019231b08 100644 --- a/backend/pkg/commons/config/config.go +++ b/backend/pkg/commons/config/config.go @@ -37,3 +37,6 @@ var MekongChainYml string //go:embed pectra-devnet-5.chain.yml var PectraDevnet5ChainYml string + +//go:embed pectra-devnet-6.chain.yml +var PectraDevnet6ChainYml string diff --git a/backend/pkg/commons/config/pectra-devnet-6.chain.yml b/backend/pkg/commons/config/pectra-devnet-6.chain.yml new file mode 100644 index 000000000..36931cb9b --- /dev/null +++ b/backend/pkg/commons/config/pectra-devnet-6.chain.yml @@ -0,0 +1,172 @@ +# Extends the mainnet preset +PRESET_BASE: mainnet +CONFIG_NAME: testnet # needs to exist because of Prysm. Otherwise it conflicts with mainnet genesis + +# Genesis +# --------------------------------------------------------------- +# `2**14` (= 16,384) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 71000 +# 2025-Feb-03 05:30:00 PM UTC +MIN_GENESIS_TIME: 1738603800 +GENESIS_FORK_VERSION: 0x10585557 +GENESIS_DELAY: 60 + + +# Forking +# --------------------------------------------------------------- +# Some forks are disabled for now: +# - These may be re-assigned to another fork-version later +# - Temporarily set to max uint64 value: 2**64 - 1 + +# Altair +ALTAIR_FORK_VERSION: 0x20585557 +ALTAIR_FORK_EPOCH: 0 +# Merge +BELLATRIX_FORK_VERSION: 0x30585557 +BELLATRIX_FORK_EPOCH: 0 +TERMINAL_TOTAL_DIFFICULTY: 0 +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + +# Capella +CAPELLA_FORK_VERSION: 0x40585557 +CAPELLA_FORK_EPOCH: 0 + +# DENEB +DENEB_FORK_VERSION: 0x50585557 +DENEB_FORK_EPOCH: 0 + +# Electra +ELECTRA_FORK_VERSION: 0x60585557 +ELECTRA_FORK_EPOCH: 10 + +# Fulu +FULU_FORK_VERSION: 0x70585557 +FULU_FORK_EPOCH: 999999 + +# EIP7732 +EIP7732_FORK_VERSION: 0x80000000 +EIP7732_FORK_EPOCH: 99999 + +# EIP7805 +EIP7805_FORK_VERSION: 0x90000000 +EIP7805_FORK_EPOCH: 99999 + +# Time parameters +# --------------------------------------------------------------- +# 12 seconds +SECONDS_PER_SLOT: 12 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 12 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 2 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**11 (= 2,048) Eth1 blocks ~8 hours +ETH1_FOLLOW_DISTANCE: 2048 + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 128 +# [New in Deneb:EIP7514] 2**3 (= 8) +MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 +# 20% +REORG_HEAD_WEIGHT_THRESHOLD: 20 +# 160% +REORG_PARENT_WEIGHT_THRESHOLD: 160 +# `2` epochs +REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + +# Deposit contract +# --------------------------------------------------------------- +DEPOSIT_CHAIN_ID: 7072151312 +DEPOSIT_NETWORK_ID: 7072151312 +DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +GOSSIP_MAX_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# `10 * 2**20` (=10485760, 10 MiB) +MAX_CHUNK_SIZE: 10485760 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 +## `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 + +# Electra +# 2**7 * 10**9 (= 128,000,000,000) +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 +# `9` +BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 +# `uint64(6)` +TARGET_BLOBS_PER_BLOCK_ELECTRA: 6 +# `uint64(9)` +MAX_BLOBS_PER_BLOCK_ELECTRA: 9 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 + +# Whisk +# `Epoch(2**8)` +WHISK_EPOCHS_PER_SHUFFLING_PHASE: 256 +# `Epoch(2)` +WHISK_PROPOSER_SELECTION_GAP: 2 + +# Fulu +NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 +MAX_BLOBS_PER_BLOCK_FULU: 12 +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + +# EIP7732 +MAX_REQUEST_PAYLOADS: 128 \ No newline at end of file diff --git a/backend/pkg/commons/db/migrations/postgres/20250131124812_change_network_data_type.sql b/backend/pkg/commons/db/migrations/postgres/20250131124812_change_network_data_type.sql new file mode 100644 index 000000000..fb661f650 --- /dev/null +++ b/backend/pkg/commons/db/migrations/postgres/20250131124812_change_network_data_type.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE users_val_dashboards ALTER COLUMN network TYPE BIGINT +ALTER TABLE network_notifications_history ALTER COLUMN network TYPE BIGINT +-- +goose StatementEnd + +-- +goose Down +SELECT 'down SQL query - we do not revert the network id to SMALLINT as this could cause an out of range error'; +-- +goose StatementBegin +-- +goose StatementEnd diff --git a/backend/pkg/commons/db/user.go b/backend/pkg/commons/db/user.go index f3de874d0..eec2bbdbd 100644 --- a/backend/pkg/commons/db/user.go +++ b/backend/pkg/commons/db/user.go @@ -24,13 +24,14 @@ const maxJsInt uint64 = 9007199254740991 // 2^53-1 (max safe int in JS) var freeTierProduct t.PremiumProduct = t.PremiumProduct{ ProductName: "Free", PremiumPerks: t.PremiumPerks{ - AdFree: false, - ValidatorDashboards: 1, - ValidatorsPerDashboard: 20, - ValidatorGroupsPerDashboard: 1, - ShareCustomDashboards: false, - ManageDashboardViaApi: false, - BulkAdding: false, + AdFree: false, + ValidatorDashboards: 1, + ValidatorsPerDashboard: 20, + EffectiveBalancePerDashboard: 20 * 1e9, + ValidatorGroupsPerDashboard: 1, + ShareCustomDashboards: false, + ManageDashboardViaApi: false, + BulkAdding: false, ChartHistorySeconds: t.ChartHistorySeconds{ Epoch: 0, Hourly: 12 * hour, @@ -55,13 +56,14 @@ var freeTierProduct t.PremiumProduct = t.PremiumProduct{ } var adminPerks = t.PremiumPerks{ - AdFree: false, // admins want to see ads to check ad configuration - ValidatorDashboards: maxJsInt, - ValidatorsPerDashboard: maxJsInt, - ValidatorGroupsPerDashboard: maxJsInt, - ShareCustomDashboards: true, - ManageDashboardViaApi: true, - BulkAdding: true, + AdFree: false, // admins want to see ads to check ad configuration + ValidatorDashboards: maxJsInt, + ValidatorsPerDashboard: maxJsInt, + EffectiveBalancePerDashboard: maxJsInt, + ValidatorGroupsPerDashboard: maxJsInt, + ShareCustomDashboards: true, + ManageDashboardViaApi: true, + BulkAdding: true, ChartHistorySeconds: t.ChartHistorySeconds{ Epoch: maxJsInt, Hourly: maxJsInt, @@ -233,6 +235,7 @@ func GetUserInfo(ctx context.Context, userId uint64, userDbReader *sqlx.DB) (*t. foundAddon = true for i := 0; i < addon.Quantity; i++ { userInfo.PremiumPerks.ValidatorsPerDashboard += p.ExtraDashboardValidators + userInfo.PremiumPerks.EffectiveBalancePerDashboard += p.ExtraDashboardEffectiveBalance userInfo.Subscriptions = append(userInfo.Subscriptions, t.UserSubscription{ ProductId: utils.PriceIdToProductId(addon.PriceId), ProductName: p.ProductName, @@ -253,6 +256,10 @@ func GetUserInfo(ctx context.Context, userId uint64, userDbReader *sqlx.DB) (*t. userInfo.PremiumPerks.ValidatorsPerDashboard = productSummary.ValidatorsPerDashboardLimit } + if productSummary.EffectiveBalancePerDashboardLimit < userInfo.PremiumPerks.EffectiveBalancePerDashboard { + userInfo.PremiumPerks.EffectiveBalancePerDashboard = productSummary.EffectiveBalancePerDashboardLimit + } + if userInfo.UserGroup == t.UserGroupAdmin { userInfo.PremiumPerks = adminPerks } @@ -335,13 +342,14 @@ func GetProductSummary(ctx context.Context) (*t.ProductSummary, error) { // TODO { ProductName: "Guppy", PremiumPerks: t.PremiumPerks{ - AdFree: true, - ValidatorDashboards: 1, - ValidatorsPerDashboard: 100, - ValidatorGroupsPerDashboard: 3, - ShareCustomDashboards: true, - ManageDashboardViaApi: false, - BulkAdding: true, + AdFree: true, + ValidatorDashboards: 1, + ValidatorsPerDashboard: 100, + EffectiveBalancePerDashboard: uint64(100 * utils.Config.Frontend.ClCurrencyDivisor), + ValidatorGroupsPerDashboard: 3, + ShareCustomDashboards: true, + ManageDashboardViaApi: false, + BulkAdding: true, ChartHistorySeconds: t.ChartHistorySeconds{ Epoch: day, Hourly: 7 * day, @@ -369,13 +377,14 @@ func GetProductSummary(ctx context.Context) (*t.ProductSummary, error) { // TODO { ProductName: "Dolphin", PremiumPerks: t.PremiumPerks{ - AdFree: true, - ValidatorDashboards: 2, - ValidatorsPerDashboard: 300, - ValidatorGroupsPerDashboard: 10, - ShareCustomDashboards: true, - ManageDashboardViaApi: false, - BulkAdding: true, + AdFree: true, + ValidatorDashboards: 2, + ValidatorsPerDashboard: 300, + EffectiveBalancePerDashboard: uint64(300 * utils.Config.Frontend.ClCurrencyDivisor), + ValidatorGroupsPerDashboard: 10, + ShareCustomDashboards: true, + ManageDashboardViaApi: false, + BulkAdding: true, ChartHistorySeconds: t.ChartHistorySeconds{ Epoch: 5 * day, Hourly: month, @@ -403,13 +412,14 @@ func GetProductSummary(ctx context.Context) (*t.ProductSummary, error) { // TODO { ProductName: "Orca", PremiumPerks: t.PremiumPerks{ - AdFree: true, - ValidatorDashboards: 2, - ValidatorsPerDashboard: 1000, - ValidatorGroupsPerDashboard: 30, - ShareCustomDashboards: true, - ManageDashboardViaApi: true, - BulkAdding: true, + AdFree: true, + ValidatorDashboards: 2, + ValidatorsPerDashboard: 1000, + EffectiveBalancePerDashboard: uint64(1000 * utils.Config.Frontend.ClCurrencyDivisor), + ValidatorGroupsPerDashboard: 30, + ShareCustomDashboards: true, + ManageDashboardViaApi: true, + BulkAdding: true, ChartHistorySeconds: t.ChartHistorySeconds{ Epoch: 3 * week, Hourly: 6 * month, diff --git a/backend/pkg/commons/utils/config.go b/backend/pkg/commons/utils/config.go index 0913e619e..d45347110 100644 --- a/backend/pkg/commons/utils/config.go +++ b/backend/pkg/commons/utils/config.go @@ -133,6 +133,10 @@ func ReadConfig(cfg *types.Config, path string) error { cfg.Chain.GenesisTimestamp = 1638993340 case "holesky": cfg.Chain.GenesisTimestamp = 1695902400 + case "pectra-devnet-5": + cfg.Chain.GenesisTimestamp = 1737034260 + case "pectra-devnet-6": + cfg.Chain.GenesisTimestamp = 1738603860 default: return fmt.Errorf("tried to set known genesis-timestamp, but unknown chain-name") } @@ -345,6 +349,10 @@ func setCLConfig(cfg *types.Config) error { err = yaml.Unmarshal([]byte(config.GnosisChainYml), &cfg.Chain.ClConfig) case "holesky": err = yaml.Unmarshal([]byte(config.HoleskyChainYml), &cfg.Chain.ClConfig) + case "pectra-devnet-5": + err = yaml.Unmarshal([]byte(config.PectraDevnet5ChainYml), &cfg.Chain.ClConfig) + case "pectra-devnet-6": + err = yaml.Unmarshal([]byte(config.PectraDevnet6ChainYml), &cfg.Chain.ClConfig) default: return fmt.Errorf("tried to set known chain-config, but unknown chain-name: %v (path: %v)", cfg.Chain.Name, cfg.Chain.ClConfigPath) } diff --git a/backend/pkg/commons/utils/units.go b/backend/pkg/commons/utils/units.go index ec92d6f47..3978942b8 100644 --- a/backend/pkg/commons/utils/units.go +++ b/backend/pkg/commons/utils/units.go @@ -30,3 +30,7 @@ func GWeiToWei(gwei *big.Int) decimal.Decimal { func GWeiBytesToWei(gwei []byte) decimal.Decimal { return GWeiToWei(new(big.Int).SetBytes(gwei)) } + +func EtherToGwei(ether *big.Int) decimal.Decimal { + return decimal.NewFromBigInt(ether, 0).Mul(decimal.NewFromInt(params.GWei)) +}