Skip to content
Open
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: 4 additions & 0 deletions backend/pkg/api/data_access/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions backend/pkg/api/data_access/vdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions backend/pkg/api/data_access/vdb_deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions backend/pkg/api/enums/validator_dashboard_enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions backend/pkg/api/handlers/validator_dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
28 changes: 27 additions & 1 deletion backend/pkg/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand All @@ -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()
Expand Down
76 changes: 76 additions & 0 deletions backend/pkg/api/router_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
15 changes: 15 additions & 0 deletions backend/pkg/api/types/validator_dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
3 changes: 2 additions & 1 deletion backend/pkg/commons/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions frontend/types/api/validator_dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,19 @@ export interface VDBExecutionDepositsTableRow {
valid: boolean;
}
export type GetValidatorDashboardExecutionLayerDepositsResponse = ApiPagingResponse<VDBExecutionDepositsTableRow>;
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<VDBExecutionDepositsTableRow_FeaturePectra>;
export interface VDBConsensusDepositsTableRow {
public_key: PubKey;
index: number /* uint64 */;
Expand Down