From 1c01012fdb40570f55852f0cf45539161b26ea49 Mon Sep 17 00:00:00 2001 From: Lucca Dukic <109136188+LuccaBitfly@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:32:49 +0200 Subject: [PATCH 1/2] feat: add pectra EL deposits --- backend/pkg/api/data_access/dummy.go | 4 ++ backend/pkg/api/data_access/vdb.go | 1 + backend/pkg/api/data_access/vdb_deposits.go | 5 +++ .../api/enums/validator_dashboard_enums.go | 35 +++++++++++++++ .../pkg/api/handlers/validator_dashboard.go | 43 +++++++++++++++++++ backend/pkg/api/router.go | 28 +++++++++++- backend/pkg/api/types/validator_dashboard.go | 15 +++++++ backend/pkg/commons/types/config.go | 3 +- frontend/types/api/validator_dashboard.ts | 13 ++++++ 9 files changed, 145 insertions(+), 2 deletions(-) diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index c874b503a8..d7d6bd86b7 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -428,6 +428,10 @@ func (*DummyService) GetValidatorDashboardElDeposits(ctx context.Context, dashbo return getDummyWithPaging[t.VDBExecutionDepositsTableRow](ctx) } +func (*DummyService) GetValidatorDashboardElDeposits_FeaturePectra(ctx context.Context, dashboardId t.VDBId, cursor string, colSort t.Sort[enums.VDBDepositsElColumn], search string, limit uint64) ([]t.VDBExecutionDepositsTableRow_FeaturePectra, *t.Paging, error) { + return getDummyWithPaging[t.VDBExecutionDepositsTableRow_FeaturePectra](ctx) +} + func (*DummyService) GetValidatorDashboardClDeposits(ctx context.Context, dashboardId t.VDBId, cursor string, limit uint64) ([]t.VDBConsensusDepositsTableRow, *t.Paging, error) { return getDummyWithPaging[t.VDBConsensusDepositsTableRow](ctx) } diff --git a/backend/pkg/api/data_access/vdb.go b/backend/pkg/api/data_access/vdb.go index e02b9bdc7b..70f5910fbc 100644 --- a/backend/pkg/api/data_access/vdb.go +++ b/backend/pkg/api/data_access/vdb.go @@ -66,6 +66,7 @@ type ValidatorDashboardRepository interface { GetValidatorDashboardGroupHeatmap(ctx context.Context, dashboardId t.VDBId, groupId uint64, protocolModes t.VDBProtocolModes, aggregation enums.ChartAggregation, timestamp uint64) (*t.VDBHeatmapTooltipData, error) GetValidatorDashboardElDeposits(ctx context.Context, dashboardId t.VDBId, cursor string, limit uint64) ([]t.VDBExecutionDepositsTableRow, *t.Paging, error) + GetValidatorDashboardElDeposits_FeaturePectra(ctx context.Context, dashboardId t.VDBId, cursor string, colSort t.Sort[enums.VDBDepositsElColumn], search string, limit uint64) ([]t.VDBExecutionDepositsTableRow_FeaturePectra, *t.Paging, error) GetValidatorDashboardClDeposits(ctx context.Context, dashboardId t.VDBId, cursor string, limit uint64) ([]t.VDBConsensusDepositsTableRow, *t.Paging, error) GetValidatorDashboardTotalElDeposits(ctx context.Context, dashboardId t.VDBId) (*t.VDBTotalExecutionDepositsData, error) GetValidatorDashboardTotalClDeposits(ctx context.Context, dashboardId t.VDBId) (*t.VDBTotalConsensusDepositsData, error) diff --git a/backend/pkg/api/data_access/vdb_deposits.go b/backend/pkg/api/data_access/vdb_deposits.go index de6972fe0f..ceb9c61715 100644 --- a/backend/pkg/api/data_access/vdb_deposits.go +++ b/backend/pkg/api/data_access/vdb_deposits.go @@ -11,6 +11,7 @@ import ( "github.com/doug-martin/goqu/v9" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/gobitfly/beaconchain/pkg/api/enums" t "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gobitfly/beaconchain/pkg/commons/db" "github.com/gobitfly/beaconchain/pkg/commons/types" @@ -220,6 +221,10 @@ func (d *DataAccessService) GetValidatorDashboardElDeposits(ctx context.Context, return responseData, p, nil } +func (d *DataAccessService) GetValidatorDashboardElDeposits_FeaturePectra(ctx context.Context, dashboardId t.VDBId, cursor string, colSort t.Sort[enums.VDBDepositsElColumn], search string, limit uint64) ([]t.VDBExecutionDepositsTableRow_FeaturePectra, *t.Paging, error) { + return d.dummy.GetValidatorDashboardElDeposits_FeaturePectra(ctx, dashboardId, cursor, colSort, search, limit) +} + func (d *DataAccessService) GetValidatorDashboardClDeposits(ctx context.Context, dashboardId t.VDBId, cursor string, limit uint64) ([]t.VDBConsensusDepositsTableRow, *t.Paging, error) { // TODO: add default sorting var err error diff --git a/backend/pkg/api/enums/validator_dashboard_enums.go b/backend/pkg/api/enums/validator_dashboard_enums.go index f013af68c7..4b37f4a3f7 100644 --- a/backend/pkg/api/enums/validator_dashboard_enums.go +++ b/backend/pkg/api/enums/validator_dashboard_enums.go @@ -298,6 +298,41 @@ var VDBManageValidatorsColumns = struct { VDBManageValidatorsWithdrawalCredential, } +// ---------------- +// Validator Dashboard EL Deposits Table + +type VDBDepositsElColumn int + +var _ EnumFactory[VDBDepositsElColumn] = VDBDepositsElColumn(0) + +const ( + VDBDepositElBlock VDBDepositsElColumn = iota + VDBDepositElAmount +) + +func (c VDBDepositsElColumn) Int() int { + return int(c) +} + +func (VDBDepositsElColumn) NewFromString(s string) VDBDepositsElColumn { + switch s { + case "", "block", "timestamp": + return VDBDepositElBlock + case "amount": + return VDBDepositElAmount + default: + return VDBDepositsElColumn(-1) + } +} + +var VDBDepositsElColumns = struct { + Block VDBDepositsElColumn + Amount VDBDepositsElColumn +}{ + VDBDepositElBlock, + VDBDepositElAmount, +} + // ---------------- // Validator Dashboard Archived Reasons diff --git a/backend/pkg/api/handlers/validator_dashboard.go b/backend/pkg/api/handlers/validator_dashboard.go index 44f60234f0..2e888aa2d0 100644 --- a/backend/pkg/api/handlers/validator_dashboard.go +++ b/backend/pkg/api/handlers/validator_dashboard.go @@ -249,3 +249,46 @@ func (h *HandlerService) GetValidatorDashboardSummaryChart(ctx context.Context, Data: *data, }, nil } + +// GetValidatorDashboardExecutionLayerDeposits godoc +// +// @Description Get execution layer deposits information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param cursor query string false "Return data for the given cursor value. Pass the `paging.next_cursor`` value of the previous response to navigate to forward, or pass the `paging.prev_cursor`` value of the previous response to navigate to backward." +// @Param limit query string false "The maximum number of results that may be returned." +// @Param sort query string false "The field you want to sort by. Append with `:desc` for descending order." Enums(block, amount) +// @Param search query string false "Search for Index, Block, Address, Group." +// @Success 200 {object} types.GetValidatorDashboardExecutionLayerDepositsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/execution-layer-deposits [get] +func (i *inputGetValidatorDashboardExecutionLayerDeposits) Validate(params map[string]string, _ io.ReadCloser) error { + var v validationError + i.Paging = v.checkPagingMap(params) + i.sort = *checkSort[enums.VDBDepositsElColumn](&v, params["sort"]) + i.dashboardIdParam = v.checkDashboardId(params["dashboard_id"]) + return v.AsError() +} + +type inputGetValidatorDashboardExecutionLayerDeposits struct { + Paging + sort types.Sort[enums.VDBDepositsElColumn] + dashboardIdParam interface{} +} + +func (h *HandlerService) GetValidatorDashboardExecutionLayerDeposits(ctx context.Context, input inputGetValidatorDashboardExecutionLayerDeposits) (types.GetValidatorDashboardExecutionLayerDepositsResponse_FeaturePectra, error) { + var r types.GetValidatorDashboardExecutionLayerDepositsResponse_FeaturePectra + dashboardId, err := h.getDashboardId(ctx, input.dashboardIdParam) + if err != nil { + return r, err + } + + data, paging, err := h.getDataAccessor(ctx).GetValidatorDashboardElDeposits_FeaturePectra(ctx, *dashboardId, input.cursor, input.sort, input.search, input.limit) + if err != nil { + return r, err + } + r.Data = data + r.Paging = *paging + return r, nil +} diff --git a/backend/pkg/api/router.go b/backend/pkg/api/router.go index 447f79f93d..999218e043 100644 --- a/backend/pkg/api/router.go +++ b/backend/pkg/api/router.go @@ -3,6 +3,8 @@ package api import ( "net/http" "regexp" + "slices" + "strings" dataaccess "github.com/gobitfly/beaconchain/pkg/api/data_access" "github.com/gobitfly/beaconchain/pkg/api/docs" @@ -285,6 +287,14 @@ func addValidatorDashboardRoutes(hs *handlers.HandlerService, publicRouter, inte } const allowMocking = true + // on prod we only allow feature flags that are in the allowed list + // otherwise we allow all feature flags + isFeatureAllowedFunc := func(featureFlag string) bool { + if cfg.DeploymentType == "production" { + return slices.Contains(cfg.AllowedFeatureFlags, featureFlag) + } + return true + } endpoints := []endpoint{ {http.MethodGet, "/{dashboard_id}", hs.PublicGetValidatorDashboard, hs.InternalGetValidatorDashboard}, {http.MethodPut, "/{dashboard_id}/name", hs.PublicPutValidatorDashboardName, hs.InternalPutValidatorDashboardName}, @@ -309,7 +319,12 @@ func addValidatorDashboardRoutes(hs *handlers.HandlerService, publicRouter, inte {http.MethodGet, "/{dashboard_id}/rewards-chart", hs.PublicGetValidatorDashboardRewardsChart, hs.InternalGetValidatorDashboardRewardsChart}, {http.MethodGet, "/{dashboard_id}/duties/{epoch}", hs.PublicGetValidatorDashboardDuties, hs.InternalGetValidatorDashboardDuties}, {http.MethodGet, "/{dashboard_id}/blocks", hs.PublicGetValidatorDashboardBlocks, hs.InternalGetValidatorDashboardBlocks}, - {http.MethodGet, "/{dashboard_id}/execution-layer-deposits", hs.PublicGetValidatorDashboardExecutionLayerDeposits, hs.InternalGetValidatorDashboardExecutionLayerDeposits}, + {http.MethodGet, "/{dashboard_id}/execution-layer-deposits", hs.PublicGetValidatorDashboardExecutionLayerDeposits, + featureFlagToggle(isFeatureAllowedFunc, "feature-pectra", + hs.InternalGetValidatorDashboardExecutionLayerDeposits, // legacy + handlers.Handle(http.StatusOK, hs.GetValidatorDashboardExecutionLayerDeposits, allowMocking), // feature + ), + }, {http.MethodGet, "/{dashboard_id}/consensus-layer-deposits", hs.PublicGetValidatorDashboardConsensusLayerDeposits, hs.InternalGetValidatorDashboardConsensusLayerDeposits}, {http.MethodGet, "/{dashboard_id}/total-execution-layer-deposits", hs.PublicGetValidatorDashboardTotalExecutionLayerDeposits, hs.InternalGetValidatorDashboardTotalExecutionLayerDeposits}, {http.MethodGet, "/{dashboard_id}/total-consensus-layer-deposits", hs.PublicGetValidatorDashboardTotalConsensusLayerDeposits, hs.InternalGetValidatorDashboardTotalConsensusLayerDeposits}, @@ -324,6 +339,17 @@ func addValidatorDashboardRoutes(hs *handlers.HandlerService, publicRouter, inte addEndpointsToRouters(endpoints, publicDashboardRouter, internalDashboardRouter) } +func featureFlagToggle(isFeatureAllowedFunc func(string) bool, featureFlag string, legacyHandler, featureHandler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + isFeatureRequested := strings.Contains(r.URL.Query().Get("feature_flags"), featureFlag) + if isFeatureRequested && isFeatureAllowedFunc(featureFlag) { + featureHandler(w, r) + return + } + legacyHandler(w, r) + } +} + func addNotificationRoutes(hs *handlers.HandlerService, publicRouter, internalRouter *mux.Router, debug bool) { path := "/users/me/notifications" publicNotificationRouter := publicRouter.PathPrefix(path).Subrouter() diff --git a/backend/pkg/api/types/validator_dashboard.go b/backend/pkg/api/types/validator_dashboard.go index 0f934b2b8b..1cc92f8db8 100644 --- a/backend/pkg/api/types/validator_dashboard.go +++ b/backend/pkg/api/types/validator_dashboard.go @@ -244,6 +244,21 @@ type VDBExecutionDepositsTableRow struct { } type GetValidatorDashboardExecutionLayerDepositsResponse ApiPagingResponse[VDBExecutionDepositsTableRow] +type VDBExecutionDepositsTableRow_FeaturePectra struct { + PublicKey PubKey `json:"public_key" faker:"pubkey"` + Index *uint64 `json:"index,omitempty"` + GroupId uint64 `json:"group_id"` + Block uint64 `json:"block"` + Timestamp int64 `json:"timestamp" faker:"past_timestamp"` + From Address `json:"-"` // TODO enable again + Depositor Address `json:"depositor"` + TxHash Hash `json:"tx_hash" faker:"tx_hash"` + WithdrawalCredential Hash `json:"withdrawal_credential" faker:"withdrawal_credentials"` + Amount decimal.Decimal `json:"amount" faker:"eth"` + Validity string `json:"validity" tstype:"'valid' | 'invalid' | 'invalid_skipped'" faker:"oneof: valid, invalid, invalid_skipped"` +} +type GetValidatorDashboardExecutionLayerDepositsResponse_FeaturePectra ApiPagingResponse[VDBExecutionDepositsTableRow_FeaturePectra] + type VDBConsensusDepositsTableRow struct { PublicKey PubKey `json:"public_key"` Index uint64 `json:"index"` diff --git a/backend/pkg/commons/types/config.go b/backend/pkg/commons/types/config.go index e2d2fcd5f8..b84a4656ab 100644 --- a/backend/pkg/commons/types/config.go +++ b/backend/pkg/commons/types/config.go @@ -231,7 +231,8 @@ type Config struct { ApiKeySecret string `yaml:"apiKeySecret" env:"API_KEY_SECRET"` CorsAllowedHosts []string `yaml:"corsAllowedHosts" env:"CORS_ALLOWED_HOSTS"` - SkipDataAccessServiceInitWait bool `yaml:"skipDataAccessServiceInitWait" env:"SKIP_DATA_ACCESS_SERVICE_INIT_WAIT"` + SkipDataAccessServiceInitWait bool `yaml:"skipDataAccessServiceInitWait" env:"SKIP_DATA_ACCESS_SERVICE_INIT_WAIT"` + AllowedFeatureFlags []string `yaml:"allowedFeatureFlags" env:"ALLOWED_FEATURE_FLAGS"` } type Chain struct { diff --git a/frontend/types/api/validator_dashboard.ts b/frontend/types/api/validator_dashboard.ts index e7e0beaea3..a14a874431 100644 --- a/frontend/types/api/validator_dashboard.ts +++ b/frontend/types/api/validator_dashboard.ts @@ -210,6 +210,19 @@ export interface VDBExecutionDepositsTableRow { valid: boolean; } export type GetValidatorDashboardExecutionLayerDepositsResponse = ApiPagingResponse; +export interface VDBExecutionDepositsTableRow_FeaturePectra { + public_key: PubKey; + index?: number /* uint64 */; + group_id: number /* uint64 */; + block: number /* uint64 */; + timestamp: number /* int64 */; + depositor: Address; + tx_hash: Hash; + withdrawal_credential: Hash; + amount: string /* decimal.Decimal */; + validity: 'valid' | 'invalid' | 'invalid_skipped'; +} +export type GetValidatorDashboardExecutionLayerDepositsResponse_FeaturePectra = ApiPagingResponse; export interface VDBConsensusDepositsTableRow { public_key: PubKey; index: number /* uint64 */; From 5b3cd18186f1fce4fee37215ff2a8a5c58bd0ab1 Mon Sep 17 00:00:00 2001 From: Lucca Dukic <109136188+LuccaBitfly@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:13:38 +0200 Subject: [PATCH 2/2] test: featureFlagToggle --- backend/pkg/api/router_test.go | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 backend/pkg/api/router_test.go diff --git a/backend/pkg/api/router_test.go b/backend/pkg/api/router_test.go new file mode 100644 index 0000000000..45e048e851 --- /dev/null +++ b/backend/pkg/api/router_test.go @@ -0,0 +1,76 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFeatureFlagToggle(t *testing.T) { + tests := []struct { + name string + requestFeature bool + isFeatureAllowed bool + expectFeature bool + }{ + { + name: "Feature requested and allowed", + requestFeature: true, + isFeatureAllowed: true, + expectFeature: true, + }, + { + name: "Feature requested but not allowed", + requestFeature: true, + isFeatureAllowed: false, + expectFeature: false, + }, + { + name: "Feature not requested and allowed", + requestFeature: false, + isFeatureAllowed: true, + expectFeature: false, + }, + { + name: "Feature not requested and not allowed", + requestFeature: false, + isFeatureAllowed: false, + expectFeature: false, + }, + } + + const featureFlag = "new_feature" + featureHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + legacyHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + } + for _, tt := range tests { + isFeatureAllowedFunc := func(string) bool { + return tt.isFeatureAllowed + } + t.Run(tt.name, func(t *testing.T) { + var queryParam string + if tt.requestFeature { + queryParam = "?feature_flags=" + featureFlag + } + req := httptest.NewRequest(http.MethodGet, "/"+queryParam, nil) + w := httptest.NewRecorder() + + handler := featureFlagToggle(isFeatureAllowedFunc, featureFlag, legacyHandler, featureHandler) + handler(w, req) + + resp := w.Result() + defer resp.Body.Close() + + if tt.expectFeature { + assert.Equal(t, http.StatusOK, resp.StatusCode) + } else { + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + }) + } +}