From 6bf0a410cac2134716637aabf18a9477d5734641 Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:18:34 +0200 Subject: [PATCH 01/16] (BEDS-425) implement endpoints for mobile app bundles (#842) --- backend/cmd/typescript_converter/main.go | 2 +- backend/pkg/api/data_access/app.go | 13 +++++++ backend/pkg/api/data_access/dummy.go | 13 +++++-- backend/pkg/api/handlers/internal.go | 46 ++++++++++++++++++++++++ backend/pkg/api/router.go | 2 ++ backend/pkg/api/types/data_access.go | 11 ++++++ backend/pkg/api/types/mobile.go | 8 +++++ frontend/types/api/mobile.ts | 12 +++++++ 8 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 backend/pkg/api/types/mobile.go create mode 100644 frontend/types/api/mobile.ts diff --git a/backend/cmd/typescript_converter/main.go b/backend/cmd/typescript_converter/main.go index 2a3ce86f5..360e57188 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 */", 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/dummy.go b/backend/pkg/api/data_access/dummy.go index 6197a72b2..36247ea76 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" @@ -637,7 +636,15 @@ 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) 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/handlers/internal.go b/backend/pkg/api/handlers/internal.go index 1c4157290..d111ab7ca 100644 --- a/backend/pkg/api/handlers/internal.go +++ b/backend/pkg/api/handlers/internal.go @@ -461,6 +461,52 @@ func (h *HandlerService) InternalGetValidatorDashboardRocketPoolMinipools(w http h.PublicGetValidatorDashboardRocketPoolMinipools(w, r) } +// -------------------------------------- +// Mobile + +func (h *HandlerService) InternalGetMobileLatestBundle(w http.ResponseWriter, r *http.Request) { + var v validationError + q := r.URL.Query() + 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 + } + stats, err := h.dai.GetLatestBundleForNativeVersion(r.Context(), nativeVersion) + if err != nil { + handleErr(w, r, err) + return + } + 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) InternalPostMobileBundleDeliveries(w http.ResponseWriter, r *http.Request) { + var v validationError + vars := mux.Vars(r) + bundleVersion := v.checkUint(vars["bundle_version"], "bundle_version") + if v.hasErrors() { + handleErr(w, r, v) + return + } + err := h.dai.IncrementBundleDeliveryCount(r.Context(), bundleVersion) + if err != nil { + handleErr(w, r, err) + return + } + returnNoContent(w, r) +} + // -------------------------------------- // Notifications diff --git a/backend/pkg/api/router.go b/backend/pkg/api/router.go index 5603e4856..0b3d6a819 100644 --- a/backend/pkg/api/router.go +++ b/backend/pkg/api/router.go @@ -93,6 +93,8 @@ func addRoutes(hs *handlers.HandlerService, publicRouter, internalRouter *mux.Ro {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}, diff --git a/backend/pkg/api/types/data_access.go b/backend/pkg/api/types/data_access.go index fb1257f62..49d88b926 100644 --- a/backend/pkg/api/types/data_access.go +++ b/backend/pkg/api/types/data_access.go @@ -227,3 +227,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/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; From e1f86fbf2284f3559e1407addf69bbdb206a2e19 Mon Sep 17 00:00:00 2001 From: MarcelBitfly <174338434+marcel-bitfly@users.noreply.github.com> Date: Thu, 12 Sep 2024 08:26:06 +0200 Subject: [PATCH 02/16] fix(DashboardHeader): only show `accounts button` in `development` --- frontend/components/dashboard/DashboardHeader.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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'), [ { From 402b034f63371392e3539d41c73f69fc5d2549b0 Mon Sep 17 00:00:00 2001 From: invis-bitfly <162128378+invis-bitfly@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:02:36 +0200 Subject: [PATCH 03/16] BEDS-306: monitoring: shorter minimum expiry delta of status reports --- backend/pkg/monitoring/services/base.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pkg/monitoring/services/base.go b/backend/pkg/monitoring/services/base.go index 549b4d67f..48effa38d 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{ From b50cbf80f075d5620b643b640da6dfe4155d6da2 Mon Sep 17 00:00:00 2001 From: invis-bitfly <162128378+invis-bitfly@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:03:47 +0200 Subject: [PATCH 04/16] BEDS-306: monitoring: close clickhouse native writer if automagically opened no way this will ever back fire right --- backend/pkg/monitoring/monitoring.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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() + } } From dd4cd42c2b1bb80523dcbb18f0043ad1749e9133 Mon Sep 17 00:00:00 2001 From: invis-bitfly <162128378+invis-bitfly@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:05:30 +0200 Subject: [PATCH 05/16] BEDS-306: monitoring: make status reports shoot and forget --- backend/pkg/monitoring/services/base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pkg/monitoring/services/base.go b/backend/pkg/monitoring/services/base.go index 48effa38d..04cbea094 100644 --- a/backend/pkg/monitoring/services/base.go +++ b/backend/pkg/monitoring/services/base.go @@ -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, From 45380c276f861e602713c3708f1fb441f2b032ca Mon Sep 17 00:00:00 2001 From: benji-bitfly Date: Tue, 10 Sep 2024 13:36:26 +0200 Subject: [PATCH 06/16] feat: add NotificationsClientTable component with store and API integration - Created a new NotificationsClientsTable.vue component to display `notifications client` data. - Implemented a new useNotificationsClientsStore for handling the API calls related to `notifications client` data. - Updated `customFetch.ts` to include the new API endpoints and methods for fetching `notifications client` data. - Added translations for the `NotificationsClientsTable.vue` component in the en.json file. - Applied code styling updates in `notifications.vue` and added the ClientsTab to display the `NotificationsClientsTable.vue` component. See: BEDS-324 --- .../NotificationsClientsTable.vue | 189 ++++++++++++++++++ frontend/locales/en.json | 11 + frontend/pages/notifications.vue | 8 +- .../notifications/managementDashboard.json | 31 --- .../useNotificationsClientsStore.ts | 69 +++++++ frontend/types/customFetch.ts | 5 + 6 files changed, 281 insertions(+), 32 deletions(-) create mode 100644 frontend/components/notifications/NotificationsClientsTable.vue delete mode 100644 frontend/public/mock/notifications/managementDashboard.json create mode 100644 frontend/stores/notifications/useNotificationsClientsStore.ts 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/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', }, From 2c55bd2d88950cdfdd1242e4760ba469e05b1fad Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 16 Sep 2024 13:24:06 +0200 Subject: [PATCH 07/16] refactor(eth1indexer): update ens in seperate go-routine (#858) --- backend/cmd/eth1indexer/main.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) 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 { From 83884000d790cb69e44b3231551967e0fa16e515 Mon Sep 17 00:00:00 2001 From: Manuel <5877862+manuelsc@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:39:16 +0200 Subject: [PATCH 08/16] BEDS-239: map apple product ids to internal ids --- .../pkg/userservice/appsubscription_oracle.go | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 { From e9cef55731f48062d232c67d094a8523fa70192e Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:10:28 +0200 Subject: [PATCH 09/16] (BEDS-479) fix notification settings dummy generation (#860) --- backend/pkg/api/data_access/dummy.go | 4 ++-- backend/pkg/api/types/notifications.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index 36247ea76..97bc9e369 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -43,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 } 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 { From fa2d7bca5bce31d583d7ea8fbb8b9c2736fdee05 Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:42:10 +0200 Subject: [PATCH 10/16] (BEDS-295) implement automated api doc generation (#748) --- .../workflows/backend-integration-test.yml | 4 +- backend/.gitignore | 3 +- backend/Makefile | 2 +- backend/cmd/typescript_converter/main.go | 2 +- backend/go.mod | 6 +++ backend/go.sum | 13 +++++- backend/pkg/api/api_test.go | 43 +++++++++++++++++++ backend/pkg/api/data_access/data_access.go | 12 +++--- backend/pkg/api/data_access/dummy.go | 6 +++ backend/pkg/api/data_access/ratelimit.go | 22 ++++++++++ backend/pkg/api/docs/static.go | 6 +++ backend/pkg/api/handlers/common.go | 3 +- backend/pkg/api/handlers/internal.go | 27 ++++++++++-- backend/pkg/api/handlers/public.go | 43 ++++++++++++++++--- backend/pkg/api/router.go | 5 +++ backend/pkg/api/types/data_access.go | 1 - backend/pkg/api/types/ratelimit.go | 10 +++++ backend/pkg/commons/ratelimit/ratelimit.go | 3 +- backend/pkg/commons/utils/config.go | 1 + frontend/types/api/ratelimit.ts | 14 ++++++ 20 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 backend/pkg/api/data_access/ratelimit.go create mode 100644 backend/pkg/api/docs/static.go create mode 100644 backend/pkg/api/types/ratelimit.go create mode 100644 frontend/types/api/ratelimit.ts 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/typescript_converter/main.go b/backend/cmd/typescript_converter/main.go index 360e57188..da6fa5be9 100644 --- a/backend/cmd/typescript_converter/main.go +++ b/backend/cmd/typescript_converter/main.go @@ -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/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 97bc9e369..4590a106b 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -636,6 +636,12 @@ func (d *DummyService) GetRocketPoolOverview(ctx context.Context) (*t.RocketPool return getDummyStruct[t.RocketPoolData]() } +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 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/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/handlers/common.go b/backend/pkg/api/handlers/common.go index ecc9794db..3a895c481 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 diff --git a/backend/pkg/api/handlers/internal.go b/backend/pkg/api/handlers/internal.go index d111ab7ca..be1d96ecb 100644 --- a/backend/pkg/api/handlers/internal.go +++ b/backend/pkg/api/handlers/internal.go @@ -26,6 +26,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 +100,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 +146,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 +176,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 +222,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 } @@ -1311,3 +1326,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..a06e798c3 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -17,6 +17,25 @@ 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 + 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 +131,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 Dashboards +// @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:
  • `ethereum`
  • `gnosis`
" +// @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) @@ -792,8 +823,8 @@ 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") + 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 @@ -828,8 +859,8 @@ 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") + 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 @@ -897,8 +928,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 diff --git a/backend/pkg/api/router.go b/backend/pkg/api/router.go index 0b3d6a819..5338b5ccd 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,6 +91,8 @@ 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}, diff --git a/backend/pkg/api/types/data_access.go b/backend/pkg/api/types/data_access.go index 49d88b926..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 { 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/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/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; From 9fbcabc81242371e0e02ed107a935f0fc50f90fd Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:21:43 +0200 Subject: [PATCH 11/16] BEDS 322/annotate endpoints (#820) --- backend/pkg/api/data_access/user.go | 7 +- backend/pkg/api/data_access/vdb_management.go | 8 +- backend/pkg/api/handlers/common.go | 2 +- backend/pkg/api/handlers/public.go | 456 +++++++++++++++++- backend/pkg/api/types/common.go | 4 +- backend/pkg/api/types/validator_dashboard.go | 34 +- 6 files changed, 469 insertions(+), 42 deletions(-) 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_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/handlers/common.go b/backend/pkg/api/handlers/common.go index 3a895c481..ca585724b 100644 --- a/backend/pkg/api/handlers/common.go +++ b/backend/pkg/api/handlers/common.go @@ -566,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/public.go b/backend/pkg/api/handlers/public.go index a06e798c3..95df23ea7 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -135,7 +135,7 @@ func (h *HandlerService) PublicPutAccountDashboardTransactionsSettings(w http.Re // // @Description Create a new validator dashboard. **Note**: New dashboards will automatically have a default group created. // @Security ApiKeyInHeader || ApiKeyInQuery -// @Tags Validator Dashboards +// @Tags Validator Dashboard // @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:
  • `ethereum`
  • `gnosis`
