diff --git a/.github/workflows/backend-integration-test.yml b/.github/workflows/backend-integration-test.yml index 48243480e..c95d43c70 100644 --- a/.github/workflows/backend-integration-test.yml +++ b/.github/workflows/backend-integration-test.yml @@ -33,7 +33,9 @@ jobs: cache-dependency-path: 'backend/go.sum' - name: Test with the Go CLI working-directory: backend - run: go test -failfast ./pkg/api/... -config "${{ secrets.CI_CONFIG_PATH }}" + run: + go install github.com/swaggo/swag/cmd/swag@latest && swag init --ot json -o ./pkg/api/docs -d ./pkg/api/ -g ./handlers/public.go + go test -failfast ./pkg/api/... -config "${{ secrets.CI_CONFIG_PATH }}" diff --git a/backend/.gitignore b/backend/.gitignore index 7006633d8..b5f10c4da 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -5,4 +5,5 @@ local_deployment/config.yml local_deployment/elconfig.json local_deployment/.env __gitignore -cmd/playground \ No newline at end of file +cmd/playground +pkg/api/docs/swagger.json diff --git a/backend/Makefile b/backend/Makefile index 01c3705ba..dd099490d 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -10,8 +10,8 @@ CGO_CFLAGS_ALLOW="-O -D__BLST_PORTABLE__" all: mkdir -p bin + go install github.com/swaggo/swag/cmd/swag@latest && swag init --ot json -o ./pkg/api/docs -d ./pkg/api/ -g ./handlers/public.go CGO_CFLAGS=${CGO_CFLAGS} CGO_CFLAGS_ALLOW=${CGO_CFLAGS_ALLOW} go build --ldflags=${LDFLAGS} -o ./bin/bc ./cmd/main.go - clean: rm -rf bin diff --git a/backend/cmd/eth1indexer/main.go b/backend/cmd/eth1indexer/main.go index badcc7745..d6198b22a 100644 --- a/backend/cmd/eth1indexer/main.go +++ b/backend/cmd/eth1indexer/main.go @@ -189,6 +189,10 @@ func Run() { }() } + if *enableEnsUpdater { + go ImportEnsUpdatesLoop(bt, client, *ensBatchSize) + } + if *enableFullBalanceUpdater { ProcessMetadataUpdates(bt, client, balanceUpdaterPrefix, *balanceUpdaterBatchSize, -1) return @@ -375,14 +379,6 @@ func Run() { ProcessMetadataUpdates(bt, client, balanceUpdaterPrefix, *balanceUpdaterBatchSize, 10) } - if *enableEnsUpdater { - err := bt.ImportEnsUpdates(client.GetNativeClient(), *ensBatchSize) - if err != nil { - log.Error(err, "error importing ens updates", 0, nil) - continue - } - } - log.Infof("index run completed") services.ReportStatus("eth1indexer", "Running", nil) } @@ -390,6 +386,19 @@ func Run() { // utils.WaitForCtrlC() } +func ImportEnsUpdatesLoop(bt *db.Bigtable, client *rpc.ErigonClient, batchSize int64) { + time.Sleep(time.Second * 5) + for { + err := bt.ImportEnsUpdates(client.GetNativeClient(), batchSize) + if err != nil { + log.Error(err, "error importing ens updates", 0, nil) + } else { + services.ReportStatus("ensIndexer", "Running", nil) + } + time.Sleep(time.Second * 5) + } +} + func UpdateTokenPrices(bt *db.Bigtable, client *rpc.ErigonClient, tokenListPath string) error { tokenListContent, err := os.ReadFile(tokenListPath) if err != nil { diff --git a/backend/cmd/typescript_converter/main.go b/backend/cmd/typescript_converter/main.go index 2a3ce86f5..da6fa5be9 100644 --- a/backend/cmd/typescript_converter/main.go +++ b/backend/cmd/typescript_converter/main.go @@ -21,7 +21,7 @@ const ( ) // Files that should not be converted to TypeScript -var ignoredFiles = []string{"data_access", "search_types"} +var ignoredFiles = []string{"data_access", "search_types", "archiver"} var typeMappings = map[string]string{ "decimal.Decimal": "string /* decimal.Decimal */", @@ -31,7 +31,7 @@ var typeMappings = map[string]string{ // Expects the following flags: // -out: Output folder for the generated TypeScript file -// Standard usage (execute in backend folder): go run cmd/typescript_converter/main.go -out ../frontend/types/api +// Standard usage (execute in backend folder): go run cmd/main.go typescript-converter -out ../frontend/types/api func Run() { var out string diff --git a/backend/go.mod b/backend/go.mod index b53006df9..59fc22b83 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -24,6 +24,7 @@ require ( github.com/fergusstrange/embedded-postgres v1.29.0 github.com/gavv/httpexpect/v2 v2.16.0 github.com/go-faker/faker/v4 v4.3.0 + github.com/go-openapi/spec v0.20.14 github.com/go-redis/redis/v8 v8.11.5 github.com/gobitfly/eth-rewards v0.1.2-0.20230403064929-411ddc40a5f7 github.com/gobitfly/eth.store v0.0.0-20240312111708-b43f13990280 @@ -139,6 +140,9 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.9 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.9.5 // indirect @@ -178,6 +182,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jbenet/goprocess v0.1.4 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect @@ -264,6 +269,7 @@ require ( lukechampine.com/blake3 v1.2.1 // indirect moul.io/http2curl/v2 v2.3.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) replace github.com/wealdtech/go-merkletree v1.0.1-0.20190605192610-2bb163c2ea2a => github.com/rocket-pool/go-merkletree v1.0.1-0.20220406020931-c262d9b976dd diff --git a/backend/go.sum b/backend/go.sum index 7d41b5693..34c1e36af 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -285,6 +285,14 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -562,6 +570,7 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8 github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -1284,5 +1293,5 @@ rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/backend/pkg/api/api_test.go b/backend/pkg/api/api_test.go index c422bf17f..a38763240 100644 --- a/backend/pkg/api/api_test.go +++ b/backend/pkg/api/api_test.go @@ -9,12 +9,14 @@ import ( "net/http/httptest" "os" "os/exec" + "slices" "sort" "testing" "time" embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/gavv/httpexpect/v2" + "github.com/go-openapi/spec" "github.com/gobitfly/beaconchain/pkg/api" dataaccess "github.com/gobitfly/beaconchain/pkg/api/data_access" api_types "github.com/gobitfly/beaconchain/pkg/api/types" @@ -25,6 +27,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" ) @@ -111,6 +114,16 @@ func setup() error { return fmt.Errorf("error inserting user 2: %w", err) } + // insert dummy api weight for testing + _, err = tempDb.Exec(` + INSERT INTO api_weights (bucket, endpoint, method, params, weight, valid_from) + VALUES ($1, $2, $3, $4, $5, TO_TIMESTAMP($6))`, + "default", "/api/v2/test-ratelimit", "GET", "", 2, time.Now().Unix(), + ) + if err != nil { + return fmt.Errorf("error inserting api weight: %w", err) + } + cfg := &types.Config{} err = utils.ReadConfig(cfg, *configPath) if err != nil { @@ -469,3 +482,33 @@ func TestPublicAndSharedDashboards(t *testing.T) { }) } } + +func TestApiDoc(t *testing.T) { + e := httpexpect.WithConfig(getExpectConfig(t, ts)) + + t.Run("test api doc json", func(t *testing.T) { + resp := spec.Swagger{} + e.GET("/api/v2/docs/swagger.json"). + Expect(). + Status(http.StatusOK).JSON().Decode(&resp) + + assert.Equal(t, "/api/v2", resp.BasePath, "swagger base path should be '/api/v2'") + require.NotNil(t, 0, resp.Paths, "swagger paths should not nil") + assert.NotEqual(t, 0, len(resp.Paths.Paths), "swagger paths should not be empty") + assert.NotEqual(t, 0, len(resp.Definitions), "swagger definitions should not be empty") + assert.NotEqual(t, 0, len(resp.Host), "swagger host should not be empty") + }) + + t.Run("test api ratelimit weights endpoint", func(t *testing.T) { + resp := api_types.InternalGetRatelimitWeightsResponse{} + e.GET("/api/i/ratelimit-weights"). + Expect(). + Status(http.StatusOK).JSON().Decode(&resp) + + assert.GreaterOrEqual(t, len(resp.Data), 1, "ratelimit weights should contain at least one entry") + testEndpointIndex := slices.IndexFunc(resp.Data, func(item api_types.ApiWeightItem) bool { + return item.Endpoint == "/api/v2/test-ratelimit" + }) + assert.GreaterOrEqual(t, testEndpointIndex, 0, "ratelimit weights should contain an entry for /api/v2/test-ratelimit") + }) +} diff --git a/backend/pkg/api/data_access/app.go b/backend/pkg/api/data_access/app.go index e3c29d621..812549b1a 100644 --- a/backend/pkg/api/data_access/app.go +++ b/backend/pkg/api/data_access/app.go @@ -1,6 +1,7 @@ package dataaccess import ( + "context" "database/sql" "fmt" "time" @@ -19,6 +20,8 @@ type AppRepository interface { AddMobileNotificationToken(userID uint64, deviceID, notifyToken string) error GetAppSubscriptionCount(userID uint64) (uint64, error) AddMobilePurchase(tx *sql.Tx, userID uint64, paymentDetails t.MobileSubscription, verifyResponse *userservice.VerifyResponse, extSubscriptionId string) error + GetLatestBundleForNativeVersion(ctx context.Context, nativeVersion uint64) (*t.MobileAppBundleStats, error) + IncrementBundleDeliveryCount(ctx context.Context, bundleVerison uint64) error } // GetUserIdByRefreshToken basically used to confirm the claimed user id with the refresh token. Returns the userId if successful @@ -105,3 +108,13 @@ func (d *DataAccessService) AddMobilePurchase(tx *sql.Tx, userID uint64, payment return err } + +func (d *DataAccessService) GetLatestBundleForNativeVersion(ctx context.Context, nativeVersion uint64) (*t.MobileAppBundleStats, error) { + // @TODO data access + return d.dummy.GetLatestBundleForNativeVersion(ctx, nativeVersion) +} + +func (d *DataAccessService) IncrementBundleDeliveryCount(ctx context.Context, bundleVerison uint64) error { + // @TODO data access + return d.dummy.IncrementBundleDeliveryCount(ctx, bundleVerison) +} diff --git a/backend/pkg/api/data_access/data_access.go b/backend/pkg/api/data_access/data_access.go index c4b3b8338..3fc31d105 100644 --- a/backend/pkg/api/data_access/data_access.go +++ b/backend/pkg/api/data_access/data_access.go @@ -13,7 +13,6 @@ import ( "github.com/gobitfly/beaconchain/pkg/commons/db" "github.com/gobitfly/beaconchain/pkg/commons/log" "github.com/gobitfly/beaconchain/pkg/commons/types" - "github.com/gobitfly/beaconchain/pkg/commons/utils" "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) @@ -29,6 +28,7 @@ type DataAccessor interface { BlockRepository ArchiverRepository ProtocolRepository + RatelimitRepository HealthzRepository StartDataAccessServices() @@ -203,7 +203,7 @@ func createDataAccessService(cfg *types.Config) *DataAccessService { wg.Add(1) go func() { defer wg.Done() - bt, err := db.InitBigtable(utils.Config.Bigtable.Project, utils.Config.Bigtable.Instance, fmt.Sprintf("%d", utils.Config.Chain.ClConfig.DepositChainID), utils.Config.RedisCacheEndpoint) + bt, err := db.InitBigtable(cfg.Bigtable.Project, cfg.Bigtable.Instance, fmt.Sprintf("%d", cfg.Chain.ClConfig.DepositChainID), cfg.RedisCacheEndpoint) if err != nil { log.Fatal(err, "error connecting to bigtable", 0) } @@ -211,11 +211,11 @@ func createDataAccessService(cfg *types.Config) *DataAccessService { }() // Initialize the tiered cache (redis) - if utils.Config.TieredCacheProvider == "redis" || len(utils.Config.RedisCacheEndpoint) != 0 { + if cfg.TieredCacheProvider == "redis" || len(cfg.RedisCacheEndpoint) != 0 { wg.Add(1) go func() { defer wg.Done() - cache.MustInitTieredCache(utils.Config.RedisCacheEndpoint) + cache.MustInitTieredCache(cfg.RedisCacheEndpoint) log.Infof("tiered Cache initialized, latest finalized epoch: %v", cache.LatestFinalizedEpoch.Get()) }() } @@ -225,7 +225,7 @@ func createDataAccessService(cfg *types.Config) *DataAccessService { go func() { defer wg.Done() rdc := redis.NewClient(&redis.Options{ - Addr: utils.Config.RedisSessionStoreEndpoint, + Addr: cfg.RedisSessionStoreEndpoint, ReadTimeout: time.Second * 60, }) @@ -237,7 +237,7 @@ func createDataAccessService(cfg *types.Config) *DataAccessService { wg.Wait() - if utils.Config.TieredCacheProvider != "redis" { + if cfg.TieredCacheProvider != "redis" { log.Fatal(fmt.Errorf("no cache provider set, please set TierdCacheProvider (example redis)"), "", 0) } diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index 6197a72b2..7aef7bc48 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -11,7 +11,6 @@ import ( "github.com/go-faker/faker/v4" "github.com/go-faker/faker/v4/pkg/options" "github.com/gobitfly/beaconchain/pkg/api/enums" - "github.com/gobitfly/beaconchain/pkg/api/types" t "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gobitfly/beaconchain/pkg/userservice" "github.com/shopspring/decimal" @@ -44,9 +43,9 @@ func NewDummyService() *DummyService { return &DummyService{} } -// generate random decimal.Decimal, should result in somewhere around 0.001 ETH (+/- a few decimal places) in Wei +// generate random decimal.Decimal, result is between 0.001 and 1000 GWei (returned in Wei) func randomEthDecimal() decimal.Decimal { - decimal, _ := decimal.NewFromString(fmt.Sprintf("%d00000000000", rand.Int64N(10000000))) //nolint:gosec + decimal, _ := decimal.NewFromString(fmt.Sprintf("%d000000", rand.Int64N(1000000)+1)) //nolint:gosec return decimal } @@ -453,11 +452,11 @@ func (d *DummyService) GetDashboardNotifications(ctx context.Context, userId uin return getDummyWithPaging[t.NotificationDashboardsTableRow]() } -func (d *DummyService) GetValidatorDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationValidatorDashboardDetail, error) { +func (d *DummyService) GetValidatorDashboardNotificationDetails(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, epoch uint64) (*t.NotificationValidatorDashboardDetail, error) { return getDummyStruct[t.NotificationValidatorDashboardDetail]() } -func (d *DummyService) GetAccountDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationAccountDashboardDetail, error) { +func (d *DummyService) GetAccountDashboardNotificationDetails(ctx context.Context, dashboardId uint64, groupId uint64, epoch uint64) (*t.NotificationAccountDashboardDetail, error) { return getDummyStruct[t.NotificationAccountDashboardDetail]() } @@ -483,10 +482,10 @@ func (d *DummyService) UpdateNotificationSettingsGeneral(ctx context.Context, us func (d *DummyService) UpdateNotificationSettingsNetworks(ctx context.Context, userId uint64, chainId uint64, settings t.NotificationSettingsNetwork) error { return nil } -func (d *DummyService) UpdateNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string, name string, IsNotificationsEnabled bool) error { +func (d *DummyService) UpdateNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string, name string, IsNotificationsEnabled bool) error { return nil } -func (d *DummyService) DeleteNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string) error { +func (d *DummyService) DeleteNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string) error { return nil } func (d *DummyService) GetNotificationSettingsDashboards(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationSettingsDashboardColumn], search string, limit uint64) ([]t.NotificationSettingsDashboardsTableRow, *t.Paging, error) { @@ -637,7 +636,21 @@ func (d *DummyService) GetRocketPoolOverview(ctx context.Context) (*t.RocketPool return getDummyStruct[t.RocketPoolData]() } -func (d *DummyService) GetHealthz(ctx context.Context, showAll bool) types.HealthzData { - r, _ := getDummyData[types.HealthzData]() +func (d *DummyService) GetApiWeights(ctx context.Context) ([]t.ApiWeightItem, error) { + r := []t.ApiWeightItem{} + err := commonFakeData(&r) + return r, err +} + +func (d *DummyService) GetHealthz(ctx context.Context, showAll bool) t.HealthzData { + r, _ := getDummyData[t.HealthzData]() return r } + +func (d *DummyService) GetLatestBundleForNativeVersion(ctx context.Context, nativeVersion uint64) (*t.MobileAppBundleStats, error) { + return getDummyStruct[t.MobileAppBundleStats]() +} + +func (d *DummyService) IncrementBundleDeliveryCount(ctx context.Context, bundleVerison uint64) error { + return nil +} diff --git a/backend/pkg/api/data_access/general.go b/backend/pkg/api/data_access/general.go new file mode 100644 index 000000000..7debc5dfc --- /dev/null +++ b/backend/pkg/api/data_access/general.go @@ -0,0 +1,48 @@ +package dataaccess + +import ( + "context" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/gobitfly/beaconchain/pkg/api/types" + "github.com/gobitfly/beaconchain/pkg/commons/db" +) + +// retrieve (primary) ens name and optional name (=label) maintained by beaconcha.in, if present +func (d *DataAccessService) GetNamesAndEnsForAddresses(ctx context.Context, addressMap map[string]*types.Address) error { + addresses := make([][]byte, 0, len(addressMap)) + ensMapping := make(map[string]string, len(addressMap)) + for address, data := range addressMap { + ensMapping[address] = "" + add, err := hexutil.Decode(address) + if err != nil { + return err + } + addresses = append(addresses, add) + if data == nil { + addressMap[address] = &types.Address{Hash: types.Hash(address)} + } + } + // determine ENS names + if err := db.GetEnsNamesForAddresses(ensMapping); err != nil { + return err + } + for address, ens := range ensMapping { + addressMap[address].Ens = ens + } + + // determine names + names := []struct { + Address []byte `db:"address"` + Name string `db:"name"` + }{} + err := d.alloyReader.SelectContext(ctx, &names, `SELECT address, name FROM address_names WHERE address = ANY($1)`, addresses) + if err != nil { + return err + } + + for _, name := range names { + addressMap[hexutil.Encode(name.Address)].Label = name.Name + } + return nil +} diff --git a/backend/pkg/api/data_access/notifications.go b/backend/pkg/api/data_access/notifications.go index 4859b770c..5389847db 100644 --- a/backend/pkg/api/data_access/notifications.go +++ b/backend/pkg/api/data_access/notifications.go @@ -12,8 +12,8 @@ type NotificationsRepository interface { GetDashboardNotifications(ctx context.Context, userId uint64, chainId uint64, cursor string, colSort t.Sort[enums.NotificationDashboardsColumn], search string, limit uint64) ([]t.NotificationDashboardsTableRow, *t.Paging, error) // depending on how notifications are implemented, we may need to use something other than `notificationId` for identifying the notification - GetValidatorDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationValidatorDashboardDetail, error) - GetAccountDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationAccountDashboardDetail, error) + GetValidatorDashboardNotificationDetails(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, epoch uint64) (*t.NotificationValidatorDashboardDetail, error) + GetAccountDashboardNotificationDetails(ctx context.Context, dashboardId uint64, groupId uint64, epoch uint64) (*t.NotificationAccountDashboardDetail, error) GetMachineNotifications(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationMachinesColumn], search string, limit uint64) ([]t.NotificationMachinesTableRow, *t.Paging, error) GetClientNotifications(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationClientsColumn], search string, limit uint64) ([]t.NotificationClientsTableRow, *t.Paging, error) @@ -23,8 +23,8 @@ type NotificationsRepository interface { GetNotificationSettings(ctx context.Context, userId uint64) (*t.NotificationSettings, error) UpdateNotificationSettingsGeneral(ctx context.Context, userId uint64, settings t.NotificationSettingsGeneral) error UpdateNotificationSettingsNetworks(ctx context.Context, userId uint64, chainId uint64, settings t.NotificationSettingsNetwork) error - UpdateNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string, name string, IsNotificationsEnabled bool) error - DeleteNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string) error + UpdateNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string, name string, IsNotificationsEnabled bool) error + DeleteNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string) error GetNotificationSettingsDashboards(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationSettingsDashboardColumn], search string, limit uint64) ([]t.NotificationSettingsDashboardsTableRow, *t.Paging, error) UpdateNotificationSettingsValidatorDashboard(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, settings t.NotificationSettingsValidatorDashboard) error UpdateNotificationSettingsAccountDashboard(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, settings t.NotificationSettingsAccountDashboard) error @@ -37,12 +37,12 @@ func (d *DataAccessService) GetDashboardNotifications(ctx context.Context, userI return d.dummy.GetDashboardNotifications(ctx, userId, chainId, cursor, colSort, search, limit) } -func (d *DataAccessService) GetValidatorDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationValidatorDashboardDetail, error) { - return d.dummy.GetValidatorDashboardNotificationDetails(ctx, notificationId) +func (d *DataAccessService) GetValidatorDashboardNotificationDetails(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, epoch uint64) (*t.NotificationValidatorDashboardDetail, error) { + return d.dummy.GetValidatorDashboardNotificationDetails(ctx, dashboardId, groupId, epoch) } -func (d *DataAccessService) GetAccountDashboardNotificationDetails(ctx context.Context, notificationId string) (*t.NotificationAccountDashboardDetail, error) { - return d.dummy.GetAccountDashboardNotificationDetails(ctx, notificationId) +func (d *DataAccessService) GetAccountDashboardNotificationDetails(ctx context.Context, dashboardId uint64, groupId uint64, epoch uint64) (*t.NotificationAccountDashboardDetail, error) { + return d.dummy.GetAccountDashboardNotificationDetails(ctx, dashboardId, groupId, epoch) } func (d *DataAccessService) GetMachineNotifications(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationMachinesColumn], search string, limit uint64) ([]t.NotificationMachinesTableRow, *t.Paging, error) { @@ -66,11 +66,11 @@ func (d *DataAccessService) UpdateNotificationSettingsGeneral(ctx context.Contex func (d *DataAccessService) UpdateNotificationSettingsNetworks(ctx context.Context, userId uint64, chainId uint64, settings t.NotificationSettingsNetwork) error { return d.dummy.UpdateNotificationSettingsNetworks(ctx, userId, chainId, settings) } -func (d *DataAccessService) UpdateNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string, name string, IsNotificationsEnabled bool) error { - return d.dummy.UpdateNotificationSettingsPairedDevice(ctx, pairedDeviceId, name, IsNotificationsEnabled) +func (d *DataAccessService) UpdateNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string, name string, IsNotificationsEnabled bool) error { + return d.dummy.UpdateNotificationSettingsPairedDevice(ctx, userId, pairedDeviceId, name, IsNotificationsEnabled) } -func (d *DataAccessService) DeleteNotificationSettingsPairedDevice(ctx context.Context, pairedDeviceId string) error { - return d.dummy.DeleteNotificationSettingsPairedDevice(ctx, pairedDeviceId) +func (d *DataAccessService) DeleteNotificationSettingsPairedDevice(ctx context.Context, userId uint64, pairedDeviceId string) error { + return d.dummy.DeleteNotificationSettingsPairedDevice(ctx, userId, pairedDeviceId) } func (d *DataAccessService) GetNotificationSettingsDashboards(ctx context.Context, userId uint64, cursor string, colSort t.Sort[enums.NotificationSettingsDashboardColumn], search string, limit uint64) ([]t.NotificationSettingsDashboardsTableRow, *t.Paging, error) { return d.dummy.GetNotificationSettingsDashboards(ctx, userId, cursor, colSort, search, limit) diff --git a/backend/pkg/api/data_access/ratelimit.go b/backend/pkg/api/data_access/ratelimit.go new file mode 100644 index 000000000..8c17c5d0f --- /dev/null +++ b/backend/pkg/api/data_access/ratelimit.go @@ -0,0 +1,22 @@ +package dataaccess + +import ( + "context" + + "github.com/gobitfly/beaconchain/pkg/api/types" +) + +type RatelimitRepository interface { + GetApiWeights(ctx context.Context) ([]types.ApiWeightItem, error) + // TODO @patrick: move queries from commons/ratelimit/ratelimit.go to here +} + +func (d *DataAccessService) GetApiWeights(ctx context.Context) ([]types.ApiWeightItem, error) { + var result []types.ApiWeightItem + err := d.userReader.SelectContext(ctx, &result, ` + SELECT bucket, endpoint, method, weight + FROM api_weights + WHERE valid_from <= NOW() + `) + return result, err +} diff --git a/backend/pkg/api/data_access/user.go b/backend/pkg/api/data_access/user.go index 598f83995..c7c22d9cf 100644 --- a/backend/pkg/api/data_access/user.go +++ b/backend/pkg/api/data_access/user.go @@ -287,7 +287,10 @@ func (d *DataAccessService) GetUserInfo(ctx context.Context, userId uint64) (*t. }{} err = d.userReader.GetContext(ctx, &result, `SELECT email, COALESCE(user_group, '') as user_group FROM users WHERE id = $1`, userId) if err != nil { - return nil, fmt.Errorf("error getting userEmail for user %v: %w", userId, err) + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("%w: user not found", ErrNotFound) + } + return nil, err } userInfo.Email = result.Email userInfo.UserGroup = result.UserGroup @@ -764,7 +767,7 @@ func (d *DataAccessService) GetUserDashboards(ctx context.Context, userId uint64 err := wg.Wait() if err != nil { - return nil, fmt.Errorf("error retrieving user dashboards data: %v", err) + return nil, fmt.Errorf("error retrieving user dashboards data: %w", err) } // Fill the result diff --git a/backend/pkg/api/data_access/vdb_blocks.go b/backend/pkg/api/data_access/vdb_blocks.go index 3780599b7..2a2a0953a 100644 --- a/backend/pkg/api/data_access/vdb_blocks.go +++ b/backend/pkg/api/data_access/vdb_blocks.go @@ -15,6 +15,7 @@ import ( "github.com/gobitfly/beaconchain/pkg/commons/cache" "github.com/gobitfly/beaconchain/pkg/commons/db" "github.com/gobitfly/beaconchain/pkg/commons/log" + "github.com/gobitfly/beaconchain/pkg/commons/types" "github.com/gobitfly/beaconchain/pkg/commons/utils" "github.com/shopspring/decimal" ) @@ -345,7 +346,8 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das } data := make([]t.VDBBlocksTableRow, len(proposals)) - ensMapping := make(map[string]string) + addressMapping := make(map[string]*t.Address) + contractStatusRequests := make([]db.ContractInteractionAtRequest, 0, len(proposals)) for i, proposal := range proposals { data[i].GroupId = proposal.Group if dashboardId.AggregateGroups { @@ -382,7 +384,13 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das Hash: t.Hash(hexutil.Encode(proposal.FeeRecipient)), } data[i].RewardRecipient = &rewardRecp - ensMapping[hexutil.Encode(proposal.FeeRecipient)] = "" + addressMapping[hexutil.Encode(proposal.FeeRecipient)] = nil + contractStatusRequests = append(contractStatusRequests, db.ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", proposal.FeeRecipient), + Block: proposal.Block.Int64, + TxIdx: -1, + TraceIdx: -1, + }) reward.El = proposal.ElReward.Decimal.Mul(decimal.NewFromInt(1e18)) } if proposal.ClReward.Valid { @@ -393,13 +401,22 @@ func (d *DataAccessService) GetValidatorDashboardBlocks(ctx context.Context, das } // determine reward recipient ENS names startTime = time.Now() - if err := db.GetEnsNamesForAddresses(ensMapping); err != nil { + // determine ens/names + if err := d.GetNamesAndEnsForAddresses(ctx, addressMapping); err != nil { return nil, nil, err } - log.Debugf("=== getting ens names took %s", time.Since(startTime)) + log.Debugf("=== getting ens + labels names took %s", time.Since(startTime)) + // determine contract statuses + contractStatuses, err := d.bigtable.GetAddressContractInteractionsAt(contractStatusRequests) + if err != nil { + return nil, nil, err + } + var contractIdx int for i := range data { if data[i].RewardRecipient != nil { - data[i].RewardRecipient.Ens = ensMapping[string(data[i].RewardRecipient.Hash)] + data[i].RewardRecipient = addressMapping[string(data[i].RewardRecipient.Hash)] + data[i].RewardRecipient.IsContract = contractStatuses[contractIdx] == types.CONTRACT_CREATION || contractStatuses[contractIdx] == types.CONTRACT_PRESENT + contractIdx += 1 } } if !moreDataFlag && !currentCursor.IsValid() { diff --git a/backend/pkg/api/data_access/vdb_deposits.go b/backend/pkg/api/data_access/vdb_deposits.go index 7d32cc002..db2026a23 100644 --- a/backend/pkg/api/data_access/vdb_deposits.go +++ b/backend/pkg/api/data_access/vdb_deposits.go @@ -14,6 +14,7 @@ import ( "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" "github.com/gobitfly/beaconchain/pkg/commons/utils" "github.com/lib/pq" "github.com/shopspring/decimal" @@ -121,16 +122,27 @@ func (d *DataAccessService) GetValidatorDashboardElDeposits(ctx context.Context, } responseData := make([]t.VDBExecutionDepositsTableRow, len(data)) + addressMapping := make(map[string]*t.Address) + fromContractStatusRequests := make([]db.ContractInteractionAtRequest, len(data)) + depositorContractStatusRequests := make([]db.ContractInteractionAtRequest, 0, len(data)) for i, row := range data { responseData[i] = t.VDBExecutionDepositsTableRow{ PublicKey: t.PubKey(pubkeys[i]), Block: uint64(row.BlockNumber), Timestamp: row.Timestamp.Unix(), - From: t.Address{Hash: t.Hash(hexutil.Encode(row.From))}, TxHash: t.Hash(hexutil.Encode(row.TxHash)), WithdrawalCredential: t.Hash(hexutil.Encode(row.WithdrawalCredentials)), Amount: utils.GWeiToWei(big.NewInt(row.Amount)), Valid: row.Valid, + From: t.Address{Hash: t.Hash(hexutil.Encode(row.From))}, + } + addressMapping[hexutil.Encode(row.From)] = nil + fromContractStatusRequests[i] = db.ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", row.From), + Block: row.BlockNumber, + // TODO not entirely correct, would need to determine tx index and itx index of tx. But good enough for now + TxIdx: -1, + TraceIdx: -1, } if row.GroupId.Valid { if dashboardId.AggregateGroups { @@ -143,6 +155,10 @@ func (d *DataAccessService) GetValidatorDashboardElDeposits(ctx context.Context, } if len(row.Depositor) > 0 { responseData[i].Depositor = t.Address{Hash: t.Hash(hexutil.Encode(row.Depositor))} + addressMapping[hexutil.Encode(row.Depositor)] = nil + depositorReq := fromContractStatusRequests[i] + depositorReq.Address = fmt.Sprintf("%x", row.Depositor) + depositorContractStatusRequests = append(depositorContractStatusRequests, depositorReq) } else { responseData[i].Depositor = responseData[i].From } @@ -150,6 +166,30 @@ func (d *DataAccessService) GetValidatorDashboardElDeposits(ctx context.Context, responseData[i].Index = &v } } + + // populate address data + if err := d.GetNamesAndEnsForAddresses(ctx, addressMapping); err != nil { + return nil, nil, err + } + fromContractStatuses, err := d.bigtable.GetAddressContractInteractionsAt(fromContractStatusRequests) + if err != nil { + return nil, nil, err + } + depositorContractStatuses, err := d.bigtable.GetAddressContractInteractionsAt(depositorContractStatusRequests) + if err != nil { + return nil, nil, err + } + var depositorIdx int + for i := range data { + responseData[i].From = *addressMapping[string(responseData[i].From.Hash)] + responseData[i].From.IsContract = fromContractStatuses[i] == types.CONTRACT_CREATION || fromContractStatuses[i] == types.CONTRACT_PRESENT + responseData[i].Depositor.IsContract = responseData[i].From.IsContract + if responseData[i].Depositor.Hash != responseData[i].From.Hash { + responseData[i].Depositor.IsContract = depositorContractStatuses[depositorIdx] == types.CONTRACT_CREATION || depositorContractStatuses[depositorIdx] == types.CONTRACT_PRESENT + depositorIdx += 1 + } + } + var paging t.Paging moreDataFlag := len(responseData) > int(limit) diff --git a/backend/pkg/api/data_access/vdb_management.go b/backend/pkg/api/data_access/vdb_management.go index 9163a51ab..b523103c6 100644 --- a/backend/pkg/api/data_access/vdb_management.go +++ b/backend/pkg/api/data_access/vdb_management.go @@ -148,7 +148,7 @@ func (d *DataAccessService) GetValidatorDashboardInfo(ctx context.Context, dashb err := wg.Wait() if err != nil { - return nil, fmt.Errorf("error retrieving user dashboards data: %v", err) + return nil, fmt.Errorf("error retrieving user dashboards data: %w", err) } return result, nil @@ -329,7 +329,7 @@ func (d *DataAccessService) GetValidatorDashboardOverview(ctx context.Context, d validators, err := d.getDashboardValidators(ctx, dashboardId, nil) if err != nil { - return fmt.Errorf("error retrieving validators from dashboard id: %v", err) + return fmt.Errorf("error retrieving validators from dashboard id: %w", err) } if dashboardId.Validators != nil || dashboardId.AggregateGroups { @@ -475,7 +475,7 @@ func (d *DataAccessService) GetValidatorDashboardOverview(ctx context.Context, d query, args, err := ds.Prepared(true).ToSQL() if err != nil { - return fmt.Errorf("error preparing query: %v", err) + return fmt.Errorf("error preparing query: %w", err) } err = d.clickhouseReader.GetContext(ctx, &queryResult, query, args...) @@ -511,7 +511,7 @@ func (d *DataAccessService) GetValidatorDashboardOverview(ctx context.Context, d err = eg.Wait() if err != nil { - return nil, fmt.Errorf("error retrieving validator dashboard overview data: %v", err) + return nil, fmt.Errorf("error retrieving validator dashboard overview data: %w", err) } return &data, nil diff --git a/backend/pkg/api/data_access/vdb_withdrawals.go b/backend/pkg/api/data_access/vdb_withdrawals.go index c3837f6f0..21a190f44 100644 --- a/backend/pkg/api/data_access/vdb_withdrawals.go +++ b/backend/pkg/api/data_access/vdb_withdrawals.go @@ -17,6 +17,7 @@ import ( t "github.com/gobitfly/beaconchain/pkg/api/types" "github.com/gobitfly/beaconchain/pkg/commons/cache" "github.com/gobitfly/beaconchain/pkg/commons/db" + "github.com/gobitfly/beaconchain/pkg/commons/types" "github.com/gobitfly/beaconchain/pkg/commons/utils" "github.com/lib/pq" "github.com/pkg/errors" @@ -111,6 +112,7 @@ func (d *DataAccessService) GetValidatorDashboardWithdrawals(ctx context.Context // Get the withdrawals for the validators queryResult := []struct { BlockSlot uint64 `db:"block_slot"` + BlockNumber uint64 `db:"exec_block_number"` WithdrawalIndex uint64 `db:"withdrawalindex"` ValidatorIndex uint64 `db:"validatorindex"` Address []byte `db:"address"` @@ -121,6 +123,7 @@ func (d *DataAccessService) GetValidatorDashboardWithdrawals(ctx context.Context withdrawalsQuery := ` SELECT w.block_slot, + b.exec_block_number, w.withdrawalindex, w.validatorindex, w.address, @@ -196,32 +199,43 @@ func (d *DataAccessService) GetValidatorDashboardWithdrawals(ctx context.Context } // Prepare the ENS map - addressEns := make(map[string]string) - for _, withdrawal := range queryResult { + addressMapping := make(map[string]*t.Address) + contractStatusRequests := make([]db.ContractInteractionAtRequest, len(queryResult)) + for i, withdrawal := range queryResult { address := hexutil.Encode(withdrawal.Address) - addressEns[address] = "" + addressMapping[address] = nil + contractStatusRequests[i] = db.ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", withdrawal.Address), + Block: int64(withdrawal.BlockNumber), + TxIdx: -1, + TraceIdx: -1, + } + } + + // Get the ENS names and (label) names for the addresses + if err := d.GetNamesAndEnsForAddresses(ctx, addressMapping); err != nil { + return nil, nil, err } - // Get the ENS names for the addresses - if err := db.GetEnsNamesForAddresses(addressEns); err != nil { + // Get the contract status for the addresses + contractStatuses, err := d.bigtable.GetAddressContractInteractionsAt(contractStatusRequests) + if err != nil { return nil, nil, err } // Create the result cursorData := make([]t.WithdrawalsCursor, 0) - for _, withdrawal := range queryResult { + for i, withdrawal := range queryResult { address := hexutil.Encode(withdrawal.Address) result = append(result, t.VDBWithdrawalsTableRow{ - Epoch: withdrawal.BlockSlot / utils.Config.Chain.ClConfig.SlotsPerEpoch, - Slot: withdrawal.BlockSlot, - Index: withdrawal.ValidatorIndex, - GroupId: validatorGroupMap[withdrawal.ValidatorIndex], - Recipient: t.Address{ - Hash: t.Hash(address), - Ens: addressEns[address], - }, - Amount: utils.GWeiToWei(big.NewInt(int64(withdrawal.Amount))), + Epoch: withdrawal.BlockSlot / utils.Config.Chain.ClConfig.SlotsPerEpoch, + Slot: withdrawal.BlockSlot, + Index: withdrawal.ValidatorIndex, + Recipient: *addressMapping[address], + GroupId: validatorGroupMap[withdrawal.ValidatorIndex], + Amount: utils.GWeiToWei(big.NewInt(int64(withdrawal.Amount))), }) + result[i].Recipient.IsContract = contractStatuses[i] == types.CONTRACT_CREATION || contractStatuses[i] == types.CONTRACT_PRESENT cursorData = append(cursorData, t.WithdrawalsCursor{ Slot: withdrawal.BlockSlot, WithdrawalIndex: withdrawal.WithdrawalIndex, @@ -256,7 +270,8 @@ func (d *DataAccessService) GetValidatorDashboardWithdrawals(ctx context.Context if nextData != nil { // Complete the next data nextData.GroupId = validatorGroupMap[nextData.Index] - nextData.Recipient.Ens = addressEns[string(nextData.Recipient.Hash)] + // TODO integrate label/ens data for "next" row + // nextData.Recipient.Ens = addressEns[string(nextData.Recipient.Hash)] } else { // If there is no next data, add a missing estimate row nextData = &t.VDBWithdrawalsTableRow{ @@ -393,12 +408,28 @@ func (d *DataAccessService) getNextWithdrawalRow(queryValidators []t.VDBValidato withdrawalAmount = 0 } + ens_name, err := db.GetEnsNameForAddress(*address, utils.SlotToTime(nextWithdrawalSlot)) + if err != sql.ErrNoRows { + return nil, err + } + + contractStatusReq := []db.ContractInteractionAtRequest{{ + Address: fmt.Sprintf("%x", address), + Block: -1, + }} + contractStatus, err := d.bigtable.GetAddressContractInteractionsAt(contractStatusReq) + if err != nil { + return nil, err + } + nextData := &t.VDBWithdrawalsTableRow{ Epoch: nextWithdrawalSlot / utils.Config.Chain.ClConfig.SlotsPerEpoch, Slot: nextWithdrawalSlot, Index: *nextValidator, Recipient: t.Address{ - Hash: t.Hash(address.String()), + Hash: t.Hash(address.String()), + Ens: ens_name, + IsContract: contractStatus[0] == types.CONTRACT_CREATION || contractStatus[0] == types.CONTRACT_PRESENT, }, Amount: utils.GWeiToWei(big.NewInt(int64(withdrawalAmount))), } diff --git a/backend/pkg/api/docs/static.go b/backend/pkg/api/docs/static.go new file mode 100644 index 000000000..93087d82f --- /dev/null +++ b/backend/pkg/api/docs/static.go @@ -0,0 +1,6 @@ +package docs + +import "embed" + +//go:embed * +var Files embed.FS diff --git a/backend/pkg/api/enums/validator_dashboard_enums.go b/backend/pkg/api/enums/validator_dashboard_enums.go index 928d5742c..fcbfec71b 100644 --- a/backend/pkg/api/enums/validator_dashboard_enums.go +++ b/backend/pkg/api/enums/validator_dashboard_enums.go @@ -437,7 +437,7 @@ func (c VDBRocketPoolMinipoolsColumn) Int() int { func (VDBRocketPoolMinipoolsColumn) NewFromString(s string) VDBRocketPoolMinipoolsColumn { switch s { - case "group": + case "group_id": return VDBRocketPoolMinipoolsGroup default: return VDBRocketPoolMinipoolsColumn(-1) diff --git a/backend/pkg/api/handlers/common.go b/backend/pkg/api/handlers/common.go index ecc9794db..ca585724b 100644 --- a/backend/pkg/api/handlers/common.go +++ b/backend/pkg/api/handlers/common.go @@ -528,8 +528,7 @@ func checkEnum[T enums.EnumFactory[T]](v *validationError, enumString string, na } // checkEnumIsAllowed checks if the given enum is in the list of allowed enums. -// precondition: the enum is the same type as the allowed enums. -func (v *validationError) checkEnumIsAllowed(enum enums.Enum, allowed []enums.Enum, name string) { +func checkEnumIsAllowed[T enums.EnumFactory[T]](v *validationError, enum T, allowed []T, name string) { if enums.IsInvalidEnum(enum) { v.add(name, "parameter is missing or invalid, please check the API documentation") return @@ -567,7 +566,7 @@ func checkSort[T enums.EnumFactory[T]](v *validationError, sortString string) *t return nil } if len(sortSplit) == 1 { - sortSplit = append(sortSplit, "") + sortSplit = append(sortSplit, ":asc") } sortCol := checkEnum[T](v, sortSplit[0], "sort") order := v.parseSortOrder(sortSplit[1]) diff --git a/backend/pkg/api/handlers/internal.go b/backend/pkg/api/handlers/internal.go index 1c4157290..0a0f5c006 100644 --- a/backend/pkg/api/handlers/internal.go +++ b/backend/pkg/api/handlers/internal.go @@ -2,7 +2,6 @@ package handlers import ( "errors" - "math" "net/http" "github.com/gobitfly/beaconchain/pkg/api/enums" @@ -26,6 +25,21 @@ func (h *HandlerService) InternalGetProductSummary(w http.ResponseWriter, r *htt returnOk(w, r, response) } +// -------------------------------------- +// API Ratelimit Weights + +func (h *HandlerService) InternalGetRatelimitWeights(w http.ResponseWriter, r *http.Request) { + data, err := h.dai.GetApiWeights(r.Context()) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetRatelimitWeightsResponse{ + Data: data, + } + returnOk(w, r, response) +} + // -------------------------------------- // Latest State @@ -85,7 +99,7 @@ func (h *HandlerService) InternalPostAdConfigurations(w http.ResponseWriter, r * handleErr(w, r, err) return } - if user.UserGroup != "ADMIN" { + if user.UserGroup != types.UserGroupAdmin { returnForbidden(w, r, errors.New("user is not an admin")) return } @@ -131,7 +145,7 @@ func (h *HandlerService) InternalGetAdConfigurations(w http.ResponseWriter, r *h handleErr(w, r, err) return } - if user.UserGroup != "ADMIN" { + if user.UserGroup != types.UserGroupAdmin { returnForbidden(w, r, errors.New("user is not an admin")) return } @@ -161,7 +175,7 @@ func (h *HandlerService) InternalPutAdConfiguration(w http.ResponseWriter, r *ht handleErr(w, r, err) return } - if user.UserGroup != "ADMIN" { + if user.UserGroup != types.UserGroupAdmin { returnForbidden(w, r, errors.New("user is not an admin")) return } @@ -207,7 +221,7 @@ func (h *HandlerService) InternalDeleteAdConfiguration(w http.ResponseWriter, r handleErr(w, r, err) return } - if user.UserGroup != "ADMIN" { + if user.UserGroup != types.UserGroupAdmin { returnForbidden(w, r, errors.New("user is not an admin")) return } @@ -462,462 +476,128 @@ func (h *HandlerService) InternalGetValidatorDashboardRocketPoolMinipools(w http } // -------------------------------------- -// Notifications - -func (h *HandlerService) InternalGetUserNotifications(w http.ResponseWriter, r *http.Request) { - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - data, err := h.dai.GetNotificationOverview(r.Context(), userId) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationsResponse{ - Data: *data, - } - returnOk(w, r, response) -} +// Mobile -func (h *HandlerService) InternalGetUserNotificationDashboards(w http.ResponseWriter, r *http.Request) { +func (h *HandlerService) InternalGetMobileLatestBundle(w http.ResponseWriter, r *http.Request) { var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationDashboardsColumn](&v, q.Get("sort")) - chainId := v.checkNetworkParameter(q.Get("network")) + force := v.checkBool(q.Get("force"), "force") + bundleVersion := v.checkUint(q.Get("bundle_version"), "bundle_version") + nativeVersion := v.checkUint(q.Get("native_version"), "native_version") if v.hasErrors() { handleErr(w, r, v) return } - data, paging, err := h.dai.GetDashboardNotifications(r.Context(), userId, chainId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + stats, err := h.dai.GetLatestBundleForNativeVersion(r.Context(), nativeVersion) if err != nil { handleErr(w, r, err) return } - response := types.InternalGetUserNotificationDashboardsResponse{ - Data: data, - Paging: *paging, + var data types.MobileBundleData + data.HasNativeUpdateAvailable = stats.MaxNativeVersion > nativeVersion + // if given bundle version is smaller than the latest and delivery count is less than target count, return the latest bundle + if force || (bundleVersion < stats.LatestBundleVersion && (stats.TargetCount == 0 || stats.DeliveryCount < stats.TargetCount)) { + data.BundleUrl = stats.BundleUrl + } + response := types.GetMobileLatestBundleResponse{ + Data: data, } returnOk(w, r, response) } -func (h *HandlerService) InternalGetUserNotificationsValidatorDashboard(w http.ResponseWriter, r *http.Request) { +func (h *HandlerService) InternalPostMobileBundleDeliveries(w http.ResponseWriter, r *http.Request) { var v validationError - notificationId := v.checkRegex(reNonEmpty, mux.Vars(r)["notification_id"], "notification_id") + vars := mux.Vars(r) + bundleVersion := v.checkUint(vars["bundle_version"], "bundle_version") if v.hasErrors() { handleErr(w, r, v) return } - data, err := h.dai.GetValidatorDashboardNotificationDetails(r.Context(), notificationId) + err := h.dai.IncrementBundleDeliveryCount(r.Context(), bundleVersion) if err != nil { handleErr(w, r, err) return } - response := types.InternalGetUserNotificationsValidatorDashboardResponse{ - Data: *data, - } - returnOk(w, r, response) + returnNoContent(w, r) +} + +// -------------------------------------- +// Notifications + +func (h *HandlerService) InternalGetUserNotifications(w http.ResponseWriter, r *http.Request) { + h.PublicGetUserNotifications(w, r) +} + +func (h *HandlerService) InternalGetUserNotificationDashboards(w http.ResponseWriter, r *http.Request) { + h.PublicGetUserNotificationDashboards(w, r) +} + +func (h *HandlerService) InternalGetUserNotificationsValidatorDashboard(w http.ResponseWriter, r *http.Request) { + h.PublicGetUserNotificationsValidatorDashboard(w, r) } func (h *HandlerService) InternalGetUserNotificationsAccountDashboard(w http.ResponseWriter, r *http.Request) { - var v validationError - notificationId := v.checkRegex(reNonEmpty, mux.Vars(r)["notification_id"], "notification_id") - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, err := h.dai.GetAccountDashboardNotificationDetails(r.Context(), notificationId) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationsAccountDashboardResponse{ - Data: *data, - } - returnOk(w, r, response) + h.PublicGetUserNotificationsAccountDashboard(w, r) } func (h *HandlerService) InternalGetUserNotificationMachines(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationMachinesColumn](&v, q.Get("sort")) - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, paging, err := h.dai.GetMachineNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationMachinesResponse{ - Data: data, - Paging: *paging, - } - returnOk(w, r, response) + h.PublicGetUserNotificationMachines(w, r) } func (h *HandlerService) InternalGetUserNotificationClients(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationClientsColumn](&v, q.Get("sort")) - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, paging, err := h.dai.GetClientNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationClientsResponse{ - Data: data, - Paging: *paging, - } - returnOk(w, r, response) + h.PublicGetUserNotificationClients(w, r) } func (h *HandlerService) InternalGetUserNotificationRocketPool(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationRocketPoolColumn](&v, q.Get("sort")) - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, paging, err := h.dai.GetRocketPoolNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationRocketPoolResponse{ - Data: data, - Paging: *paging, - } - returnOk(w, r, response) + h.PublicGetUserNotificationRocketPool(w, r) } func (h *HandlerService) InternalGetUserNotificationNetworks(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationNetworksColumn](&v, q.Get("sort")) - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, paging, err := h.dai.GetNetworkNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationNetworksResponse{ - Data: data, - Paging: *paging, - } - returnOk(w, r, response) + h.PublicGetUserNotificationNetworks(w, r) } func (h *HandlerService) InternalGetUserNotificationSettings(w http.ResponseWriter, r *http.Request) { - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - data, err := h.dai.GetNotificationSettings(r.Context(), userId) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationSettingsResponse{ - Data: *data, - } - returnOk(w, r, response) + h.PublicGetUserNotificationSettings(w, r) } func (h *HandlerService) InternalPutUserNotificationSettingsGeneral(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - var req types.NotificationSettingsGeneral - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - checkMinMax(&v, req.MachineStorageUsageThreshold, 0, 1, "machine_storage_usage_threshold") - checkMinMax(&v, req.MachineCpuUsageThreshold, 0, 1, "machine_cpu_usage_threshold") - checkMinMax(&v, req.MachineMemoryUsageThreshold, 0, 1, "machine_memory_usage_threshold") - checkMinMax(&v, req.RocketPoolMaxCollateralThreshold, 0, 1, "rocket_pool_max_collateral_threshold") - checkMinMax(&v, req.RocketPoolMinCollateralThreshold, 0, 1, "rocket_pool_min_collateral_threshold") - // TODO: check validity of clients - if v.hasErrors() { - handleErr(w, r, v) - return - } - err = h.dai.UpdateNotificationSettingsGeneral(r.Context(), userId, req) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalPutUserNotificationSettingsGeneralResponse{ - Data: req, - } - returnOk(w, r, response) + h.PublicPutUserNotificationSettingsGeneral(w, r) } func (h *HandlerService) InternalPutUserNotificationSettingsNetworks(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - var req types.NotificationSettingsNetwork - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - checkMinMax(&v, req.ParticipationRateThreshold, 0, 1, "participation_rate_threshold") - - chainId := v.checkNetworkParameter(mux.Vars(r)["network"]) - if v.hasErrors() { - handleErr(w, r, v) - return - } - err = h.dai.UpdateNotificationSettingsNetworks(r.Context(), userId, chainId, req) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalPutUserNotificationSettingsNetworksResponse{ - Data: types.NotificationNetwork{ - ChainId: chainId, - Settings: req, - }, - } - returnOk(w, r, response) + h.PublicPutUserNotificationSettingsNetworks(w, r) } func (h *HandlerService) InternalPutUserNotificationSettingsPairedDevices(w http.ResponseWriter, r *http.Request) { - var v validationError - req := struct { - Name string `json:"name,omitempty"` - IsNotificationsEnabled bool `json:"is_notifications_enabled"` - }{} - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - // TODO use a better way to validate the paired device id - pairedDeviceId := v.checkRegex(reNonEmpty, mux.Vars(r)["paired_device_id"], "paired_device_id") - name := v.checkNameNotEmpty(req.Name) - if v.hasErrors() { - handleErr(w, r, v) - return - } - err := h.dai.UpdateNotificationSettingsPairedDevice(r.Context(), pairedDeviceId, name, req.IsNotificationsEnabled) - if err != nil { - handleErr(w, r, err) - return - } - // TODO timestamp - response := types.InternalPutUserNotificationSettingsPairedDevicesResponse{ - Data: types.NotificationPairedDevice{ - Id: pairedDeviceId, - Name: req.Name, - IsNotificationsEnabled: req.IsNotificationsEnabled, - }, - } - - returnOk(w, r, response) + h.PublicPutUserNotificationSettingsPairedDevices(w, r) } func (h *HandlerService) InternalDeleteUserNotificationSettingsPairedDevices(w http.ResponseWriter, r *http.Request) { - var v validationError - // TODO use a better way to validate the paired device id - pairedDeviceId := v.checkRegex(reNonEmpty, mux.Vars(r)["paired_device_id"], "paired_device_id") - if v.hasErrors() { - handleErr(w, r, v) - return - } - err := h.dai.DeleteNotificationSettingsPairedDevice(r.Context(), pairedDeviceId) - if err != nil { - handleErr(w, r, err) - return - } - returnNoContent(w, r) + h.PublicDeleteUserNotificationSettingsPairedDevices(w, r) } func (h *HandlerService) InternalGetUserNotificationSettingsDashboards(w http.ResponseWriter, r *http.Request) { - var v validationError - userId, err := GetUserIdByContext(r) - if err != nil { - handleErr(w, r, err) - return - } - q := r.URL.Query() - pagingParams := v.checkPagingParams(q) - sort := checkSort[enums.NotificationSettingsDashboardColumn](&v, q.Get("sort")) - if v.hasErrors() { - handleErr(w, r, v) - return - } - data, paging, err := h.dai.GetNotificationSettingsDashboards(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationSettingsDashboardsResponse{ - Data: data, - Paging: *paging, - } - returnOk(w, r, response) + h.PublicGetUserNotificationSettingsDashboards(w, r) } func (h *HandlerService) InternalPutUserNotificationSettingsValidatorDashboard(w http.ResponseWriter, r *http.Request) { - var v validationError - var req types.NotificationSettingsValidatorDashboard - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - checkMinMax(&v, req.GroupOfflineThreshold, 0, 1, "group_offline_threshold") - vars := mux.Vars(r) - dashboardId := v.checkPrimaryDashboardId(vars["dashboard_id"]) - groupId := v.checkExistingGroupId(vars["group_id"]) - if v.hasErrors() { - handleErr(w, r, v) - return - } - err := h.dai.UpdateNotificationSettingsValidatorDashboard(r.Context(), dashboardId, groupId, req) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalPutUserNotificationSettingsValidatorDashboardResponse{ - Data: req, - } - returnOk(w, r, response) + h.PublicPutUserNotificationSettingsValidatorDashboard(w, r) } func (h *HandlerService) InternalPutUserNotificationSettingsAccountDashboard(w http.ResponseWriter, r *http.Request) { - var v validationError - req := struct { - WebhookUrl string `json:"webhook_url"` - IsWebhookDiscordEnabled bool `json:"is_webhook_discord_enabled"` - IsIgnoreSpamTransactionsEnabled bool `json:"is_ignore_spam_transactions_enabled"` - SubscribedChainIds []intOrString `json:"subscribed_chain_ids"` - - IsIncomingTransactionsSubscribed bool `json:"is_incoming_transactions_subscribed"` - IsOutgoingTransactionsSubscribed bool `json:"is_outgoing_transactions_subscribed"` - IsERC20TokenTransfersSubscribed bool `json:"is_erc20_token_transfers_subscribed"` - ERC20TokenTransfersValueThreshold float64 `json:"erc20_token_transfers_value_threshold"` // 0 does not disable, is_erc20_token_transfers_subscribed determines if it's enabled - IsERC721TokenTransfersSubscribed bool `json:"is_erc721_token_transfers_subscribed"` - IsERC1155TokenTransfersSubscribed bool `json:"is_erc1155_token_transfers_subscribed"` - }{} - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - chainIdMap := v.checkNetworkSlice(req.SubscribedChainIds) - // convert to uint64[] slice - chainIds := make([]uint64, len(chainIdMap)) - i := 0 - for k := range chainIdMap { - chainIds[i] = k - i++ - } - checkMinMax(&v, req.ERC20TokenTransfersValueThreshold, 0, math.MaxFloat64, "group_offline_threshold") - vars := mux.Vars(r) - dashboardId := v.checkPrimaryDashboardId(vars["dashboard_id"]) - groupId := v.checkExistingGroupId(vars["group_id"]) - if v.hasErrors() { - handleErr(w, r, v) - return - } - settings := types.NotificationSettingsAccountDashboard{ - WebhookUrl: req.WebhookUrl, - IsWebhookDiscordEnabled: req.IsWebhookDiscordEnabled, - IsIgnoreSpamTransactionsEnabled: req.IsIgnoreSpamTransactionsEnabled, - SubscribedChainIds: chainIds, - - IsIncomingTransactionsSubscribed: req.IsIncomingTransactionsSubscribed, - IsOutgoingTransactionsSubscribed: req.IsOutgoingTransactionsSubscribed, - IsERC20TokenTransfersSubscribed: req.IsERC20TokenTransfersSubscribed, - ERC20TokenTransfersValueThreshold: req.ERC20TokenTransfersValueThreshold, - IsERC721TokenTransfersSubscribed: req.IsERC721TokenTransfersSubscribed, - IsERC1155TokenTransfersSubscribed: req.IsERC1155TokenTransfersSubscribed, - } - err := h.dai.UpdateNotificationSettingsAccountDashboard(r.Context(), dashboardId, groupId, settings) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalPutUserNotificationSettingsAccountDashboardResponse{ - Data: settings, - } - returnOk(w, r, response) + h.PublicPutUserNotificationSettingsAccountDashboard(w, r) } func (h *HandlerService) InternalPostUserNotificationsTestEmail(w http.ResponseWriter, r *http.Request) { - // TODO - returnOk(w, r, nil) + h.PublicPostUserNotificationsTestEmail(w, r) } func (h *HandlerService) InternalPostUserNotificationsTestPush(w http.ResponseWriter, r *http.Request) { - // TODO - returnOk(w, r, nil) + h.PublicPostUserNotificationsTestPush(w, r) } func (h *HandlerService) InternalPostUserNotificationsTestWebhook(w http.ResponseWriter, r *http.Request) { - var v validationError - req := struct { - WebhookUrl string `json:"webhook_url"` - IsDiscordWebhookEnabled bool `json:"is_discord_webhook_enabled,omitempty"` - }{} - if err := v.checkBody(&req, r); err != nil { - handleErr(w, r, err) - return - } - if v.hasErrors() { - handleErr(w, r, v) - return - } - // TODO - returnOk(w, r, nil) + h.PublicPostUserNotificationsTestWebhook(w, r) } // -------------------------------------- @@ -1265,3 +945,7 @@ func (h *HandlerService) InternalGetSlotBlobs(w http.ResponseWriter, r *http.Req } returnOk(w, r, response) } + +func (h *HandlerService) ReturnOk(w http.ResponseWriter, r *http.Request) { + returnOk(w, r, nil) +} diff --git a/backend/pkg/api/handlers/public.go b/backend/pkg/api/handlers/public.go index 3e80c2769..b3ffd15dd 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "net/http" "reflect" "time" @@ -17,6 +18,27 @@ import ( // Public handlers may only be authenticated by an API key // Public handlers must never call internal handlers +// @title beaconcha.in API +// @version 2.0 +// @description To authenticate your API request beaconcha.in uses API Keys. Set your API Key either by: +// @description - Setting the `Authorization` header in the following format: `Authorization: Bearer `. (recommended) +// @description - Setting the URL query parameter in the following format: `api_key={your_api_key}`.\ +// @description Example: `https://beaconcha.in/api/v2/example?field=value&api_key={your_api_key}` + +// @host beaconcha.in +// @BasePath /api/v2 + +// @securitydefinitions.apikey ApiKeyInHeader +// @in header +// @name Authorization +// @description Use your API key as a Bearer token, e.g. `Bearer ` + +// @securitydefinitions.apikey ApiKeyInQuery +// @in query +// @name api_key + +// @Validator Dashboard Management.n + func (h *HandlerService) PublicGetHealthz(w http.ResponseWriter, r *http.Request) { var v validationError showAll := v.checkBool(r.URL.Query().Get("show_all"), "show_all") @@ -112,6 +134,18 @@ func (h *HandlerService) PublicPutAccountDashboardTransactionsSettings(w http.Re returnOk(w, r, nil) } +// PublicPostValidatorDashboards godoc +// +// @Description Create a new validator dashboard. **Note**: New dashboards will automatically have a default group created. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param request body handlers.PublicPostValidatorDashboards.request true "`name`: Specify the name of the dashboard.
`network`: Specify the network for the dashboard. Possible options are:" +// @Success 201 {object} types.ApiDataResponse[types.VDBPostReturnData] +// @Failure 400 {object} types.ApiErrorResponse +// @Failure 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their dashboard limit." +// @Router /validator-dashboards [post] func (h *HandlerService) PublicPostValidatorDashboards(w http.ResponseWriter, r *http.Request) { var v validationError userId, err := GetUserIdByContext(r) @@ -162,6 +196,16 @@ func (h *HandlerService) PublicPostValidatorDashboards(w http.ResponseWriter, r returnCreated(w, r, response) } +// PublicGetValidatorDashboards godoc +// +// @Description Get overview information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardResponse +// @Failure 400 {object} types.ApiErrorResponse "Bad Request" +// @Router /validator-dashboards/{dashboard_id} [get] func (h *HandlerService) PublicGetValidatorDashboard(w http.ResponseWriter, r *http.Request) { var v validationError dashboardIdParam := mux.Vars(r)["dashboard_id"] @@ -213,6 +257,16 @@ func (h *HandlerService) PublicGetValidatorDashboard(w http.ResponseWriter, r *h returnOk(w, r, response) } +// PublicPutValidatorDashboard godoc +// +// @Description Delete a specified validator dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Success 204 "Dashboard deleted successfully." +// @Failure 400 {object} types.ApiErrorResponse "Bad Request" +// @Router /validator-dashboards/{dashboard_id} [delete] func (h *HandlerService) PublicDeleteValidatorDashboard(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) @@ -228,6 +282,18 @@ func (h *HandlerService) PublicDeleteValidatorDashboard(w http.ResponseWriter, r returnNoContent(w, r) } +// PublicPutValidatorDashboard godoc +// +// @Description Update the name of a specified validator dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicPutValidatorDashboardName.request true "request" +// @Success 200 {object} types.ApiDataResponse[types.VDBPostReturnData] +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/name [put] func (h *HandlerService) PublicPutValidatorDashboardName(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) @@ -255,6 +321,19 @@ func (h *HandlerService) PublicPutValidatorDashboardName(w http.ResponseWriter, returnOk(w, r, response) } +// PublicPostValidatorDashboardGroups godoc +// +// @Description Create a new group in a specified validator dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicPostValidatorDashboardGroups.request true "request" +// @Success 201 {object} types.ApiDataResponse[types.VDBPostCreateGroupData] +// @Failure 400 {object} types.ApiErrorResponse +// @Failure 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their group limit." +// @Router /validator-dashboards/{dashboard_id}/groups [post] func (h *HandlerService) PublicPostValidatorDashboardGroups(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) @@ -306,6 +385,19 @@ func (h *HandlerService) PublicPostValidatorDashboardGroups(w http.ResponseWrite returnCreated(w, r, response) } +// PublicGetValidatorDashboardGroups godoc +// +// @Description Update a groups name in a specified validator dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param request body handlers.PublicPutValidatorDashboardGroups.request true "request" +// @Success 200 {object} types.ApiDataResponse[types.VDBPostCreateGroupData] +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups/{group_id} [put] func (h *HandlerService) PublicPutValidatorDashboardGroups(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -346,6 +438,18 @@ func (h *HandlerService) PublicPutValidatorDashboardGroups(w http.ResponseWriter returnOk(w, r, response) } +// PublicDeleteValidatorDashboardGroups godoc +// +// @Description Delete a group in a specified validator dashboard. +// @Tags Validator Dashboard Management +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Success 204 "Group deleted successfully." +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups/{group_id} [delete] func (h *HandlerService) PublicDeleteValidatorDashboardGroup(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -377,6 +481,19 @@ func (h *HandlerService) PublicDeleteValidatorDashboardGroup(w http.ResponseWrit returnNoContent(w, r) } +// PublicGetValidatorDashboardGroups godoc +// +// @Description Add new validators to a specified dashboard or update the group of already-added validators. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicPostValidatorDashboardValidators.request true "`group_id`: (optional) Provide a single group id, to which all validators get added to. If omitted, the default group will be used.

To add validators, only one of the following fields can be set:" +// @Success 201 {object} types.ApiDataResponse[[]types.VDBPostValidatorsData] "Returns a list of added validators." +// @Failure 400 {object} types.ApiErrorResponse +// @Failure 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their validator limit." +// @Router /validator-dashboards/{dashboard_id}/validators [post] func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) @@ -500,6 +617,19 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW returnCreated(w, r, response) } +// PublicGetValidatorDashboardValidators godoc +// +// @Description Get a list of groups in a specified validator dashboard. +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id query string false "The ID of the group." +// @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(index, public_key, balance, status, withdrawal_credentials) +// @Param search query string false "Search for Address, ENS." +// @Success 200 {object} types.GetValidatorDashboardValidatorsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups [get] func (h *HandlerService) PublicGetValidatorDashboardValidators(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -527,19 +657,30 @@ func (h *HandlerService) PublicGetValidatorDashboardValidators(w http.ResponseWr returnOk(w, r, response) } +// PublicDeleteValidatorDashboardValidators godoc +// +// @Description Remove validators from a specified dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicDeleteValidatorDashboardValidators.request true "`validators`: Provide an array of validator indices or public keys that should get removed from the dashboard." +// @Success 204 "Validators removed successfully." +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/validators/bulk-deletions [post] func (h *HandlerService) PublicDeleteValidatorDashboardValidators(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) - var indices []uint64 - var publicKeys []string - req := struct { + type request struct { Validators []intOrString `json:"validators"` - }{} + } + var req request if err := v.checkBody(&req, r); err != nil { handleErr(w, r, err) return } - indices, publicKeys = v.checkValidators(req.Validators, false) + indices, publicKeys := v.checkValidators(req.Validators, false) if v.hasErrors() { handleErr(w, r, v) return @@ -558,6 +699,19 @@ func (h *HandlerService) PublicDeleteValidatorDashboardValidators(w http.Respons returnNoContent(w, r) } +// PublicPostValidatorDashboardPublicIds godoc +// +// @Description Create a new public ID for a specified dashboard. This can be used as an ID by other users for non-modyfing (i.e. GET) endpoints only. Currently limited to one per dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicPostValidatorDashboardPublicIds.request true "`name`: Provide a public name for the dashboard
`share_settings`:" +// @Success 201 {object} types.ApiDataResponse[types.VDBPublicId] +// @Failure 400 {object} types.ApiErrorResponse +// @Failure 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their public ID limit." +// @Router /validator-dashboards/{dashboard_id}/public-ids [post] func (h *HandlerService) PublicPostValidatorDashboardPublicIds(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) @@ -592,13 +746,26 @@ func (h *HandlerService) PublicPostValidatorDashboardPublicIds(w http.ResponseWr handleErr(w, r, err) return } - response := types.ApiResponse{ - Data: data, + response := types.ApiDataResponse[types.VDBPublicId]{ + Data: *data, } returnCreated(w, r, response) } +// PublicPutValidatorDashboardPublicId godoc +// +// @Description Update a specified public ID for a specified dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param public_id path string true "The ID of the public ID." +// @Param request body handlers.PublicPutValidatorDashboardPublicId.request true "`name`: Provide a public name for the dashboard
`share_settings`:" +// @Success 200 {object} types.ApiDataResponse[types.VDBPublicId] +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/public-ids/{public_id} [put] func (h *HandlerService) PublicPutValidatorDashboardPublicId(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -635,13 +802,24 @@ func (h *HandlerService) PublicPutValidatorDashboardPublicId(w http.ResponseWrit handleErr(w, r, err) return } - response := types.ApiResponse{ - Data: data, + response := types.ApiDataResponse[types.VDBPublicId]{ + Data: *data, } returnOk(w, r, response) } +// PublicDeleteValidatorDashboardPublicId godoc +// +// @Description Delete a specified public ID for a specified dashboard. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param public_id path string true "The ID of the public ID." +// @Success 204 "Public ID deleted successfully." +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/public-ids/{public_id} [delete] func (h *HandlerService) PublicDeleteValidatorDashboardPublicId(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -670,12 +848,26 @@ func (h *HandlerService) PublicDeleteValidatorDashboardPublicId(w http.ResponseW returnNoContent(w, r) } +// PublicPutValidatorDashboardArchiving godoc +// +// @Description Archive or unarchive a specified validator dashboard. Archived dashboards cannot be accessed by other endpoints. Archiving happens automatically if the number of dashboards, validators, or groups exceeds the limit allowed by your subscription plan. For example, this might occur if you downgrade your subscription to a lower tier. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Validator Dashboard Management +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param request body handlers.PublicPutValidatorDashboardArchiving.request true "request" +// @Success 200 {object} types.ApiDataResponse[types.VDBPostArchivingReturnData] +// @Failure 400 {object} types.ApiErrorResponse +// @Conflict 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their subscription limit." +// @Router /validator-dashboards/{dashboard_id}/archiving [put] func (h *HandlerService) PublicPutValidatorDashboardArchiving(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId := v.checkPrimaryDashboardId(mux.Vars(r)["dashboard_id"]) - req := struct { + type request struct { IsArchived bool `json:"is_archived"` - }{} + } + var req request if err := v.checkBody(&req, r); err != nil { handleErr(w, r, err) return @@ -753,6 +945,16 @@ func (h *HandlerService) PublicPutValidatorDashboardArchiving(w http.ResponseWri returnOk(w, r, response) } +// PublicGetValidatorDashboardSlotViz godoc +// +// @Description Get slot viz information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_ids query string false "Provide a comma separated list of group IDs to filter the results by. If omitted, all groups will be included." +// @Success 200 {object} types.GetValidatorDashboardSlotVizResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/slot-viz [get] func (h *HandlerService) PublicGetValidatorDashboardSlotViz(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -778,6 +980,23 @@ func (h *HandlerService) PublicGetValidatorDashboardSlotViz(w http.ResponseWrite returnOk(w, r, response) } +var summaryAllowedPeriods = []enums.TimePeriod{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} + +// PublicGetValidatorDashboardSummary godoc +// +// @Description Get summary information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param period query string true "Time period to get data for." Enums(all_time, last_30d, last_7d, last_24h, last_1h) +// @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(group_id, validators, efficiency, attestations, proposals, reward) +// @Param search query string false "Search for Index, Public Key, Group." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardSummaryResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/summary [get] func (h *HandlerService) PublicGetValidatorDashboardSummary(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -792,8 +1011,7 @@ func (h *HandlerService) PublicGetValidatorDashboardSummary(w http.ResponseWrite period := checkEnum[enums.TimePeriod](&v, q.Get("period"), "period") // allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h - allowedPeriods := []enums.Enum{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} - v.checkEnumIsAllowed(period, allowedPeriods, "period") + checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period") if v.hasErrors() { handleErr(w, r, v) return @@ -811,6 +1029,18 @@ func (h *HandlerService) PublicGetValidatorDashboardSummary(w http.ResponseWrite returnOk(w, r, response) } +// PublicGetValidatorDashboardGroupSummary godoc +// +// @Description Get summary information for a specified group in a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param period query string true "Time period to get data for." Enums(all_time, last_30d, last_7d, last_24h, last_1h) +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardGroupSummaryResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups/{group_id}/summary [get] func (h *HandlerService) PublicGetValidatorDashboardGroupSummary(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -828,8 +1058,7 @@ func (h *HandlerService) PublicGetValidatorDashboardGroupSummary(w http.Response groupId := v.checkGroupId(vars["group_id"], forbidEmpty) period := checkEnum[enums.TimePeriod](&v, r.URL.Query().Get("period"), "period") // allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h - allowedPeriods := []enums.Enum{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} - v.checkEnumIsAllowed(period, allowedPeriods, "period") + checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period") if v.hasErrors() { handleErr(w, r, v) return @@ -846,6 +1075,20 @@ func (h *HandlerService) PublicGetValidatorDashboardGroupSummary(w http.Response returnOk(w, r, response) } +// PublicGetValidatorDashboardSummaryChart godoc +// +// @Description Get summary chart data for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_ids query string false "Provide a comma separated list of group IDs to filter the results by." +// @Param efficiency_type query string false "Efficiency type to get data for." Enums(all, attestation, sync, proposal) +// @Param aggregation query string false "Aggregation type to get data for." Enums(epoch, hourly, daily, weekly) Default(hourly) +// @Param after_ts query string false "Return data after this timestamp." +// @Param before_ts query string false "Return data before this timestamp." +// @Success 200 {object} types.GetValidatorDashboardSummaryChartResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/summary-chart [get] func (h *HandlerService) PublicGetValidatorDashboardSummaryChart(w http.ResponseWriter, r *http.Request) { var v validationError ctx := r.Context() @@ -885,6 +1128,18 @@ func (h *HandlerService) PublicGetValidatorDashboardSummaryChart(w http.Response returnOk(w, r, response) } +// PublicGetValidatorDashboardSummaryValidators godoc +// +// @Description Get summary information for validators in a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id query string false "The ID of the group." +// @Param duty query string false "Validator duty to get data for." Enums(none, sync, slashed, proposal) Default(none) +// @Param period query string true "Time period to get data for." Enums(all_time, last_30d, last_7d, last_24h, last_1h) +// @Success 200 {object} types.GetValidatorDashboardSummaryValidatorsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/summary/validators [get] func (h *HandlerService) PublicGetValidatorDashboardSummaryValidators(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -897,8 +1152,8 @@ func (h *HandlerService) PublicGetValidatorDashboardSummaryValidators(w http.Res duty := checkEnum[enums.ValidatorDuty](&v, q.Get("duty"), "duty") period := checkEnum[enums.TimePeriod](&v, q.Get("period"), "period") // allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h - allowedPeriods := []enums.Enum{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} - v.checkEnumIsAllowed(period, allowedPeriods, "period") + allowedPeriods := []enums.TimePeriod{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} + checkEnumIsAllowed(&v, period, allowedPeriods, "period") if v.hasErrors() { handleErr(w, r, v) return @@ -935,6 +1190,20 @@ func (h *HandlerService) PublicGetValidatorDashboardSummaryValidators(w http.Res returnOk(w, r, response) } +// PublicGetValidatorDashboardRewards godoc +// +// @Description Get rewards 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(epoch) +// @Param search query string false "Search for Epoch, Index, Public Key, Group." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardRewardsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/rewards [get] func (h *HandlerService) PublicGetValidatorDashboardRewards(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -963,6 +1232,18 @@ func (h *HandlerService) PublicGetValidatorDashboardRewards(w http.ResponseWrite returnOk(w, r, response) } +// PublicGetValidatorDashboardGroupRewards godoc +// +// @Description Get rewards information for a specified group in a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param epoch path string true "The epoch to get data for." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardGroupRewardsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups/{group_id}/rewards/{epoch} [get] func (h *HandlerService) PublicGetValidatorDashboardGroupRewards(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -991,6 +1272,16 @@ func (h *HandlerService) PublicGetValidatorDashboardGroupRewards(w http.Response returnOk(w, r, response) } +// PublicGetValidatorDashboardRewardsChart godoc +// +// @Description Get rewards chart data for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardRewardsChartResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/rewards-chart [get] func (h *HandlerService) PublicGetValidatorDashboardRewardsChart(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -1017,6 +1308,22 @@ func (h *HandlerService) PublicGetValidatorDashboardRewardsChart(w http.Response returnOk(w, r, response) } +// PublicGetValidatorDashboardDuties godoc +// +// @Description Get duties information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param epoch path string true "The epoch to get data for." +// @Param group_id query string false "The ID of the group." +// @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(validator, reward) +// @Param search query string false "Search for Index, Public Key." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardDutiesResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/duties/{epoch} [get] func (h *HandlerService) PublicGetValidatorDashboardDuties(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -1048,6 +1355,20 @@ func (h *HandlerService) PublicGetValidatorDashboardDuties(w http.ResponseWriter returnOk(w, r, response) } +// PublicGetValidatorDashboardBlocks godoc +// +// @Description Get blocks 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(proposer, slot, block, status, reward) +// @Param search query string false "Search for Index, Public Key, Group." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardBlocksResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/blocks [get] func (h *HandlerService) PublicGetValidatorDashboardBlocks(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1076,6 +1397,19 @@ func (h *HandlerService) PublicGetValidatorDashboardBlocks(w http.ResponseWriter returnOk(w, r, response) } +// PublicGetValidatorDashboardHeatmap godoc +// +// @Description Get heatmap information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param aggregation query string false "Aggregation type to get data for." Enums(epoch, hourly, daily, weekly) Default(hourly) +// @Param after_ts query string false "Return data after this timestamp." +// @Param before_ts query string false "Return data before this timestamp." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardHeatmapResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/heatmap [get] func (h *HandlerService) PublicGetValidatorDashboardHeatmap(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1112,6 +1446,19 @@ func (h *HandlerService) PublicGetValidatorDashboardHeatmap(w http.ResponseWrite returnOk(w, r, response) } +// PublicGetValidatorDashboardGroupHeatmap godoc +// +// @Description Get heatmap information for a specified group in a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param timestamp path string true "The timestamp to get data for." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Param aggregation query string false "Aggregation type to get data for." Enums(epoch, hourly, daily, weekly) Default(hourly) +// @Success 200 {object} types.GetValidatorDashboardGroupHeatmapResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/groups/{group_id}/heatmap/{timestamp} [get] func (h *HandlerService) PublicGetValidatorDashboardGroupHeatmap(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -1149,6 +1496,17 @@ func (h *HandlerService) PublicGetValidatorDashboardGroupHeatmap(w http.Response returnOk(w, r, response) } +// PublicGetValidatorDashboardExecutionLayerDeposits 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." +// @Success 200 {object} types.GetValidatorDashboardExecutionLayerDepositsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/execution-layer-deposits [get] func (h *HandlerService) PublicGetValidatorDashboardExecutionLayerDeposits(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1174,6 +1532,17 @@ func (h *HandlerService) PublicGetValidatorDashboardExecutionLayerDeposits(w htt returnOk(w, r, response) } +// PublicGetValidatorDashboardConsensusLayerDeposits godoc +// +// @Description Get consensus 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." +// @Success 200 {object} types.GetValidatorDashboardConsensusLayerDepositsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/consensus-layer-deposits [get] func (h *HandlerService) PublicGetValidatorDashboardConsensusLayerDeposits(w http.ResponseWriter, r *http.Request) { var v validationError dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1200,6 +1569,15 @@ func (h *HandlerService) PublicGetValidatorDashboardConsensusLayerDeposits(w htt returnOk(w, r, response) } +// PublicGetValidatorDashboardTotalConsensusLayerDeposits godoc +// +// @Description Get total consensus layer deposits information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Success 200 {object} types.GetValidatorDashboardTotalConsensusDepositsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/total-consensus-layer-deposits [get] func (h *HandlerService) PublicGetValidatorDashboardTotalConsensusLayerDeposits(w http.ResponseWriter, r *http.Request) { var err error dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1219,6 +1597,15 @@ func (h *HandlerService) PublicGetValidatorDashboardTotalConsensusLayerDeposits( returnOk(w, r, response) } +// PublicGetValidatorDashboardTotalExecutionLayerDeposits godoc +// +// @Description Get total execution layer deposits information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Success 200 {object} types.GetValidatorDashboardTotalExecutionDepositsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/total-execution-layer-deposits [get] func (h *HandlerService) PublicGetValidatorDashboardTotalExecutionLayerDeposits(w http.ResponseWriter, r *http.Request) { var err error dashboardId, err := h.handleDashboardId(r.Context(), mux.Vars(r)["dashboard_id"]) @@ -1238,6 +1625,20 @@ func (h *HandlerService) PublicGetValidatorDashboardTotalExecutionLayerDeposits( returnOk(w, r, response) } +// PublicGetValidatorDashboardWithdrawals godoc +// +// @Description Get withdrawals 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(epoch, slot, index, recipient, amount) +// @Param search query string false "Search for Index, Public Key, Address." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardWithdrawalsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/withdrawals [get] func (h *HandlerService) PublicGetValidatorDashboardWithdrawals(w http.ResponseWriter, r *http.Request) { var v validationError q := r.URL.Query() @@ -1266,6 +1667,16 @@ func (h *HandlerService) PublicGetValidatorDashboardWithdrawals(w http.ResponseW returnOk(w, r, response) } +// PublicGetValidatorDashboardTotalWithdrawals godoc +// +// @Description Get total withdrawals information for a specified dashboard +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param modes query string false "Provide a comma separated list of protocol modes which should be respected for validator calculations. Possible values are `rocket_pool``." +// @Success 200 {object} types.GetValidatorDashboardTotalWithdrawalsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/total-withdrawals [get] func (h *HandlerService) PublicGetValidatorDashboardTotalWithdrawals(w http.ResponseWriter, r *http.Request) { var v validationError q := r.URL.Query() @@ -1293,6 +1704,19 @@ func (h *HandlerService) PublicGetValidatorDashboardTotalWithdrawals(w http.Resp returnOk(w, r, response) } +// PublicGetValidatorDashboardRocketPool godoc +// +// @Description Get an aggregated list of the Rocket Pool nodes details associated with 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(node, minipools, collateral, rpl, effective_rpl, rpl_apr, smoothing_pool) +// @Param search query string false "Search for Node address." +// @Success 200 {object} types.GetValidatorDashboardRocketPoolResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/rocket-pool [get] func (h *HandlerService) PublicGetValidatorDashboardRocketPool(w http.ResponseWriter, r *http.Request) { var v validationError q := r.URL.Query() @@ -1320,6 +1744,15 @@ func (h *HandlerService) PublicGetValidatorDashboardRocketPool(w http.ResponseWr returnOk(w, r, response) } +// PublicGetValidatorDashboardTotalRocketPool godoc +// +// @Description Get a summary of all Rocket Pool nodes details associated with a specified dashboard. +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Success 200 {object} types.GetValidatorDashboardTotalRocketPoolResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/total-rocket-pool [get] func (h *HandlerService) PublicGetValidatorDashboardTotalRocketPool(w http.ResponseWriter, r *http.Request) { var v validationError q := r.URL.Query() @@ -1345,6 +1778,16 @@ func (h *HandlerService) PublicGetValidatorDashboardTotalRocketPool(w http.Respo returnOk(w, r, response) } +// PublicGetValidatorDashboardNodeRocketPool godoc +// +// @Description Get details for a specific Rocket Pool node associated with a specified dashboard. +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param node_address path string true "The address of the node." +// @Success 200 {object} types.GetValidatorDashboardNodeRocketPoolResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/rocket-pool/{node_address} [get] func (h *HandlerService) PublicGetValidatorDashboardNodeRocketPool(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -1371,6 +1814,20 @@ func (h *HandlerService) PublicGetValidatorDashboardNodeRocketPool(w http.Respon returnOk(w, r, response) } +// PublicGetValidatorDashboardRocketPoolMinipools godoc +// +// @Description Get minipools information for a specified Rocket Pool node associated with a specified dashboard. +// @Tags Validator Dashboard +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param node_address path string true "The address of the node." +// @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(group_id) +// @Param search query string false "Search for Index, Node." +// @Success 200 {object} types.GetValidatorDashboardRocketPoolMinipoolsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /validator-dashboards/{dashboard_id}/rocket-pool/{node_address}/minipools [get] func (h *HandlerService) PublicGetValidatorDashboardRocketPoolMinipools(w http.ResponseWriter, r *http.Request) { var v validationError vars := mux.Vars(r) @@ -1401,6 +1858,703 @@ func (h *HandlerService) PublicGetValidatorDashboardRocketPoolMinipools(w http.R returnOk(w, r, response) } +// ---------------------------------------------- +// Notifications +// ---------------------------------------------- + +// PublicGetUserNotifications godoc +// +// @Description Get an overview of your recent notifications. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @Success 200 {object} types.InternalGetUserNotificationsResponse +// @Router /users/me/notifications [get] +func (h *HandlerService) PublicGetUserNotifications(w http.ResponseWriter, r *http.Request) { + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + data, err := h.dai.GetNotificationOverview(r.Context(), userId) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationsResponse{ + Data: *data, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationDashboards godoc +// +// @Description Get a list of triggered notifications related to your dashboards. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @Param network query string false "If set, results will be filtered to only include networks given. Provide a comma separated list." +// @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(chain_id, timestamp, dashboard_id) +// @Param search query string false "Search for Dashboard, Group" +// @Success 200 {object} types.InternalGetUserNotificationDashboardsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/dashboards [get] +func (h *HandlerService) PublicGetUserNotificationDashboards(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationDashboardsColumn](&v, q.Get("sort")) + chainId := v.checkNetworkParameter(q.Get("network")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetDashboardNotifications(r.Context(), userId, chainId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationDashboardsResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationValidators godoc +// +// @Description Get a detailed view of a triggered notification related to a validator dashboard group at a specific epoch. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param epoch path string true "The epoch of the notification." +// @Success 200 {object} types.InternalGetUserNotificationsValidatorDashboardResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/validator-dashboards/{dashboard_id}/groups/{group_id}/epochs/{epoch} [get] +func (h *HandlerService) PublicGetUserNotificationsValidatorDashboard(w http.ResponseWriter, r *http.Request) { + var v validationError + vars := mux.Vars(r) + dashboardId := v.checkPrimaryDashboardId(vars["dashboard_id"]) + groupId := v.checkExistingGroupId(vars["group_id"]) + epoch := v.checkUint(vars["epoch"], "epoch") + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, err := h.dai.GetValidatorDashboardNotificationDetails(r.Context(), dashboardId, groupId, epoch) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationsValidatorDashboardResponse{ + Data: *data, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationsAccountDashboard godoc +// +// @Description Get a detailed view of a triggered notification related to an account dashboard group at a specific epoch. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param epoch path string true "The epoch of the notification." +// @Success 200 {object} types.InternalGetUserNotificationsAccountDashboardResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/account-dashboards/{dashboard_id}/groups/{group_id}/epochs/{epoch} [get] +func (h *HandlerService) PublicGetUserNotificationsAccountDashboard(w http.ResponseWriter, r *http.Request) { + var v validationError + vars := mux.Vars(r) + dashboardId := v.checkUint(vars["dashboard_id"], "dashboard_id") + groupId := v.checkExistingGroupId(vars["group_id"]) + epoch := v.checkUint(vars["epoch"], "epoch") + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, err := h.dai.GetAccountDashboardNotificationDetails(r.Context(), dashboardId, groupId, epoch) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationsAccountDashboardResponse{ + Data: *data, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationMachines godoc +// +// @Description Get a list of triggered notifications related to your machines. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @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(machine_name, threshold, event_type, timestamp) +// @Param search query string false "Search for Machine" +// @Success 200 {object} types.InternalGetUserNotificationMachinesResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/machines [get] +func (h *HandlerService) PublicGetUserNotificationMachines(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationMachinesColumn](&v, q.Get("sort")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetMachineNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationMachinesResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationClients godoc +// +// @Description Get a list of triggered notifications related to your clients. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @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(client_name, timestamp) +// @Param search query string false "Search for Client" +// @Success 200 {object} types.InternalGetUserNotificationClientsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/clients [get] +func (h *HandlerService) PublicGetUserNotificationClients(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationClientsColumn](&v, q.Get("sort")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetClientNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationClientsResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationRocketPool godoc +// +// @Description Get a list of triggered notifications related to Rocket Pool. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @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(timestamp, event_type, node_address) +// @Param search query string false "Search for TODO" +// @Success 200 {object} types.InternalGetUserNotificationRocketPoolResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/rocket-pool [get] +func (h *HandlerService) PublicGetUserNotificationRocketPool(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationRocketPoolColumn](&v, q.Get("sort")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetRocketPoolNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationRocketPoolResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationNetworks godoc +// +// @Description Get a list of triggered notifications related to networks. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notifications +// @Produce json +// @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(timestamp, event_type) +// @Param search query string false "Search for TODO" +// @Success 200 {object} types.InternalGetUserNotificationNetworksResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/networks [get] +func (h *HandlerService) PublicGetUserNotificationNetworks(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationNetworksColumn](&v, q.Get("sort")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetNetworkNotifications(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationNetworksResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicGetUserNotificationPairedDevices godoc +// +// @Description Get notification settings for the authenticated user. Excludes dashboard notification settings. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Produce json +// @Success 200 {object} types.InternalGetUserNotificationSettingsResponse +// @Router /users/me/notifications/settings [get] +func (h *HandlerService) PublicGetUserNotificationSettings(w http.ResponseWriter, r *http.Request) { + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + data, err := h.dai.GetNotificationSettings(r.Context(), userId) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationSettingsResponse{ + Data: *data, + } + returnOk(w, r, response) +} + +// PublicPutUserNotificationSettingsGeneral godoc +// +// @Description Update general notification settings for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param request body types.NotificationSettingsGeneral true "Notification settings" +// @Success 200 {object} types.InternalPutUserNotificationSettingsGeneralResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/general [put] +func (h *HandlerService) PublicPutUserNotificationSettingsGeneral(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + var req types.NotificationSettingsGeneral + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + checkMinMax(&v, req.MachineStorageUsageThreshold, 0, 1, "machine_storage_usage_threshold") + checkMinMax(&v, req.MachineCpuUsageThreshold, 0, 1, "machine_cpu_usage_threshold") + checkMinMax(&v, req.MachineMemoryUsageThreshold, 0, 1, "machine_memory_usage_threshold") + checkMinMax(&v, req.RocketPoolMaxCollateralThreshold, 0, 1, "rocket_pool_max_collateral_threshold") + checkMinMax(&v, req.RocketPoolMinCollateralThreshold, 0, 1, "rocket_pool_min_collateral_threshold") + // TODO: check validity of clients + if v.hasErrors() { + handleErr(w, r, v) + return + } + err = h.dai.UpdateNotificationSettingsGeneral(r.Context(), userId, req) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalPutUserNotificationSettingsGeneralResponse{ + Data: req, + } + returnOk(w, r, response) +} + +// PublicPutUserNotificationSettingsNetworks godoc +// +// @Description Update network notification settings for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param network path string true "The networks name or chain ID." +// @Param request body types.NotificationSettingsNetwork true "Notification settings" +// @Success 200 {object} types.InternalPutUserNotificationSettingsNetworksResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/networks/{network} [put] +func (h *HandlerService) PublicPutUserNotificationSettingsNetworks(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + var req types.NotificationSettingsNetwork + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + checkMinMax(&v, req.ParticipationRateThreshold, 0, 1, "participation_rate_threshold") + + chainId := v.checkNetworkParameter(mux.Vars(r)["network"]) + if v.hasErrors() { + handleErr(w, r, v) + return + } + err = h.dai.UpdateNotificationSettingsNetworks(r.Context(), userId, chainId, req) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalPutUserNotificationSettingsNetworksResponse{ + Data: types.NotificationNetwork{ + ChainId: chainId, + Settings: req, + }, + } + returnOk(w, r, response) +} + +// PublicPutUserNotificationSettingsPairedDevices godoc +// +// @Description Update paired device notification settings for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param paired_device_id path string true "The paired device ID." +// @Param request body handlers.PublicPutUserNotificationSettingsPairedDevices.request true "Notification settings" +// @Success 200 {object} types.InternalPutUserNotificationSettingsPairedDevicesResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/paired-devices/{paired_device_id} [put] +func (h *HandlerService) PublicPutUserNotificationSettingsPairedDevices(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + type request struct { + Name string `json:"name,omitempty"` + IsNotificationsEnabled bool `json:"is_notifications_enabled"` + } + var req request + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + // TODO use a better way to validate the paired device id + pairedDeviceId := v.checkRegex(reNonEmpty, mux.Vars(r)["paired_device_id"], "paired_device_id") + name := v.checkNameNotEmpty(req.Name) + if v.hasErrors() { + handleErr(w, r, v) + return + } + err = h.dai.UpdateNotificationSettingsPairedDevice(r.Context(), userId, pairedDeviceId, name, req.IsNotificationsEnabled) + if err != nil { + handleErr(w, r, err) + return + } + // TODO timestamp + response := types.InternalPutUserNotificationSettingsPairedDevicesResponse{ + Data: types.NotificationPairedDevice{ + Id: pairedDeviceId, + Name: req.Name, + IsNotificationsEnabled: req.IsNotificationsEnabled, + }, + } + + returnOk(w, r, response) +} + +// PublicDeleteUserNotificationSettingsPairedDevices godoc +// +// @Description Delete paired device notification settings for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Produce json +// @Param paired_device_id path string true "The paired device ID." +// @Success 204 +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/paired-devices/{paired_device_id} [delete] +func (h *HandlerService) PublicDeleteUserNotificationSettingsPairedDevices(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + // TODO use a better way to validate the paired device id + pairedDeviceId := v.checkRegex(reNonEmpty, mux.Vars(r)["paired_device_id"], "paired_device_id") + if v.hasErrors() { + handleErr(w, r, v) + return + } + err = h.dai.DeleteNotificationSettingsPairedDevice(r.Context(), userId, pairedDeviceId) + if err != nil { + handleErr(w, r, err) + return + } + returnNoContent(w, r) +} + +// PublicGetUserNotificationSettingsDashboards godoc +// +// @Description Get a list of notification settings for the dashboards of the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Produce json +// @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 (dashboard_id, group_name) +// @Param search query string false "Search for Dashboard, Group" +// @Success 200 {object} types.InternalGetUserNotificationSettingsDashboardsResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/dashboards [get] +func (h *HandlerService) PublicGetUserNotificationSettingsDashboards(w http.ResponseWriter, r *http.Request) { + var v validationError + userId, err := GetUserIdByContext(r) + if err != nil { + handleErr(w, r, err) + return + } + q := r.URL.Query() + pagingParams := v.checkPagingParams(q) + sort := checkSort[enums.NotificationSettingsDashboardColumn](&v, q.Get("sort")) + if v.hasErrors() { + handleErr(w, r, v) + return + } + data, paging, err := h.dai.GetNotificationSettingsDashboards(r.Context(), userId, pagingParams.cursor, *sort, pagingParams.search, pagingParams.limit) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalGetUserNotificationSettingsDashboardsResponse{ + Data: data, + Paging: *paging, + } + returnOk(w, r, response) +} + +// PublicPutUserNotificationSettingsValidatorDashboard godoc +// +// @Description Update the notification settings for a specific group of a validator dashboard for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param request body types.NotificationSettingsValidatorDashboard true "Notification settings" +// @Success 200 {object} types.InternalPutUserNotificationSettingsValidatorDashboardResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/validator-dashboards/{dashboard_id}/groups/{group_id} [put] +func (h *HandlerService) PublicPutUserNotificationSettingsValidatorDashboard(w http.ResponseWriter, r *http.Request) { + var v validationError + var req types.NotificationSettingsValidatorDashboard + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + checkMinMax(&v, req.GroupOfflineThreshold, 0, 1, "group_offline_threshold") + vars := mux.Vars(r) + dashboardId := v.checkPrimaryDashboardId(vars["dashboard_id"]) + groupId := v.checkExistingGroupId(vars["group_id"]) + if v.hasErrors() { + handleErr(w, r, v) + return + } + err := h.dai.UpdateNotificationSettingsValidatorDashboard(r.Context(), dashboardId, groupId, req) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalPutUserNotificationSettingsValidatorDashboardResponse{ + Data: req, + } + returnOk(w, r, response) +} + +// PublicPutUserNotificationSettingsAccountDashboard godoc +// +// @Description Update the notification settings for a specific group of an account dashboard for the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param dashboard_id path string true "The ID of the dashboard." +// @Param group_id path string true "The ID of the group." +// @Param request body handlers.PublicPutUserNotificationSettingsAccountDashboard.request true "Notification settings" +// @Success 200 {object} types.InternalPutUserNotificationSettingsAccountDashboardResponse +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/settings/account-dashboards/{dashboard_id}/groups/{group_id} [put] +func (h *HandlerService) PublicPutUserNotificationSettingsAccountDashboard(w http.ResponseWriter, r *http.Request) { + var v validationError + // uses a different struct due to `subscribed_chain_ids`, which is a slice of intOrString in the payload but a slice of uint64 in the response + type request struct { + WebhookUrl string `json:"webhook_url"` + IsWebhookDiscordEnabled bool `json:"is_webhook_discord_enabled"` + IsIgnoreSpamTransactionsEnabled bool `json:"is_ignore_spam_transactions_enabled"` + SubscribedChainIds []intOrString `json:"subscribed_chain_ids"` + + IsIncomingTransactionsSubscribed bool `json:"is_incoming_transactions_subscribed"` + IsOutgoingTransactionsSubscribed bool `json:"is_outgoing_transactions_subscribed"` + IsERC20TokenTransfersSubscribed bool `json:"is_erc20_token_transfers_subscribed"` + ERC20TokenTransfersValueThreshold float64 `json:"erc20_token_transfers_value_threshold"` // 0 does not disable, is_erc20_token_transfers_subscribed determines if it's enabled + IsERC721TokenTransfersSubscribed bool `json:"is_erc721_token_transfers_subscribed"` + IsERC1155TokenTransfersSubscribed bool `json:"is_erc1155_token_transfers_subscribed"` + } + var req request + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + chainIdMap := v.checkNetworkSlice(req.SubscribedChainIds) + // convert to uint64[] slice + chainIds := make([]uint64, len(chainIdMap)) + i := 0 + for k := range chainIdMap { + chainIds[i] = k + i++ + } + checkMinMax(&v, req.ERC20TokenTransfersValueThreshold, 0, math.MaxFloat64, "group_offline_threshold") + vars := mux.Vars(r) + dashboardId := v.checkPrimaryDashboardId(vars["dashboard_id"]) + groupId := v.checkExistingGroupId(vars["group_id"]) + if v.hasErrors() { + handleErr(w, r, v) + return + } + settings := types.NotificationSettingsAccountDashboard{ + WebhookUrl: req.WebhookUrl, + IsWebhookDiscordEnabled: req.IsWebhookDiscordEnabled, + IsIgnoreSpamTransactionsEnabled: req.IsIgnoreSpamTransactionsEnabled, + SubscribedChainIds: chainIds, + + IsIncomingTransactionsSubscribed: req.IsIncomingTransactionsSubscribed, + IsOutgoingTransactionsSubscribed: req.IsOutgoingTransactionsSubscribed, + IsERC20TokenTransfersSubscribed: req.IsERC20TokenTransfersSubscribed, + ERC20TokenTransfersValueThreshold: req.ERC20TokenTransfersValueThreshold, + IsERC721TokenTransfersSubscribed: req.IsERC721TokenTransfersSubscribed, + IsERC1155TokenTransfersSubscribed: req.IsERC1155TokenTransfersSubscribed, + } + err := h.dai.UpdateNotificationSettingsAccountDashboard(r.Context(), dashboardId, groupId, settings) + if err != nil { + handleErr(w, r, err) + return + } + response := types.InternalPutUserNotificationSettingsAccountDashboardResponse{ + Data: settings, + } + returnOk(w, r, response) +} + +// PublicPostUserNotificationsTestEmail godoc +// +// @Description Send a test email notification to the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Produce json +// @Success 204 +// @Router /users/me/notifications/test-email [post] +func (h *HandlerService) PublicPostUserNotificationsTestEmail(w http.ResponseWriter, r *http.Request) { + // TODO + returnNoContent(w, r) +} + +// PublicPostUserNotificationsTestPush godoc +// +// @Description Send a test push notification to the authenticated user. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Produce json +// @Success 204 +// @Router /users/me/notifications/test-push [post] +func (h *HandlerService) PublicPostUserNotificationsTestPush(w http.ResponseWriter, r *http.Request) { + // TODO + returnNoContent(w, r) +} + +// PublicPostUserNotificationsTestWebhook godoc +// +// @Description Send a test webhook notification from the authenticated user to the given URL. +// @Security ApiKeyInHeader || ApiKeyInQuery +// @Tags Notification Settings +// @Accept json +// @Produce json +// @Param request body handlers.PublicPostUserNotificationsTestWebhook.request true "Request" +// @Success 204 +// @Failure 400 {object} types.ApiErrorResponse +// @Router /users/me/notifications/test-webhook [post] +func (h *HandlerService) PublicPostUserNotificationsTestWebhook(w http.ResponseWriter, r *http.Request) { + var v validationError + type request struct { + WebhookUrl string `json:"webhook_url"` + IsDiscordWebhookEnabled bool `json:"is_discord_webhook_enabled,omitempty"` + } + var req request + if err := v.checkBody(&req, r); err != nil { + handleErr(w, r, err) + return + } + if v.hasErrors() { + handleErr(w, r, v) + return + } + // TODO + returnNoContent(w, r) +} + func (h *HandlerService) PublicGetNetworkValidators(w http.ResponseWriter, r *http.Request) { returnOk(w, r, nil) } diff --git a/backend/pkg/api/router.go b/backend/pkg/api/router.go index 5603e4856..3238257bd 100644 --- a/backend/pkg/api/router.go +++ b/backend/pkg/api/router.go @@ -5,6 +5,7 @@ import ( "regexp" dataaccess "github.com/gobitfly/beaconchain/pkg/api/data_access" + "github.com/gobitfly/beaconchain/pkg/api/docs" handlers "github.com/gobitfly/beaconchain/pkg/api/handlers" "github.com/gobitfly/beaconchain/pkg/commons/log" "github.com/gobitfly/beaconchain/pkg/commons/metrics" @@ -39,6 +40,8 @@ func NewApiRouter(dataAccessor dataaccess.DataAccessor, cfg *types.Config) *mux. addRoutes(handlerService, publicRouter, internalRouter, cfg) + // serve static files + publicRouter.PathPrefix("/docs/").Handler(http.StripPrefix("/api/v2/docs/", http.FileServer(http.FS(docs.Files)))) router.Use(metrics.HttpMiddleware) return router @@ -88,11 +91,15 @@ func addRoutes(hs *handlers.HandlerService, publicRouter, internalRouter *mux.Ro {http.MethodGet, "/healthz", hs.PublicGetHealthz, nil}, {http.MethodGet, "/healthz-loadbalancer", hs.PublicGetHealthzLoadbalancer, nil}, + {http.MethodGet, "/ratelimit-weights", nil, hs.InternalGetRatelimitWeights}, + {http.MethodPost, "/login", nil, hs.InternalPostLogin}, {http.MethodGet, "/mobile/authorize", nil, hs.InternalPostMobileAuthorize}, {http.MethodPost, "/mobile/equivalent-exchange", nil, hs.InternalPostMobileEquivalentExchange}, {http.MethodPost, "/mobile/purchase", nil, hs.InternalHandleMobilePurchase}, + {http.MethodGet, "/mobile/latest-bundle", nil, hs.InternalGetMobileLatestBundle}, + {http.MethodPost, "/mobile/bundles/{bundle_version}/deliveries", nil, hs.InternalPostMobileBundleDeliveries}, {http.MethodPost, "/logout", nil, hs.InternalPostLogout}, @@ -310,35 +317,36 @@ func addNotificationRoutes(hs *handlers.HandlerService, publicRouter, internalRo publicNotificationRouter.Use(hs.ManageViaApiCheckMiddleware) } endpoints := []endpoint{ - {http.MethodGet, "", nil, hs.InternalGetUserNotifications}, - {http.MethodGet, "/dashboards", nil, hs.InternalGetUserNotificationDashboards}, - {http.MethodGet, "/validator-dashboards/{notification_id}", nil, hs.InternalGetUserNotificationsValidatorDashboard}, - {http.MethodGet, "/account-dashboards/{notification_id}", nil, hs.InternalGetUserNotificationsAccountDashboard}, - {http.MethodGet, "/machines", nil, hs.InternalGetUserNotificationMachines}, - {http.MethodGet, "/clients", nil, hs.InternalGetUserNotificationClients}, - {http.MethodGet, "/rocket-pool", nil, hs.InternalGetUserNotificationRocketPool}, - {http.MethodGet, "/networks", nil, hs.InternalGetUserNotificationNetworks}, - {http.MethodGet, "/settings", nil, hs.InternalGetUserNotificationSettings}, - {http.MethodPut, "/settings/general", nil, hs.InternalPutUserNotificationSettingsGeneral}, - {http.MethodPut, "/settings/networks/{network}", nil, hs.InternalPutUserNotificationSettingsNetworks}, - {http.MethodPut, "/settings/paired-devices/{paired_device_id}", nil, hs.InternalPutUserNotificationSettingsPairedDevices}, - {http.MethodDelete, "/settings/paired-devices/{paired_device_id}", nil, hs.InternalDeleteUserNotificationSettingsPairedDevices}, - {http.MethodGet, "/settings/dashboards", nil, hs.InternalGetUserNotificationSettingsDashboards}, - {http.MethodPost, "/test-email", nil, hs.InternalPostUserNotificationsTestEmail}, - {http.MethodPost, "/test-push", nil, hs.InternalPostUserNotificationsTestPush}, - {http.MethodPost, "/test-webhook", nil, hs.InternalPostUserNotificationsTestWebhook}, + {http.MethodGet, "", hs.PublicGetUserNotifications, hs.InternalGetUserNotifications}, + {http.MethodGet, "/dashboards", hs.PublicGetUserNotificationDashboards, hs.InternalGetUserNotificationDashboards}, + {http.MethodGet, "/machines", hs.PublicGetUserNotificationMachines, hs.InternalGetUserNotificationMachines}, + {http.MethodGet, "/clients", hs.PublicGetUserNotificationClients, hs.InternalGetUserNotificationClients}, + {http.MethodGet, "/rocket-pool", hs.PublicGetUserNotificationRocketPool, hs.InternalGetUserNotificationRocketPool}, + {http.MethodGet, "/networks", hs.PublicGetUserNotificationNetworks, hs.InternalGetUserNotificationNetworks}, + {http.MethodGet, "/settings", hs.PublicGetUserNotificationSettings, hs.InternalGetUserNotificationSettings}, + {http.MethodPut, "/settings/general", hs.PublicPutUserNotificationSettingsGeneral, hs.InternalPutUserNotificationSettingsGeneral}, + {http.MethodPut, "/settings/networks/{network}", hs.PublicPutUserNotificationSettingsNetworks, hs.InternalPutUserNotificationSettingsNetworks}, + {http.MethodPut, "/settings/paired-devices/{paired_device_id}", hs.PublicPutUserNotificationSettingsPairedDevices, hs.InternalPutUserNotificationSettingsPairedDevices}, + {http.MethodDelete, "/settings/paired-devices/{paired_device_id}", hs.PublicDeleteUserNotificationSettingsPairedDevices, hs.InternalDeleteUserNotificationSettingsPairedDevices}, + {http.MethodGet, "/settings/dashboards", hs.PublicGetUserNotificationSettingsDashboards, hs.InternalGetUserNotificationSettingsDashboards}, + {http.MethodPost, "/test-email", hs.PublicPostUserNotificationsTestEmail, hs.InternalPostUserNotificationsTestEmail}, + {http.MethodPost, "/test-push", hs.PublicPostUserNotificationsTestPush, hs.InternalPostUserNotificationsTestPush}, + {http.MethodPost, "/test-webhook", hs.PublicPostUserNotificationsTestWebhook, hs.InternalPostUserNotificationsTestWebhook}, } addEndpointsToRouters(endpoints, publicNotificationRouter, internalNotificationRouter) publicDashboardNotificationSettingsRouter := publicNotificationRouter.NewRoute().Subrouter() internalDashboardNotificationSettingsRouter := internalNotificationRouter.NewRoute().Subrouter() + // TODO add adb auth middleware to account dashboard endpoints once they are implemented if !debug { publicDashboardNotificationSettingsRouter.Use(hs.VDBAuthMiddleware) internalDashboardNotificationSettingsRouter.Use(hs.VDBAuthMiddleware) } dashboardSettingsEndpoints := []endpoint{ - {http.MethodPut, "/settings/validator-dashboards/{dashboard_id}/groups/{group_id}", nil, hs.InternalPutUserNotificationSettingsValidatorDashboard}, - {http.MethodPut, "/settings/account-dashboards/{dashboard_id}/groups/{group_id}", nil, hs.InternalPutUserNotificationSettingsAccountDashboard}, + {http.MethodGet, "/validator-dashboards/{dashboard_id}/groups/{group_id}/epochs/{epoch}", hs.PublicGetUserNotificationsValidatorDashboard, hs.InternalGetUserNotificationsValidatorDashboard}, + {http.MethodGet, "/account-dashboards/{dashboard_id}/groups/{group_id}/epochs/{epoch}", hs.PublicGetUserNotificationsAccountDashboard, hs.InternalGetUserNotificationsAccountDashboard}, + {http.MethodPut, "/settings/validator-dashboards/{dashboard_id}/groups/{group_id}", hs.PublicPutUserNotificationSettingsValidatorDashboard, hs.InternalPutUserNotificationSettingsValidatorDashboard}, + {http.MethodPut, "/settings/account-dashboards/{dashboard_id}/groups/{group_id}", hs.PublicPutUserNotificationSettingsAccountDashboard, hs.InternalPutUserNotificationSettingsAccountDashboard}, } addEndpointsToRouters(dashboardSettingsEndpoints, publicDashboardNotificationSettingsRouter, internalDashboardNotificationSettingsRouter) } diff --git a/backend/pkg/api/types/common.go b/backend/pkg/api/types/common.go index e658afa80..2deebb1b9 100644 --- a/backend/pkg/api/types/common.go +++ b/backend/pkg/api/types/common.go @@ -34,14 +34,16 @@ type PubKey string type Hash string // blocks, txs etc. type Address struct { - Hash Hash `json:"hash"` - Ens string `json:"ens,omitempty"` + Hash Hash `json:"hash"` + IsContract bool `json:"is_contract"` + Ens string `json:"ens,omitempty"` + Label string `json:"label,omitempty"` } type LuckItem struct { Percent float64 `json:"percent"` - Expected time.Time `json:"expected"` - Average time.Duration `json:"average"` + Expected time.Time `json:"expected" swaggertype:"string" format:"date-time"` + Average time.Duration `json:"average" swaggertype:"primitive,integer"` } type Luck struct { diff --git a/backend/pkg/api/types/data_access.go b/backend/pkg/api/types/data_access.go index fb1257f62..b24a6b76c 100644 --- a/backend/pkg/api/types/data_access.go +++ b/backend/pkg/api/types/data_access.go @@ -212,7 +212,6 @@ type VDBValidatorSummaryChartRow struct { SyncScheduled float64 `db:"sync_scheduled"` } -// ------------------------- // healthz structs type HealthzResult struct { @@ -227,3 +226,14 @@ type HealthzData struct { DeploymentType string `json:"deployment_type"` Reports map[string][]HealthzResult `json:"status_reports"` } + +// ------------------------- +// Mobile structs + +type MobileAppBundleStats struct { + LatestBundleVersion uint64 + BundleUrl string + TargetCount uint64 // coalesce to 0 if column is null + DeliveryCount uint64 + MaxNativeVersion uint64 // the max native version of the whole table for the given environment +} diff --git a/backend/pkg/api/types/mobile.go b/backend/pkg/api/types/mobile.go new file mode 100644 index 000000000..7f84a999d --- /dev/null +++ b/backend/pkg/api/types/mobile.go @@ -0,0 +1,8 @@ +package types + +type MobileBundleData struct { + BundleUrl string `json:"bundle_url,omitempty"` + HasNativeUpdateAvailable bool `json:"has_native_update_available"` +} + +type GetMobileLatestBundleResponse ApiDataResponse[MobileBundleData] diff --git a/backend/pkg/api/types/notifications.go b/backend/pkg/api/types/notifications.go index 323f6ca00..f183988b1 100644 --- a/backend/pkg/api/types/notifications.go +++ b/backend/pkg/api/types/notifications.go @@ -141,8 +141,8 @@ type InternalGetUserNotificationNetworksResponse ApiPagingResponse[NotificationN // ------------------------------------------------------------ // Notification Settings type NotificationSettingsNetwork struct { - GasAboveThreshold decimal.Decimal `json:"gas_above_threshold" faker:"boundary_start=0, boundary_end=1"` // 0 is disabled - GasBelowThreshold decimal.Decimal `json:"gas_below_threshold" faker:"boundary_start=0, boundary_end=1"` // 0 is disabled + GasAboveThreshold decimal.Decimal `json:"gas_above_threshold" faker:"eth"` // 0 is disabled + GasBelowThreshold decimal.Decimal `json:"gas_below_threshold" faker:"eth"` // 0 is disabled ParticipationRateThreshold float64 `json:"participation_rate_threshold" faker:"boundary_start=0, boundary_end=1"` // 0 is disabled } type NotificationNetwork struct { diff --git a/backend/pkg/api/types/ratelimit.go b/backend/pkg/api/types/ratelimit.go new file mode 100644 index 000000000..6a1155096 --- /dev/null +++ b/backend/pkg/api/types/ratelimit.go @@ -0,0 +1,10 @@ +package types + +type ApiWeightItem struct { + Bucket string `db:"bucket"` + Endpoint string `db:"endpoint"` + Method string `db:"method"` + Weight int `db:"weight"` +} + +type InternalGetRatelimitWeightsResponse ApiDataResponse[[]ApiWeightItem] diff --git a/backend/pkg/api/types/validator_dashboard.go b/backend/pkg/api/types/validator_dashboard.go index 39d5e5d0d..380d035d4 100644 --- a/backend/pkg/api/types/validator_dashboard.go +++ b/backend/pkg/api/types/validator_dashboard.go @@ -27,7 +27,7 @@ type VDBOverviewBalances struct { } type VDBOverviewData struct { - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty" extensions:"x-order=1"` Network uint64 `json:"network"` Groups []VDBOverviewGroup `json:"groups"` Validators VDBOverviewValidators `json:"validators"` @@ -60,7 +60,7 @@ type VDBSummaryValidators struct { } type VDBSummaryTableRow struct { - GroupId int64 `json:"group_id"` + GroupId int64 `json:"group_id" extensions:"x-order=1"` Status VDBSummaryStatus `json:"status"` Validators VDBSummaryValidators `json:"validators"` Efficiency float64 `json:"efficiency"` @@ -116,7 +116,7 @@ type GetValidatorDashboardSummaryChartResponse ApiDataResponse[ChartData[int, fl // ------------------------------------------------------------ // Summary Validators type VDBSummaryValidator struct { - Index uint64 `json:"index"` + Index uint64 `json:"index" extensions:"x-order=1"` DutyObjects []uint64 `json:"duty_objects,omitempty"` } type VDBSummaryValidatorsData struct { @@ -169,7 +169,7 @@ type GetValidatorDashboardRewardsChartResponse ApiDataResponse[ChartData[int, de // Duties Modal type VDBEpochDutiesTableRow struct { - Validator uint64 `json:"validator"` + Validator uint64 `json:"validator" extensions:"x-order=1"` Duties ValidatorHistoryDuties `json:"duties"` } type GetValidatorDashboardDutiesResponse ApiPagingResponse[VDBEpochDutiesTableRow] @@ -177,12 +177,12 @@ type GetValidatorDashboardDutiesResponse ApiPagingResponse[VDBEpochDutiesTableRo // ------------------------------------------------------------ // Blocks Tab type VDBBlocksTableRow struct { - Proposer uint64 `json:"proposer"` - GroupId uint64 `json:"group_id"` - Epoch uint64 `json:"epoch"` - Slot uint64 `json:"slot"` + Proposer uint64 `json:"proposer" extensions:"x-order=1"` + GroupId uint64 `json:"group_id" extensions:"x-order=2"` + Epoch uint64 `json:"epoch" extensions:"x-order=3"` + Slot uint64 `json:"slot" extensions:"x-order=4"` + Block *uint64 `json:"block,omitempty" extensions:"x-order=5"` Status string `json:"status" tstype:"'success' | 'missed' | 'orphaned' | 'scheduled'" faker:"oneof: success, missed, orphaned, scheduled"` - Block *uint64 `json:"block,omitempty"` RewardRecipient *Address `json:"reward_recipient,omitempty"` Reward *ClElValue[decimal.Decimal] `json:"reward,omitempty"` Graffiti *string `json:"graffiti,omitempty"` @@ -198,22 +198,22 @@ type VDBHeatmapEvents struct { Sync bool `json:"sync"` } type VDBHeatmapCell struct { - X int64 `json:"x"` // Timestamp - Y uint64 `json:"y"` // Group ID + X int64 `json:"x" extensions:"x-order=1"` // Timestamp + Y uint64 `json:"y" extensions:"x-order=2"` // Group ID - Value float64 `json:"value"` // Attestaton Rewards + Value float64 `json:"value" extensions:"x-order=3"` // Attestaton Rewards Events *VDBHeatmapEvents `json:"events,omitempty"` } type VDBHeatmap struct { - Timestamps []int64 `json:"timestamps"` // X-Axis Categories (unix timestamp) - GroupIds []uint64 `json:"group_ids"` // Y-Axis Categories - Data []VDBHeatmapCell `json:"data"` + Timestamps []int64 `json:"timestamps" extensions:"x-order=1"` // X-Axis Categories (unix timestamp) + GroupIds []uint64 `json:"group_ids" extensions:"x-order=2"` // Y-Axis Categories + Data []VDBHeatmapCell `json:"data" extensions:"x-order=3"` Aggregation string `json:"aggregation" tstype:"'epoch' | 'hourly' | 'daily' | 'weekly'" faker:"oneof: epoch, hourly, daily, weekly"` } type GetValidatorDashboardHeatmapResponse ApiDataResponse[VDBHeatmap] type VDBHeatmapTooltipData struct { - Timestamp int64 `json:"timestamp"` + Timestamp int64 `json:"timestamp" extensions:"x-order=1"` Proposers StatusCount `json:"proposers"` Syncs uint64 `json:"syncs"` @@ -290,7 +290,7 @@ type GetValidatorDashboardTotalWithdrawalsResponse ApiDataResponse[VDBTotalWithd // ------------------------------------------------------------ // Rocket Pool Tab type VDBRocketPoolTableRow struct { - Node Address `json:"node"` + Node Address `json:"node" extensions:"x-order=1"` Staked struct { Eth decimal.Decimal `json:"eth"` Rpl decimal.Decimal `json:"rpl"` diff --git a/backend/pkg/commons/db/bigtable_eth1.go b/backend/pkg/commons/db/bigtable_eth1.go index e2fca3052..3861374f2 100644 --- a/backend/pkg/commons/db/bigtable_eth1.go +++ b/backend/pkg/commons/db/bigtable_eth1.go @@ -3138,7 +3138,7 @@ func (bigtable *Bigtable) GetAddressName(address []byte) (string, error) { add := common.Address{} add.SetBytes(address) - name, err := GetEnsNameForAddress(add) + name, err := GetEnsNameForAddress(add, time.Time{}) if err == nil && len(name) > 0 { return name, nil } @@ -3215,11 +3215,11 @@ type isContractInfo struct { ts gcp_bigtable.Timestamp } -type contractInteractionAtRequest struct { - address string - block int64 - txIdx int64 - traceIdx int64 +type ContractInteractionAtRequest struct { + Address string // expected all lowercase without 0x prefix + Block int64 + TxIdx int64 + TraceIdx int64 } func (bigtable *Bigtable) getAddressIsContractHistories(histories map[string][]isContractInfo) error { @@ -3270,7 +3270,7 @@ func (bigtable *Bigtable) getAddressIsContractHistories(histories map[string][]i // returns account state after the given execution state // -1 is latest (e.g. "txIdx" = -1 returns the contract state after execution of "block", "block" = -1 returns the state at chain head) -func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []contractInteractionAtRequest) ([]types.ContractInteractionType, error) { +func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []ContractInteractionAtRequest) ([]types.ContractInteractionType, error) { results := make([]types.ContractInteractionType, len(requests)) if len(requests) == 0 { return results, nil @@ -3279,7 +3279,7 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []contractIn // get histories histories := make(map[string][]isContractInfo, len(requests)) for _, request := range requests { - histories[request.address] = nil + histories[request.Address] = nil } err := bigtable.getAddressIsContractHistories(histories) if err != nil { @@ -3288,22 +3288,22 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []contractIn // evaluate requests; CONTRACT_NONE is default for i, request := range requests { - history, ok := histories[request.address] + history, ok := histories[request.Address] if !ok || history == nil || len(history) == 0 { continue } latestUpdateIdxBeforeReq := 0 - if request.block != -1 { + if request.Block != -1 { var block, tx, itx uint64 - if request.txIdx == -1 { - block = uint64(request.block + 1) - } else if request.traceIdx == -1 { - block = uint64(request.block) - tx = uint64(request.txIdx + 1) + if request.TxIdx == -1 { + block = uint64(request.Block + 1) + } else if request.TraceIdx == -1 { + block = uint64(request.Block) + tx = uint64(request.TxIdx + 1) } else { - block = uint64(request.block) - tx = uint64(request.txIdx) - itx = uint64(request.traceIdx + 1) + block = uint64(request.Block) + tx = uint64(request.TxIdx) + itx = uint64(request.TraceIdx + 1) } req_ts, err := encodeIsContractUpdateTs(block, tx, itx) if err != nil { @@ -3319,7 +3319,7 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []contractIn } b, tx, trace := decodeIsContractUpdateTs(history[latestUpdateIdxBeforeReq].ts) - exact_match := request.block == -1 || request.block == int64(b) && (request.txIdx == -1 || request.txIdx == int64(tx) && (request.traceIdx == -1 || request.traceIdx == int64(trace))) + exact_match := request.Block == -1 || request.Block == int64(b) && (request.TxIdx == -1 || request.TxIdx == int64(tx) && (request.TraceIdx == -1 || request.TraceIdx == int64(trace))) if exact_match { results[i] = types.CONTRACT_DESTRUCTION @@ -3343,17 +3343,17 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAt(requests []contractIn // convenience function to get contract interaction status per transaction of a block func (bigtable *Bigtable) GetAddressContractInteractionsAtBlock(block *types.Eth1Block) ([]types.ContractInteractionType, error) { - requests := make([]contractInteractionAtRequest, len(block.GetTransactions())) + requests := make([]ContractInteractionAtRequest, len(block.GetTransactions())) for i, tx := range block.GetTransactions() { address := tx.GetTo() if len(address) == 0 { address = tx.GetContractAddress() } - requests[i] = contractInteractionAtRequest{ - address: fmt.Sprintf("%x", address), - block: int64(block.GetNumber()), - txIdx: int64(i), - traceIdx: -1, + requests[i] = ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", address), + Block: int64(block.GetNumber()), + TxIdx: int64(i), + TraceIdx: -1, } } @@ -3363,19 +3363,19 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAtBlock(block *types.Eth // convenience function to get contract interaction status per subtransaction of a transaction // 2nd parameter specifies [tx_idx, trace_idx] for each internal tx func (bigtable *Bigtable) GetAddressContractInteractionsAtITransactions(itransactions []*types.Eth1InternalTransactionIndexed, idxs [][2]int64) ([][2]types.ContractInteractionType, error) { - requests := make([]contractInteractionAtRequest, 0, len(itransactions)*2) + requests := make([]ContractInteractionAtRequest, 0, len(itransactions)*2) for i, tx := range itransactions { - requests = append(requests, contractInteractionAtRequest{ - address: fmt.Sprintf("%x", tx.GetFrom()), - block: int64(tx.GetBlockNumber()), - txIdx: idxs[i][0], - traceIdx: idxs[i][1], + requests = append(requests, ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", tx.GetFrom()), + Block: int64(tx.GetBlockNumber()), + TxIdx: idxs[i][0], + TraceIdx: idxs[i][1], }) - requests = append(requests, contractInteractionAtRequest{ - address: fmt.Sprintf("%x", tx.GetTo()), - block: int64(tx.GetBlockNumber()), - txIdx: idxs[i][0], - traceIdx: idxs[i][1], + requests = append(requests, ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", tx.GetTo()), + Block: int64(tx.GetBlockNumber()), + TxIdx: idxs[i][0], + TraceIdx: idxs[i][1], }) } results, err := bigtable.GetAddressContractInteractionsAt(requests) @@ -3392,20 +3392,20 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAtITransactions(itransac // convenience function to get contract interaction status per parity trace func (bigtable *Bigtable) GetAddressContractInteractionsAtParityTraces(traces []*rpc.ParityTraceResult) ([][2]types.ContractInteractionType, error) { - requests := make([]contractInteractionAtRequest, 0, len(traces)*2) + requests := make([]ContractInteractionAtRequest, 0, len(traces)*2) for i, itx := range traces { from, to, _, _ := itx.ConvertFields() - requests = append(requests, contractInteractionAtRequest{ - address: fmt.Sprintf("%x", from), - block: int64(itx.BlockNumber), - txIdx: int64(itx.TransactionPosition), - traceIdx: int64(i), + requests = append(requests, ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", from), + Block: int64(itx.BlockNumber), + TxIdx: int64(itx.TransactionPosition), + TraceIdx: int64(i), }) - requests = append(requests, contractInteractionAtRequest{ - address: fmt.Sprintf("%x", to), - block: int64(itx.BlockNumber), - txIdx: int64(itx.TransactionPosition), - traceIdx: int64(i), + requests = append(requests, ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", to), + Block: int64(itx.BlockNumber), + TxIdx: int64(itx.TransactionPosition), + TraceIdx: int64(i), }) } results, err := bigtable.GetAddressContractInteractionsAt(requests) @@ -3422,13 +3422,13 @@ func (bigtable *Bigtable) GetAddressContractInteractionsAtParityTraces(traces [] // convenience function to get contract interaction status per transaction func (bigtable *Bigtable) GetAddressContractInteractionsAtTransactions(transactions []*types.Eth1TransactionIndexed, idxs []int64) ([]types.ContractInteractionType, error) { - requests := make([]contractInteractionAtRequest, len(transactions)) + requests := make([]ContractInteractionAtRequest, len(transactions)) for i, tx := range transactions { - requests[i] = contractInteractionAtRequest{ - address: fmt.Sprintf("%x", tx.GetTo()), - block: int64(tx.GetBlockNumber()), - txIdx: idxs[i], - traceIdx: -1, + requests[i] = ContractInteractionAtRequest{ + Address: fmt.Sprintf("%x", tx.GetTo()), + Block: int64(tx.GetBlockNumber()), + TxIdx: idxs[i], + TraceIdx: -1, } } return bigtable.GetAddressContractInteractionsAt(requests) diff --git a/backend/pkg/commons/db/ens.go b/backend/pkg/commons/db/ens.go index 46001df56..119f6c00b 100644 --- a/backend/pkg/commons/db/ens.go +++ b/backend/pkg/commons/db/ens.go @@ -436,7 +436,7 @@ func validateEnsAddress(client *ethclient.Client, address common.Address, alread err.Error() == "no resolution" || err.Error() == "execution reverted" || strings.HasPrefix(err.Error(), "name is not valid") { - log.Warnf("reverse resolving address [%v] resulted in a skippable error [%s], skipping it", address, err.Error()) + // log.Warnf("reverse resolving address [%v] resulted in a skippable error [%s], skipping it", address, err.Error()) } else { return fmt.Errorf("error could not reverse resolve address [%v]: %w", address, err) } @@ -475,7 +475,7 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC nameHash, err := go_ens.NameHash(name) if err != nil { - log.Warnf("error could not hash name [%v]: %v -> removing ens entry", name, err) + // log.Warnf("error could not hash name [%v]: %v -> removing ens entry", name, err) err = removeEnsName(name) if err != nil { return fmt.Errorf("error removing ens name [%v]: %w", name, err) @@ -488,12 +488,12 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC if err.Error() == "unregistered name" || err.Error() == "no address" || err.Error() == "no resolver" || - err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" || + err.Error() == "abi: attempting to unmarshal an empty string while arguments are expected" || strings.Contains(err.Error(), "execution reverted") || err.Error() == "invalid jump destination" || err.Error() == "invalid opcode: INVALID" { // the given name is not available anymore or resolving it did not work properly => we can remove it from the db (if it is there) - log.Warnf("could not resolve name [%v]: %v -> removing ens entry", name, err) + // log.Warnf("could not resolve name [%v]: %v -> removing ens entry", name, err) err = removeEnsName(name) if err != nil { return fmt.Errorf("error removing ens name after resolve failed [%v]: %w", name, err) @@ -516,7 +516,7 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC reverseName, err := go_ens.ReverseResolve(client, addr) if err != nil { if err.Error() == "not a resolver" || err.Error() == "no resolution" || err.Error() == "execution reverted" { - log.Warnf("reverse resolving address [%v] for name [%v] resulted in an error [%s], marking entry as not primary", addr, name, err.Error()) + // log.Warnf("reverse resolving address [%v] for name [%v] resulted in an error [%s], marking entry as not primary", addr, name, err.Error()) } else { return fmt.Errorf("error could not reverse resolve address [%v]: %w", addr, err) } @@ -549,12 +549,12 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC return fmt.Errorf("error writing ens data for name [%v]: %w", name, err) } - log.InfoWithFields(log.Fields{ - "name": name, - "address": addr, - "expires": expires, - "reverseName": reverseName, - }, "validated ens name") + // log.InfoWithFields(log.Fields{ + // "name": name, + // "address": addr, + // "expires": expires, + // "reverseName": reverseName, + // }, "validated ens name") return nil } @@ -609,15 +609,19 @@ func GetAddressForEnsName(name string) (address *common.Address, err error) { return address, err } -func GetEnsNameForAddress(address common.Address) (name string, err error) { +// pass invalid time to get latest data +func GetEnsNameForAddress(address common.Address, validUntil time.Time) (name string, err error) { + if validUntil.IsZero() { + validUntil = time.Now() + } err = ReaderDb.Get(&name, ` SELECT ens_name FROM ens WHERE address = $1 AND is_primary_name AND - valid_to >= now() - ;`, address.Bytes()) + valid_to >= $2 + ;`, address.Bytes(), validUntil) return name, err } diff --git a/backend/pkg/commons/db/migrations/postgres/20240822134034_add_address_tags.sql b/backend/pkg/commons/db/migrations/postgres/20240822134034_add_address_tags.sql new file mode 100644 index 000000000..3579f7ef6 --- /dev/null +++ b/backend/pkg/commons/db/migrations/postgres/20240822134034_add_address_tags.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +SELECT('up SQL query - create address_names table'); +CREATE TABLE IF NOT EXISTS address_names ( + address bytea NOT NULL UNIQUE, + name TEXT NOT NULL, + PRIMARY KEY (address, name) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT('down SQL query - drop address_names table'); +DROP TABLE IF EXISTS address_names; +-- +goose StatementEnd diff --git a/backend/pkg/commons/ratelimit/ratelimit.go b/backend/pkg/commons/ratelimit/ratelimit.go index 8a79d9a75..a57900bba 100644 --- a/backend/pkg/commons/ratelimit/ratelimit.go +++ b/backend/pkg/commons/ratelimit/ratelimit.go @@ -56,6 +56,7 @@ const ( FallbackRateLimitSecond = 20 // RateLimit per second for when redis is offline FallbackRateLimitBurst = 20 // RateLimit burst for when redis is offline + defaultWeight = 1 // if no weight is set for a route, use this one defaultBucket = "default" // if no bucket is set for a route, use this one statsTruncateDuration = time.Hour * 1 // ratelimit-stats are truncated to this duration @@ -951,7 +952,7 @@ func getWeight(r *http.Request) (cost int64, identifier, bucket string) { bucket, bucketOk := buckets[route] weightsMu.RUnlock() if !weightOk { - weight = 1 + weight = defaultWeight } if !bucketOk { bucket = defaultBucket diff --git a/backend/pkg/commons/utils/config.go b/backend/pkg/commons/utils/config.go index 1cc3179eb..ad8529fd0 100644 --- a/backend/pkg/commons/utils/config.go +++ b/backend/pkg/commons/utils/config.go @@ -262,6 +262,7 @@ func ReadConfig(cfg *types.Config, path string) error { "mainCurrency": cfg.Frontend.MainCurrency, }, "did init config") + Config = cfg return nil } diff --git a/backend/pkg/monitoring/monitoring.go b/backend/pkg/monitoring/monitoring.go index b24c3cb9a..dbf021a77 100644 --- a/backend/pkg/monitoring/monitoring.go +++ b/backend/pkg/monitoring/monitoring.go @@ -1,6 +1,9 @@ package monitoring import ( + "sync" + "sync/atomic" + "github.com/gobitfly/beaconchain/pkg/commons/db" "github.com/gobitfly/beaconchain/pkg/commons/log" "github.com/gobitfly/beaconchain/pkg/commons/metrics" @@ -11,11 +14,17 @@ import ( ) var monitoredServices []services.Service +var startedClickhouse atomic.Bool +var initMutex = sync.Mutex{} func Init(full bool) { + initMutex.Lock() + defer initMutex.Unlock() metrics.UUID.WithLabelValues(utils.GetUUID()).Set(1) // so we can find out where the uuid is set metrics.DeploymentType.WithLabelValues(utils.Config.DeploymentType).Set(1) if db.ClickHouseNativeWriter == nil { + log.Infof("initializing clickhouse writer") + startedClickhouse.Store(true) db.ClickHouseNativeWriter = db.MustInitClickhouseNative(&types.DatabaseConfig{ Username: utils.Config.ClickHouse.WriterDatabase.Username, Password: utils.Config.ClickHouse.WriterDatabase.Password, @@ -58,4 +67,7 @@ func Stop() { } // this prevents status reports that werent shut down cleanly from triggering alerts services.NewStatusReport(constants.CleanShutdownEvent, constants.Default, constants.Default)(constants.Success, nil) + if startedClickhouse.Load() { + db.ClickHouseNativeWriter.Close() + } } diff --git a/backend/pkg/monitoring/services/base.go b/backend/pkg/monitoring/services/base.go index 549b4d67f..04cbea094 100644 --- a/backend/pkg/monitoring/services/base.go +++ b/backend/pkg/monitoring/services/base.go @@ -65,8 +65,8 @@ func NewStatusReport(id string, timeout time.Duration, check_interval time.Durat if timeout != constants.Default { timeouts_at = now.Add(timeout) } - expires_at := timeouts_at.Add(5 * time.Minute) - if check_interval >= 5*time.Minute { + expires_at := timeouts_at.Add(1 * time.Minute) + if check_interval >= 1*time.Minute { expires_at = timeouts_at.Add(check_interval) } log.TraceWithFields(log.Fields{ @@ -83,7 +83,7 @@ func NewStatusReport(id string, timeout time.Duration, check_interval time.Durat err = db.ClickHouseNativeWriter.AsyncInsert( ctx, "INSERT INTO status_reports (emitter, event_id, deployment_type, insert_id, expires_at, timeouts_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", - true, + false, // true means wait for settlement, but we want to shoot and forget. false does mean we cant log any errors that occur during settlement utils.GetUUID(), id, utils.Config.DeploymentType, diff --git a/backend/pkg/userservice/appsubscription_oracle.go b/backend/pkg/userservice/appsubscription_oracle.go index 0b078674e..0f50ed916 100644 --- a/backend/pkg/userservice/appsubscription_oracle.go +++ b/backend/pkg/userservice/appsubscription_oracle.go @@ -233,13 +233,29 @@ func rejectReason(valid bool) string { return "expired" } +// first 3 trillion dollar company and you can't reuse ids +func mapAppleProductID(productID string) string { + mappings := map[string]string{ + "orca.yearly.apple": "orca.yearly", + "orca.apple": "orca", + "dolphin.yearly.apple": "dolphin.yearly", + "dolphin.apple": "dolphin", + "guppy.yearly.apple": "guppy.yearly", + "guppy.apple": "guppy", + } + if mapped, ok := mappings[productID]; ok { + return mapped + } + return productID +} + func verifyApple(apple *api.StoreClient, receipt *types.PremiumData) (*VerifyResponse, error) { response := &VerifyResponse{ Valid: false, ExpirationDate: 0, RejectReason: "", - ProductID: receipt.ProductID, // may be changed by this function to be different than receipt.ProductID - Receipt: receipt.Receipt, // may be changed by this function to be different than receipt.Receipt + ProductID: mapAppleProductID(receipt.ProductID), // may be changed by this function to be different than receipt.ProductID + Receipt: receipt.Receipt, // may be changed by this function to be different than receipt.Receipt } if apple == nil { @@ -300,7 +316,7 @@ func verifyApple(apple *api.StoreClient, receipt *types.PremiumData) (*VerifyRes response.RejectReason = "invalid_product_id" return response, nil } - response.ProductID = productId // update response to reflect the resolved product id + response.ProductID = mapAppleProductID(productId) // update response to reflect the resolved product id expiresDateFloat, ok := claims["expiresDate"].(float64) if !ok { diff --git a/frontend/components/dashboard/DashboardHeader.vue b/frontend/components/dashboard/DashboardHeader.vue index 8db7f0d54..1e1f0b3e1 100644 --- a/frontend/components/dashboard/DashboardHeader.vue +++ b/frontend/components/dashboard/DashboardHeader.vue @@ -90,10 +90,12 @@ const items = computed(() => { const cd = db as CookieDashboard return createMenuBarButton('validator', getDashboardName(cd), `${cd.hash !== undefined ? cd.hash : cd.id}`) })) - addToSortedItems($t('dashboard.header.account'), dashboards.value?.validator_dashboards?.slice(0, 1).map((db) => { - const cd = db as CookieDashboard - return createMenuBarButton('account', getDashboardName(cd), `${cd.hash ?? cd.id}`) - })) + if (showInDevelopment) { + addToSortedItems($t('dashboard.header.account'), dashboards.value?.validator_dashboards?.slice(0, 1).map((db) => { + const cd = db as CookieDashboard + return createMenuBarButton('account', getDashboardName(cd), `${cd.hash ?? cd.id}`) + })) + } const disabledTooltip = !showInDevelopment ? $t('common.coming_soon') : undefined const onNotificationsPage = dashboardType.value === 'notifications' addToSortedItems($t('notifications.title'), [ { diff --git a/frontend/components/notifications/NotificationsClientsTable.vue b/frontend/components/notifications/NotificationsClientsTable.vue new file mode 100644 index 000000000..470396516 --- /dev/null +++ b/frontend/components/notifications/NotificationsClientsTable.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 33f2e6705..302cfa091 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -561,6 +561,17 @@ "yes": "Yes" }, "notifications": { + "clients": { + "col": { + "client_name": "Client ", + "version": "Version" + }, + "footer":{ + "subscriptions": "Clients ({count} Subscriptions)" + }, + "search_placeholder":"Client", + "title": "Clients" + }, "col": { "dashboard": "Dashboard", "group": "Group", diff --git a/frontend/pages/notifications.vue b/frontend/pages/notifications.vue index 968b4fe73..5d591e793 100644 --- a/frontend/pages/notifications.vue +++ b/frontend/pages/notifications.vue @@ -97,7 +97,8 @@ const openManageNotifications = () => { /> { @open-dialog="openManageNotifications" /> + diff --git a/frontend/public/mock/notifications/managementDashboard.json b/frontend/public/mock/notifications/managementDashboard.json deleted file mode 100644 index eb0d14c72..000000000 --- a/frontend/public/mock/notifications/managementDashboard.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "paging": { - "total_count": 999 - }, - "data": [ - { - "group_id": 123, - "dashboard_id": 222, - "dashboard_name": "My test dashboard", - "dashboard_type": "validator", - "subscriptions": ["missed_attestation"], - "webhook": { - "url": "", - "via_discord": false - }, - "networks": [1] - }, - { - "group_id": 123, - "dashboard_id": 222, - "dashboard_name": "My account dashboard", - "dashboard_type": "account", - "subscriptions": ["missed_attestation", "proposed_attestation"], - "webhook": { - "url": "https://discord.com/some-webhook-link", - "via_discord": false - }, - "networks": [1, 10, 42161, 8453] - } - ] -} diff --git a/frontend/stores/notifications/useNotificationsClientsStore.ts b/frontend/stores/notifications/useNotificationsClientsStore.ts new file mode 100644 index 000000000..d5dbe9a9d --- /dev/null +++ b/frontend/stores/notifications/useNotificationsClientsStore.ts @@ -0,0 +1,69 @@ +import { defineStore } from 'pinia' +import type { InternalGetUserNotificationClientsResponse } from '~/types/api/notifications' +import { API_PATH } from '~/types/customFetch' +import type { TableQueryParams } from '~/types/datatable' + +const notificationsClientStore = defineStore('notifications-clients-store', () => { + const data = ref() + return { data } +}) + +export function useNotificationsClientStore() { + const { isLoggedIn } = useUserStore() + + const { fetch } = useCustomFetch() + const { data } = storeToRefs(notificationsClientStore()) + const { + cursor, isStoredQuery, onSort, pageSize, pendingQuery, query, setCursor, setPageSize, setSearch, setStoredQuery, + } = useTableQuery({ + limit: 10, sort: 'timestamp:desc', + }, 10) + const isLoading = ref(false) + + async function loadClientsNotifications(q: TableQueryParams) { + isLoading.value = true + setStoredQuery(q) + try { + const result = await fetch( + API_PATH.NOTIFICATIONS_CLIENTS, + undefined, + undefined, + q, + ) + + isLoading.value = false + if (!isStoredQuery(q)) { + return // in case some query params change while loading + } + + data.value = result + } + catch (e) { + data.value = undefined + isLoading.value = false + } + return data.value + } + + const clientsNotifications = computed(() => { + return data.value + }) + + watch(query, (q) => { + if (q) { + isLoggedIn.value && loadClientsNotifications(q) + } + }, { immediate: true }) + + return { + clientsNotifications, + cursor, + isLoading, + onSort, + pageSize, + query: pendingQuery, + setCursor, + setPageSize, + setSearch, + } +} diff --git a/frontend/types/api/archiver.ts b/frontend/types/api/archiver.ts deleted file mode 100644 index a0ecfe481..000000000 --- a/frontend/types/api/archiver.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Code generated by tygo. DO NOT EDIT. -/* eslint-disable */ - -////////// -// source: archiver.go - -export interface ArchiverDashboard { - DashboardId: number /* uint64 */; - IsArchived: boolean; - GroupCount: number /* uint64 */; - ValidatorCount: number /* uint64 */; -} -export interface ArchiverDashboardArchiveReason { - DashboardId: number /* uint64 */; - ArchivedReason: any /* enums.VDBArchivedReason */; -} diff --git a/frontend/types/api/common.ts b/frontend/types/api/common.ts index ecce7231e..3c8c9506a 100644 --- a/frontend/types/api/common.ts +++ b/frontend/types/api/common.ts @@ -27,7 +27,9 @@ export type PubKey = string; export type Hash = string; // blocks, txs etc. export interface Address { hash: Hash; + is_contract: boolean; ens?: string; + label?: string; } export interface LuckItem { percent: number /* float64 */; diff --git a/frontend/types/api/mobile.ts b/frontend/types/api/mobile.ts new file mode 100644 index 000000000..d6b234a18 --- /dev/null +++ b/frontend/types/api/mobile.ts @@ -0,0 +1,12 @@ +// Code generated by tygo. DO NOT EDIT. +/* eslint-disable */ +import type { ApiDataResponse } from './common' + +////////// +// source: mobile.go + +export interface MobileBundleData { + bundle_url?: string; + has_native_update_available: boolean; +} +export type GetMobileLatestBundleResponse = ApiDataResponse; diff --git a/frontend/types/api/ratelimit.ts b/frontend/types/api/ratelimit.ts new file mode 100644 index 000000000..aec92ffb8 --- /dev/null +++ b/frontend/types/api/ratelimit.ts @@ -0,0 +1,14 @@ +// Code generated by tygo. DO NOT EDIT. +/* eslint-disable */ +import type { ApiDataResponse } from './common' + +////////// +// source: ratelimit.go + +export interface ApiWeightItem { + Bucket: string; + Endpoint: string; + Method: string; + Weight: number /* int */; +} +export type InternalGetRatelimitWeightsResponse = ApiDataResponse; diff --git a/frontend/types/api/validator_dashboard.ts b/frontend/types/api/validator_dashboard.ts index 10fb9d3e1..03f07f548 100644 --- a/frontend/types/api/validator_dashboard.ts +++ b/frontend/types/api/validator_dashboard.ts @@ -160,8 +160,8 @@ export interface VDBBlocksTableRow { group_id: number /* uint64 */; epoch: number /* uint64 */; slot: number /* uint64 */; - status: 'success' | 'missed' | 'orphaned' | 'scheduled'; block?: number /* uint64 */; + status: 'success' | 'missed' | 'orphaned' | 'scheduled'; reward_recipient?: Address; reward?: ClElValue; graffiti?: string; diff --git a/frontend/types/customFetch.ts b/frontend/types/customFetch.ts index dcc3ec474..18b2d3ea1 100644 --- a/frontend/types/customFetch.ts +++ b/frontend/types/customFetch.ts @@ -40,6 +40,7 @@ export enum API_PATH { LATEST_STATE = '/latestState', LOGIN = '/login', LOGOUT = '/logout', + NOTIFICATIONS_CLIENTS = '/notifications/clients', NOTIFICATIONS_DASHBOARDS = '/notifications/dashboards', NOTIFICATIONS_MACHINE = '/notifications/machines', NOTIFICATIONS_MANAGEMENT_GENERAL = '/notifications/managementGeneral', @@ -279,6 +280,10 @@ export const mapping: Record = { mock: false, path: '/logout', }, + [API_PATH.NOTIFICATIONS_CLIENTS]: { + method: 'GET', + path: '/users/me/notifications/clients', + }, [API_PATH.NOTIFICATIONS_DASHBOARDS]: { path: '/users/me/notifications/dashboards', },