" @@ -193,6 +193,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"] @@ -244,6 +254,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"]) @@ -259,6 +279,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"]) @@ -286,6 +318,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"]) @@ -337,6 +382,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) @@ -377,6 +435,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) @@ -408,6 +478,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:
  • `validators`: Provide a list of validator indices or public keys to add to the dashboard.
  • `deposit_address`: (limited to subscription tiers with 'Bulk adding') Provide a deposit address from which as many validators as possible will be added to the dashboard.
  • `withdrawal_address`: (limited to subscription tiers with 'Bulk adding') Provide a withdrawal address from which as many validators as possible will be added to the dashboard.
  • `graffiti`: (limited to subscription tiers with 'Bulk adding') Provide a graffiti string from which as many validators as possible will be added to the dashboard.
" +// @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"]) @@ -531,6 +614,20 @@ 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 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(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"]) @@ -558,19 +655,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 @@ -589,6 +697,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`:
  • `share_groups`: If set to `true`, accessing the dashboard through the public ID will not reveal any group information.
" +// @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"]) @@ -623,13 +744,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`:
  • `share_groups`: If set to `true`, accessing the dashboard through the public ID will not reveal any group information.
" +// @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) @@ -666,13 +800,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) @@ -701,12 +846,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 @@ -784,6 +943,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"]) @@ -809,6 +978,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"]) @@ -823,8 +1009,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.TimePeriod{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} - checkEnumIsAllowed(&v, period, allowedPeriods, "period") + checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period") if v.hasErrors() { handleErr(w, r, v) return @@ -842,6 +1027,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) @@ -859,8 +1056,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.TimePeriod{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h} - checkEnumIsAllowed(&v, period, allowedPeriods, "period") + checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period") if v.hasErrors() { handleErr(w, r, v) return @@ -877,6 +1073,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() @@ -916,6 +1126,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"]) @@ -966,6 +1188,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"]) @@ -994,6 +1230,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) @@ -1022,6 +1270,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) @@ -1048,6 +1306,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) @@ -1079,6 +1353,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"]) @@ -1107,6 +1395,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"]) @@ -1143,6 +1444,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) @@ -1180,6 +1494,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"]) @@ -1205,6 +1530,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"]) @@ -1231,6 +1567,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"]) @@ -1250,6 +1595,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"]) @@ -1269,6 +1623,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() @@ -1297,6 +1665,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() @@ -1324,6 +1702,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() @@ -1351,6 +1742,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() @@ -1376,6 +1776,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) @@ -1402,6 +1812,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. Possible values are TODO." +// @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) diff --git a/backend/pkg/api/types/common.go b/backend/pkg/api/types/common.go index e658afa80..2c7253623 100644 --- a/backend/pkg/api/types/common.go +++ b/backend/pkg/api/types/common.go @@ -40,8 +40,8 @@ type Address struct { 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/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"` From 9d1196c558e4fc2ba69ff28500aadda108be1d6f Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:29:09 +0200 Subject: [PATCH 12/16] (BEDS-464) annotate notification endpoint doc (#864) --- backend/pkg/api/data_access/dummy.go | 4 +- backend/pkg/api/data_access/notifications.go | 12 +- .../api/enums/validator_dashboard_enums.go | 2 +- backend/pkg/api/handlers/internal.go | 419 +---------- backend/pkg/api/handlers/public.go | 701 +++++++++++++++++- backend/pkg/api/router.go | 39 +- 6 files changed, 743 insertions(+), 434 deletions(-) diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index 4590a106b..1dc80125d 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -452,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]() } diff --git a/backend/pkg/api/data_access/notifications.go b/backend/pkg/api/data_access/notifications.go index 4859b770c..94530d441 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) @@ -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) { 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/internal.go b/backend/pkg/api/handlers/internal.go index be1d96ecb..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" @@ -526,459 +525,79 @@ func (h *HandlerService) InternalPostMobileBundleDeliveries(w http.ResponseWrite // 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) + h.PublicGetUserNotifications(w, r) } func (h *HandlerService) InternalGetUserNotificationDashboards(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) + h.PublicGetUserNotificationDashboards(w, r) } func (h *HandlerService) InternalGetUserNotificationsValidatorDashboard(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.GetValidatorDashboardNotificationDetails(r.Context(), notificationId) - if err != nil { - handleErr(w, r, err) - return - } - response := types.InternalGetUserNotificationsValidatorDashboardResponse{ - Data: *data, - } - returnOk(w, r, response) + 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) } // -------------------------------------- diff --git a/backend/pkg/api/handlers/public.go b/backend/pkg/api/handlers/public.go index 95df23ea7..a31ad6bb2 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" @@ -36,6 +37,8 @@ import ( // @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") @@ -135,7 +138,7 @@ func (h *HandlerService) PublicPutAccountDashboardTransactionsSettings(w http.Re // // @Description Create a new validator dashboard. **Note**: New dashboards will automatically have a default group created. // @Security ApiKeyInHeader || ApiKeyInQuery -// @Tags Validator Dashboard +// @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:
  • `ethereum`
  • `gnosis`
" @@ -621,7 +624,6 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW // @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 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(index, public_key, balance, status, withdrawal_credentials) // @Param search query string false "Search for Address, ENS." @@ -662,8 +664,8 @@ func (h *HandlerService) PublicGetValidatorDashboardValidators(w http.ResponseWr // @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." +// @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] @@ -857,7 +859,7 @@ func (h *HandlerService) PublicDeleteValidatorDashboardPublicId(w http.ResponseW // @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." +// @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 @@ -1821,7 +1823,7 @@ func (h *HandlerService) PublicGetValidatorDashboardNodeRocketPool(w http.Respon // @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. Possible values are TODO." +// @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 @@ -1856,6 +1858,693 @@ 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 + 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(), 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 + // 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) +} + +// 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 5338b5ccd..3238257bd 100644 --- a/backend/pkg/api/router.go +++ b/backend/pkg/api/router.go @@ -317,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) } From 6779fe4e8cd3878215d460dbcde29bb9eada120a Mon Sep 17 00:00:00 2001 From: Lucca <109136188+LuccaBitfly@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:31:50 +0200 Subject: [PATCH 13/16] (BEDS-487) pass user id when updating paired devices (#865) --- backend/pkg/api/data_access/dummy.go | 4 ++-- backend/pkg/api/data_access/notifications.go | 12 ++++++------ backend/pkg/api/handlers/public.go | 14 ++++++++++++-- frontend/types/api/archiver.ts | 16 ---------------- frontend/types/api/validator_dashboard.ts | 2 +- 5 files changed, 21 insertions(+), 27 deletions(-) delete mode 100644 frontend/types/api/archiver.ts diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index 1dc80125d..7aef7bc48 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -482,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) { diff --git a/backend/pkg/api/data_access/notifications.go b/backend/pkg/api/data_access/notifications.go index 94530d441..5389847db 100644 --- a/backend/pkg/api/data_access/notifications.go +++ b/backend/pkg/api/data_access/notifications.go @@ -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 @@ -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/handlers/public.go b/backend/pkg/api/handlers/public.go index a31ad6bb2..b3ffd15dd 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -2278,6 +2278,11 @@ func (h *HandlerService) PublicPutUserNotificationSettingsNetworks(w http.Respon // @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"` @@ -2294,7 +2299,7 @@ func (h *HandlerService) PublicPutUserNotificationSettingsPairedDevices(w http.R handleErr(w, r, v) return } - err := h.dai.UpdateNotificationSettingsPairedDevice(r.Context(), pairedDeviceId, name, req.IsNotificationsEnabled) + err = h.dai.UpdateNotificationSettingsPairedDevice(r.Context(), userId, pairedDeviceId, name, req.IsNotificationsEnabled) if err != nil { handleErr(w, r, err) return @@ -2323,13 +2328,18 @@ func (h *HandlerService) PublicPutUserNotificationSettingsPairedDevices(w http.R // @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(), pairedDeviceId) + err = h.dai.DeleteNotificationSettingsPairedDevice(r.Context(), userId, pairedDeviceId) if err != nil { handleErr(w, r, err) return 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/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; From 49359ba42b76807335ace7275843885e67760ed2 Mon Sep 17 00:00:00 2001 From: remoterami <142154971+remoterami@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:39:12 +0200 Subject: [PATCH 14/16] (BEDS-155) Data Access: adjusted address struct (#733) * adjusted address struct * adding contract data (WIP) * added contract status * retrieving address details (label, ens, ...) from single method * writing labels to result, migration fixed * CR feedback * renamed tags to names --- backend/pkg/api/data_access/general.go | 48 ++++++++ backend/pkg/api/data_access/vdb_blocks.go | 27 ++++- backend/pkg/api/data_access/vdb_deposits.go | 41 ++++++- .../pkg/api/data_access/vdb_withdrawals.go | 65 ++++++++--- backend/pkg/api/types/common.go | 6 +- backend/pkg/commons/db/bigtable_eth1.go | 106 +++++++++--------- backend/pkg/commons/db/ens.go | 10 +- .../20240822134034_add_address_tags.sql | 15 +++ frontend/types/api/common.ts | 2 + 9 files changed, 239 insertions(+), 81 deletions(-) create mode 100644 backend/pkg/api/data_access/general.go create mode 100644 backend/pkg/commons/db/migrations/postgres/20240822134034_add_address_tags.sql 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/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..8c1d49003 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,17 +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, } + 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 { responseData[i].GroupId = t.DefaultGroupId @@ -143,6 +154,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 +165,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_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/types/common.go b/backend/pkg/api/types/common.go index 2c7253623..2deebb1b9 100644 --- a/backend/pkg/api/types/common.go +++ b/backend/pkg/api/types/common.go @@ -34,8 +34,10 @@ 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 { 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..5020f31e6 100644 --- a/backend/pkg/commons/db/ens.go +++ b/backend/pkg/commons/db/ens.go @@ -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/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 */; From c55824620a7a396b8cc637d751cda4a64ff6f31a Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 18 Sep 2024 12:50:22 +0200 Subject: [PATCH 15/16] fix(eth1indexer): fix handling of specific error (#687) (BEDS-90) * eth1indexer: less logging on ens * eth1indexer: fix handling of specific error --- backend/pkg/commons/db/ens.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/pkg/commons/db/ens.go b/backend/pkg/commons/db/ens.go index 5020f31e6..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 } From 685d019444c8b3f813c8d12b17f3e090374df9a9 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 18 Sep 2024 14:13:52 +0200 Subject: [PATCH 16/16] fix(api): fix GetValidatorDashboardElDeposits (#866) BEDS-155 --- backend/pkg/api/data_access/vdb_deposits.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/pkg/api/data_access/vdb_deposits.go b/backend/pkg/api/data_access/vdb_deposits.go index 8c1d49003..db2026a23 100644 --- a/backend/pkg/api/data_access/vdb_deposits.go +++ b/backend/pkg/api/data_access/vdb_deposits.go @@ -134,6 +134,7 @@ func (d *DataAccessService) GetValidatorDashboardElDeposits(ctx context.Context, 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{