From 5bfc659c20f3dab9fab2d66dd8d00abc6018d249 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 6 Nov 2024 17:32:32 +0300 Subject: [PATCH 1/5] Feature: apps --- .github/workflows/build.yml | 19 + .vscode/launch.json | 13 +- Makefile | 5 +- build/private_api/Dockerfile | 38 ++ cmd/api/init.go | 2 +- cmd/private_api/config.go | 22 ++ cmd/private_api/handler/app.go | 338 +++++++++++++++++ cmd/private_api/handler/bind.go | 17 + cmd/private_api/handler/error.go | 73 ++++ cmd/private_api/handler/status.go | 21 ++ cmd/private_api/handler/validators.go | 62 +++ cmd/private_api/init.go | 185 +++++++++ cmd/private_api/main.go | 49 +++ configs/dipdup.yml | 5 + .../01_refresh_materialized_view.sql | 8 + .../02_add_job_refresh_materialized_view.sql | 10 + database/views/12_app_stats_by_hour.sql | 15 + database/views/13_app_stats_by_day.sql | 15 + database/views/14_app_stats_by_month.sql | 15 + database/views/15_leaderboard.sql | 34 ++ docker-compose.yml | 14 + go.mod | 2 + go.sum | 4 + internal/storage/app.go | 90 +++++ internal/storage/app_bridge.go | 23 ++ internal/storage/app_id.go | 22 ++ internal/storage/generic.go | 11 + internal/storage/mock/app.go | 354 ++++++++++++++++++ internal/storage/mock/generic.go | 314 ++++++++++++++++ internal/storage/postgres/app.go | 52 +++ internal/storage/postgres/app_test.go | 78 ++++ internal/storage/postgres/core.go | 35 +- internal/storage/postgres/custom_types.go | 20 + .../20241106_rollup_action_sender_id.up.sql | 16 + .../storage/postgres/migrations/migrations.go | 21 ++ internal/storage/postgres/scopes.go | 6 + internal/storage/postgres/stats_test.go | 2 +- internal/storage/postgres/storage_test.go | 2 +- internal/storage/postgres/transaction.go | 123 ++++++ internal/storage/postgres/transaction_test.go | 2 +- internal/storage/rollup_action.go | 8 +- internal/storage/types/app_types.go | 27 ++ internal/storage/types/app_types_enum.go | 250 +++++++++++++ internal/storage/views.go | 1 + pkg/indexer/indexer.go | 2 +- pkg/indexer/storage/storage.go | 3 + pkg/indexer/storage/storage_test.go | 2 +- test/data/app.yml | 15 + test/data/app_bridge.yml | 3 + test/data/app_id.yaml | 3 + test/data/rollup_action.yml | 1 + 51 files changed, 2439 insertions(+), 13 deletions(-) create mode 100644 build/private_api/Dockerfile create mode 100644 cmd/private_api/config.go create mode 100644 cmd/private_api/handler/app.go create mode 100644 cmd/private_api/handler/bind.go create mode 100644 cmd/private_api/handler/error.go create mode 100644 cmd/private_api/handler/status.go create mode 100644 cmd/private_api/handler/validators.go create mode 100644 cmd/private_api/init.go create mode 100644 cmd/private_api/main.go create mode 100644 database/functions/01_refresh_materialized_view.sql create mode 100644 database/functions/02_add_job_refresh_materialized_view.sql create mode 100644 database/views/12_app_stats_by_hour.sql create mode 100644 database/views/13_app_stats_by_day.sql create mode 100644 database/views/14_app_stats_by_month.sql create mode 100644 database/views/15_leaderboard.sql create mode 100644 internal/storage/app.go create mode 100644 internal/storage/app_bridge.go create mode 100644 internal/storage/app_id.go create mode 100644 internal/storage/mock/app.go create mode 100644 internal/storage/postgres/app.go create mode 100644 internal/storage/postgres/app_test.go create mode 100644 internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql create mode 100644 internal/storage/postgres/migrations/migrations.go create mode 100644 internal/storage/types/app_types.go create mode 100644 internal/storage/types/app_types_enum.go create mode 100644 test/data/app.yml create mode 100644 test/data/app_bridge.yml create mode 100644 test/data/app_id.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6f91a8..2e5ca84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,3 +68,22 @@ jobs: cache-to: type=gha,mode=max tags: ${{ steps.meta-api.outputs.tags }} labels: ${{ steps.meta-api.outputs.labels }} + + # Private API + + - name: API image tags & labels + id: private-api + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE_BASE }}-private-api + + - name: Private API image build & push + uses: docker/build-push-action@v5 + with: + context: . + file: build/private_api/Dockerfile + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + tags: ${{ steps.meta-api.outputs.tags }} + labels: ${{ steps.meta-api.outputs.labels }} diff --git a/.vscode/launch.json b/.vscode/launch.json index c49e60f..639c008 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,6 +26,17 @@ "../../configs/dipdup.yml", ], "envFile": "${workspaceFolder}/.env" - } + },{ + "name": "Launch Private API", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/private_api", + "args": [ + "-c", + "../../configs/dipdup.yml", + ], + "envFile": "${workspaceFolder}/.env" + }, ] } \ No newline at end of file diff --git a/Makefile b/Makefile index 6f6661b..1965cbb 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ indexer: api: cd cmd/api && go run . -c ../../configs/dipdup.yml +private_api: + cd cmd/private_api && go run . -c ../../configs/dipdup.yml + generate: go generate -v ./internal/storage ./internal/storage/types ./pkg/node @@ -34,4 +37,4 @@ license-header: build: docker-compose up -d --build -.PHONY: indexer api generate test lint cover api-docs ga license-header build \ No newline at end of file +.PHONY: indexer api generate test lint cover api-docs ga license-header build private_api \ No newline at end of file diff --git a/build/private_api/Dockerfile b/build/private_api/Dockerfile new file mode 100644 index 0000000..a8b135a --- /dev/null +++ b/build/private_api/Dockerfile @@ -0,0 +1,38 @@ +# --------------------------------------------------------------------- +# The first stage container, for building the application +# --------------------------------------------------------------------- +FROM golang:1.23-alpine as builder + +ENV CGO_ENABLED=0 +ENV GO111MODULE=on +ENV GOOS=linux + +RUN apk --no-cache add ca-certificates +RUN apk add --update git + +RUN mkdir -p $GOPATH/src/github.com/celenium-io/astria-indexer/ + +COPY ./go.* $GOPATH/src/github.com/celenium-io/astria-indexer/ +WORKDIR $GOPATH/src/github.com/celenium-io/astria-indexer +RUN go mod download + +COPY cmd/private_api cmd/private_api +COPY internal internal +COPY pkg pkg + +WORKDIR $GOPATH/src/github.com/celenium-io/astria-indexer/cmd/private_api/ +RUN go build -a -installsuffix cgo -o /go/bin/private_api . + +# --------------------------------------------------------------------- +# The second stage container, for running the application +# --------------------------------------------------------------------- +FROM scratch + +WORKDIR /app/private_api + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /go/bin/private_api /go/bin/private_api +COPY ./configs/dipdup.yml ./ +COPY database database + +ENTRYPOINT ["/go/bin/private_api", "-c", "dipdup.yml"] \ No newline at end of file diff --git a/cmd/api/init.go b/cmd/api/init.go index 66283da..ae55cd8 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -244,7 +244,7 @@ func initDispatcher(ctx context.Context, db postgres.Storage) { func initDatabase(cfg config.Database, viewsDir string) postgres.Storage { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - db, err := postgres.Create(ctx, cfg, viewsDir) + db, err := postgres.Create(ctx, cfg, viewsDir, false) if err != nil { panic(err) } diff --git a/cmd/private_api/config.go b/cmd/private_api/config.go new file mode 100644 index 0000000..426a326 --- /dev/null +++ b/cmd/private_api/config.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package main + +import ( + indexerConfig "github.com/celenium-io/astria-indexer/pkg/indexer/config" + "github.com/dipdup-net/go-lib/config" +) + +type Config struct { + *config.Config `yaml:",inline"` + LogLevel string `validate:"omitempty,oneof=debug trace info warn error fatal panic" yaml:"log_level"` + Indexer indexerConfig.Indexer `validate:"required" yaml:"indexer"` + ApiConfig ApiConfig `validate:"required" yaml:"private_api"` +} + +type ApiConfig struct { + Bind string `validate:"required,hostname_port" yaml:"bind"` + RateLimit float64 `validate:"omitempty,min=0" yaml:"rate_limit"` + RequestTimeout int `validate:"omitempty,min=1" yaml:"request_timeout"` +} diff --git a/cmd/private_api/handler/app.go b/cmd/private_api/handler/app.go new file mode 100644 index 0000000..cd7f550 --- /dev/null +++ b/cmd/private_api/handler/app.go @@ -0,0 +1,338 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "context" + "encoding/base64" + + "github.com/celenium-io/astria-indexer/internal/astria" + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/postgres" + "github.com/celenium-io/astria-indexer/internal/storage/types" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" + "github.com/gosimple/slug" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" +) + +type AppHandler struct { + apps storage.IApp + address storage.IAddress + bridge storage.IBridge + rollup storage.IRollup + tx sdk.Transactable +} + +func NewAppHandler( + apps storage.IApp, + address storage.IAddress, + bridge storage.IBridge, + rollup storage.IRollup, + tx sdk.Transactable, +) AppHandler { + return AppHandler{ + apps: apps, + address: address, + bridge: bridge, + rollup: rollup, + tx: tx, + } +} + +type createAppRequest struct { + Group string `json:"group" validate:"omitempty,min=1"` + Name string `json:"name" validate:"required,min=1"` + Description string `json:"description" validate:"required,min=1"` + Website string `json:"website" validate:"omitempty,url"` + GitHub string `json:"github" validate:"omitempty,url"` + Twitter string `json:"twitter" validate:"omitempty,url"` + Logo string `json:"logo" validate:"omitempty,url"` + L2Beat string `json:"l2beat" validate:"omitempty,url"` + Explorer string `json:"explorer" validate:"omitempty,url"` + Stack string `json:"stack" validate:"omitempty"` + Links []string `json:"links" validate:"omitempty,dive,url"` + Category string `json:"category" validate:"omitempty,app_category"` + Type string `json:"type" validate:"omitempty,app_type"` + VM string `json:"vm" validate:"omitempty"` + Provider string `json:"provider" validate:"omitempty"` + + AppIds []appId `json:"app_ids" validate:"required,min=1"` + Bridges []bridge `json:"bridges" validate:"omitempty"` +} + +type appId struct { + Rollup string `json:"rollup" validate:"omitempty,base64"` + Address string `json:"address" validate:"required,address"` +} + +type bridge struct { + Address string `json:"address" validate:"required,address"` + Native bool `json:"native" validate:"omitempty"` +} + +func (handler AppHandler) Create(c echo.Context) error { + req, err := bindAndValidate[createAppRequest](c) + if err != nil { + return badRequestError(c, err) + } + + if err := handler.createApp(c.Request().Context(), req); err != nil { + return handleError(c, err, handler.apps) + } + + return success(c) +} + +func (handler AppHandler) createApp(ctx context.Context, req *createAppRequest) error { + tx, err := postgres.BeginTransaction(ctx, handler.tx) + if err != nil { + return err + } + + rollup := storage.App{ + Group: req.Group, + Name: req.Name, + Description: req.Description, + Website: req.Website, + Github: req.GitHub, + Twitter: req.Twitter, + Logo: req.Logo, + L2Beat: req.L2Beat, + Explorer: req.Explorer, + Stack: req.Stack, + Links: req.Links, + Provider: req.Provider, + VM: req.VM, + Type: types.AppType(req.Type), + Category: types.AppCategory(req.Category), + Slug: slug.Make(req.Name), + } + + if err := tx.SaveApp(ctx, &rollup); err != nil { + return tx.HandleError(ctx, err) + } + + appIds, err := handler.createAppIds(ctx, rollup.Id, req.AppIds...) + if err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.SaveAppId(ctx, appIds...); err != nil { + return tx.HandleError(ctx, err) + } + + bridges, err := handler.createBridges(ctx, rollup.Id, req.Bridges...) + if err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.SaveAppBridges(ctx, bridges...); err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.RefreshLeaderboard(ctx); err != nil { + return tx.HandleError(ctx, err) + } + + return tx.Flush(ctx) +} + +func (handler AppHandler) createAppIds(ctx context.Context, id uint64, data ...appId) ([]storage.AppId, error) { + providers := make([]storage.AppId, len(data)) + for i := range data { + providers[i].AppId = id + + if !astria.IsAddress(data[i].Address) { + return nil, errors.Wrap(errInvalidAddress, data[i].Address) + } + + address, err := handler.address.ByHash(ctx, data[i].Address) + if err != nil { + return nil, err + } + providers[i].AddressId = address.Id + + if data[i].Rollup != "" { + hashRollup, err := base64.StdEncoding.DecodeString(data[i].Rollup) + if err != nil { + return nil, err + } + rollup, err := handler.rollup.ByHash(ctx, hashRollup) + if err != nil { + return nil, err + } + providers[i].RolllupId = rollup.Id + } + } + return providers, nil +} + +func (handler AppHandler) createBridges(ctx context.Context, id uint64, data ...bridge) ([]storage.AppBridge, error) { + bridges := make([]storage.AppBridge, len(data)) + for i := range data { + bridges[i].AppId = id + + if !astria.IsAddress(data[i].Address) { + return nil, errors.Wrap(errInvalidAddress, data[i].Address) + } + + address, err := handler.address.ByHash(ctx, data[i].Address) + if err != nil { + return nil, err + } + + b, err := handler.bridge.ByAddress(ctx, address.Id) + if err != nil { + return nil, err + } + bridges[i].BridgeId = b.Id + bridges[i].Native = data[i].Native + } + return bridges, nil +} + +type updateAppRequest struct { + Id uint64 `param:"id" validate:"required,min=1"` + + Group string `json:"group" validate:"omitempty,min=1"` + Name string `json:"name" validate:"omitempty,min=1"` + Description string `json:"description" validate:"omitempty,min=1"` + Website string `json:"website" validate:"omitempty,url"` + GitHub string `json:"github" validate:"omitempty,url"` + Twitter string `json:"twitter" validate:"omitempty,url"` + Logo string `json:"logo" validate:"omitempty,url"` + L2Beat string `json:"l2beat" validate:"omitempty,url"` + Explorer string `json:"explorer" validate:"omitempty,url"` + Stack string `json:"stack" validate:"omitempty"` + Links []string `json:"links" validate:"omitempty,dive,url"` + Category string `json:"category" validate:"omitempty,app_category"` + Type string `json:"type" validate:"omitempty,app_type"` + VM string `json:"vm" validate:"omitempty"` + Provider string `json:"provider" validate:"omitempty"` + + AppIds []appId `json:"app_ids" validate:"omitempty,min=1"` + Bridges []bridge `json:"bridges" validate:"omitempty"` +} + +func (handler AppHandler) Update(c echo.Context) error { + req, err := bindAndValidate[updateAppRequest](c) + if err != nil { + return badRequestError(c, err) + } + + if err := handler.updateRollup(c.Request().Context(), req); err != nil { + return handleError(c, err, handler.apps) + } + + return success(c) +} + +func (handler AppHandler) updateRollup(ctx context.Context, req *updateAppRequest) error { + tx, err := postgres.BeginTransaction(ctx, handler.tx) + if err != nil { + return err + } + + if _, err := handler.apps.GetByID(ctx, req.Id); err != nil { + return err + } + + app := storage.App{ + Id: req.Id, + Name: req.Name, + Slug: slug.Make(req.Name), + Description: req.Description, + Website: req.Website, + Github: req.GitHub, + Twitter: req.Twitter, + Logo: req.Logo, + L2Beat: req.L2Beat, + Explorer: req.Explorer, + Stack: req.Stack, + Provider: req.Provider, + VM: req.VM, + Type: types.AppType(req.Type), + Category: types.AppCategory(req.Category), + Links: req.Links, + } + + if err := tx.UpdateApp(ctx, &app); err != nil { + return tx.HandleError(ctx, err) + } + + if len(req.AppIds) > 0 { + if err := tx.DeleteAppId(ctx, req.Id); err != nil { + return tx.HandleError(ctx, err) + } + + appIds, err := handler.createAppIds(ctx, app.Id, req.AppIds...) + if err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.SaveAppId(ctx, appIds...); err != nil { + return tx.HandleError(ctx, err) + } + } + + if len(req.Bridges) > 0 { + if err := tx.DeleteAppBridges(ctx, req.Id); err != nil { + return tx.HandleError(ctx, err) + } + + bridges, err := handler.createBridges(ctx, app.Id, req.Bridges...) + if err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.SaveAppBridges(ctx, bridges...); err != nil { + return tx.HandleError(ctx, err) + } + } + + if err := tx.RefreshLeaderboard(ctx); err != nil { + return tx.HandleError(ctx, err) + } + + return tx.Flush(ctx) +} + +type deleteRollupRequest struct { + Id uint64 `param:"id" validate:"required,min=1"` +} + +func (handler AppHandler) Delete(c echo.Context) error { + req, err := bindAndValidate[deleteRollupRequest](c) + if err != nil { + return badRequestError(c, err) + } + + if err := handler.deleteRollup(c.Request().Context(), req.Id); err != nil { + return handleError(c, err, handler.apps) + } + + return success(c) +} + +func (handler AppHandler) deleteRollup(ctx context.Context, id uint64) error { + tx, err := postgres.BeginTransaction(ctx, handler.tx) + if err != nil { + return err + } + + if err := tx.DeleteAppId(ctx, id); err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.DeleteAppBridges(ctx, id); err != nil { + return tx.HandleError(ctx, err) + } + + if err := tx.DeleteApp(ctx, id); err != nil { + return tx.HandleError(ctx, err) + } + + return tx.Flush(ctx) +} diff --git a/cmd/private_api/handler/bind.go b/cmd/private_api/handler/bind.go new file mode 100644 index 0000000..e919aae --- /dev/null +++ b/cmd/private_api/handler/bind.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import "github.com/labstack/echo/v4" + +func bindAndValidate[T any](c echo.Context) (*T, error) { + req := new(T) + if err := c.Bind(req); err != nil { + return req, err + } + if err := c.Validate(req); err != nil { + return req, err + } + return req, nil +} diff --git a/cmd/private_api/handler/error.go b/cmd/private_api/handler/error.go new file mode 100644 index 0000000..7af9e52 --- /dev/null +++ b/cmd/private_api/handler/error.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "context" + "net/http" + + sentryecho "github.com/getsentry/sentry-go/echo" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" +) + +var ( + errInvalidAddress = errors.New("invalid address") + errCancelRequest = "pq: canceling statement due to user request" +) + +type NoRows interface { + IsNoRows(err error) bool +} + +type Error struct { + Message string `json:"message"` +} + +func badRequestError(c echo.Context, err error) error { + return c.JSON(http.StatusBadRequest, Error{ + Message: err.Error(), + }) +} + +func internalServerError(c echo.Context, err error) error { + if hub := sentryecho.GetHubFromContext(c); hub != nil { + hub.CaptureMessage(err.Error()) + } + return c.JSON(http.StatusInternalServerError, Error{ + Message: err.Error(), + }) +} + +func handleError(c echo.Context, err error, noRows NoRows) error { + if err == nil { + return nil + } + if err.Error() == errCancelRequest { + return nil + } + if errors.Is(err, context.DeadlineExceeded) { + return c.JSON(http.StatusRequestTimeout, Error{ + Message: "timeout", + }) + } + if errors.Is(err, context.Canceled) { + return c.JSON(http.StatusBadGateway, Error{ + Message: err.Error(), + }) + } + if noRows.IsNoRows(err) { + return c.NoContent(http.StatusNoContent) + } + if errors.Is(err, errInvalidAddress) { + return badRequestError(c, err) + } + return internalServerError(c, err) +} + +func success(c echo.Context) error { + return c.JSON(http.StatusOK, echo.Map{ + "message": "success", + }) +} diff --git a/cmd/private_api/handler/status.go b/cmd/private_api/handler/status.go new file mode 100644 index 0000000..aee30fe --- /dev/null +++ b/cmd/private_api/handler/status.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +type StatusHandler struct { +} + +func NewStatusHandler() StatusHandler { + return StatusHandler{} +} + +func (handler StatusHandler) Status(c echo.Context) error { + return c.String(http.StatusOK, "OK") +} diff --git a/cmd/private_api/handler/validators.go b/cmd/private_api/handler/validators.go new file mode 100644 index 0000000..09798d4 --- /dev/null +++ b/cmd/private_api/handler/validators.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "net/http" + + "github.com/celenium-io/astria-indexer/internal/astria" + "github.com/celenium-io/astria-indexer/internal/storage/types" + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" +) + +type ApiValidator struct { + validator *validator.Validate +} + +func NewApiValidator() *ApiValidator { + v := validator.New() + if err := v.RegisterValidation("address", addressValidator()); err != nil { + panic(err) + } + if err := v.RegisterValidation("app_category", categoryValidator()); err != nil { + panic(err) + } + if err := v.RegisterValidation("app_type", appTypeValidator()); err != nil { + panic(err) + } + return &ApiValidator{validator: v} +} + +func (v *ApiValidator) Validate(i interface{}) error { + if err := v.validator.Struct(i); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return nil +} + +func isAddress(address string) bool { + return astria.IsAddress(address) +} + +func addressValidator() validator.Func { + return func(fl validator.FieldLevel) bool { + return isAddress(fl.Field().String()) + } +} + +func categoryValidator() validator.Func { + return func(fl validator.FieldLevel) bool { + _, err := types.ParseAppCategory(fl.Field().String()) + return err == nil + } +} + +func appTypeValidator() validator.Func { + return func(fl validator.FieldLevel) bool { + _, err := types.ParseAppType(fl.Field().String()) + return err == nil + } +} diff --git a/cmd/private_api/init.go b/cmd/private_api/init.go new file mode 100644 index 0000000..577a57c --- /dev/null +++ b/cmd/private_api/init.go @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "net/http" + "os" + "strconv" + "time" + + "github.com/celenium-io/astria-indexer/cmd/private_api/handler" + "github.com/celenium-io/astria-indexer/internal/storage/postgres" + "github.com/dipdup-net/go-lib/config" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "golang.org/x/time/rate" +) + +func init() { + log.Logger = log.Output(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: "2006-01-02 15:04:05", + }) +} + +func initConfig() (*Config, error) { + configPath := rootCmd.PersistentFlags().StringP("config", "c", "dipdup.yml", "path to YAML config file") + if err := rootCmd.Execute(); err != nil { + log.Panic().Err(err).Msg("command line execute") + return nil, err + } + + if err := rootCmd.MarkFlagRequired("config"); err != nil { + log.Panic().Err(err).Msg("config command line arg is required") + return nil, err + } + + var cfg Config + if err := config.Parse(*configPath, &cfg); err != nil { + log.Panic().Err(err).Msg("parsing config file") + return nil, err + } + + if cfg.LogLevel == "" { + cfg.LogLevel = zerolog.LevelInfoValue + } + + return &cfg, nil +} + +func initLogger(level string) error { + logLevel, err := zerolog.ParseLevel(level) + if err != nil { + log.Panic().Err(err).Msg("parsing log level") + return err + } + zerolog.SetGlobalLevel(logLevel) + zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { + short := file + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + short = file[i+1:] + break + } + } + file = short + return file + ":" + strconv.Itoa(line) + } + log.Logger = log.Logger.With().Caller().Logger() + + return nil +} + +func initEcho(cfg ApiConfig) *echo.Echo { + e := echo.New() + e.Validator = handler.NewApiValidator() + + timeout := 30 * time.Second + if cfg.RequestTimeout > 0 { + timeout = time.Duration(cfg.RequestTimeout) * time.Second + } + e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ + Skipper: middleware.DefaultSkipper, + Timeout: timeout, + })) + + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogLatency: true, + LogMethod: true, + LogUserAgent: true, + LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + switch { + case v.Status == http.StatusOK || v.Status == http.StatusNoContent: + log.Info(). + Str("uri", v.URI). + Int("status", v.Status). + Dur("latency", v.Latency). + Str("method", v.Method). + Str("user-agent", v.UserAgent). + Str("ip", c.RealIP()). + Msg("request") + case v.Status >= 500: + log.Error(). + Str("uri", v.URI). + Int("status", v.Status). + Dur("latency", v.Latency). + Str("method", v.Method). + Str("user-agent", v.UserAgent). + Str("ip", c.RealIP()). + Msg("request") + default: + log.Warn(). + Str("uri", v.URI). + Int("status", v.Status). + Dur("latency", v.Latency). + Str("method", v.Method). + Str("user-agent", v.UserAgent). + Str("ip", c.RealIP()). + Msg("request") + } + + return nil + }, + })) + e.Use(middleware.BodyLimit("2M")) + e.Use(middleware.CORS()) + e.Use(middleware.Recover()) + e.Use(middleware.Secure()) + e.Pre(middleware.RemoveTrailingSlash()) + + if cfg.RateLimit > 0 { + e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStore(rate.Limit(cfg.RateLimit)), + })) + + } + + e.Server.IdleTimeout = time.Second * 30 + + return e +} + +func initDatabase(cfg config.Database, viewsDir string) postgres.Storage { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + db, err := postgres.Create(ctx, cfg, viewsDir, false) + if err != nil { + panic(err) + } + return db +} + +func initHandlers(e *echo.Echo, db postgres.Storage) { + v1 := e.Group("v1") + + keyMiddleware := middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: "header:Authorization", + Validator: func(key string, c echo.Context) (bool, error) { + return key == os.Getenv("PRIVATE_API_AUTH_KEY"), nil + }, + }) + + appAuthHandler := handler.NewAppHandler(db.App, db.Address, db.Bridges, db.Rollup, db.Transactable) + app := v1.Group("/app") + { + app.POST("", appAuthHandler.Create, keyMiddleware) + app.PATCH("/:id", appAuthHandler.Update, keyMiddleware) + app.DELETE("/:id", appAuthHandler.Delete, keyMiddleware) + } + + statusHandler := handler.NewStatusHandler() + v1.GET("/status", statusHandler.Status) + + log.Info().Msg("API routes:") + for _, route := range e.Routes() { + log.Info().Msgf("[%s] %s -> %s", route.Method, route.Path, route.Name) + } +} diff --git a/cmd/private_api/main.go b/cmd/private_api/main.go new file mode 100644 index 0000000..b3baa29 --- /dev/null +++ b/cmd/private_api/main.go @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "private_api", +} + +func main() { + cfg, err := initConfig() + if err != nil { + return + } + + if err = initLogger(cfg.LogLevel); err != nil { + return + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + db := initDatabase(cfg.Database, cfg.Indexer.ScriptsDir) + e := initEcho(cfg.ApiConfig) + initHandlers(e, db) + + go func() { + if err := e.Start(cfg.ApiConfig.Bind); err != nil && errors.Is(err, http.ErrServerClosed) { + e.Logger.Fatal("shutting down the server") + } + }() + + <-ctx.Done() + cancel() + + if err := e.Shutdown(ctx); err != nil { + e.Logger.Fatal(err) + } +} diff --git a/configs/dipdup.yml b/configs/dipdup.yml index 55f7823..6370263 100644 --- a/configs/dipdup.yml +++ b/configs/dipdup.yml @@ -32,6 +32,11 @@ api: sentry_dsn: ${SENTRY_DSN} websocket: ${API_WEBSOCKET_ENABLED:-true} +private_api: + bind: ${PRIVATE_API_HOST:-0.0.0.0}:${PRIVATE_API_PORT:-9877} + rate_limit: ${PRIVATE_API_RATE_LIMIT:-0} + request_timeout: ${PRIVATE_API_REQUEST_TIMEOUT:-30} + environment: ${ASTRIA_ENV:-production} profiler: diff --git a/database/functions/01_refresh_materialized_view.sql b/database/functions/01_refresh_materialized_view.sql new file mode 100644 index 0000000..df8ee13 --- /dev/null +++ b/database/functions/01_refresh_materialized_view.sql @@ -0,0 +1,8 @@ + +CREATE OR REPLACE PROCEDURE refresh_materialized_view(job_id INT, config JSONB) + LANGUAGE PLPGSQL AS + $$ + BEGIN + REFRESH MATERIALIZED VIEW leaderboard; + END + $$; diff --git a/database/functions/02_add_job_refresh_materialized_view.sql b/database/functions/02_add_job_refresh_materialized_view.sql new file mode 100644 index 0000000..3d9146e --- /dev/null +++ b/database/functions/02_add_job_refresh_materialized_view.sql @@ -0,0 +1,10 @@ +CREATE OR REPLACE PROCEDURE add_job_refresh_materialized_view() + LANGUAGE PLPGSQL AS + $$ + BEGIN + if not exists (select from timescaledb_information.jobs where proc_name = 'refresh_materialized_view') + then + PERFORM add_job('refresh_materialized_view', '1h', config => NULL); + end if; + END + $$; \ No newline at end of file diff --git a/database/views/12_app_stats_by_hour.sql b/database/views/12_app_stats_by_hour.sql new file mode 100644 index 0000000..5883dd6 --- /dev/null +++ b/database/views/12_app_stats_by_hour.sql @@ -0,0 +1,15 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_hour +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 hour'::interval, rollup_action.time) AS time, + rollup_action.rollup_id, + rollup_action.sender_id, + sum(rollup_action.size) as size, + count(*) as actions_count, + max(rollup_action.time) as last_time, + min(rollup_action.time) as first_time + from rollup_action + group by 1, 2, 3 + with no data; + +CALL add_view_refresh_job('app_stats_by_hour', NULL, INTERVAL '1 minute'); \ No newline at end of file diff --git a/database/views/13_app_stats_by_day.sql b/database/views/13_app_stats_by_day.sql new file mode 100644 index 0000000..61f153f --- /dev/null +++ b/database/views/13_app_stats_by_day.sql @@ -0,0 +1,15 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_day +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 day'::interval, actions.time) AS time, + actions.rollup_id, + actions.sender_id, + sum(actions.size) as size, + sum(actions.actions_count) as actions_count, + max(actions.last_time) as last_time, + min(actions.first_time) as first_time + from app_stats_by_hour as actions + group by 1, 2, 3 + with no data; + +CALL add_view_refresh_job('app_stats_by_day', NULL, INTERVAL '5 minute'); \ No newline at end of file diff --git a/database/views/14_app_stats_by_month.sql b/database/views/14_app_stats_by_month.sql new file mode 100644 index 0000000..83e390b --- /dev/null +++ b/database/views/14_app_stats_by_month.sql @@ -0,0 +1,15 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_month +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 month'::interval, actions.time) AS time, + actions.rollup_id, + actions.sender_id, + sum(actions.size) as size, + sum(actions.actions_count) as actions_count, + max(actions.last_time) as last_time, + min(actions.first_time) as first_time + from app_stats_by_day as actions + group by 1, 2, 3 + with no data; + +CALL add_view_refresh_job('app_stats_by_month', NULL, INTERVAL '1 hour'); \ No newline at end of file diff --git a/database/views/15_leaderboard.sql b/database/views/15_leaderboard.sql new file mode 100644 index 0000000..b30e2ae --- /dev/null +++ b/database/views/15_leaderboard.sql @@ -0,0 +1,34 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard AS + with board as ( + select + app_id, + sum(size) as size, + sum(actions_count) as actions_count, + max(last_time) as last_time, + min(first_time) as first_time + from ( + select + rollup_id, + sender_id, + sum(size) as size, + sum(actions_count) as actions_count, + max(last_time) as last_time, + min(first_time) as first_time + from app_stats_by_month + group by 1, 2 + ) as agg + inner join app_id on app_id.address_id = agg.sender_id AND (app_id.rollup_id = agg.rollup_id OR app_id.rollup_id = 0) + group by 1 + ) + select + board.size, + board.actions_count, + board.last_time, + board.first_time, + board.size / (select sum(size) from board) as size_pct, + board.actions_count / (select sum(actions_count) from board)as actions_count_pct, + app.* + from board + inner join app on app.id = board.app_id; + +CALL add_job_refresh_materialized_view(); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 013ce41..e78f31f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,20 @@ services: - "127.0.0.1:9876:9876" logging: *astria-logging + private-api: + restart: always + image: ghcr.io/celenium-io/astria-indexer-private-api:${TAG:-main} + build: + context: . + dockerfile: build/private_api/Dockerfile + env_file: + - .env + depends_on: + - db + ports: + - "127.0.0.1:9877:9877" + logging: *astria-logging + db: command: - -cshared_preload_libraries=timescaledb,pg_stat_statements diff --git a/go.mod b/go.mod index bcf5ffb..2a1a541 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/goccy/go-json v0.10.2 github.com/gogo/protobuf v1.3.2 github.com/gorilla/websocket v1.5.3 + github.com/gosimple/slug v1.14.0 github.com/grafana/pyroscope-go v1.1.2 github.com/json-iterator/go v1.1.12 github.com/labstack/echo-contrib v0.15.0 @@ -139,6 +140,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect diff --git a/go.sum b/go.sum index 7ad6aa6..f7e21c9 100644 --- a/go.sum +++ b/go.sum @@ -452,6 +452,10 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= +github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grafana/pyroscope-go v1.1.2 h1:7vCfdORYQMCxIzI3NlYAs3FcBP760+gWuYWOyiVyYx8= github.com/grafana/pyroscope-go v1.1.2/go.mod h1:HSSmHo2KRn6FasBA4vK7BMiQqyQq8KSuBKvrhkXxYPU= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= diff --git a/internal/storage/app.go b/internal/storage/app.go new file mode 100644 index 0000000..559e49c --- /dev/null +++ b/internal/storage/app.go @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import ( + "context" + "time" + + "github.com/celenium-io/astria-indexer/internal/storage/types" + "github.com/dipdup-net/indexer-sdk/pkg/storage" + "github.com/uptrace/bun" +) + +type LeaderboardFilters struct { + SortField string + Sort storage.SortOrder + Limit int + Offset int + Category []types.AppCategory +} + +//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed +type IApp interface { + storage.Table[*App] + + Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]RollupWithStats, error) +} + +type App struct { + bun.BaseModel `bun:"app" comment:"Table with applications."` + + Id uint64 `bun:"id,pk,notnull,autoincrement" comment:"Unique internal identity"` + Group string `bun:"group" comment:"Application group"` + Name string `bun:"name" comment:"Application name"` + Slug string `bun:"slug,unique:app_slug" comment:"Application slug"` + Github string `bun:"github" comment:"Application github link"` + Twitter string `bun:"twitter" comment:"Application twitter account link"` + Website string `bun:"website" comment:"Application website link"` + Logo string `bun:"logo" comment:"Application logo link"` + Description string `bun:"description" comment:"Application description"` + Explorer string `bun:"explorer" comment:"Application explorer link"` + L2Beat string `bun:"l2beat" comment:"Link to L2Beat"` + Links []string `bun:"links,array" comment:"Additional links"` + Stack string `bun:"stack" comment:"Using stack"` + VM string `bun:"vm" comment:"Virtual machine"` + Provider string `bun:"provider" comment:"RaaS"` + Type types.AppType `bun:"type,type:app_type" comment:"Type of application: settled or sovereign"` + Category types.AppCategory `bun:"category,type:app_category" comment:"Category of applications"` + + AppIds []*AppId `bun:"rel:has-many,join:id=app_id"` + Bridges []*AppBridge `bun:"rel:has-many,join:id=app_id"` +} + +func (App) TableName() string { + return "app" +} + +func (app App) IsEmpty() bool { + return app.Group == "" && + app.Name == "" && + app.Slug == "" && + app.Github == "" && + app.Twitter == "" && + app.Website == "" && + app.Logo == "" && + app.Description == "" && + app.Explorer == "" && + app.L2Beat == "" && + app.Links == nil && + app.Stack == "" && + app.VM == "" && + app.Provider == "" && + app.Type == "" && + app.Category == "" +} + +type RollupWithStats struct { + App + AppStats +} + +type AppStats struct { + Size int64 `bun:"size"` + ActionsCount int64 `bun:"actions_count"` + LastActionTime time.Time `bun:"last_time"` + FirstActionTime time.Time `bun:"first_time"` + SizePct float64 `bun:"size_pct"` + ActionsCountPct float64 `bun:"actions_count_pct"` +} diff --git a/internal/storage/app_bridge.go b/internal/storage/app_bridge.go new file mode 100644 index 0000000..50ddcf7 --- /dev/null +++ b/internal/storage/app_bridge.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import ( + "github.com/uptrace/bun" +) + +type AppBridge struct { + bun.BaseModel `bun:"app_bridge" comment:"Table with application bridges"` + + AppId uint64 `bun:"app_id,pk" comment:"Application id"` + BridgeId uint64 `bun:"bridge_id,pk" comment:"Bridge id"` + Native bool `bun:"native,default:false" comment:"Is native bridge for this application"` + + App *App `bun:"rel:belongs-to,join:app_id=id"` + Bridge *Bridge `bun:"rel:belongs-to,join:bridge_id=id"` +} + +func (AppBridge) TableName() string { + return "app_bridge" +} diff --git a/internal/storage/app_id.go b/internal/storage/app_id.go new file mode 100644 index 0000000..edcc77d --- /dev/null +++ b/internal/storage/app_id.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import "github.com/uptrace/bun" + +type AppId struct { + bun.BaseModel `bun:"app_id" comment:"Table with application bridges"` + + AppId uint64 `bun:"app_id,pk" comment:"Application id"` + RolllupId uint64 `bun:"rollup_id,pk" comment:"Rollup id"` + AddressId uint64 `bun:"address_id,pk" comment:"Address id"` + + App *App `bun:"rel:belongs-to,join:app_id=id"` + Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` + Address *Address `bun:"rel:belongs-to,join:address_id=id"` +} + +func (AppId) TableName() string { + return "app_id" +} diff --git a/internal/storage/generic.go b/internal/storage/generic.go index 443c25c..56ae4ef 100644 --- a/internal/storage/generic.go +++ b/internal/storage/generic.go @@ -39,6 +39,9 @@ var Models = []any{ &Fee{}, &Transfer{}, &Deposit{}, + &App{}, + &AppBridge{}, + &AppId{}, } //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed @@ -61,6 +64,13 @@ type Transaction interface { SaveFees(ctx context.Context, fees ...*Fee) error SaveTransfers(ctx context.Context, transfers ...*Transfer) error SaveDeposits(ctx context.Context, deposits ...*Deposit) error + SaveApp(ctx context.Context, app *App) error + UpdateApp(ctx context.Context, app *App) error + SaveAppId(ctx context.Context, ids ...AppId) error + DeleteAppId(ctx context.Context, appId uint64) error + SaveAppBridges(ctx context.Context, bridges ...AppBridge) error + DeleteAppBridges(ctx context.Context, appId uint64) error + DeleteApp(ctx context.Context, appId uint64) error RetentionBlockSignatures(ctx context.Context, height types.Level) error RollbackActions(ctx context.Context, height types.Level) (actions []Action, err error) @@ -92,6 +102,7 @@ type Transaction interface { Validators(ctx context.Context) ([]Validator, error) GetBridgeIdByAddressId(ctx context.Context, id uint64) (uint64, error) GetAddressId(ctx context.Context, hash string) (uint64, error) + RefreshLeaderboard(ctx context.Context) error } type SearchResult struct { diff --git a/internal/storage/mock/app.go b/internal/storage/mock/app.go new file mode 100644 index 0000000..1e10278 --- /dev/null +++ b/internal/storage/mock/app.go @@ -0,0 +1,354 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: app.go +// +// Generated by this command: +// +// mockgen -source=app.go -destination=mock/app.go -package=mock -typed +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + storage "github.com/celenium-io/astria-indexer/internal/storage" + storage0 "github.com/dipdup-net/indexer-sdk/pkg/storage" + gomock "go.uber.org/mock/gomock" +) + +// MockIApp is a mock of IApp interface. +type MockIApp struct { + ctrl *gomock.Controller + recorder *MockIAppMockRecorder +} + +// MockIAppMockRecorder is the mock recorder for MockIApp. +type MockIAppMockRecorder struct { + mock *MockIApp +} + +// NewMockIApp creates a new mock instance. +func NewMockIApp(ctrl *gomock.Controller) *MockIApp { + mock := &MockIApp{ctrl: ctrl} + mock.recorder = &MockIAppMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIApp) EXPECT() *MockIAppMockRecorder { + return m.recorder +} + +// CursorList mocks base method. +func (m *MockIApp) CursorList(ctx context.Context, id, limit uint64, order storage0.SortOrder, cmp storage0.Comparator) ([]*storage.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CursorList", ctx, id, limit, order, cmp) + ret0, _ := ret[0].([]*storage.App) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CursorList indicates an expected call of CursorList. +func (mr *MockIAppMockRecorder) CursorList(ctx, id, limit, order, cmp any) *MockIAppCursorListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CursorList", reflect.TypeOf((*MockIApp)(nil).CursorList), ctx, id, limit, order, cmp) + return &MockIAppCursorListCall{Call: call} +} + +// MockIAppCursorListCall wrap *gomock.Call +type MockIAppCursorListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppCursorListCall) Return(arg0 []*storage.App, arg1 error) *MockIAppCursorListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppCursorListCall) Do(f func(context.Context, uint64, uint64, storage0.SortOrder, storage0.Comparator) ([]*storage.App, error)) *MockIAppCursorListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppCursorListCall) DoAndReturn(f func(context.Context, uint64, uint64, storage0.SortOrder, storage0.Comparator) ([]*storage.App, error)) *MockIAppCursorListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetByID mocks base method. +func (m *MockIApp) GetByID(ctx context.Context, id uint64) (*storage.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", ctx, id) + ret0, _ := ret[0].(*storage.App) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockIAppMockRecorder) GetByID(ctx, id any) *MockIAppGetByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockIApp)(nil).GetByID), ctx, id) + return &MockIAppGetByIDCall{Call: call} +} + +// MockIAppGetByIDCall wrap *gomock.Call +type MockIAppGetByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppGetByIDCall) Return(arg0 *storage.App, arg1 error) *MockIAppGetByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppGetByIDCall) Do(f func(context.Context, uint64) (*storage.App, error)) *MockIAppGetByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppGetByIDCall) DoAndReturn(f func(context.Context, uint64) (*storage.App, error)) *MockIAppGetByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IsNoRows mocks base method. +func (m *MockIApp) IsNoRows(err error) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsNoRows", err) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsNoRows indicates an expected call of IsNoRows. +func (mr *MockIAppMockRecorder) IsNoRows(err any) *MockIAppIsNoRowsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNoRows", reflect.TypeOf((*MockIApp)(nil).IsNoRows), err) + return &MockIAppIsNoRowsCall{Call: call} +} + +// MockIAppIsNoRowsCall wrap *gomock.Call +type MockIAppIsNoRowsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppIsNoRowsCall) Return(arg0 bool) *MockIAppIsNoRowsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppIsNoRowsCall) Do(f func(error) bool) *MockIAppIsNoRowsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppIsNoRowsCall) DoAndReturn(f func(error) bool) *MockIAppIsNoRowsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// LastID mocks base method. +func (m *MockIApp) LastID(ctx context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastID", ctx) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastID indicates an expected call of LastID. +func (mr *MockIAppMockRecorder) LastID(ctx any) *MockIAppLastIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastID", reflect.TypeOf((*MockIApp)(nil).LastID), ctx) + return &MockIAppLastIDCall{Call: call} +} + +// MockIAppLastIDCall wrap *gomock.Call +type MockIAppLastIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppLastIDCall) Return(arg0 uint64, arg1 error) *MockIAppLastIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppLastIDCall) Do(f func(context.Context) (uint64, error)) *MockIAppLastIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppLastIDCall) DoAndReturn(f func(context.Context) (uint64, error)) *MockIAppLastIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Leaderboard mocks base method. +func (m *MockIApp) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) ([]storage.RollupWithStats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Leaderboard", ctx, fltrs) + ret0, _ := ret[0].([]storage.RollupWithStats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Leaderboard indicates an expected call of Leaderboard. +func (mr *MockIAppMockRecorder) Leaderboard(ctx, fltrs any) *MockIAppLeaderboardCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Leaderboard", reflect.TypeOf((*MockIApp)(nil).Leaderboard), ctx, fltrs) + return &MockIAppLeaderboardCall{Call: call} +} + +// MockIAppLeaderboardCall wrap *gomock.Call +type MockIAppLeaderboardCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppLeaderboardCall) Return(arg0 []storage.RollupWithStats, arg1 error) *MockIAppLeaderboardCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppLeaderboardCall) Do(f func(context.Context, storage.LeaderboardFilters) ([]storage.RollupWithStats, error)) *MockIAppLeaderboardCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppLeaderboardCall) DoAndReturn(f func(context.Context, storage.LeaderboardFilters) ([]storage.RollupWithStats, error)) *MockIAppLeaderboardCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// List mocks base method. +func (m *MockIApp) List(ctx context.Context, limit, offset uint64, order storage0.SortOrder) ([]*storage.App, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, limit, offset, order) + ret0, _ := ret[0].([]*storage.App) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockIAppMockRecorder) List(ctx, limit, offset, order any) *MockIAppListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIApp)(nil).List), ctx, limit, offset, order) + return &MockIAppListCall{Call: call} +} + +// MockIAppListCall wrap *gomock.Call +type MockIAppListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppListCall) Return(arg0 []*storage.App, arg1 error) *MockIAppListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppListCall) Do(f func(context.Context, uint64, uint64, storage0.SortOrder) ([]*storage.App, error)) *MockIAppListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppListCall) DoAndReturn(f func(context.Context, uint64, uint64, storage0.SortOrder) ([]*storage.App, error)) *MockIAppListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Save mocks base method. +func (m_2 *MockIApp) Save(ctx context.Context, m *storage.App) error { + m_2.ctrl.T.Helper() + ret := m_2.ctrl.Call(m_2, "Save", ctx, m) + ret0, _ := ret[0].(error) + return ret0 +} + +// Save indicates an expected call of Save. +func (mr *MockIAppMockRecorder) Save(ctx, m any) *MockIAppSaveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockIApp)(nil).Save), ctx, m) + return &MockIAppSaveCall{Call: call} +} + +// MockIAppSaveCall wrap *gomock.Call +type MockIAppSaveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppSaveCall) Return(arg0 error) *MockIAppSaveCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppSaveCall) Do(f func(context.Context, *storage.App) error) *MockIAppSaveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppSaveCall) DoAndReturn(f func(context.Context, *storage.App) error) *MockIAppSaveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Update mocks base method. +func (m_2 *MockIApp) Update(ctx context.Context, m *storage.App) error { + m_2.ctrl.T.Helper() + ret := m_2.ctrl.Call(m_2, "Update", ctx, m) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockIAppMockRecorder) Update(ctx, m any) *MockIAppUpdateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockIApp)(nil).Update), ctx, m) + return &MockIAppUpdateCall{Call: call} +} + +// MockIAppUpdateCall wrap *gomock.Call +type MockIAppUpdateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppUpdateCall) Return(arg0 error) *MockIAppUpdateCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppUpdateCall) Do(f func(context.Context, *storage.App) error) *MockIAppUpdateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppUpdateCall) DoAndReturn(f func(context.Context, *storage.App) error) *MockIAppUpdateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/storage/mock/generic.go b/internal/storage/mock/generic.go index bc1d2bd..95dfffa 100644 --- a/internal/storage/mock/generic.go +++ b/internal/storage/mock/generic.go @@ -199,6 +199,120 @@ func (c *MockTransactionCopyFromCall) DoAndReturn(f func(context.Context, string return c } +// DeleteApp mocks base method. +func (m *MockTransaction) DeleteApp(ctx context.Context, appId uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteApp", ctx, appId) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteApp indicates an expected call of DeleteApp. +func (mr *MockTransactionMockRecorder) DeleteApp(ctx, appId any) *MockTransactionDeleteAppCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApp", reflect.TypeOf((*MockTransaction)(nil).DeleteApp), ctx, appId) + return &MockTransactionDeleteAppCall{Call: call} +} + +// MockTransactionDeleteAppCall wrap *gomock.Call +type MockTransactionDeleteAppCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionDeleteAppCall) Return(arg0 error) *MockTransactionDeleteAppCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionDeleteAppCall) Do(f func(context.Context, uint64) error) *MockTransactionDeleteAppCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionDeleteAppCall) DoAndReturn(f func(context.Context, uint64) error) *MockTransactionDeleteAppCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// DeleteAppBridges mocks base method. +func (m *MockTransaction) DeleteAppBridges(ctx context.Context, appId uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAppBridges", ctx, appId) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAppBridges indicates an expected call of DeleteAppBridges. +func (mr *MockTransactionMockRecorder) DeleteAppBridges(ctx, appId any) *MockTransactionDeleteAppBridgesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAppBridges", reflect.TypeOf((*MockTransaction)(nil).DeleteAppBridges), ctx, appId) + return &MockTransactionDeleteAppBridgesCall{Call: call} +} + +// MockTransactionDeleteAppBridgesCall wrap *gomock.Call +type MockTransactionDeleteAppBridgesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionDeleteAppBridgesCall) Return(arg0 error) *MockTransactionDeleteAppBridgesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionDeleteAppBridgesCall) Do(f func(context.Context, uint64) error) *MockTransactionDeleteAppBridgesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionDeleteAppBridgesCall) DoAndReturn(f func(context.Context, uint64) error) *MockTransactionDeleteAppBridgesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// DeleteAppId mocks base method. +func (m *MockTransaction) DeleteAppId(ctx context.Context, appId uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAppId", ctx, appId) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAppId indicates an expected call of DeleteAppId. +func (mr *MockTransactionMockRecorder) DeleteAppId(ctx, appId any) *MockTransactionDeleteAppIdCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAppId", reflect.TypeOf((*MockTransaction)(nil).DeleteAppId), ctx, appId) + return &MockTransactionDeleteAppIdCall{Call: call} +} + +// MockTransactionDeleteAppIdCall wrap *gomock.Call +type MockTransactionDeleteAppIdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionDeleteAppIdCall) Return(arg0 error) *MockTransactionDeleteAppIdCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionDeleteAppIdCall) Do(f func(context.Context, uint64) error) *MockTransactionDeleteAppIdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionDeleteAppIdCall) DoAndReturn(f func(context.Context, uint64) error) *MockTransactionDeleteAppIdCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Exec mocks base method. func (m *MockTransaction) Exec(ctx context.Context, query string, params ...any) (int64, error) { m.ctrl.T.Helper() @@ -553,6 +667,44 @@ func (c *MockTransactionLastNonceCall) DoAndReturn(f func(context.Context, uint6 return c } +// RefreshLeaderboard mocks base method. +func (m *MockTransaction) RefreshLeaderboard(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RefreshLeaderboard", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// RefreshLeaderboard indicates an expected call of RefreshLeaderboard. +func (mr *MockTransactionMockRecorder) RefreshLeaderboard(ctx any) *MockTransactionRefreshLeaderboardCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshLeaderboard", reflect.TypeOf((*MockTransaction)(nil).RefreshLeaderboard), ctx) + return &MockTransactionRefreshLeaderboardCall{Call: call} +} + +// MockTransactionRefreshLeaderboardCall wrap *gomock.Call +type MockTransactionRefreshLeaderboardCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionRefreshLeaderboardCall) Return(arg0 error) *MockTransactionRefreshLeaderboardCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionRefreshLeaderboardCall) Do(f func(context.Context) error) *MockTransactionRefreshLeaderboardCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionRefreshLeaderboardCall) DoAndReturn(f func(context.Context) error) *MockTransactionRefreshLeaderboardCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // RetentionBlockSignatures mocks base method. func (m *MockTransaction) RetentionBlockSignatures(ctx context.Context, height types.Level) error { m.ctrl.T.Helper() @@ -1414,6 +1566,130 @@ func (c *MockTransactionSaveAddressesCall) DoAndReturn(f func(context.Context, . return c } +// SaveApp mocks base method. +func (m *MockTransaction) SaveApp(ctx context.Context, app *storage.App) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveApp", ctx, app) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveApp indicates an expected call of SaveApp. +func (mr *MockTransactionMockRecorder) SaveApp(ctx, app any) *MockTransactionSaveAppCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveApp", reflect.TypeOf((*MockTransaction)(nil).SaveApp), ctx, app) + return &MockTransactionSaveAppCall{Call: call} +} + +// MockTransactionSaveAppCall wrap *gomock.Call +type MockTransactionSaveAppCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionSaveAppCall) Return(arg0 error) *MockTransactionSaveAppCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionSaveAppCall) Do(f func(context.Context, *storage.App) error) *MockTransactionSaveAppCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionSaveAppCall) DoAndReturn(f func(context.Context, *storage.App) error) *MockTransactionSaveAppCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SaveAppBridges mocks base method. +func (m *MockTransaction) SaveAppBridges(ctx context.Context, bridges ...storage.AppBridge) error { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range bridges { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SaveAppBridges", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveAppBridges indicates an expected call of SaveAppBridges. +func (mr *MockTransactionMockRecorder) SaveAppBridges(ctx any, bridges ...any) *MockTransactionSaveAppBridgesCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, bridges...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAppBridges", reflect.TypeOf((*MockTransaction)(nil).SaveAppBridges), varargs...) + return &MockTransactionSaveAppBridgesCall{Call: call} +} + +// MockTransactionSaveAppBridgesCall wrap *gomock.Call +type MockTransactionSaveAppBridgesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionSaveAppBridgesCall) Return(arg0 error) *MockTransactionSaveAppBridgesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionSaveAppBridgesCall) Do(f func(context.Context, ...storage.AppBridge) error) *MockTransactionSaveAppBridgesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionSaveAppBridgesCall) DoAndReturn(f func(context.Context, ...storage.AppBridge) error) *MockTransactionSaveAppBridgesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SaveAppId mocks base method. +func (m *MockTransaction) SaveAppId(ctx context.Context, ids ...storage.AppId) error { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range ids { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SaveAppId", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveAppId indicates an expected call of SaveAppId. +func (mr *MockTransactionMockRecorder) SaveAppId(ctx any, ids ...any) *MockTransactionSaveAppIdCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, ids...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAppId", reflect.TypeOf((*MockTransaction)(nil).SaveAppId), varargs...) + return &MockTransactionSaveAppIdCall{Call: call} +} + +// MockTransactionSaveAppIdCall wrap *gomock.Call +type MockTransactionSaveAppIdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionSaveAppIdCall) Return(arg0 error) *MockTransactionSaveAppIdCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionSaveAppIdCall) Do(f func(context.Context, ...storage.AppId) error) *MockTransactionSaveAppIdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionSaveAppIdCall) DoAndReturn(f func(context.Context, ...storage.AppId) error) *MockTransactionSaveAppIdCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveBalanceUpdates mocks base method. func (m *MockTransaction) SaveBalanceUpdates(ctx context.Context, updates ...storage.BalanceUpdate) error { m.ctrl.T.Helper() @@ -2133,6 +2409,44 @@ func (c *MockTransactionUpdateAddressesCall) DoAndReturn(f func(context.Context, return c } +// UpdateApp mocks base method. +func (m *MockTransaction) UpdateApp(ctx context.Context, app *storage.App) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateApp", ctx, app) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateApp indicates an expected call of UpdateApp. +func (mr *MockTransactionMockRecorder) UpdateApp(ctx, app any) *MockTransactionUpdateAppCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApp", reflect.TypeOf((*MockTransaction)(nil).UpdateApp), ctx, app) + return &MockTransactionUpdateAppCall{Call: call} +} + +// MockTransactionUpdateAppCall wrap *gomock.Call +type MockTransactionUpdateAppCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionUpdateAppCall) Return(arg0 error) *MockTransactionUpdateAppCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionUpdateAppCall) Do(f func(context.Context, *storage.App) error) *MockTransactionUpdateAppCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionUpdateAppCall) DoAndReturn(f func(context.Context, *storage.App) error) *MockTransactionUpdateAppCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // UpdateConstants mocks base method. func (m *MockTransaction) UpdateConstants(ctx context.Context, constants ...*storage.Constant) error { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/app.go b/internal/storage/postgres/app.go new file mode 100644 index 0000000..19375b4 --- /dev/null +++ b/internal/storage/postgres/app.go @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package postgres + +import ( + "context" + + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/dipdup-net/go-lib/database" + "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +// App - +type App struct { + *postgres.Table[*storage.App] +} + +// NewApp - +func NewApp(db *database.Bun) *App { + return &App{ + Table: postgres.NewTable[*storage.App](db), + } +} + +func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) (rollups []storage.RollupWithStats, err error) { + switch fltrs.SortField { + case columnTime: + fltrs.SortField = "last_time" + case columnSize, columnActionsCount: + case "": + fltrs.SortField = columnSize + default: + return nil, errors.Errorf("unknown sort field: %s", fltrs.SortField) + } + + query := app.DB().NewSelect(). + Table(storage.ViewLeaderboard). + ColumnExpr("*"). + Offset(fltrs.Offset) + + if len(fltrs.Category) > 0 { + query = query.Where("category IN (?)", bun.In(fltrs.Category)) + } + + query = sortScope(query, fltrs.SortField, fltrs.Sort) + query = limitScope(query, fltrs.Limit) + err = query.Scan(ctx, &rollups) + return +} diff --git a/internal/storage/postgres/app_test.go b/internal/storage/postgres/app_test.go new file mode 100644 index 0000000..99ba3db --- /dev/null +++ b/internal/storage/postgres/app_test.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package postgres + +import ( + "context" + "time" + + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/types" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" +) + +func (s *StorageTestSuite) TestLeaderboard() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + _, err := s.storage.Connection().Exec(ctx, "REFRESH MATERIALIZED VIEW leaderboard;") + s.Require().NoError(err) + + for _, column := range []string{ + columnSize, columnActionsCount, columnTime, "", + } { + + apps, err := s.storage.App.Leaderboard(ctx, storage.LeaderboardFilters{ + SortField: column, + Sort: sdk.SortOrderDesc, + Limit: 10, + Offset: 0, + }) + s.Require().NoError(err, column) + s.Require().Len(apps, 1, column) + + app := apps[0] + s.Require().EqualValues("App 1", app.Name, column) + s.Require().EqualValues(34, app.Size, column) + s.Require().EqualValues(3, app.ActionsCount, column) + s.Require().False(app.LastActionTime.IsZero()) + s.Require().False(app.FirstActionTime.IsZero()) + s.Require().EqualValues(0.42857142857142855, app.ActionsCountPct) + s.Require().EqualValues(0.3953488372093023, app.SizePct) + } +} + +func (s *StorageTestSuite) TestLeaderboardWithCategory() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + _, err := s.storage.Connection().Exec(ctx, "REFRESH MATERIALIZED VIEW leaderboard;") + s.Require().NoError(err) + + for _, column := range []string{ + columnSize, columnActionsCount, columnTime, "", + } { + + apps, err := s.storage.App.Leaderboard(ctx, storage.LeaderboardFilters{ + SortField: column, + Sort: sdk.SortOrderDesc, + Limit: 10, + Offset: 0, + Category: []types.AppCategory{types.AppCategorySocial}, + }) + s.Require().NoError(err, column) + s.Require().Len(apps, 1, column) + + app := apps[0] + s.Require().EqualValues("Rollup 3", app.Name, column) + s.Require().EqualValues("The third", app.Description, column) + s.Require().EqualValues(34, app.Size, column) + s.Require().EqualValues(3, app.ActionsCount, column) + s.Require().False(app.LastActionTime.IsZero()) + s.Require().False(app.FirstActionTime.IsZero()) + s.Require().EqualValues(0.42857142857142855, app.ActionsCountPct) + s.Require().EqualValues(0.3953488372093023, app.SizePct) + s.Require().EqualValues("nft", app.Category) + } +} diff --git a/internal/storage/postgres/core.go b/internal/storage/postgres/core.go index 31bd0a0..2abeb2f 100644 --- a/internal/storage/postgres/core.go +++ b/internal/storage/postgres/core.go @@ -7,12 +7,14 @@ import ( "context" models "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/postgres/migrations" "github.com/dipdup-net/go-lib/config" "github.com/dipdup-net/go-lib/database" "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" "github.com/pkg/errors" "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" "go.opentelemetry.io/otel/trace" ) @@ -39,12 +41,17 @@ type Storage struct { State models.IState Search models.ISearch Stats models.IStats + App models.IApp Notificator *Notificator } // Create - -func Create(ctx context.Context, cfg config.Database, scriptsDir string) (Storage, error) { - strg, err := postgres.Create(ctx, cfg, initDatabase) +func Create(ctx context.Context, cfg config.Database, scriptsDir string, withMigrations bool) (Storage, error) { + init := initDatabase + if withMigrations { + init = initDatabaseWithMigrations + } + strg, err := postgres.Create(ctx, cfg, init) if err != nil { return Storage{}, err } @@ -68,6 +75,7 @@ func Create(ctx context.Context, cfg config.Database, scriptsDir string) (Storag Validator: NewValidator(strg.Connection()), State: NewState(strg.Connection()), Search: NewSearch(strg.Connection()), + App: NewApp(strg.Connection()), Stats: NewStats(strg.Connection()), Notificator: NewNotificator(cfg, strg.Connection().DB()), } @@ -96,6 +104,8 @@ func initDatabase(ctx context.Context, conn *database.Bun) error { (*models.RollupAction)(nil), (*models.RollupAddress)(nil), (*models.AddressAction)(nil), + (*models.AppId)(nil), + (*models.AppBridge)(nil), ) if err := database.CreateTables(ctx, conn, models.Models...); err != nil { @@ -122,6 +132,27 @@ func initDatabase(ctx context.Context, conn *database.Bun) error { return createIndices(ctx, conn) } +func initDatabaseWithMigrations(ctx context.Context, conn *database.Bun) error { + if err := initDatabase(ctx, conn); err != nil { + return err + } + return migrateDatabase(ctx, conn) +} + +func migrateDatabase(ctx context.Context, db *database.Bun) error { + migrator := migrate.NewMigrator(db.DB(), migrations.Migrations) + if err := migrator.Init(ctx); err != nil { + return err + } + if err := migrator.Lock(ctx); err != nil { + return err + } + defer migrator.Unlock(ctx) //nolint:errcheck + + _, err := migrator.Migrate(ctx) + return err +} + func createHypertables(ctx context.Context, conn *database.Bun) error { return conn.DB().RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { for _, model := range []storage.Model{ diff --git a/internal/storage/postgres/custom_types.go b/internal/storage/postgres/custom_types.go index 73ed94e..7eb4a4d 100644 --- a/internal/storage/postgres/custom_types.go +++ b/internal/storage/postgres/custom_types.go @@ -54,6 +54,26 @@ func createTypes(ctx context.Context, conn *database.Bun) error { ); err != nil { return err } + + if _, err := tx.ExecContext( + ctx, + createTypeQuery, + "app_type", + bun.Safe("app_type"), + bun.In(types.AppTypeValues()), + ); err != nil { + return err + } + + if _, err := tx.ExecContext( + ctx, + createTypeQuery, + "app_category", + bun.Safe("app_category"), + bun.In(types.AppCategoryValues()), + ); err != nil { + return err + } return nil }) } diff --git a/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql b/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql new file mode 100644 index 0000000..6626485 --- /dev/null +++ b/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql @@ -0,0 +1,16 @@ +ALTER TABLE IF EXISTS public.rollup_action ADD COLUMN IF NOT EXISTS sender_id int8 NOT NULL DEFAULT 0; + +--bun:split + +COMMENT ON COLUMN public.rollup_action.sender_id IS 'Internal id of sender address'; + +--bun:split + +with actions as ( + select signer_id, rollup_action.tx_id from rollup_action + left join tx on tx_id = tx.id +) +update rollup_action as ra +set sender_id = actions.signer_id +from actions +where ra.tx_id = actions.tx_id; diff --git a/internal/storage/postgres/migrations/migrations.go b/internal/storage/postgres/migrations/migrations.go new file mode 100644 index 0000000..cb6412b --- /dev/null +++ b/internal/storage/postgres/migrations/migrations.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package migrations + +import ( + "embed" + + "github.com/uptrace/bun/migrate" +) + +var Migrations = migrate.NewMigrations() + +//go:embed *.sql +var sqlMigrations embed.FS + +func init() { + if err := Migrations.Discover(sqlMigrations); err != nil { + panic(err) + } +} diff --git a/internal/storage/postgres/scopes.go b/internal/storage/postgres/scopes.go index 9a358bf..a40df5b 100644 --- a/internal/storage/postgres/scopes.go +++ b/internal/storage/postgres/scopes.go @@ -9,6 +9,12 @@ import ( "github.com/uptrace/bun" ) +const ( + columnSize = "size" + columnActionsCount = "actions_count" + columnTime = "time" +) + func limitScope(q *bun.SelectQuery, limit int) *bun.SelectQuery { if limit < 1 || limit > 100 { limit = 10 diff --git a/internal/storage/postgres/stats_test.go b/internal/storage/postgres/stats_test.go index 9737469..3ef3032 100644 --- a/internal/storage/postgres/stats_test.go +++ b/internal/storage/postgres/stats_test.go @@ -46,7 +46,7 @@ func (s *StatsTestSuite) SetupSuite() { Password: s.psqlContainer.Config.Password, Host: s.psqlContainer.Config.Host, Port: s.psqlContainer.MappedPort().Int(), - }, "../../../database") + }, "../../../database", false) s.Require().NoError(err) s.storage = strg diff --git a/internal/storage/postgres/storage_test.go b/internal/storage/postgres/storage_test.go index 48d1017..3b19cf1 100644 --- a/internal/storage/postgres/storage_test.go +++ b/internal/storage/postgres/storage_test.go @@ -44,7 +44,7 @@ func (s *StorageTestSuite) SetupSuite() { Password: s.psqlContainer.Config.Password, Host: s.psqlContainer.Config.Host, Port: s.psqlContainer.MappedPort().Int(), - }, "../../../database") + }, "../../../database", false) s.Require().NoError(err) s.storage = strg diff --git a/internal/storage/postgres/transaction.go b/internal/storage/postgres/transaction.go index e8c226a..3c8f286 100644 --- a/internal/storage/postgres/transaction.go +++ b/internal/storage/postgres/transaction.go @@ -7,6 +7,7 @@ import ( "context" "github.com/celenium-io/astria-indexer/pkg/types" + "github.com/lib/pq" "github.com/uptrace/bun" models "github.com/celenium-io/astria-indexer/internal/storage" @@ -510,3 +511,125 @@ func (tx Transaction) GetAddressId(ctx context.Context, hash string) (addrId uin Scan(ctx, &addrId) return } + +func (tx Transaction) SaveApp(ctx context.Context, app *models.App) error { + if app == nil { + return nil + } + _, err := tx.Tx().NewInsert().Model(app).Exec(ctx) + return err +} + +func (tx Transaction) UpdateApp(ctx context.Context, app *models.App) error { + if app == nil || app.IsEmpty() { + return nil + } + + query := tx.Tx().NewUpdate().Model(app).WherePK() + + if app.Group != "" { + query = query.Set("group = ?", app.Group) + } + if app.Name != "" { + query = query.Set("name = ?", app.Name) + } + if app.Slug != "" { + query = query.Set("slug = ?", app.Slug) + } + if app.Description != "" { + query = query.Set("description = ?", app.Description) + } + if app.Twitter != "" { + query = query.Set("twitter = ?", app.Twitter) + } + if app.Github != "" { + query = query.Set("github = ?", app.Github) + } + if app.Website != "" { + query = query.Set("website = ?", app.Website) + } + if app.Logo != "" { + query = query.Set("logo = ?", app.Logo) + } + if app.L2Beat != "" { + query = query.Set("l2beat = ?", app.L2Beat) + } + if app.Explorer != "" { + query = query.Set("explorer = ?", app.Explorer) + } + if app.Stack != "" { + query = query.Set("stack = ?", app.Stack) + } + if app.Links != nil { + query = query.Set("links = ?", pq.Array(app.Links)) + } + if app.Type != "" { + query = query.Set("type = ?", app.Type) + } + if app.Category != "" { + query = query.Set("category = ?", app.Category) + } + if app.Provider != "" { + query = query.Set("provider = ?", app.Provider) + } + if app.VM != "" { + query = query.Set("vm = ?", app.VM) + } + + _, err := query.Exec(ctx) + return err +} + +func (tx Transaction) SaveAppId(ctx context.Context, ids ...models.AppId) error { + if len(ids) == 0 { + return nil + } + _, err := tx.Tx().NewInsert().Model(&ids).Exec(ctx) + return err +} + +func (tx Transaction) DeleteAppId(ctx context.Context, appId uint64) error { + if appId == 0 { + return nil + } + _, err := tx.Tx().NewDelete(). + Model((*models.AppId)(nil)). + Where("app_id = ?", appId). + Exec(ctx) + return err +} + +func (tx Transaction) SaveAppBridges(ctx context.Context, bridges ...models.AppBridge) error { + if len(bridges) == 0 { + return nil + } + _, err := tx.Tx().NewInsert().Model(&bridges).Exec(ctx) + return err +} + +func (tx Transaction) DeleteAppBridges(ctx context.Context, appId uint64) error { + if appId == 0 { + return nil + } + _, err := tx.Tx().NewDelete(). + Model((*models.AppBridge)(nil)). + Where("app_id = ?", appId). + Exec(ctx) + return err +} + +func (tx Transaction) DeleteApp(ctx context.Context, appId uint64) error { + if appId == 0 { + return nil + } + _, err := tx.Tx().NewDelete(). + Model((*models.App)(nil)). + Where("id = ?", appId). + Exec(ctx) + return err +} + +func (tx Transaction) RefreshLeaderboard(ctx context.Context) error { + _, err := tx.Tx().ExecContext(ctx, "REFRESH MATERIALIZED VIEW leaderboard;") + return err +} diff --git a/internal/storage/postgres/transaction_test.go b/internal/storage/postgres/transaction_test.go index 85637b4..70babe8 100644 --- a/internal/storage/postgres/transaction_test.go +++ b/internal/storage/postgres/transaction_test.go @@ -54,7 +54,7 @@ func (s *TransactionTestSuite) SetupSuite() { Password: s.psqlContainer.Config.Password, Host: s.psqlContainer.Config.Host, Port: s.psqlContainer.MappedPort().Int(), - }, "../../../database") + }, "../../../database", false) s.Require().NoError(err) s.storage = strg } diff --git a/internal/storage/rollup_action.go b/internal/storage/rollup_action.go index 56f60ac..c9a4ea1 100644 --- a/internal/storage/rollup_action.go +++ b/internal/storage/rollup_action.go @@ -17,14 +17,16 @@ type RollupAction struct { RollupId uint64 `bun:"rollup_id,pk" comment:"Rollup internal id"` ActionId uint64 `bun:"action_id,pk" comment:"Action internal id"` Time time.Time `bun:"time,notnull,pk" comment:"Action time"` + SenderId uint64 `bun:"sender_id,notnull" comment:"Internal id of sender address"` ActionType types.ActionType `bun:"action_type,type:action_type" comment:"Action type"` Height pkgTypes.Level `bun:"height" comment:"Action block height"` TxId uint64 `bun:"tx_id" comment:"Transaction internal id"` Size int64 `bun:"size" comment:"Count bytes which was pushed to the rollup"` - Action *Action `bun:"rel:belongs-to,join:action_id=id"` - Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` - Tx *Tx `bun:"rel:belongs-to,join:tx_id=id"` + Action *Action `bun:"rel:belongs-to,join:action_id=id"` + Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` + Tx *Tx `bun:"rel:belongs-to,join:tx_id=id"` + Sender *Address `bun:"rel:belongs-to,join:sender_id=id"` } func (RollupAction) TableName() string { diff --git a/internal/storage/types/app_types.go b/internal/storage/types/app_types.go new file mode 100644 index 0000000..e2226aa --- /dev/null +++ b/internal/storage/types/app_types.go @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package types + +// swagger:enum AppCategory +/* + ENUM( + uncategorized, + finance, + gaming, + nft, + social + ) +*/ +//go:generate go-enum --marshal --sql --values --names +type AppCategory string + +// swagger:enum AppType +/* + ENUM( + sovereign, + settled + ) +*/ +//go:generate go-enum --marshal --sql --values --names +type AppType string diff --git a/internal/storage/types/app_types_enum.go b/internal/storage/types/app_types_enum.go new file mode 100644 index 0000000..1ce84d1 --- /dev/null +++ b/internal/storage/types/app_types_enum.go @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +// Code generated by go-enum DO NOT EDIT. +// Version: 0.5.7 +// Revision: bf63e108589bbd2327b13ec2c5da532aad234029 +// Build Date: 2023-07-25T23:27:55Z +// Built By: goreleaser + +package types + +import ( + "database/sql/driver" + "errors" + "fmt" + "strings" +) + +const ( + // AppCategoryUncategorized is a AppCategory of type uncategorized. + AppCategoryUncategorized AppCategory = "uncategorized" + // AppCategoryFinance is a AppCategory of type finance. + AppCategoryFinance AppCategory = "finance" + // AppCategoryGaming is a AppCategory of type gaming. + AppCategoryGaming AppCategory = "gaming" + // AppCategoryNft is a AppCategory of type nft. + AppCategoryNft AppCategory = "nft" + // AppCategorySocial is a AppCategory of type social. + AppCategorySocial AppCategory = "social" +) + +var ErrInvalidAppCategory = fmt.Errorf("not a valid AppCategory, try [%s]", strings.Join(_AppCategoryNames, ", ")) + +var _AppCategoryNames = []string{ + string(AppCategoryUncategorized), + string(AppCategoryFinance), + string(AppCategoryGaming), + string(AppCategoryNft), + string(AppCategorySocial), +} + +// AppCategoryNames returns a list of possible string values of AppCategory. +func AppCategoryNames() []string { + tmp := make([]string, len(_AppCategoryNames)) + copy(tmp, _AppCategoryNames) + return tmp +} + +// AppCategoryValues returns a list of the values for AppCategory +func AppCategoryValues() []AppCategory { + return []AppCategory{ + AppCategoryUncategorized, + AppCategoryFinance, + AppCategoryGaming, + AppCategoryNft, + AppCategorySocial, + } +} + +// String implements the Stringer interface. +func (x AppCategory) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AppCategory) IsValid() bool { + _, err := ParseAppCategory(string(x)) + return err == nil +} + +var _AppCategoryValue = map[string]AppCategory{ + "uncategorized": AppCategoryUncategorized, + "finance": AppCategoryFinance, + "gaming": AppCategoryGaming, + "nft": AppCategoryNft, + "social": AppCategorySocial, +} + +// ParseAppCategory attempts to convert a string to a AppCategory. +func ParseAppCategory(name string) (AppCategory, error) { + if x, ok := _AppCategoryValue[name]; ok { + return x, nil + } + return AppCategory(""), fmt.Errorf("%s is %w", name, ErrInvalidAppCategory) +} + +// MarshalText implements the text marshaller method. +func (x AppCategory) MarshalText() ([]byte, error) { + return []byte(string(x)), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *AppCategory) UnmarshalText(text []byte) error { + tmp, err := ParseAppCategory(string(text)) + if err != nil { + return err + } + *x = tmp + return nil +} + +var errAppCategoryNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AppCategory) Scan(value interface{}) (err error) { + if value == nil { + *x = AppCategory("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAppCategory(v) + case []byte: + *x, err = ParseAppCategory(string(v)) + case AppCategory: + *x = v + case *AppCategory: + if v == nil { + return errAppCategoryNilPtr + } + *x = *v + case *string: + if v == nil { + return errAppCategoryNilPtr + } + *x, err = ParseAppCategory(*v) + default: + return errors.New("invalid type for AppCategory") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AppCategory) Value() (driver.Value, error) { + return x.String(), nil +} + +const ( + // AppTypeSovereign is a AppType of type sovereign. + AppTypeSovereign AppType = "sovereign" + // AppTypeSettled is a AppType of type settled. + AppTypeSettled AppType = "settled" +) + +var ErrInvalidAppType = fmt.Errorf("not a valid AppType, try [%s]", strings.Join(_AppTypeNames, ", ")) + +var _AppTypeNames = []string{ + string(AppTypeSovereign), + string(AppTypeSettled), +} + +// AppTypeNames returns a list of possible string values of AppType. +func AppTypeNames() []string { + tmp := make([]string, len(_AppTypeNames)) + copy(tmp, _AppTypeNames) + return tmp +} + +// AppTypeValues returns a list of the values for AppType +func AppTypeValues() []AppType { + return []AppType{ + AppTypeSovereign, + AppTypeSettled, + } +} + +// String implements the Stringer interface. +func (x AppType) String() string { + return string(x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x AppType) IsValid() bool { + _, err := ParseAppType(string(x)) + return err == nil +} + +var _AppTypeValue = map[string]AppType{ + "sovereign": AppTypeSovereign, + "settled": AppTypeSettled, +} + +// ParseAppType attempts to convert a string to a AppType. +func ParseAppType(name string) (AppType, error) { + if x, ok := _AppTypeValue[name]; ok { + return x, nil + } + return AppType(""), fmt.Errorf("%s is %w", name, ErrInvalidAppType) +} + +// MarshalText implements the text marshaller method. +func (x AppType) MarshalText() ([]byte, error) { + return []byte(string(x)), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *AppType) UnmarshalText(text []byte) error { + tmp, err := ParseAppType(string(text)) + if err != nil { + return err + } + *x = tmp + return nil +} + +var errAppTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *AppType) Scan(value interface{}) (err error) { + if value == nil { + *x = AppType("") + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case string: + *x, err = ParseAppType(v) + case []byte: + *x, err = ParseAppType(string(v)) + case AppType: + *x = v + case *AppType: + if v == nil { + return errAppTypeNilPtr + } + *x = *v + case *string: + if v == nil { + return errAppTypeNilPtr + } + *x, err = ParseAppType(*v) + default: + return errors.New("invalid type for AppType") + } + + return +} + +// Value implements the driver Valuer interface. +func (x AppType) Value() (driver.Value, error) { + return x.String(), nil +} diff --git a/internal/storage/views.go b/internal/storage/views.go index 8074757..4403412 100644 --- a/internal/storage/views.go +++ b/internal/storage/views.go @@ -16,4 +16,5 @@ const ( ViewTransferStatsByHour = "transfer_stats_by_hour" ViewTransferStatsByDay = "transfer_stats_by_day" ViewTransferStatsByMonth = "transfer_stats_by_month" + ViewLeaderboard = "leaderboard" ) diff --git a/pkg/indexer/indexer.go b/pkg/indexer/indexer.go index 41f9740..607e5e6 100644 --- a/pkg/indexer/indexer.go +++ b/pkg/indexer/indexer.go @@ -39,7 +39,7 @@ type Indexer struct { } func New(ctx context.Context, cfg config.Config, stopperModule modules.Module) (Indexer, error) { - pg, err := postgres.Create(ctx, cfg.Database, cfg.Indexer.ScriptsDir) + pg, err := postgres.Create(ctx, cfg.Database, cfg.Indexer.ScriptsDir, true) if err != nil { return Indexer{}, errors.Wrap(err, "while creating pg context") } diff --git a/pkg/indexer/storage/storage.go b/pkg/indexer/storage/storage.go index 4273cee..9ecc51b 100644 --- a/pkg/indexer/storage/storage.go +++ b/pkg/indexer/storage/storage.go @@ -195,6 +195,9 @@ func (module *Module) processBlockInTransaction(ctx context.Context, tx storage. for i := range block.Txs { for j := range block.Txs[i].Actions { block.Txs[i].Actions[j].TxId = block.Txs[i].Id + if block.Txs[i].Actions[j].RollupAction != nil { + block.Txs[i].Actions[j].RollupAction.SenderId = block.Txs[i].SignerId + } actions = append(actions, &block.Txs[i].Actions[j]) } } diff --git a/pkg/indexer/storage/storage_test.go b/pkg/indexer/storage/storage_test.go index 0a1d336..55eb5a3 100644 --- a/pkg/indexer/storage/storage_test.go +++ b/pkg/indexer/storage/storage_test.go @@ -50,7 +50,7 @@ func (s *ModuleTestSuite) SetupSuite() { Password: s.psqlContainer.Config.Password, Host: s.psqlContainer.Config.Host, Port: s.psqlContainer.MappedPort().Int(), - }, "../../../database") + }, "../../../database", false) s.Require().NoError(err) s.storage = strg } diff --git a/test/data/app.yml b/test/data/app.yml new file mode 100644 index 0000000..a7d4cc2 --- /dev/null +++ b/test/data/app.yml @@ -0,0 +1,15 @@ +- id: 1 + name: App 1 + group: Group 1 + description: Very interesting application + website: https://astrotrek.io + github: https://github.com/celenium-io + twitter: https://x.com/EclipseFND + logo: https://celenium.fra1.cdn.digitaloceanspaces.com/rollups/Eclipse.jpg + l2beat: https://l2beat.com/bridges/projects/eclipse + explorer: https://explorer.eclipse.xyz/ + stack: SVM + type: sovereign + category: social + provider: best + vm: SVM \ No newline at end of file diff --git a/test/data/app_bridge.yml b/test/data/app_bridge.yml new file mode 100644 index 0000000..e798b45 --- /dev/null +++ b/test/data/app_bridge.yml @@ -0,0 +1,3 @@ +- app_id: 1 + bridge_id: 1 + native: true \ No newline at end of file diff --git a/test/data/app_id.yaml b/test/data/app_id.yaml new file mode 100644 index 0000000..2d8ca68 --- /dev/null +++ b/test/data/app_id.yaml @@ -0,0 +1,3 @@ +- app_id: 1 + rollup_id: 1 + address_id: 2 \ No newline at end of file diff --git a/test/data/rollup_action.yml b/test/data/rollup_action.yml index f04039e..d789285 100644 --- a/test/data/rollup_action.yml +++ b/test/data/rollup_action.yml @@ -3,3 +3,4 @@ time: '2023-11-30T23:52:23.265Z' height: 7316 tx_id: 1 + sender_id: 1 From 95f8d9b731684ff6d4e9e2c96967363e05215276 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 7 Nov 2024 01:49:40 +0300 Subject: [PATCH 2/5] Add some endpoints --- cmd/api/docs/docs.go | 239 ++++++++++++++++++++++++++ cmd/api/docs/swagger.json | 239 ++++++++++++++++++++++++++ cmd/api/docs/swagger.yaml | 174 +++++++++++++++++++ cmd/api/handler/app.go | 121 +++++++++++++ cmd/api/handler/app_test.go | 157 +++++++++++++++++ cmd/api/handler/responses/app.go | 105 +++++++++++ cmd/api/handler/validators.go | 10 ++ cmd/api/init.go | 10 ++ internal/storage/app.go | 5 +- internal/storage/mock/app.go | 49 +++++- internal/storage/postgres/app.go | 11 +- internal/storage/postgres/app_test.go | 35 +++- test/data/app.yml | 3 +- test/data/app_id.yaml | 2 +- test/data/rollup_action.yml | 1 + 15 files changed, 1142 insertions(+), 19 deletions(-) create mode 100644 cmd/api/handler/app.go create mode 100644 cmd/api/handler/app_test.go create mode 100644 cmd/api/handler/responses/app.go diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 58b0ddc..1996b7a 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -18,6 +18,129 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/app": { + "get": { + "description": "List applications info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "List applications info", + "operationId": "list-applications", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order. Default: desc", + "name": "sort", + "in": "query" + }, + { + "enum": [ + "time", + "actions_count", + "size" + ], + "type": "string", + "description": "Sort field. Default: size", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated application category list", + "name": "category", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.AppWithStats" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, + "/app/{slug}": { + "get": { + "description": "Get application info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application info", + "operationId": "get-application", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.AppWithStats" + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", @@ -2807,6 +2930,122 @@ const docTemplate = `{ } } }, + "responses.AppWithStats": { + "type": "object", + "properties": { + "actions_count": { + "type": "integer", + "format": "integer", + "example": 2 + }, + "actions_count_pct": { + "type": "number", + "format": "float", + "example": 0.9876 + }, + "category": { + "type": "string", + "format": "string", + "example": "nft" + }, + "description": { + "type": "string", + "format": "string", + "example": "Long rollup description" + }, + "explorer": { + "type": "string", + "format": "string", + "example": "https://explorer.karak.network/" + }, + "first_message_time": { + "type": "string", + "format": "date-time", + "example": "2023-07-04T03:10:57+00:00" + }, + "github": { + "type": "string", + "format": "string", + "example": "https://github.com/account" + }, + "id": { + "type": "integer", + "format": "integer", + "example": 321 + }, + "l2_beat": { + "type": "string", + "format": "string", + "example": "https://l2beat.com/scaling/projects/karak" + }, + "last_message_time": { + "type": "string", + "format": "date-time", + "example": "2023-07-04T03:10:57+00:00" + }, + "links": { + "type": "array", + "items": { + "type": "string" + } + }, + "logo": { + "type": "string", + "format": "string", + "example": "https://some_link.com/image.png" + }, + "name": { + "type": "string", + "format": "string", + "example": "Rollup name" + }, + "provider": { + "type": "string", + "format": "string", + "example": "name" + }, + "size": { + "type": "integer", + "format": "integer", + "example": 1000 + }, + "size_pct": { + "type": "number", + "format": "float", + "example": 0.9876 + }, + "slug": { + "type": "string", + "format": "string", + "example": "rollup_slug" + }, + "stack": { + "type": "string", + "format": "string", + "example": "op_stack" + }, + "twitter": { + "type": "string", + "format": "string", + "example": "https://x.com/account" + }, + "type": { + "type": "string", + "format": "string", + "example": "settled" + }, + "vm": { + "type": "string", + "format": "string", + "example": "evm" + }, + "website": { + "type": "string", + "format": "string", + "example": "https://website.com" + } + } + }, "responses.Balance": { "description": "Balance of address information", "type": "object", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 3f494d8..4d4f368 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -8,6 +8,129 @@ }, "host": "api-dusk.astrotrek.io", "paths": { + "/app": { + "get": { + "description": "List applications info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "List applications info", + "operationId": "list-applications", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order. Default: desc", + "name": "sort", + "in": "query" + }, + { + "enum": [ + "time", + "actions_count", + "size" + ], + "type": "string", + "description": "Sort field. Default: size", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated application category list", + "name": "category", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.AppWithStats" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, + "/app/{slug}": { + "get": { + "description": "Get application info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application info", + "operationId": "get-application", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.AppWithStats" + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", @@ -2797,6 +2920,122 @@ } } }, + "responses.AppWithStats": { + "type": "object", + "properties": { + "actions_count": { + "type": "integer", + "format": "integer", + "example": 2 + }, + "actions_count_pct": { + "type": "number", + "format": "float", + "example": 0.9876 + }, + "category": { + "type": "string", + "format": "string", + "example": "nft" + }, + "description": { + "type": "string", + "format": "string", + "example": "Long rollup description" + }, + "explorer": { + "type": "string", + "format": "string", + "example": "https://explorer.karak.network/" + }, + "first_message_time": { + "type": "string", + "format": "date-time", + "example": "2023-07-04T03:10:57+00:00" + }, + "github": { + "type": "string", + "format": "string", + "example": "https://github.com/account" + }, + "id": { + "type": "integer", + "format": "integer", + "example": 321 + }, + "l2_beat": { + "type": "string", + "format": "string", + "example": "https://l2beat.com/scaling/projects/karak" + }, + "last_message_time": { + "type": "string", + "format": "date-time", + "example": "2023-07-04T03:10:57+00:00" + }, + "links": { + "type": "array", + "items": { + "type": "string" + } + }, + "logo": { + "type": "string", + "format": "string", + "example": "https://some_link.com/image.png" + }, + "name": { + "type": "string", + "format": "string", + "example": "Rollup name" + }, + "provider": { + "type": "string", + "format": "string", + "example": "name" + }, + "size": { + "type": "integer", + "format": "integer", + "example": 1000 + }, + "size_pct": { + "type": "number", + "format": "float", + "example": 0.9876 + }, + "slug": { + "type": "string", + "format": "string", + "example": "rollup_slug" + }, + "stack": { + "type": "string", + "format": "string", + "example": "op_stack" + }, + "twitter": { + "type": "string", + "format": "string", + "example": "https://x.com/account" + }, + "type": { + "type": "string", + "format": "string", + "example": "settled" + }, + "vm": { + "type": "string", + "format": "string", + "example": "evm" + }, + "website": { + "type": "string", + "format": "string", + "example": "https://website.com" + } + } + }, "responses.Balance": { "description": "Balance of address information", "type": "object", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index b6a3ef1..6465092 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -76,6 +76,97 @@ definitions: example: 10 type: integer type: object + responses.AppWithStats: + properties: + actions_count: + example: 2 + format: integer + type: integer + actions_count_pct: + example: 0.9876 + format: float + type: number + category: + example: nft + format: string + type: string + description: + example: Long rollup description + format: string + type: string + explorer: + example: https://explorer.karak.network/ + format: string + type: string + first_message_time: + example: "2023-07-04T03:10:57+00:00" + format: date-time + type: string + github: + example: https://github.com/account + format: string + type: string + id: + example: 321 + format: integer + type: integer + l2_beat: + example: https://l2beat.com/scaling/projects/karak + format: string + type: string + last_message_time: + example: "2023-07-04T03:10:57+00:00" + format: date-time + type: string + links: + items: + type: string + type: array + logo: + example: https://some_link.com/image.png + format: string + type: string + name: + example: Rollup name + format: string + type: string + provider: + example: name + format: string + type: string + size: + example: 1000 + format: integer + type: integer + size_pct: + example: 0.9876 + format: float + type: number + slug: + example: rollup_slug + format: string + type: string + stack: + example: op_stack + format: string + type: string + twitter: + example: https://x.com/account + format: string + type: string + type: + example: settled + format: string + type: string + vm: + example: evm + format: string + type: string + website: + example: https://website.com + format: string + type: string + type: object responses.Balance: description: Balance of address information properties: @@ -678,6 +769,89 @@ info: title: Swagger Astria Explorer API version: "1.0" paths: + /app: + get: + description: List applications info + operationId: list-applications + parameters: + - description: Count of requested entities + in: query + maximum: 100 + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + - description: 'Sort order. Default: desc' + enum: + - asc + - desc + in: query + name: sort + type: string + - description: 'Sort field. Default: size' + enum: + - time + - actions_count + - size + in: query + name: sort_by + type: string + - description: Comma-separated application category list + in: query + name: category + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/responses.AppWithStats' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: List applications info + tags: + - applications + /app/{slug}: + get: + description: Get application info + operationId: get-application + parameters: + - description: Slug + in: path + name: slug + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.AppWithStats' + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Get application info + tags: + - applications /v1/address: get: description: List address info diff --git a/cmd/api/handler/app.go b/cmd/api/handler/app.go new file mode 100644 index 0000000..fd67127 --- /dev/null +++ b/cmd/api/handler/app.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "net/http" + + "github.com/celenium-io/astria-indexer/cmd/api/handler/responses" + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/types" + "github.com/labstack/echo/v4" +) + +type AppHandler struct { + apps storage.IApp +} + +func NewAppHandler( + apps storage.IApp, +) *AppHandler { + return &AppHandler{ + apps: apps, + } +} + +type leaderboardRequest struct { + Limit int `query:"limit" validate:"omitempty,min=1,max=100"` + Offset int `query:"offset" validate:"omitempty,min=0"` + Sort string `query:"sort" validate:"omitempty,oneof=asc desc"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=time actions_count size"` + Category StringArray `query:"category" validate:"omitempty,dive,app_category"` +} + +func (p *leaderboardRequest) SetDefault() { + if p.Limit == 0 { + p.Limit = 10 + } + if p.Sort == "" { + p.Sort = desc + } + if p.SortBy == "" { + p.SortBy = "size" + } +} + +// Leaderboard godoc +// +// @Summary List applications info +// @Description List applications info +// @Tags applications +// @ID list-applications +// @Param limit query integer false "Count of requested entities" mininum(1) maximum(100) +// @Param offset query integer false "Offset" mininum(1) +// @Param sort query string false "Sort order. Default: desc" Enums(asc, desc) +// @Param sort_by query string false "Sort field. Default: size" Enums(time, actions_count, size) +// @Param category query string false "Comma-separated application category list" +// @Produce json +// @Success 200 {array} responses.AppWithStats +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /app [get] +func (handler AppHandler) Leaderboard(c echo.Context) error { + req, err := bindAndValidate[leaderboardRequest](c) + if err != nil { + return badRequestError(c, err) + } + req.SetDefault() + + categories := make([]types.AppCategory, len(req.Category)) + for i := range categories { + categories[i] = types.AppCategory(req.Category[i]) + } + + apps, err := handler.apps.Leaderboard(c.Request().Context(), storage.LeaderboardFilters{ + SortField: req.SortBy, + Sort: pgSort(req.Sort), + Limit: req.Limit, + Offset: req.Offset, + Category: categories, + }) + if err != nil { + return handleError(c, err, handler.apps) + } + response := make([]responses.AppWithStats, len(apps)) + for i := range apps { + response[i] = responses.NewAppWithStats(apps[i]) + } + return returnArray(c, response) +} + +type getAppRequest struct { + Slug string `param:"slug" validate:"required"` +} + +// Get godoc +// +// @Summary Get application info +// @Description Get application info +// @Tags applications +// @ID get-application +// @Param slug path string true "Slug" +// @Produce json +// @Success 200 {object} responses.AppWithStats +// @Success 204 +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /app/{slug} [get] +func (handler AppHandler) Get(c echo.Context) error { + req, err := bindAndValidate[getAppRequest](c) + if err != nil { + return badRequestError(c, err) + } + + rollup, err := handler.apps.BySlug(c.Request().Context(), req.Slug) + if err != nil { + return handleError(c, err, handler.apps) + } + + return c.JSON(http.StatusOK, responses.NewAppWithStats(rollup)) +} diff --git a/cmd/api/handler/app_test.go b/cmd/api/handler/app_test.go new file mode 100644 index 0000000..8490a16 --- /dev/null +++ b/cmd/api/handler/app_test.go @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/celenium-io/astria-indexer/cmd/api/handler/responses" + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/mock" + "github.com/celenium-io/astria-indexer/internal/storage/types" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +var ( + testApplication = storage.App{ + Id: 1, + Name: "test app", + Description: "loooooooooooooooooong description", + Website: "https://website.com", + Github: "https://githib.com", + Twitter: "https://x.com", + Logo: "image.png", + Slug: "test-app", + Category: types.AppCategoryNft, + } + testRollupWithStats = storage.AppWithStats{ + App: testApplication, + AppStats: storage.AppStats{ + ActionsCount: 100, + Size: 1000, + LastActionTime: testTime, + FirstActionTime: testTime, + ActionsCountPct: 0.1, + SizePct: 0.3, + }, + } +) + +// AppTestSuite - +type AppTestSuite struct { + suite.Suite + apps *mock.MockIApp + echo *echo.Echo + handler *AppHandler + ctrl *gomock.Controller +} + +// SetupSuite - +func (s *AppTestSuite) SetupSuite() { + s.echo = echo.New() + s.echo.Validator = NewApiValidator() + s.ctrl = gomock.NewController(s.T()) + s.apps = mock.NewMockIApp(s.ctrl) + s.handler = NewAppHandler(s.apps) +} + +// TearDownSuite - +func (s *AppTestSuite) TearDownSuite() { + s.ctrl.Finish() + s.Require().NoError(s.echo.Shutdown(context.Background())) +} + +func TestSuiteApp_Run(t *testing.T) { + suite.Run(t, new(AppTestSuite)) +} + +func (s *AppTestSuite) TestLeaderboard() { + for _, sort := range []string{ + "actions_count", + "time", + "size", + } { + q := make(url.Values) + q.Add("sort_by", sort) + q.Add("category", "nft,gaming") + + req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/app") + + s.apps.EXPECT(). + Leaderboard(gomock.Any(), storage.LeaderboardFilters{ + SortField: sort, + Sort: sdk.SortOrderDesc, + Limit: 10, + Offset: 0, + Category: []types.AppCategory{ + types.AppCategoryNft, + types.AppCategoryGaming, + }, + }). + Return([]storage.AppWithStats{testRollupWithStats}, nil). + Times(1) + + s.Require().NoError(s.handler.Leaderboard(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var rollups []responses.AppWithStats + err := json.NewDecoder(rec.Body).Decode(&rollups) + s.Require().NoError(err) + s.Require().Len(rollups, 1) + + rollup := rollups[0] + s.Require().EqualValues(1, rollup.Id) + s.Require().EqualValues("test app", rollup.Name) + s.Require().EqualValues("image.png", rollup.Logo) + s.Require().EqualValues("test-app", rollup.Slug) + s.Require().EqualValues(100, rollup.ActionsCount) + s.Require().EqualValues(1000, rollup.Size) + s.Require().EqualValues(testTime, rollup.LastAction) + s.Require().EqualValues(testTime, rollup.FirstAction) + s.Require().EqualValues(0.1, rollup.ActionsCountPct) + s.Require().EqualValues(0.3, rollup.SizePct) + } +} + +func (s *AppTestSuite) TestGet() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/app/:slug") + c.SetParamNames("slug") + c.SetParamValues("test-app") + + s.apps.EXPECT(). + BySlug(gomock.Any(), "test-app"). + Return(testRollupWithStats, nil). + Times(1) + + s.Require().NoError(s.handler.Get(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var rollup responses.AppWithStats + err := json.NewDecoder(rec.Body).Decode(&rollup) + s.Require().NoError(err) + s.Require().EqualValues(1, rollup.Id) + s.Require().EqualValues("test app", rollup.Name) + s.Require().EqualValues("image.png", rollup.Logo) + s.Require().EqualValues("test-app", rollup.Slug) + s.Require().EqualValues(100, rollup.ActionsCount) + s.Require().EqualValues(1000, rollup.Size) + s.Require().EqualValues(testTime, rollup.LastAction) + s.Require().EqualValues(testTime, rollup.FirstAction) + s.Require().EqualValues(0.1, rollup.ActionsCountPct) + s.Require().EqualValues(0.3, rollup.SizePct) +} diff --git a/cmd/api/handler/responses/app.go b/cmd/api/handler/responses/app.go new file mode 100644 index 0000000..34951b1 --- /dev/null +++ b/cmd/api/handler/responses/app.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package responses + +import ( + "time" + + "github.com/celenium-io/astria-indexer/internal/storage" +) + +type AppWithStats struct { + Id uint64 `example:"321" format:"integer" json:"id" swaggertype:"integer"` + Name string `example:"Rollup name" format:"string" json:"name" swaggertype:"string"` + Description string `example:"Long rollup description" format:"string" json:"description,omitempty" swaggertype:"string"` + Website string `example:"https://website.com" format:"string" json:"website,omitempty" swaggertype:"string"` + Twitter string `example:"https://x.com/account" format:"string" json:"twitter,omitempty" swaggertype:"string"` + Github string `example:"https://github.com/account" format:"string" json:"github,omitempty" swaggertype:"string"` + Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` + Slug string `example:"rollup_slug" format:"string" json:"slug" swaggertype:"string"` + L2Beat string `example:"https://l2beat.com/scaling/projects/karak" format:"string" json:"l2_beat,omitempty" swaggertype:"string"` + Explorer string `example:"https://explorer.karak.network/" format:"string" json:"explorer,omitempty" swaggertype:"string"` + Stack string `example:"op_stack" format:"string" json:"stack,omitempty" swaggertype:"string"` + Type string `example:"settled" format:"string" json:"type,omitempty" swaggertype:"string"` + Category string `example:"nft" format:"string" json:"category,omitempty" swaggertype:"string"` + VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` + Provider string `example:"name" format:"string" json:"provider,omitempty" swaggertype:"string"` + + ActionsCount int64 `example:"2" format:"integer" json:"actions_count" swaggertype:"integer"` + Size int64 `example:"1000" format:"integer" json:"size" swaggertype:"integer"` + LastAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"last_message_time" swaggertype:"string"` + FirstAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"first_message_time" swaggertype:"string"` + SizePct float64 `example:"0.9876" format:"float" json:"size_pct" swaggertype:"number"` + ActionsCountPct float64 `example:"0.9876" format:"float" json:"actions_count_pct" swaggertype:"number"` + + Links []string `json:"links,omitempty"` +} + +func NewAppWithStats(r storage.AppWithStats) AppWithStats { + return AppWithStats{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Github: r.Github, + Twitter: r.Twitter, + Website: r.Website, + Logo: r.Logo, + L2Beat: r.L2Beat, + Explorer: r.Explorer, + Links: r.Links, + Stack: r.Stack, + Slug: r.Slug, + ActionsCount: r.ActionsCount, + Size: r.Size, + SizePct: r.SizePct, + ActionsCountPct: r.ActionsCountPct, + LastAction: r.LastActionTime, + FirstAction: r.FirstActionTime, + Category: r.Category.String(), + Type: r.Type.String(), + Provider: r.Provider, + VM: r.VM, + } +} + +// type App struct { +// Id uint64 `example:"321" format:"integer" json:"id" swaggertype:"integer"` +// Name string `example:"Rollup name" format:"string" json:"name" swaggertype:"string"` +// Description string `example:"Long rollup description" format:"string" json:"description,omitempty" swaggertype:"string"` +// Website string `example:"https://website.com" format:"string" json:"website,omitempty" swaggertype:"string"` +// Twitter string `example:"https://x.com/account" format:"string" json:"twitter,omitempty" swaggertype:"string"` +// Github string `example:"https://github.com/account" format:"string" json:"github,omitempty" swaggertype:"string"` +// Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` +// Slug string `example:"rollup_slug" format:"string" json:"slug" swaggertype:"string"` +// L2Beat string `example:"https://github.com/account" format:"string" json:"l2_beat,omitempty" swaggertype:"string"` +// Explorer string `example:"https://explorer.karak.network/" format:"string" json:"explorer,omitempty" swaggertype:"string"` +// Stack string `example:"op_stack" format:"string" json:"stack,omitempty" swaggertype:"string"` +// Type string `example:"settled" format:"string" json:"type,omitempty" swaggertype:"string"` +// Category string `example:"nft" format:"string" json:"category,omitempty" swaggertype:"string"` +// Provider string `example:"name" format:"string" json:"provider,omitempty" swaggertype:"string"` +// VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` + +// Links []string `json:"links,omitempty"` +// } + +// func NewApp(r *storage.App) App { +// return App{ +// Id: r.Id, +// Name: r.Name, +// Description: r.Description, +// Github: r.Github, +// Twitter: r.Twitter, +// Website: r.Website, +// Logo: r.Logo, +// Slug: r.Slug, +// L2Beat: r.L2Beat, +// Stack: r.Stack, +// Explorer: r.Explorer, +// Links: r.Links, +// Category: r.Category.String(), +// Type: r.Type.String(), +// Provider: r.Provider, +// VM: r.VM, +// } +// } diff --git a/cmd/api/handler/validators.go b/cmd/api/handler/validators.go index ef979df..90f102b 100644 --- a/cmd/api/handler/validators.go +++ b/cmd/api/handler/validators.go @@ -27,6 +27,9 @@ func NewApiValidator() *ApiValidator { if err := v.RegisterValidation("action_type", actionTypeValidator()); err != nil { panic(err) } + if err := v.RegisterValidation("app_category", categoryValidator()); err != nil { + panic(err) + } return &ApiValidator{validator: v} } @@ -60,3 +63,10 @@ func actionTypeValidator() validator.Func { return err == nil } } + +func categoryValidator() validator.Func { + return func(fl validator.FieldLevel) bool { + _, err := types.ParseAppCategory(fl.Field().String()) + return err == nil + } +} diff --git a/cmd/api/init.go b/cmd/api/init.go index ae55cd8..4c4e022 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -372,6 +372,16 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto } } + appHandler := handler.NewAppHandler(db.App) + apps := v1.Group("/app") + { + apps.GET("", appHandler.Leaderboard) + app := apps.Group("/:slug") + { + app.GET("", appHandler.Get) + } + } + if cfg.ApiConfig.Prometheus { e.GET("/metrics", echoprometheus.NewHandler()) } diff --git a/internal/storage/app.go b/internal/storage/app.go index 559e49c..43f7a64 100644 --- a/internal/storage/app.go +++ b/internal/storage/app.go @@ -24,7 +24,8 @@ type LeaderboardFilters struct { type IApp interface { storage.Table[*App] - Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]RollupWithStats, error) + Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]AppWithStats, error) + BySlug(ctx context.Context, slug string) (AppWithStats, error) } type App struct { @@ -75,7 +76,7 @@ func (app App) IsEmpty() bool { app.Category == "" } -type RollupWithStats struct { +type AppWithStats struct { App AppStats } diff --git a/internal/storage/mock/app.go b/internal/storage/mock/app.go index 1e10278..d441cdf 100644 --- a/internal/storage/mock/app.go +++ b/internal/storage/mock/app.go @@ -44,6 +44,45 @@ func (m *MockIApp) EXPECT() *MockIAppMockRecorder { return m.recorder } +// BySlug mocks base method. +func (m *MockIApp) BySlug(ctx context.Context, slug string) (storage.AppWithStats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BySlug", ctx, slug) + ret0, _ := ret[0].(storage.AppWithStats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BySlug indicates an expected call of BySlug. +func (mr *MockIAppMockRecorder) BySlug(ctx, slug any) *MockIAppBySlugCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BySlug", reflect.TypeOf((*MockIApp)(nil).BySlug), ctx, slug) + return &MockIAppBySlugCall{Call: call} +} + +// MockIAppBySlugCall wrap *gomock.Call +type MockIAppBySlugCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppBySlugCall) Return(arg0 storage.AppWithStats, arg1 error) *MockIAppBySlugCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppBySlugCall) Do(f func(context.Context, string) (storage.AppWithStats, error)) *MockIAppBySlugCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppBySlugCall) DoAndReturn(f func(context.Context, string) (storage.AppWithStats, error)) *MockIAppBySlugCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // CursorList mocks base method. func (m *MockIApp) CursorList(ctx context.Context, id, limit uint64, order storage0.SortOrder, cmp storage0.Comparator) ([]*storage.App, error) { m.ctrl.T.Helper() @@ -200,10 +239,10 @@ func (c *MockIAppLastIDCall) DoAndReturn(f func(context.Context) (uint64, error) } // Leaderboard mocks base method. -func (m *MockIApp) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) ([]storage.RollupWithStats, error) { +func (m *MockIApp) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) ([]storage.AppWithStats, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Leaderboard", ctx, fltrs) - ret0, _ := ret[0].([]storage.RollupWithStats) + ret0, _ := ret[0].([]storage.AppWithStats) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -221,19 +260,19 @@ type MockIAppLeaderboardCall struct { } // Return rewrite *gomock.Call.Return -func (c *MockIAppLeaderboardCall) Return(arg0 []storage.RollupWithStats, arg1 error) *MockIAppLeaderboardCall { +func (c *MockIAppLeaderboardCall) Return(arg0 []storage.AppWithStats, arg1 error) *MockIAppLeaderboardCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockIAppLeaderboardCall) Do(f func(context.Context, storage.LeaderboardFilters) ([]storage.RollupWithStats, error)) *MockIAppLeaderboardCall { +func (c *MockIAppLeaderboardCall) Do(f func(context.Context, storage.LeaderboardFilters) ([]storage.AppWithStats, error)) *MockIAppLeaderboardCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockIAppLeaderboardCall) DoAndReturn(f func(context.Context, storage.LeaderboardFilters) ([]storage.RollupWithStats, error)) *MockIAppLeaderboardCall { +func (c *MockIAppLeaderboardCall) DoAndReturn(f func(context.Context, storage.LeaderboardFilters) ([]storage.AppWithStats, error)) *MockIAppLeaderboardCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/storage/postgres/app.go b/internal/storage/postgres/app.go index 19375b4..2c6bc6a 100644 --- a/internal/storage/postgres/app.go +++ b/internal/storage/postgres/app.go @@ -25,7 +25,7 @@ func NewApp(db *database.Bun) *App { } } -func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) (rollups []storage.RollupWithStats, err error) { +func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilters) (rollups []storage.AppWithStats, err error) { switch fltrs.SortField { case columnTime: fltrs.SortField = "last_time" @@ -50,3 +50,12 @@ func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilter err = query.Scan(ctx, &rollups) return } + +func (app *App) BySlug(ctx context.Context, slug string) (result storage.AppWithStats, err error) { + err = app.DB().NewSelect(). + Table(storage.ViewLeaderboard). + Where("slug = ?", slug). + Limit(1). + Scan(ctx, &result) + return +} diff --git a/internal/storage/postgres/app_test.go b/internal/storage/postgres/app_test.go index 99ba3db..0f1c182 100644 --- a/internal/storage/postgres/app_test.go +++ b/internal/storage/postgres/app_test.go @@ -35,11 +35,11 @@ func (s *StorageTestSuite) TestLeaderboard() { app := apps[0] s.Require().EqualValues("App 1", app.Name, column) s.Require().EqualValues(34, app.Size, column) - s.Require().EqualValues(3, app.ActionsCount, column) + s.Require().EqualValues(1, app.ActionsCount, column) s.Require().False(app.LastActionTime.IsZero()) s.Require().False(app.FirstActionTime.IsZero()) - s.Require().EqualValues(0.42857142857142855, app.ActionsCountPct) - s.Require().EqualValues(0.3953488372093023, app.SizePct) + s.Require().EqualValues(1, app.ActionsCountPct) + s.Require().EqualValues(1, app.SizePct) } } @@ -65,14 +65,31 @@ func (s *StorageTestSuite) TestLeaderboardWithCategory() { s.Require().Len(apps, 1, column) app := apps[0] - s.Require().EqualValues("Rollup 3", app.Name, column) - s.Require().EqualValues("The third", app.Description, column) + s.Require().EqualValues("App 1", app.Name, column) s.Require().EqualValues(34, app.Size, column) - s.Require().EqualValues(3, app.ActionsCount, column) + s.Require().EqualValues(1, app.ActionsCount, column) s.Require().False(app.LastActionTime.IsZero()) s.Require().False(app.FirstActionTime.IsZero()) - s.Require().EqualValues(0.42857142857142855, app.ActionsCountPct) - s.Require().EqualValues(0.3953488372093023, app.SizePct) - s.Require().EqualValues("nft", app.Category) + s.Require().EqualValues(1, app.ActionsCountPct) + s.Require().EqualValues(1, app.SizePct) } } + +func (s *StorageTestSuite) TestAppBySlug() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + _, err := s.storage.Connection().Exec(ctx, "REFRESH MATERIALIZED VIEW leaderboard;") + s.Require().NoError(err) + + app, err := s.storage.App.BySlug(ctx, "app-1") + s.Require().NoError(err) + + s.Require().EqualValues("App 1", app.Name) + s.Require().EqualValues(34, app.Size) + s.Require().EqualValues(1, app.ActionsCount) + s.Require().False(app.LastActionTime.IsZero()) + s.Require().False(app.FirstActionTime.IsZero()) + s.Require().EqualValues(1, app.ActionsCountPct) + s.Require().EqualValues(1, app.SizePct) +} diff --git a/test/data/app.yml b/test/data/app.yml index a7d4cc2..ccfc615 100644 --- a/test/data/app.yml +++ b/test/data/app.yml @@ -12,4 +12,5 @@ type: sovereign category: social provider: best - vm: SVM \ No newline at end of file + vm: SVM + slug: app-1 \ No newline at end of file diff --git a/test/data/app_id.yaml b/test/data/app_id.yaml index 2d8ca68..535e540 100644 --- a/test/data/app_id.yaml +++ b/test/data/app_id.yaml @@ -1,3 +1,3 @@ - app_id: 1 rollup_id: 1 - address_id: 2 \ No newline at end of file + address_id: 1 \ No newline at end of file diff --git a/test/data/rollup_action.yml b/test/data/rollup_action.yml index d789285..382de15 100644 --- a/test/data/rollup_action.yml +++ b/test/data/rollup_action.yml @@ -4,3 +4,4 @@ height: 7316 tx_id: 1 sender_id: 1 + size: 34 From 30c56885f32893054622ed591c60550c013469ba Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 7 Nov 2024 16:07:49 +0300 Subject: [PATCH 3/5] Add actions endpoint --- cmd/api/docs/docs.go | 70 +++++++++++++++++++++++++++ cmd/api/docs/swagger.json | 70 +++++++++++++++++++++++++++ cmd/api/docs/swagger.yaml | 48 ++++++++++++++++++ cmd/api/handler/app.go | 50 +++++++++++++++++++ cmd/api/handler/responses/action.go | 20 ++++++++ cmd/api/init.go | 1 + internal/storage/app.go | 1 + internal/storage/mock/app.go | 39 +++++++++++++++ internal/storage/postgres/app.go | 41 ++++++++++++++++ internal/storage/postgres/app_test.go | 22 +++++++++ test/data/rollup_action.yml | 1 + 11 files changed, 363 insertions(+) diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 1996b7a..544020f 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -141,6 +141,76 @@ const docTemplate = `{ } } }, + "/app/{slug}/actions": { + "get": { + "description": "Get application info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application info", + "operationId": "get-application-actions", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.Action" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 4d4f368..5c47749 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -131,6 +131,76 @@ } } }, + "/app/{slug}/actions": { + "get": { + "description": "Get application info", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application info", + "operationId": "get-application-actions", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.Action" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 6465092..708a181 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -852,6 +852,54 @@ paths: summary: Get application info tags: - applications + /app/{slug}/actions: + get: + description: Get application info + operationId: get-application-actions + parameters: + - description: Slug + in: path + name: slug + required: true + type: string + - description: Count of requested entities + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - description: Offset + in: query + minimum: 1 + name: offset + type: integer + - description: Sort order + enum: + - asc + - desc + in: query + name: sort + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/responses.Action' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Get application info + tags: + - applications /v1/address: get: description: List address info diff --git a/cmd/api/handler/app.go b/cmd/api/handler/app.go index fd67127..d07b591 100644 --- a/cmd/api/handler/app.go +++ b/cmd/api/handler/app.go @@ -119,3 +119,53 @@ func (handler AppHandler) Get(c echo.Context) error { return c.JSON(http.StatusOK, responses.NewAppWithStats(rollup)) } + +type getAppActionsRequest struct { + Slug string `param:"slug" validate:"required"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100"` + Offset int `query:"offset" validate:"omitempty,min=0"` + Sort string `query:"sort" validate:"omitempty,oneof=asc desc"` +} + +func (p *getAppActionsRequest) SetDefault() { + if p.Limit == 0 { + p.Limit = 10 + } + if p.Sort == "" { + p.Sort = asc + } +} + +// Get godoc +// +// @Summary Get application info +// @Description Get application info +// @Tags applications +// @ID get-application-actions +// @Param slug path string true "Slug" +// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) +// @Param offset query integer false "Offset" minimum(1) +// @Param sort query string false "Sort order" Enums(asc, desc) +// @Produce json +// @Success 200 {array} responses.Action +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /app/{slug}/actions [get] +func (handler AppHandler) Actions(c echo.Context) error { + req, err := bindAndValidate[getAppActionsRequest](c) + if err != nil { + return badRequestError(c, err) + } + req.SetDefault() + + actions, err := handler.apps.Actions(c.Request().Context(), req.Slug, req.Limit, req.Offset, pgSort(req.Sort)) + if err != nil { + return handleError(c, err, handler.apps) + } + + result := make([]responses.Action, len(actions)) + for i := range actions { + result[i] = responses.NewActionFromRollupAction(actions[i]) + } + return returnArray(c, result) +} diff --git a/cmd/api/handler/responses/action.go b/cmd/api/handler/responses/action.go index a37f82a..119f45a 100644 --- a/cmd/api/handler/responses/action.go +++ b/cmd/api/handler/responses/action.go @@ -90,3 +90,23 @@ func NewAddressAction(action storage.AddressAction) Action { return result } + +func NewActionFromRollupAction(action storage.RollupAction) Action { + result := Action{ + Id: action.ActionId, + Height: action.Height, + Time: action.Time, + Type: action.ActionType, + } + + if action.Tx != nil { + result.TxHash = hex.EncodeToString(action.Tx.Hash) + } + if action.Action != nil { + result.Data = action.Action.Data + result.Position = action.Action.Position + result.Fee = NewFee(action.Action.Fee) + } + + return result +} diff --git a/cmd/api/init.go b/cmd/api/init.go index 4c4e022..8184221 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -379,6 +379,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto app := apps.Group("/:slug") { app.GET("", appHandler.Get) + app.GET("/actions", appHandler.Actions) } } diff --git a/internal/storage/app.go b/internal/storage/app.go index 43f7a64..2a8eea0 100644 --- a/internal/storage/app.go +++ b/internal/storage/app.go @@ -26,6 +26,7 @@ type IApp interface { Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]AppWithStats, error) BySlug(ctx context.Context, slug string) (AppWithStats, error) + Actions(ctx context.Context, slug string, limit, offset int, sort storage.SortOrder) ([]RollupAction, error) } type App struct { diff --git a/internal/storage/mock/app.go b/internal/storage/mock/app.go index d441cdf..ecbd2a8 100644 --- a/internal/storage/mock/app.go +++ b/internal/storage/mock/app.go @@ -44,6 +44,45 @@ func (m *MockIApp) EXPECT() *MockIAppMockRecorder { return m.recorder } +// Actions mocks base method. +func (m *MockIApp) Actions(ctx context.Context, slug string, limit, offset int, sort storage0.SortOrder) ([]storage.RollupAction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Actions", ctx, slug, limit, offset, sort) + ret0, _ := ret[0].([]storage.RollupAction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Actions indicates an expected call of Actions. +func (mr *MockIAppMockRecorder) Actions(ctx, slug, limit, offset, sort any) *MockIAppActionsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Actions", reflect.TypeOf((*MockIApp)(nil).Actions), ctx, slug, limit, offset, sort) + return &MockIAppActionsCall{Call: call} +} + +// MockIAppActionsCall wrap *gomock.Call +type MockIAppActionsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppActionsCall) Return(arg0 []storage.RollupAction, arg1 error) *MockIAppActionsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppActionsCall) Do(f func(context.Context, string, int, int, storage0.SortOrder) ([]storage.RollupAction, error)) *MockIAppActionsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppActionsCall) DoAndReturn(f func(context.Context, string, int, int, storage0.SortOrder) ([]storage.RollupAction, error)) *MockIAppActionsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // BySlug mocks base method. func (m *MockIApp) BySlug(ctx context.Context, slug string) (storage.AppWithStats, error) { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/app.go b/internal/storage/postgres/app.go index 2c6bc6a..83c4ca2 100644 --- a/internal/storage/postgres/app.go +++ b/internal/storage/postgres/app.go @@ -8,6 +8,7 @@ import ( "github.com/celenium-io/astria-indexer/internal/storage" "github.com/dipdup-net/go-lib/database" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -59,3 +60,43 @@ func (app *App) BySlug(ctx context.Context, slug string) (result storage.AppWith Scan(ctx, &result) return } + +func (app *App) Actions(ctx context.Context, slug string, limit, offset int, sort sdk.SortOrder) (result []storage.RollupAction, err error) { + var id uint64 + if err = app.DB().NewSelect(). + Column("id"). + Model((*storage.App)(nil)). + Where("slug = ?", slug). + Limit(1). + Scan(ctx, &id); err != nil { + return + } + + var appIds []storage.AppId + if err = app.DB().NewSelect(). + Model(&appIds). + Where("app_id = ?", id). + Scan(ctx); err != nil { + return + } + + subQuery := app.DB().NewSelect(). + Model((*storage.RollupAction)(nil)) + + subQuery = limitScope(subQuery, limit) + subQuery = offsetScope(subQuery, offset) + subQuery = sortScope(subQuery, "action_id", sort) + + err = app.DB().NewSelect(). + TableExpr("(?) as rollup_action", subQuery). + ColumnExpr("rollup_action.*"). + ColumnExpr("action.data as action__data, action.position as action__position"). + ColumnExpr("tx.hash as tx__hash"). + ColumnExpr("fee.asset as action__fee__asset, fee.amount as action__fee__amount"). + Join("left join fee on fee.action_id = rollup_action.action_id"). + Join("left join action on action.id = rollup_action.action_id"). + Join("left join tx on tx.id = rollup_action.tx_id"). + Scan(ctx, &result) + + return +} diff --git a/internal/storage/postgres/app_test.go b/internal/storage/postgres/app_test.go index 0f1c182..ff0e826 100644 --- a/internal/storage/postgres/app_test.go +++ b/internal/storage/postgres/app_test.go @@ -5,6 +5,7 @@ package postgres import ( "context" + "encoding/hex" "time" "github.com/celenium-io/astria-indexer/internal/storage" @@ -93,3 +94,24 @@ func (s *StorageTestSuite) TestAppBySlug() { s.Require().EqualValues(1, app.ActionsCountPct) s.Require().EqualValues(1, app.SizePct) } + +func (s *StorageTestSuite) TestAppActions() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + actions, err := s.storage.App.Actions(ctx, "app-1", 10, 0, sdk.SortOrderAsc) + s.Require().NoError(err) + s.Require().Len(actions, 1) + + action := actions[0] + s.Require().EqualValues(1, action.RollupId) + s.Require().EqualValues(1, action.ActionId) + s.Require().EqualValues(1, action.TxId) + s.Require().EqualValues(1, action.SenderId) + s.Require().EqualValues(34, action.Size) + s.Require().EqualValues(7316, action.Height) + s.Require().EqualValues("20b0e6310801e7b2a16c69aace7b1a1d550e5c49c80f546941bb1ac747487fe5", hex.EncodeToString(action.Tx.Hash)) + s.Require().EqualValues(types.ActionTypeRollupDataSubmission, action.ActionType) + s.Require().NotNil(action.Action.Data) + s.Require().NotNil(action.Action.Fee) +} diff --git a/test/data/rollup_action.yml b/test/data/rollup_action.yml index 382de15..8e300d4 100644 --- a/test/data/rollup_action.yml +++ b/test/data/rollup_action.yml @@ -5,3 +5,4 @@ tx_id: 1 sender_id: 1 size: 34 + action_type: rollup_data_submission From 5666f2c7ce56aaeb1da96e27a38a409fc17f94db Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 7 Nov 2024 17:32:26 +0300 Subject: [PATCH 4/5] Add series endpoint and tests --- cmd/api/docs/docs.go | 85 ++++++++++++++++++++++++++- cmd/api/docs/swagger.json | 85 ++++++++++++++++++++++++++- cmd/api/docs/swagger.yaml | 60 ++++++++++++++++++- cmd/api/handler/app.go | 54 ++++++++++++++++- cmd/api/handler/app_test.go | 66 +++++++++++++++++++++ cmd/api/handler/block_test.go | 9 ++- cmd/api/init.go | 1 + internal/storage/app.go | 1 + internal/storage/mock/app.go | 39 ++++++++++++ internal/storage/postgres/app.go | 83 ++++++++++++++++++++++++++ internal/storage/postgres/app_test.go | 20 +++++++ 11 files changed, 491 insertions(+), 12 deletions(-) diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 544020f..c317300 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -143,14 +143,14 @@ const docTemplate = `{ }, "/app/{slug}/actions": { "get": { - "description": "Get application info", + "description": "Get application actions", "produces": [ "application/json" ], "tags": [ "applications" ], - "summary": "Get application info", + "summary": "Get application actions", "operationId": "get-application-actions", "parameters": [ { @@ -211,6 +211,87 @@ const docTemplate = `{ } } }, + "/app/{slug}/series/{name}/{timeframe}": { + "get": { + "description": "Get application series", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application series", + "operationId": "get-application-series", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + }, + { + "enum": [ + "actions_count", + "size", + "size_per_action" + ], + "type": "string", + "description": "Series name", + "name": "name", + "in": "path", + "required": true + }, + { + "enum": [ + "hour", + "day", + "month" + ], + "type": "string", + "description": "Timeframe", + "name": "timeframe", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Time from in unix timestamp", + "name": "from", + "in": "query" + }, + { + "type": "integer", + "description": "Time to in unix timestamp", + "name": "to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.SeriesItem" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 5c47749..759137d 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -133,14 +133,14 @@ }, "/app/{slug}/actions": { "get": { - "description": "Get application info", + "description": "Get application actions", "produces": [ "application/json" ], "tags": [ "applications" ], - "summary": "Get application info", + "summary": "Get application actions", "operationId": "get-application-actions", "parameters": [ { @@ -201,6 +201,87 @@ } } }, + "/app/{slug}/series/{name}/{timeframe}": { + "get": { + "description": "Get application series", + "produces": [ + "application/json" + ], + "tags": [ + "applications" + ], + "summary": "Get application series", + "operationId": "get-application-series", + "parameters": [ + { + "type": "string", + "description": "Slug", + "name": "slug", + "in": "path", + "required": true + }, + { + "enum": [ + "actions_count", + "size", + "size_per_action" + ], + "type": "string", + "description": "Series name", + "name": "name", + "in": "path", + "required": true + }, + { + "enum": [ + "hour", + "day", + "month" + ], + "type": "string", + "description": "Timeframe", + "name": "timeframe", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Time from in unix timestamp", + "name": "from", + "in": "query" + }, + { + "type": "integer", + "description": "Time to in unix timestamp", + "name": "to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.SeriesItem" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 708a181..0ed1057 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -854,7 +854,7 @@ paths: - applications /app/{slug}/actions: get: - description: Get application info + description: Get application actions operationId: get-application-actions parameters: - description: Slug @@ -897,7 +897,63 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/handler.Error' - summary: Get application info + summary: Get application actions + tags: + - applications + /app/{slug}/series/{name}/{timeframe}: + get: + description: Get application series + operationId: get-application-series + parameters: + - description: Slug + in: path + name: slug + required: true + type: string + - description: Series name + enum: + - actions_count + - size + - size_per_action + in: path + name: name + required: true + type: string + - description: Timeframe + enum: + - hour + - day + - month + in: path + name: timeframe + required: true + type: string + - description: Time from in unix timestamp + in: query + name: from + type: integer + - description: Time to in unix timestamp + in: query + name: to + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/responses.SeriesItem' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Get application series tags: - applications /v1/address: diff --git a/cmd/api/handler/app.go b/cmd/api/handler/app.go index d07b591..b298811 100644 --- a/cmd/api/handler/app.go +++ b/cmd/api/handler/app.go @@ -136,10 +136,10 @@ func (p *getAppActionsRequest) SetDefault() { } } -// Get godoc +// Actions godoc // -// @Summary Get application info -// @Description Get application info +// @Summary Get application actions +// @Description Get application actions // @Tags applications // @ID get-application-actions // @Param slug path string true "Slug" @@ -169,3 +169,51 @@ func (handler AppHandler) Actions(c echo.Context) error { } return returnArray(c, result) } + +type appStatsRequest struct { + Slug string `example:"app" param:"slug" swaggertype:"string" validate:"required"` + Timeframe storage.Timeframe `example:"hour" param:"timeframe" swaggertype:"string" validate:"required,oneof=hour day month"` + SeriesName string `example:"size" param:"name" swaggertype:"string" validate:"required,oneof=actions_count size size_per_action"` + From int64 `example:"1692892095" query:"from" swaggertype:"integer" validate:"omitempty,min=1"` + To int64 `example:"1692892095" query:"to" swaggertype:"integer" validate:"omitempty,min=1"` +} + +// Series godoc +// +// @Summary Get application series +// @Description Get application series +// @Tags applications +// @ID get-application-series +// @Param slug path string true "Slug" +// @Param name path string true "Series name" Enums(actions_count, size, size_per_action) +// @Param timeframe path string true "Timeframe" Enums(hour, day, month) +// @Param from query integer false "Time from in unix timestamp" mininum(1) +// @Param to query integer false "Time to in unix timestamp" mininum(1) +// @Produce json +// @Success 200 {array} responses.SeriesItem +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /app/{slug}/series/{name}/{timeframe} [get] +func (handler AppHandler) Series(c echo.Context) error { + req, err := bindAndValidate[appStatsRequest](c) + if err != nil { + return badRequestError(c, err) + } + + histogram, err := handler.apps.Series( + c.Request().Context(), + req.Slug, + req.Timeframe, + req.SeriesName, + storage.NewSeriesRequest(req.From, req.To), + ) + if err != nil { + return handleError(c, err, handler.apps) + } + + response := make([]responses.SeriesItem, len(histogram)) + for i := range histogram { + response[i] = responses.NewSeriesItem(histogram[i]) + } + return returnArray(c, response) +} diff --git a/cmd/api/handler/app_test.go b/cmd/api/handler/app_test.go index 8490a16..c5bdb9f 100644 --- a/cmd/api/handler/app_test.go +++ b/cmd/api/handler/app_test.go @@ -155,3 +155,69 @@ func (s *AppTestSuite) TestGet() { s.Require().EqualValues(0.1, rollup.ActionsCountPct) s.Require().EqualValues(0.3, rollup.SizePct) } + +func (s *AppTestSuite) TestActions() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/app/:slug/actions") + c.SetParamNames("slug") + c.SetParamValues("test-app") + + s.apps.EXPECT(). + Actions(gomock.Any(), "test-app", 10, 0, sdk.SortOrderAsc). + Return([]storage.RollupAction{ + testRollupAction, + }, nil). + Times(1) + + s.Require().NoError(s.handler.Actions(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var actions []responses.Action + err := json.NewDecoder(rec.Body).Decode(&actions) + s.Require().NoError(err) + s.Require().Len(actions, 1) + s.Require().EqualValues(1, actions[0].Id) + s.Require().EqualValues(100, actions[0].Height) + s.Require().EqualValues(1, actions[0].Position) + s.Require().Equal(testTime, actions[0].Time) + s.Require().EqualValues(string(types.ActionTypeRollupDataSubmission), actions[0].Type) +} + +func (s *AppTestSuite) TestSeries() { + for _, name := range []string{ + "size", "actions_count", "size_per_action", + } { + for _, tf := range []storage.Timeframe{ + "hour", "day", "month", + } { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/app/:slug/stats/:name/:timeframe") + c.SetParamNames("slug", "name", "timeframe") + c.SetParamValues("test-app", name, string(tf)) + + s.apps.EXPECT(). + Series(gomock.Any(), "test-app", tf, name, storage.NewSeriesRequest(0, 0)). + Return([]storage.SeriesItem{ + { + Value: "10000", + Time: testTime, + }, + }, nil). + Times(1) + + s.Require().NoError(s.handler.Series(c)) + s.Require().Equal(http.StatusOK, rec.Code, name, tf) + + var items []responses.SeriesItem + err := json.NewDecoder(rec.Body).Decode(&items) + s.Require().NoError(err, name, tf) + s.Require().Len(items, 1, name, tf) + s.Require().EqualValues("10000", items[0].Value, name, tf) + s.Require().EqualValues(testTime.UTC(), items[0].Time.UTC(), name, tf) + } + } +} diff --git a/cmd/api/handler/block_test.go b/cmd/api/handler/block_test.go index 8308220..5a5ab85 100644 --- a/cmd/api/handler/block_test.go +++ b/cmd/api/handler/block_test.go @@ -101,9 +101,12 @@ var ( "data": testsuite.MustHexDecode("deadbeaf"), }, }, - Rollup: &testRollup, - RollupId: testRollup.Id, - ActionId: 1, + Rollup: &testRollup, + RollupId: testRollup.Id, + ActionId: 1, + ActionType: types.ActionTypeRollupDataSubmission, + Height: 100, + Time: testTime, } testTx = storage.Tx{ diff --git a/cmd/api/init.go b/cmd/api/init.go index 8184221..9fca7fa 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -380,6 +380,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto { app.GET("", appHandler.Get) app.GET("/actions", appHandler.Actions) + app.GET("/series/:name/:timeframe", appHandler.Series) } } diff --git a/internal/storage/app.go b/internal/storage/app.go index 2a8eea0..51fa1f1 100644 --- a/internal/storage/app.go +++ b/internal/storage/app.go @@ -27,6 +27,7 @@ type IApp interface { Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]AppWithStats, error) BySlug(ctx context.Context, slug string) (AppWithStats, error) Actions(ctx context.Context, slug string, limit, offset int, sort storage.SortOrder) ([]RollupAction, error) + Series(ctx context.Context, slug string, timeframe Timeframe, column string, req SeriesRequest) (items []SeriesItem, err error) } type App struct { diff --git a/internal/storage/mock/app.go b/internal/storage/mock/app.go index ecbd2a8..b2e1392 100644 --- a/internal/storage/mock/app.go +++ b/internal/storage/mock/app.go @@ -393,6 +393,45 @@ func (c *MockIAppSaveCall) DoAndReturn(f func(context.Context, *storage.App) err return c } +// Series mocks base method. +func (m *MockIApp) Series(ctx context.Context, slug string, timeframe storage.Timeframe, column string, req storage.SeriesRequest) ([]storage.SeriesItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Series", ctx, slug, timeframe, column, req) + ret0, _ := ret[0].([]storage.SeriesItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Series indicates an expected call of Series. +func (mr *MockIAppMockRecorder) Series(ctx, slug, timeframe, column, req any) *MockIAppSeriesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Series", reflect.TypeOf((*MockIApp)(nil).Series), ctx, slug, timeframe, column, req) + return &MockIAppSeriesCall{Call: call} +} + +// MockIAppSeriesCall wrap *gomock.Call +type MockIAppSeriesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAppSeriesCall) Return(items []storage.SeriesItem, err error) *MockIAppSeriesCall { + c.Call = c.Call.Return(items, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAppSeriesCall) Do(f func(context.Context, string, storage.Timeframe, string, storage.SeriesRequest) ([]storage.SeriesItem, error)) *MockIAppSeriesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAppSeriesCall) DoAndReturn(f func(context.Context, string, storage.Timeframe, string, storage.SeriesRequest) ([]storage.SeriesItem, error)) *MockIAppSeriesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Update mocks base method. func (m_2 *MockIApp) Update(ctx context.Context, m *storage.App) error { m_2.ctrl.T.Helper() diff --git a/internal/storage/postgres/app.go b/internal/storage/postgres/app.go index 83c4ca2..968718f 100644 --- a/internal/storage/postgres/app.go +++ b/internal/storage/postgres/app.go @@ -80,9 +80,23 @@ func (app *App) Actions(ctx context.Context, slug string, limit, offset int, sor return } + if len(appIds) == 0 { + return + } + subQuery := app.DB().NewSelect(). Model((*storage.RollupAction)(nil)) + subQuery.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { + for i := range appIds { + sq = sq.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { + return q.Where("rollup_id = ?", appIds[i].RolllupId).Where("sender_id = ?", appIds[i].AddressId) + }) + } + + return sq + }) + subQuery = limitScope(subQuery, limit) subQuery = offsetScope(subQuery, offset) subQuery = sortScope(subQuery, "action_id", sort) @@ -100,3 +114,72 @@ func (app *App) Actions(ctx context.Context, slug string, limit, offset int, sor return } + +func (app *App) Series(ctx context.Context, slug string, timeframe storage.Timeframe, column string, req storage.SeriesRequest) (items []storage.SeriesItem, err error) { + var id uint64 + if err = app.DB().NewSelect(). + Column("id"). + Model((*storage.App)(nil)). + Where("slug = ?", slug). + Limit(1). + Scan(ctx, &id); err != nil { + return + } + + var appIds []storage.AppId + if err = app.DB().NewSelect(). + Model(&appIds). + Where("app_id = ?", id). + Scan(ctx); err != nil { + return + } + + if len(appIds) == 0 { + return + } + + query := app.DB().NewSelect().Order("time desc").Limit(100).Group("time") + + switch timeframe { + case storage.TimeframeHour: + query = query.Table("app_stats_by_hour") + case storage.TimeframeDay: + query = query.Table("app_stats_by_day") + case storage.TimeframeMonth: + query = query.Table("app_stats_by_month") + default: + return nil, errors.Errorf("invalid timeframe: %s", timeframe) + } + + switch column { + case "actions_count": + query = query.ColumnExpr("sum(actions_count) as value, time as ts") + case "size": + query = query.ColumnExpr("sum(size) as value, time as ts") + case "size_per_action": + query = query.ColumnExpr("(sum(size) / sum(actions_count)) as value, time as ts") + default: + return nil, errors.Errorf("invalid column: %s", column) + } + + if !req.From.IsZero() { + query = query.Where("time >= ?", req.From) + } + if !req.To.IsZero() { + query = query.Where("time < ?", req.To) + } + + query.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { + for i := range appIds { + sq = sq.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { + return q.Where("rollup_id = ?", appIds[i].RolllupId).Where("sender_id = ?", appIds[i].AddressId) + }) + } + + return sq + }) + + err = query.Scan(ctx, &items) + + return +} diff --git a/internal/storage/postgres/app_test.go b/internal/storage/postgres/app_test.go index ff0e826..4a5cd00 100644 --- a/internal/storage/postgres/app_test.go +++ b/internal/storage/postgres/app_test.go @@ -115,3 +115,23 @@ func (s *StorageTestSuite) TestAppActions() { s.Require().NotNil(action.Action.Data) s.Require().NotNil(action.Action.Fee) } + +func (s *StorageTestSuite) TestAppSeries() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + for _, tf := range []storage.Timeframe{ + "day", "hour", "month", + } { + for _, column := range []string{ + "size", "actions_count", "size_per_action", + } { + series, err := s.storage.App.Series(ctx, "app-1", tf, column, storage.SeriesRequest{ + From: time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC), + }) + s.Require().NoError(err, column, tf) + s.Require().Len(series, 1, column, tf) + + } + } +} From 54ca5873f3389865df1469fffe5d777a2581030c Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 8 Nov 2024 17:40:45 +0300 Subject: [PATCH 5/5] Fix: leaderboard --- cmd/api/docs/docs.go | 151 ------------ cmd/api/docs/swagger.json | 151 ------------ cmd/api/docs/swagger.yaml | 104 -------- cmd/api/handler/app.go | 98 -------- cmd/api/handler/app_test.go | 72 ------ cmd/api/handler/responses/app.go | 141 +++++------ cmd/api/init.go | 2 - cmd/private_api/handler/app.go | 226 ++++++------------ cmd/private_api/init.go | 2 +- database/views/03_rollup_stats_by_hour.sql | 4 +- database/views/04_rollup_stats_by_day.sql | 4 +- database/views/05_rollup_stats_by_month.sql | 4 +- database/views/12_app_stats_by_hour.sql | 15 -- database/views/12_leaderboard.sql | 26 ++ database/views/13_app_stats_by_day.sql | 15 -- database/views/14_app_stats_by_month.sql | 15 -- database/views/15_leaderboard.sql | 34 --- internal/storage/app.go | 51 ++-- internal/storage/app_bridge.go | 23 -- internal/storage/app_id.go | 22 -- internal/storage/generic.go | 6 - internal/storage/mock/app.go | 78 ------ internal/storage/mock/generic.go | 162 ------------- internal/storage/postgres/app.go | 140 ++--------- internal/storage/postgres/app_test.go | 59 +---- internal/storage/postgres/core.go | 29 +-- .../20241106_rollup_action_sender_id.up.sql | 16 -- .../storage/postgres/migrations/migrations.go | 21 -- internal/storage/postgres/transaction.go | 41 +--- internal/storage/rollup_action.go | 8 +- pkg/indexer/storage/storage.go | 3 - test/data/app.yml | 4 +- test/data/app_bridge.yml | 3 - test/data/app_id.yaml | 3 - test/data/rollup_action.yml | 1 - 35 files changed, 241 insertions(+), 1493 deletions(-) delete mode 100644 database/views/12_app_stats_by_hour.sql create mode 100644 database/views/12_leaderboard.sql delete mode 100644 database/views/13_app_stats_by_day.sql delete mode 100644 database/views/14_app_stats_by_month.sql delete mode 100644 database/views/15_leaderboard.sql delete mode 100644 internal/storage/app_bridge.go delete mode 100644 internal/storage/app_id.go delete mode 100644 internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql delete mode 100644 internal/storage/postgres/migrations/migrations.go delete mode 100644 test/data/app_bridge.yml delete mode 100644 test/data/app_id.yaml diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index c317300..1996b7a 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -141,157 +141,6 @@ const docTemplate = `{ } } }, - "/app/{slug}/actions": { - "get": { - "description": "Get application actions", - "produces": [ - "application/json" - ], - "tags": [ - "applications" - ], - "summary": "Get application actions", - "operationId": "get-application-actions", - "parameters": [ - { - "type": "string", - "description": "Slug", - "name": "slug", - "in": "path", - "required": true - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "Count of requested entities", - "name": "limit", - "in": "query" - }, - { - "minimum": 1, - "type": "integer", - "description": "Offset", - "name": "offset", - "in": "query" - }, - { - "enum": [ - "asc", - "desc" - ], - "type": "string", - "description": "Sort order", - "name": "sort", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.Action" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.Error" - } - } - } - } - }, - "/app/{slug}/series/{name}/{timeframe}": { - "get": { - "description": "Get application series", - "produces": [ - "application/json" - ], - "tags": [ - "applications" - ], - "summary": "Get application series", - "operationId": "get-application-series", - "parameters": [ - { - "type": "string", - "description": "Slug", - "name": "slug", - "in": "path", - "required": true - }, - { - "enum": [ - "actions_count", - "size", - "size_per_action" - ], - "type": "string", - "description": "Series name", - "name": "name", - "in": "path", - "required": true - }, - { - "enum": [ - "hour", - "day", - "month" - ], - "type": "string", - "description": "Timeframe", - "name": "timeframe", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Time from in unix timestamp", - "name": "from", - "in": "query" - }, - { - "type": "integer", - "description": "Time to in unix timestamp", - "name": "to", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.SeriesItem" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.Error" - } - } - } - } - }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 759137d..4d4f368 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -131,157 +131,6 @@ } } }, - "/app/{slug}/actions": { - "get": { - "description": "Get application actions", - "produces": [ - "application/json" - ], - "tags": [ - "applications" - ], - "summary": "Get application actions", - "operationId": "get-application-actions", - "parameters": [ - { - "type": "string", - "description": "Slug", - "name": "slug", - "in": "path", - "required": true - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "Count of requested entities", - "name": "limit", - "in": "query" - }, - { - "minimum": 1, - "type": "integer", - "description": "Offset", - "name": "offset", - "in": "query" - }, - { - "enum": [ - "asc", - "desc" - ], - "type": "string", - "description": "Sort order", - "name": "sort", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.Action" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.Error" - } - } - } - } - }, - "/app/{slug}/series/{name}/{timeframe}": { - "get": { - "description": "Get application series", - "produces": [ - "application/json" - ], - "tags": [ - "applications" - ], - "summary": "Get application series", - "operationId": "get-application-series", - "parameters": [ - { - "type": "string", - "description": "Slug", - "name": "slug", - "in": "path", - "required": true - }, - { - "enum": [ - "actions_count", - "size", - "size_per_action" - ], - "type": "string", - "description": "Series name", - "name": "name", - "in": "path", - "required": true - }, - { - "enum": [ - "hour", - "day", - "month" - ], - "type": "string", - "description": "Timeframe", - "name": "timeframe", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Time from in unix timestamp", - "name": "from", - "in": "query" - }, - { - "type": "integer", - "description": "Time to in unix timestamp", - "name": "to", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/responses.SeriesItem" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.Error" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.Error" - } - } - } - } - }, "/v1/address": { "get": { "description": "List address info", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 0ed1057..6465092 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -852,110 +852,6 @@ paths: summary: Get application info tags: - applications - /app/{slug}/actions: - get: - description: Get application actions - operationId: get-application-actions - parameters: - - description: Slug - in: path - name: slug - required: true - type: string - - description: Count of requested entities - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - description: Offset - in: query - minimum: 1 - name: offset - type: integer - - description: Sort order - enum: - - asc - - desc - in: query - name: sort - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/responses.Action' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.Error' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.Error' - summary: Get application actions - tags: - - applications - /app/{slug}/series/{name}/{timeframe}: - get: - description: Get application series - operationId: get-application-series - parameters: - - description: Slug - in: path - name: slug - required: true - type: string - - description: Series name - enum: - - actions_count - - size - - size_per_action - in: path - name: name - required: true - type: string - - description: Timeframe - enum: - - hour - - day - - month - in: path - name: timeframe - required: true - type: string - - description: Time from in unix timestamp - in: query - name: from - type: integer - - description: Time to in unix timestamp - in: query - name: to - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/responses.SeriesItem' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.Error' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.Error' - summary: Get application series - tags: - - applications /v1/address: get: description: List address info diff --git a/cmd/api/handler/app.go b/cmd/api/handler/app.go index b298811..fd67127 100644 --- a/cmd/api/handler/app.go +++ b/cmd/api/handler/app.go @@ -119,101 +119,3 @@ func (handler AppHandler) Get(c echo.Context) error { return c.JSON(http.StatusOK, responses.NewAppWithStats(rollup)) } - -type getAppActionsRequest struct { - Slug string `param:"slug" validate:"required"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100"` - Offset int `query:"offset" validate:"omitempty,min=0"` - Sort string `query:"sort" validate:"omitempty,oneof=asc desc"` -} - -func (p *getAppActionsRequest) SetDefault() { - if p.Limit == 0 { - p.Limit = 10 - } - if p.Sort == "" { - p.Sort = asc - } -} - -// Actions godoc -// -// @Summary Get application actions -// @Description Get application actions -// @Tags applications -// @ID get-application-actions -// @Param slug path string true "Slug" -// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) -// @Param offset query integer false "Offset" minimum(1) -// @Param sort query string false "Sort order" Enums(asc, desc) -// @Produce json -// @Success 200 {array} responses.Action -// @Failure 400 {object} Error -// @Failure 500 {object} Error -// @Router /app/{slug}/actions [get] -func (handler AppHandler) Actions(c echo.Context) error { - req, err := bindAndValidate[getAppActionsRequest](c) - if err != nil { - return badRequestError(c, err) - } - req.SetDefault() - - actions, err := handler.apps.Actions(c.Request().Context(), req.Slug, req.Limit, req.Offset, pgSort(req.Sort)) - if err != nil { - return handleError(c, err, handler.apps) - } - - result := make([]responses.Action, len(actions)) - for i := range actions { - result[i] = responses.NewActionFromRollupAction(actions[i]) - } - return returnArray(c, result) -} - -type appStatsRequest struct { - Slug string `example:"app" param:"slug" swaggertype:"string" validate:"required"` - Timeframe storage.Timeframe `example:"hour" param:"timeframe" swaggertype:"string" validate:"required,oneof=hour day month"` - SeriesName string `example:"size" param:"name" swaggertype:"string" validate:"required,oneof=actions_count size size_per_action"` - From int64 `example:"1692892095" query:"from" swaggertype:"integer" validate:"omitempty,min=1"` - To int64 `example:"1692892095" query:"to" swaggertype:"integer" validate:"omitempty,min=1"` -} - -// Series godoc -// -// @Summary Get application series -// @Description Get application series -// @Tags applications -// @ID get-application-series -// @Param slug path string true "Slug" -// @Param name path string true "Series name" Enums(actions_count, size, size_per_action) -// @Param timeframe path string true "Timeframe" Enums(hour, day, month) -// @Param from query integer false "Time from in unix timestamp" mininum(1) -// @Param to query integer false "Time to in unix timestamp" mininum(1) -// @Produce json -// @Success 200 {array} responses.SeriesItem -// @Failure 400 {object} Error -// @Failure 500 {object} Error -// @Router /app/{slug}/series/{name}/{timeframe} [get] -func (handler AppHandler) Series(c echo.Context) error { - req, err := bindAndValidate[appStatsRequest](c) - if err != nil { - return badRequestError(c, err) - } - - histogram, err := handler.apps.Series( - c.Request().Context(), - req.Slug, - req.Timeframe, - req.SeriesName, - storage.NewSeriesRequest(req.From, req.To), - ) - if err != nil { - return handleError(c, err, handler.apps) - } - - response := make([]responses.SeriesItem, len(histogram)) - for i := range histogram { - response[i] = responses.NewSeriesItem(histogram[i]) - } - return returnArray(c, response) -} diff --git a/cmd/api/handler/app_test.go b/cmd/api/handler/app_test.go index c5bdb9f..0aa2706 100644 --- a/cmd/api/handler/app_test.go +++ b/cmd/api/handler/app_test.go @@ -40,8 +40,6 @@ var ( Size: 1000, LastActionTime: testTime, FirstActionTime: testTime, - ActionsCountPct: 0.1, - SizePct: 0.3, }, } ) @@ -120,8 +118,6 @@ func (s *AppTestSuite) TestLeaderboard() { s.Require().EqualValues(1000, rollup.Size) s.Require().EqualValues(testTime, rollup.LastAction) s.Require().EqualValues(testTime, rollup.FirstAction) - s.Require().EqualValues(0.1, rollup.ActionsCountPct) - s.Require().EqualValues(0.3, rollup.SizePct) } } @@ -152,72 +148,4 @@ func (s *AppTestSuite) TestGet() { s.Require().EqualValues(1000, rollup.Size) s.Require().EqualValues(testTime, rollup.LastAction) s.Require().EqualValues(testTime, rollup.FirstAction) - s.Require().EqualValues(0.1, rollup.ActionsCountPct) - s.Require().EqualValues(0.3, rollup.SizePct) -} - -func (s *AppTestSuite) TestActions() { - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := s.echo.NewContext(req, rec) - c.SetPath("/app/:slug/actions") - c.SetParamNames("slug") - c.SetParamValues("test-app") - - s.apps.EXPECT(). - Actions(gomock.Any(), "test-app", 10, 0, sdk.SortOrderAsc). - Return([]storage.RollupAction{ - testRollupAction, - }, nil). - Times(1) - - s.Require().NoError(s.handler.Actions(c)) - s.Require().Equal(http.StatusOK, rec.Code) - - var actions []responses.Action - err := json.NewDecoder(rec.Body).Decode(&actions) - s.Require().NoError(err) - s.Require().Len(actions, 1) - s.Require().EqualValues(1, actions[0].Id) - s.Require().EqualValues(100, actions[0].Height) - s.Require().EqualValues(1, actions[0].Position) - s.Require().Equal(testTime, actions[0].Time) - s.Require().EqualValues(string(types.ActionTypeRollupDataSubmission), actions[0].Type) -} - -func (s *AppTestSuite) TestSeries() { - for _, name := range []string{ - "size", "actions_count", "size_per_action", - } { - for _, tf := range []storage.Timeframe{ - "hour", "day", "month", - } { - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := s.echo.NewContext(req, rec) - c.SetPath("/app/:slug/stats/:name/:timeframe") - c.SetParamNames("slug", "name", "timeframe") - c.SetParamValues("test-app", name, string(tf)) - - s.apps.EXPECT(). - Series(gomock.Any(), "test-app", tf, name, storage.NewSeriesRequest(0, 0)). - Return([]storage.SeriesItem{ - { - Value: "10000", - Time: testTime, - }, - }, nil). - Times(1) - - s.Require().NoError(s.handler.Series(c)) - s.Require().Equal(http.StatusOK, rec.Code, name, tf) - - var items []responses.SeriesItem - err := json.NewDecoder(rec.Body).Decode(&items) - s.Require().NoError(err, name, tf) - s.Require().Len(items, 1, name, tf) - s.Require().EqualValues("10000", items[0].Value, name, tf) - s.Require().EqualValues(testTime.UTC(), items[0].Time.UTC(), name, tf) - } - } } diff --git a/cmd/api/handler/responses/app.go b/cmd/api/handler/responses/app.go index 34951b1..9cf4b3d 100644 --- a/cmd/api/handler/responses/app.go +++ b/cmd/api/handler/responses/app.go @@ -4,102 +4,75 @@ package responses import ( + "encoding/base64" "time" "github.com/celenium-io/astria-indexer/internal/storage" ) type AppWithStats struct { - Id uint64 `example:"321" format:"integer" json:"id" swaggertype:"integer"` - Name string `example:"Rollup name" format:"string" json:"name" swaggertype:"string"` - Description string `example:"Long rollup description" format:"string" json:"description,omitempty" swaggertype:"string"` - Website string `example:"https://website.com" format:"string" json:"website,omitempty" swaggertype:"string"` - Twitter string `example:"https://x.com/account" format:"string" json:"twitter,omitempty" swaggertype:"string"` - Github string `example:"https://github.com/account" format:"string" json:"github,omitempty" swaggertype:"string"` - Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` - Slug string `example:"rollup_slug" format:"string" json:"slug" swaggertype:"string"` - L2Beat string `example:"https://l2beat.com/scaling/projects/karak" format:"string" json:"l2_beat,omitempty" swaggertype:"string"` - Explorer string `example:"https://explorer.karak.network/" format:"string" json:"explorer,omitempty" swaggertype:"string"` - Stack string `example:"op_stack" format:"string" json:"stack,omitempty" swaggertype:"string"` - Type string `example:"settled" format:"string" json:"type,omitempty" swaggertype:"string"` - Category string `example:"nft" format:"string" json:"category,omitempty" swaggertype:"string"` - VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` - Provider string `example:"name" format:"string" json:"provider,omitempty" swaggertype:"string"` + Id uint64 `example:"321" format:"integer" json:"id" swaggertype:"integer"` + Name string `example:"Rollup name" format:"string" json:"name" swaggertype:"string"` + Description string `example:"Long rollup description" format:"string" json:"description,omitempty" swaggertype:"string"` + Website string `example:"https://website.com" format:"string" json:"website,omitempty" swaggertype:"string"` + Twitter string `example:"https://x.com/account" format:"string" json:"twitter,omitempty" swaggertype:"string"` + Github string `example:"https://github.com/account" format:"string" json:"github,omitempty" swaggertype:"string"` + Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` + Slug string `example:"rollup_slug" format:"string" json:"slug" swaggertype:"string"` + L2Beat string `example:"https://l2beat.com/scaling/projects/karak" format:"string" json:"l2_beat,omitempty" swaggertype:"string"` + Explorer string `example:"https://explorer.karak.network/" format:"string" json:"explorer,omitempty" swaggertype:"string"` + Stack string `example:"op_stack" format:"string" json:"stack,omitempty" swaggertype:"string"` + Type string `example:"settled" format:"string" json:"type,omitempty" swaggertype:"string"` + Category string `example:"nft" format:"string" json:"category,omitempty" swaggertype:"string"` + VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` + Provider string `example:"name" format:"string" json:"provider,omitempty" swaggertype:"string"` + NativeBridge string `example:"astria1phym4uktjn6gjle226009ge7u82w0dgtszs8x2" format:"string" json:"native_bridge" swaggertype:"string"` + Rollup string `example:"O0Ia+lPYYMf3iFfxBaWXCSdlhphc6d4ZoBXINov6Tjc=" format:"string" json:"rollup" swaggertype:"string"` - ActionsCount int64 `example:"2" format:"integer" json:"actions_count" swaggertype:"integer"` - Size int64 `example:"1000" format:"integer" json:"size" swaggertype:"integer"` - LastAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"last_message_time" swaggertype:"string"` - FirstAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"first_message_time" swaggertype:"string"` - SizePct float64 `example:"0.9876" format:"float" json:"size_pct" swaggertype:"number"` - ActionsCountPct float64 `example:"0.9876" format:"float" json:"actions_count_pct" swaggertype:"number"` + ActionsCount int64 `example:"2" format:"integer" json:"actions_count" swaggertype:"integer"` + Size int64 `example:"1000" format:"integer" json:"size" swaggertype:"integer"` + MinSize int64 `example:"1000" format:"integer" json:"min_size" swaggertype:"integer"` + MaxSize int64 `example:"1000" format:"integer" json:"max_size" swaggertype:"integer"` + AvgSize int64 `example:"1000" format:"integer" json:"avg_size" swaggertype:"integer"` + LastAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"last_message_time" swaggertype:"string"` + FirstAction time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"first_message_time" swaggertype:"string"` Links []string `json:"links,omitempty"` } func NewAppWithStats(r storage.AppWithStats) AppWithStats { - return AppWithStats{ - Id: r.Id, - Name: r.Name, - Description: r.Description, - Github: r.Github, - Twitter: r.Twitter, - Website: r.Website, - Logo: r.Logo, - L2Beat: r.L2Beat, - Explorer: r.Explorer, - Links: r.Links, - Stack: r.Stack, - Slug: r.Slug, - ActionsCount: r.ActionsCount, - Size: r.Size, - SizePct: r.SizePct, - ActionsCountPct: r.ActionsCountPct, - LastAction: r.LastActionTime, - FirstAction: r.FirstActionTime, - Category: r.Category.String(), - Type: r.Type.String(), - Provider: r.Provider, - VM: r.VM, + app := AppWithStats{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Github: r.Github, + Twitter: r.Twitter, + Website: r.Website, + Logo: r.Logo, + L2Beat: r.L2Beat, + Explorer: r.Explorer, + Links: r.Links, + Stack: r.Stack, + Slug: r.Slug, + ActionsCount: r.ActionsCount, + Size: r.Size, + MinSize: r.MinSize, + MaxSize: r.MaxSize, + AvgSize: int64(r.AvgSize), + LastAction: r.LastActionTime, + FirstAction: r.FirstActionTime, + Category: r.Category.String(), + Type: r.Type.String(), + Provider: r.Provider, + VM: r.VM, } -} - -// type App struct { -// Id uint64 `example:"321" format:"integer" json:"id" swaggertype:"integer"` -// Name string `example:"Rollup name" format:"string" json:"name" swaggertype:"string"` -// Description string `example:"Long rollup description" format:"string" json:"description,omitempty" swaggertype:"string"` -// Website string `example:"https://website.com" format:"string" json:"website,omitempty" swaggertype:"string"` -// Twitter string `example:"https://x.com/account" format:"string" json:"twitter,omitempty" swaggertype:"string"` -// Github string `example:"https://github.com/account" format:"string" json:"github,omitempty" swaggertype:"string"` -// Logo string `example:"https://some_link.com/image.png" format:"string" json:"logo,omitempty" swaggertype:"string"` -// Slug string `example:"rollup_slug" format:"string" json:"slug" swaggertype:"string"` -// L2Beat string `example:"https://github.com/account" format:"string" json:"l2_beat,omitempty" swaggertype:"string"` -// Explorer string `example:"https://explorer.karak.network/" format:"string" json:"explorer,omitempty" swaggertype:"string"` -// Stack string `example:"op_stack" format:"string" json:"stack,omitempty" swaggertype:"string"` -// Type string `example:"settled" format:"string" json:"type,omitempty" swaggertype:"string"` -// Category string `example:"nft" format:"string" json:"category,omitempty" swaggertype:"string"` -// Provider string `example:"name" format:"string" json:"provider,omitempty" swaggertype:"string"` -// VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` -// Links []string `json:"links,omitempty"` -// } + if r.Rollup != nil { + app.Rollup = base64.StdEncoding.EncodeToString(r.Rollup.AstriaId) + } + if r.Bridge != nil { + app.NativeBridge = r.Bridge.Hash + } -// func NewApp(r *storage.App) App { -// return App{ -// Id: r.Id, -// Name: r.Name, -// Description: r.Description, -// Github: r.Github, -// Twitter: r.Twitter, -// Website: r.Website, -// Logo: r.Logo, -// Slug: r.Slug, -// L2Beat: r.L2Beat, -// Stack: r.Stack, -// Explorer: r.Explorer, -// Links: r.Links, -// Category: r.Category.String(), -// Type: r.Type.String(), -// Provider: r.Provider, -// VM: r.VM, -// } -// } + return app +} diff --git a/cmd/api/init.go b/cmd/api/init.go index 9fca7fa..4c4e022 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -379,8 +379,6 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto app := apps.Group("/:slug") { app.GET("", appHandler.Get) - app.GET("/actions", appHandler.Actions) - app.GET("/series/:name/:timeframe", appHandler.Series) } } diff --git a/cmd/private_api/handler/app.go b/cmd/private_api/handler/app.go index cd7f550..b4a8d00 100644 --- a/cmd/private_api/handler/app.go +++ b/cmd/private_api/handler/app.go @@ -7,7 +7,6 @@ import ( "context" "encoding/base64" - "github.com/celenium-io/astria-indexer/internal/astria" "github.com/celenium-io/astria-indexer/internal/storage" "github.com/celenium-io/astria-indexer/internal/storage/postgres" "github.com/celenium-io/astria-indexer/internal/storage/types" @@ -20,7 +19,6 @@ import ( type AppHandler struct { apps storage.IApp address storage.IAddress - bridge storage.IBridge rollup storage.IRollup tx sdk.Transactable } @@ -28,48 +26,35 @@ type AppHandler struct { func NewAppHandler( apps storage.IApp, address storage.IAddress, - bridge storage.IBridge, rollup storage.IRollup, tx sdk.Transactable, ) AppHandler { return AppHandler{ apps: apps, address: address, - bridge: bridge, rollup: rollup, tx: tx, } } type createAppRequest struct { - Group string `json:"group" validate:"omitempty,min=1"` - Name string `json:"name" validate:"required,min=1"` - Description string `json:"description" validate:"required,min=1"` - Website string `json:"website" validate:"omitempty,url"` - GitHub string `json:"github" validate:"omitempty,url"` - Twitter string `json:"twitter" validate:"omitempty,url"` - Logo string `json:"logo" validate:"omitempty,url"` - L2Beat string `json:"l2beat" validate:"omitempty,url"` - Explorer string `json:"explorer" validate:"omitempty,url"` - Stack string `json:"stack" validate:"omitempty"` - Links []string `json:"links" validate:"omitempty,dive,url"` - Category string `json:"category" validate:"omitempty,app_category"` - Type string `json:"type" validate:"omitempty,app_type"` - VM string `json:"vm" validate:"omitempty"` - Provider string `json:"provider" validate:"omitempty"` - - AppIds []appId `json:"app_ids" validate:"required,min=1"` - Bridges []bridge `json:"bridges" validate:"omitempty"` -} - -type appId struct { - Rollup string `json:"rollup" validate:"omitempty,base64"` - Address string `json:"address" validate:"required,address"` -} - -type bridge struct { - Address string `json:"address" validate:"required,address"` - Native bool `json:"native" validate:"omitempty"` + Group string `json:"group" validate:"omitempty,min=1"` + Name string `json:"name" validate:"required,min=1"` + Description string `json:"description" validate:"required,min=1"` + Website string `json:"website" validate:"omitempty,url"` + GitHub string `json:"github" validate:"omitempty,url"` + Twitter string `json:"twitter" validate:"omitempty,url"` + Logo string `json:"logo" validate:"omitempty,url"` + L2Beat string `json:"l2beat" validate:"omitempty,url"` + Explorer string `json:"explorer" validate:"omitempty,url"` + Stack string `json:"stack" validate:"omitempty"` + Links []string `json:"links" validate:"omitempty,dive,url"` + Category string `json:"category" validate:"omitempty,app_category"` + Type string `json:"type" validate:"omitempty,app_type"` + VM string `json:"vm" validate:"omitempty"` + Provider string `json:"provider" validate:"omitempty"` + Rollup string `json:"rollup" validate:"required,base64"` + NativeBridge string `json:"native_bridge" validate:"omitempty,address"` } func (handler AppHandler) Create(c echo.Context) error { @@ -91,7 +76,16 @@ func (handler AppHandler) createApp(ctx context.Context, req *createAppRequest) return err } - rollup := storage.App{ + hash, err := base64.StdEncoding.DecodeString(req.Rollup) + if err != nil { + return err + } + rollup, err := handler.rollup.ByHash(ctx, hash) + if err != nil { + return err + } + + app := storage.App{ Group: req.Group, Name: req.Name, Description: req.Description, @@ -108,27 +102,22 @@ func (handler AppHandler) createApp(ctx context.Context, req *createAppRequest) Type: types.AppType(req.Type), Category: types.AppCategory(req.Category), Slug: slug.Make(req.Name), + RollupId: rollup.Id, } - if err := tx.SaveApp(ctx, &rollup); err != nil { - return tx.HandleError(ctx, err) - } - - appIds, err := handler.createAppIds(ctx, rollup.Id, req.AppIds...) - if err != nil { - return tx.HandleError(ctx, err) - } - - if err := tx.SaveAppId(ctx, appIds...); err != nil { - return tx.HandleError(ctx, err) - } + if req.NativeBridge != "" { + addr, err := handler.address.ByHash(ctx, req.NativeBridge) + if err != nil { + return err + } + if !addr.IsBridge { + return tx.HandleError(ctx, errors.Errorf("address %s is not a bridge", req.NativeBridge)) + } - bridges, err := handler.createBridges(ctx, rollup.Id, req.Bridges...) - if err != nil { - return tx.HandleError(ctx, err) + app.NativeBridgeId = addr.Id } - if err := tx.SaveAppBridges(ctx, bridges...); err != nil { + if err := tx.SaveApp(ctx, &app); err != nil { return tx.HandleError(ctx, err) } @@ -139,81 +128,26 @@ func (handler AppHandler) createApp(ctx context.Context, req *createAppRequest) return tx.Flush(ctx) } -func (handler AppHandler) createAppIds(ctx context.Context, id uint64, data ...appId) ([]storage.AppId, error) { - providers := make([]storage.AppId, len(data)) - for i := range data { - providers[i].AppId = id - - if !astria.IsAddress(data[i].Address) { - return nil, errors.Wrap(errInvalidAddress, data[i].Address) - } - - address, err := handler.address.ByHash(ctx, data[i].Address) - if err != nil { - return nil, err - } - providers[i].AddressId = address.Id - - if data[i].Rollup != "" { - hashRollup, err := base64.StdEncoding.DecodeString(data[i].Rollup) - if err != nil { - return nil, err - } - rollup, err := handler.rollup.ByHash(ctx, hashRollup) - if err != nil { - return nil, err - } - providers[i].RolllupId = rollup.Id - } - } - return providers, nil -} - -func (handler AppHandler) createBridges(ctx context.Context, id uint64, data ...bridge) ([]storage.AppBridge, error) { - bridges := make([]storage.AppBridge, len(data)) - for i := range data { - bridges[i].AppId = id - - if !astria.IsAddress(data[i].Address) { - return nil, errors.Wrap(errInvalidAddress, data[i].Address) - } - - address, err := handler.address.ByHash(ctx, data[i].Address) - if err != nil { - return nil, err - } - - b, err := handler.bridge.ByAddress(ctx, address.Id) - if err != nil { - return nil, err - } - bridges[i].BridgeId = b.Id - bridges[i].Native = data[i].Native - } - return bridges, nil -} - type updateAppRequest struct { Id uint64 `param:"id" validate:"required,min=1"` - Group string `json:"group" validate:"omitempty,min=1"` - Name string `json:"name" validate:"omitempty,min=1"` - Description string `json:"description" validate:"omitempty,min=1"` - Website string `json:"website" validate:"omitempty,url"` - GitHub string `json:"github" validate:"omitempty,url"` - Twitter string `json:"twitter" validate:"omitempty,url"` - Logo string `json:"logo" validate:"omitempty,url"` - L2Beat string `json:"l2beat" validate:"omitempty,url"` - Explorer string `json:"explorer" validate:"omitempty,url"` - Stack string `json:"stack" validate:"omitempty"` - Links []string `json:"links" validate:"omitempty,dive,url"` - Category string `json:"category" validate:"omitempty,app_category"` - Type string `json:"type" validate:"omitempty,app_type"` - VM string `json:"vm" validate:"omitempty"` - Provider string `json:"provider" validate:"omitempty"` - - AppIds []appId `json:"app_ids" validate:"omitempty,min=1"` - Bridges []bridge `json:"bridges" validate:"omitempty"` + Group string `json:"group" validate:"omitempty,min=1"` + Name string `json:"name" validate:"omitempty,min=1"` + Description string `json:"description" validate:"omitempty,min=1"` + Website string `json:"website" validate:"omitempty,url"` + GitHub string `json:"github" validate:"omitempty,url"` + Twitter string `json:"twitter" validate:"omitempty,url"` + Logo string `json:"logo" validate:"omitempty,url"` + L2Beat string `json:"l2beat" validate:"omitempty,url"` + Explorer string `json:"explorer" validate:"omitempty,url"` + Stack string `json:"stack" validate:"omitempty"` + Links []string `json:"links" validate:"omitempty,dive,url"` + Category string `json:"category" validate:"omitempty,app_category"` + Type string `json:"type" validate:"omitempty,app_type"` + VM string `json:"vm" validate:"omitempty"` + Provider string `json:"provider" validate:"omitempty"` + Rollup string `json:"rollup" validate:"omitempty,base64"` + NativeBridge string `json:"native_bridge" validate:"omitempty,address"` } func (handler AppHandler) Update(c echo.Context) error { @@ -258,38 +192,32 @@ func (handler AppHandler) updateRollup(ctx context.Context, req *updateAppReques Links: req.Links, } - if err := tx.UpdateApp(ctx, &app); err != nil { - return tx.HandleError(ctx, err) - } - - if len(req.AppIds) > 0 { - if err := tx.DeleteAppId(ctx, req.Id); err != nil { - return tx.HandleError(ctx, err) - } - - appIds, err := handler.createAppIds(ctx, app.Id, req.AppIds...) + if req.Rollup != "" { + hash, err := base64.StdEncoding.DecodeString(req.Rollup) if err != nil { - return tx.HandleError(ctx, err) + return err } - - if err := tx.SaveAppId(ctx, appIds...); err != nil { - return tx.HandleError(ctx, err) + rollup, err := handler.rollup.ByHash(ctx, hash) + if err != nil { + return err } + app.RollupId = rollup.Id } - if len(req.Bridges) > 0 { - if err := tx.DeleteAppBridges(ctx, req.Id); err != nil { - return tx.HandleError(ctx, err) - } - - bridges, err := handler.createBridges(ctx, app.Id, req.Bridges...) + if req.NativeBridge != "" { + addr, err := handler.address.ByHash(ctx, req.NativeBridge) if err != nil { - return tx.HandleError(ctx, err) + return err } - - if err := tx.SaveAppBridges(ctx, bridges...); err != nil { - return tx.HandleError(ctx, err) + if !addr.IsBridge { + return tx.HandleError(ctx, errors.Errorf("address %s is not a bridge", req.NativeBridge)) } + + app.NativeBridgeId = addr.Id + } + + if err := tx.UpdateApp(ctx, &app); err != nil { + return tx.HandleError(ctx, err) } if err := tx.RefreshLeaderboard(ctx); err != nil { @@ -322,14 +250,6 @@ func (handler AppHandler) deleteRollup(ctx context.Context, id uint64) error { return err } - if err := tx.DeleteAppId(ctx, id); err != nil { - return tx.HandleError(ctx, err) - } - - if err := tx.DeleteAppBridges(ctx, id); err != nil { - return tx.HandleError(ctx, err) - } - if err := tx.DeleteApp(ctx, id); err != nil { return tx.HandleError(ctx, err) } diff --git a/cmd/private_api/init.go b/cmd/private_api/init.go index 577a57c..b8e5cb5 100644 --- a/cmd/private_api/init.go +++ b/cmd/private_api/init.go @@ -167,7 +167,7 @@ func initHandlers(e *echo.Echo, db postgres.Storage) { }, }) - appAuthHandler := handler.NewAppHandler(db.App, db.Address, db.Bridges, db.Rollup, db.Transactable) + appAuthHandler := handler.NewAppHandler(db.App, db.Address, db.Rollup, db.Transactable) app := v1.Group("/app") { app.POST("", appAuthHandler.Create, keyMiddleware) diff --git a/database/views/03_rollup_stats_by_hour.sql b/database/views/03_rollup_stats_by_hour.sql index f46a88b..2ac728d 100644 --- a/database/views/03_rollup_stats_by_hour.sql +++ b/database/views/03_rollup_stats_by_hour.sql @@ -8,7 +8,9 @@ WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS min(size) as min_size, max(size) as max_size, avg(size) as avg_size, - percentile_agg(size) as size_pct + percentile_agg(size) as size_pct, + min(time) as first_time, + max(time) as last_time from rollup_action group by 1, 2 order by 1 desc; diff --git a/database/views/04_rollup_stats_by_day.sql b/database/views/04_rollup_stats_by_day.sql index 35477d4..7613e04 100644 --- a/database/views/04_rollup_stats_by_day.sql +++ b/database/views/04_rollup_stats_by_day.sql @@ -8,7 +8,9 @@ WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS min(min_size) as min_size, max(max_size) as max_size, mean(rollup(size_pct)) as avg_size, - rollup(size_pct) as size_pct + rollup(size_pct) as size_pct, + min(first_time) as first_time, + max(last_time) as last_time from rollup_stats_by_hour group by 1, 2 order by 1 desc; diff --git a/database/views/05_rollup_stats_by_month.sql b/database/views/05_rollup_stats_by_month.sql index 7d0065f..d8c4760 100644 --- a/database/views/05_rollup_stats_by_month.sql +++ b/database/views/05_rollup_stats_by_month.sql @@ -8,7 +8,9 @@ WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS min(min_size) as min_size, max(max_size) as max_size, mean(rollup(size_pct)) as avg_size, - rollup(size_pct) as size_pct + rollup(size_pct) as size_pct, + min(first_time) as first_time, + max(last_time) as last_time from rollup_stats_by_day group by 1, 2 order by 1 desc; diff --git a/database/views/12_app_stats_by_hour.sql b/database/views/12_app_stats_by_hour.sql deleted file mode 100644 index 5883dd6..0000000 --- a/database/views/12_app_stats_by_hour.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_hour -WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS - select - time_bucket('1 hour'::interval, rollup_action.time) AS time, - rollup_action.rollup_id, - rollup_action.sender_id, - sum(rollup_action.size) as size, - count(*) as actions_count, - max(rollup_action.time) as last_time, - min(rollup_action.time) as first_time - from rollup_action - group by 1, 2, 3 - with no data; - -CALL add_view_refresh_job('app_stats_by_hour', NULL, INTERVAL '1 minute'); \ No newline at end of file diff --git a/database/views/12_leaderboard.sql b/database/views/12_leaderboard.sql new file mode 100644 index 0000000..3b99bdd --- /dev/null +++ b/database/views/12_leaderboard.sql @@ -0,0 +1,26 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard AS + select + app.*, + agg.size, + agg.min_size, + agg.max_size, + agg.avg_size, + agg.actions_count, + agg.last_time, + agg.first_time + from ( + select + rollup_id, + sum(size) as size, + min(min_size) as min_size, + max(max_size) as max_size, + mean(rollup(size_pct)) as avg_size, + sum(actions_count) as actions_count, + max(last_time) as last_time, + min(first_time) as first_time + from rollup_stats_by_month + group by rollup_id + ) as agg + inner join app on app.rollup_id = agg.rollup_id; + +CALL add_job_refresh_materialized_view(); \ No newline at end of file diff --git a/database/views/13_app_stats_by_day.sql b/database/views/13_app_stats_by_day.sql deleted file mode 100644 index 61f153f..0000000 --- a/database/views/13_app_stats_by_day.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_day -WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS - select - time_bucket('1 day'::interval, actions.time) AS time, - actions.rollup_id, - actions.sender_id, - sum(actions.size) as size, - sum(actions.actions_count) as actions_count, - max(actions.last_time) as last_time, - min(actions.first_time) as first_time - from app_stats_by_hour as actions - group by 1, 2, 3 - with no data; - -CALL add_view_refresh_job('app_stats_by_day', NULL, INTERVAL '5 minute'); \ No newline at end of file diff --git a/database/views/14_app_stats_by_month.sql b/database/views/14_app_stats_by_month.sql deleted file mode 100644 index 83e390b..0000000 --- a/database/views/14_app_stats_by_month.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE MATERIALIZED VIEW IF NOT EXISTS app_stats_by_month -WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS - select - time_bucket('1 month'::interval, actions.time) AS time, - actions.rollup_id, - actions.sender_id, - sum(actions.size) as size, - sum(actions.actions_count) as actions_count, - max(actions.last_time) as last_time, - min(actions.first_time) as first_time - from app_stats_by_day as actions - group by 1, 2, 3 - with no data; - -CALL add_view_refresh_job('app_stats_by_month', NULL, INTERVAL '1 hour'); \ No newline at end of file diff --git a/database/views/15_leaderboard.sql b/database/views/15_leaderboard.sql deleted file mode 100644 index b30e2ae..0000000 --- a/database/views/15_leaderboard.sql +++ /dev/null @@ -1,34 +0,0 @@ -CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard AS - with board as ( - select - app_id, - sum(size) as size, - sum(actions_count) as actions_count, - max(last_time) as last_time, - min(first_time) as first_time - from ( - select - rollup_id, - sender_id, - sum(size) as size, - sum(actions_count) as actions_count, - max(last_time) as last_time, - min(first_time) as first_time - from app_stats_by_month - group by 1, 2 - ) as agg - inner join app_id on app_id.address_id = agg.sender_id AND (app_id.rollup_id = agg.rollup_id OR app_id.rollup_id = 0) - group by 1 - ) - select - board.size, - board.actions_count, - board.last_time, - board.first_time, - board.size / (select sum(size) from board) as size_pct, - board.actions_count / (select sum(actions_count) from board)as actions_count_pct, - app.* - from board - inner join app on app.id = board.app_id; - -CALL add_job_refresh_materialized_view(); \ No newline at end of file diff --git a/internal/storage/app.go b/internal/storage/app.go index 51fa1f1..b29ab87 100644 --- a/internal/storage/app.go +++ b/internal/storage/app.go @@ -26,33 +26,33 @@ type IApp interface { Leaderboard(ctx context.Context, fltrs LeaderboardFilters) ([]AppWithStats, error) BySlug(ctx context.Context, slug string) (AppWithStats, error) - Actions(ctx context.Context, slug string, limit, offset int, sort storage.SortOrder) ([]RollupAction, error) - Series(ctx context.Context, slug string, timeframe Timeframe, column string, req SeriesRequest) (items []SeriesItem, err error) } type App struct { bun.BaseModel `bun:"app" comment:"Table with applications."` - Id uint64 `bun:"id,pk,notnull,autoincrement" comment:"Unique internal identity"` - Group string `bun:"group" comment:"Application group"` - Name string `bun:"name" comment:"Application name"` - Slug string `bun:"slug,unique:app_slug" comment:"Application slug"` - Github string `bun:"github" comment:"Application github link"` - Twitter string `bun:"twitter" comment:"Application twitter account link"` - Website string `bun:"website" comment:"Application website link"` - Logo string `bun:"logo" comment:"Application logo link"` - Description string `bun:"description" comment:"Application description"` - Explorer string `bun:"explorer" comment:"Application explorer link"` - L2Beat string `bun:"l2beat" comment:"Link to L2Beat"` - Links []string `bun:"links,array" comment:"Additional links"` - Stack string `bun:"stack" comment:"Using stack"` - VM string `bun:"vm" comment:"Virtual machine"` - Provider string `bun:"provider" comment:"RaaS"` - Type types.AppType `bun:"type,type:app_type" comment:"Type of application: settled or sovereign"` - Category types.AppCategory `bun:"category,type:app_category" comment:"Category of applications"` + Id uint64 `bun:"id,pk,notnull,autoincrement" comment:"Unique internal identity"` + Group string `bun:"group" comment:"Application group"` + Name string `bun:"name" comment:"Application name"` + Slug string `bun:"slug,unique:app_slug" comment:"Application slug"` + Github string `bun:"github" comment:"Application github link"` + Twitter string `bun:"twitter" comment:"Application twitter account link"` + Website string `bun:"website" comment:"Application website link"` + Logo string `bun:"logo" comment:"Application logo link"` + Description string `bun:"description" comment:"Application description"` + Explorer string `bun:"explorer" comment:"Application explorer link"` + L2Beat string `bun:"l2beat" comment:"Link to L2Beat"` + Links []string `bun:"links,array" comment:"Additional links"` + Stack string `bun:"stack" comment:"Using stack"` + VM string `bun:"vm" comment:"Virtual machine"` + Provider string `bun:"provider" comment:"RaaS"` + Type types.AppType `bun:"type,type:app_type" comment:"Type of application: settled or sovereign"` + Category types.AppCategory `bun:"category,type:app_category" comment:"Category of applications"` + RollupId uint64 `bun:"rollup_id,notnull,unique:app_rollup_id" comment:"Rollup internal identity"` + NativeBridgeId uint64 `bun:"native_bridge_id" comment:"Native bridge internal id"` - AppIds []*AppId `bun:"rel:has-many,join:id=app_id"` - Bridges []*AppBridge `bun:"rel:has-many,join:id=app_id"` + Bridge *Address `bun:"rel:belongs-to"` + Rollup *Rollup `bun:"rel:belongs-to"` } func (App) TableName() string { @@ -75,7 +75,9 @@ func (app App) IsEmpty() bool { app.VM == "" && app.Provider == "" && app.Type == "" && - app.Category == "" + app.Category == "" && + app.RollupId == 0 && + app.NativeBridgeId == 0 } type AppWithStats struct { @@ -85,9 +87,10 @@ type AppWithStats struct { type AppStats struct { Size int64 `bun:"size"` + MinSize int64 `bun:"min_size"` + MaxSize int64 `bun:"max_size"` + AvgSize float64 `bun:"avg_size"` ActionsCount int64 `bun:"actions_count"` LastActionTime time.Time `bun:"last_time"` FirstActionTime time.Time `bun:"first_time"` - SizePct float64 `bun:"size_pct"` - ActionsCountPct float64 `bun:"actions_count_pct"` } diff --git a/internal/storage/app_bridge.go b/internal/storage/app_bridge.go deleted file mode 100644 index 50ddcf7..0000000 --- a/internal/storage/app_bridge.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2024 PK Lab AG -// SPDX-License-Identifier: MIT - -package storage - -import ( - "github.com/uptrace/bun" -) - -type AppBridge struct { - bun.BaseModel `bun:"app_bridge" comment:"Table with application bridges"` - - AppId uint64 `bun:"app_id,pk" comment:"Application id"` - BridgeId uint64 `bun:"bridge_id,pk" comment:"Bridge id"` - Native bool `bun:"native,default:false" comment:"Is native bridge for this application"` - - App *App `bun:"rel:belongs-to,join:app_id=id"` - Bridge *Bridge `bun:"rel:belongs-to,join:bridge_id=id"` -} - -func (AppBridge) TableName() string { - return "app_bridge" -} diff --git a/internal/storage/app_id.go b/internal/storage/app_id.go deleted file mode 100644 index edcc77d..0000000 --- a/internal/storage/app_id.go +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2024 PK Lab AG -// SPDX-License-Identifier: MIT - -package storage - -import "github.com/uptrace/bun" - -type AppId struct { - bun.BaseModel `bun:"app_id" comment:"Table with application bridges"` - - AppId uint64 `bun:"app_id,pk" comment:"Application id"` - RolllupId uint64 `bun:"rollup_id,pk" comment:"Rollup id"` - AddressId uint64 `bun:"address_id,pk" comment:"Address id"` - - App *App `bun:"rel:belongs-to,join:app_id=id"` - Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` - Address *Address `bun:"rel:belongs-to,join:address_id=id"` -} - -func (AppId) TableName() string { - return "app_id" -} diff --git a/internal/storage/generic.go b/internal/storage/generic.go index 56ae4ef..308f517 100644 --- a/internal/storage/generic.go +++ b/internal/storage/generic.go @@ -40,8 +40,6 @@ var Models = []any{ &Transfer{}, &Deposit{}, &App{}, - &AppBridge{}, - &AppId{}, } //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed @@ -66,10 +64,6 @@ type Transaction interface { SaveDeposits(ctx context.Context, deposits ...*Deposit) error SaveApp(ctx context.Context, app *App) error UpdateApp(ctx context.Context, app *App) error - SaveAppId(ctx context.Context, ids ...AppId) error - DeleteAppId(ctx context.Context, appId uint64) error - SaveAppBridges(ctx context.Context, bridges ...AppBridge) error - DeleteAppBridges(ctx context.Context, appId uint64) error DeleteApp(ctx context.Context, appId uint64) error RetentionBlockSignatures(ctx context.Context, height types.Level) error diff --git a/internal/storage/mock/app.go b/internal/storage/mock/app.go index b2e1392..d441cdf 100644 --- a/internal/storage/mock/app.go +++ b/internal/storage/mock/app.go @@ -44,45 +44,6 @@ func (m *MockIApp) EXPECT() *MockIAppMockRecorder { return m.recorder } -// Actions mocks base method. -func (m *MockIApp) Actions(ctx context.Context, slug string, limit, offset int, sort storage0.SortOrder) ([]storage.RollupAction, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Actions", ctx, slug, limit, offset, sort) - ret0, _ := ret[0].([]storage.RollupAction) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Actions indicates an expected call of Actions. -func (mr *MockIAppMockRecorder) Actions(ctx, slug, limit, offset, sort any) *MockIAppActionsCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Actions", reflect.TypeOf((*MockIApp)(nil).Actions), ctx, slug, limit, offset, sort) - return &MockIAppActionsCall{Call: call} -} - -// MockIAppActionsCall wrap *gomock.Call -type MockIAppActionsCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockIAppActionsCall) Return(arg0 []storage.RollupAction, arg1 error) *MockIAppActionsCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockIAppActionsCall) Do(f func(context.Context, string, int, int, storage0.SortOrder) ([]storage.RollupAction, error)) *MockIAppActionsCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockIAppActionsCall) DoAndReturn(f func(context.Context, string, int, int, storage0.SortOrder) ([]storage.RollupAction, error)) *MockIAppActionsCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // BySlug mocks base method. func (m *MockIApp) BySlug(ctx context.Context, slug string) (storage.AppWithStats, error) { m.ctrl.T.Helper() @@ -393,45 +354,6 @@ func (c *MockIAppSaveCall) DoAndReturn(f func(context.Context, *storage.App) err return c } -// Series mocks base method. -func (m *MockIApp) Series(ctx context.Context, slug string, timeframe storage.Timeframe, column string, req storage.SeriesRequest) ([]storage.SeriesItem, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Series", ctx, slug, timeframe, column, req) - ret0, _ := ret[0].([]storage.SeriesItem) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Series indicates an expected call of Series. -func (mr *MockIAppMockRecorder) Series(ctx, slug, timeframe, column, req any) *MockIAppSeriesCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Series", reflect.TypeOf((*MockIApp)(nil).Series), ctx, slug, timeframe, column, req) - return &MockIAppSeriesCall{Call: call} -} - -// MockIAppSeriesCall wrap *gomock.Call -type MockIAppSeriesCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockIAppSeriesCall) Return(items []storage.SeriesItem, err error) *MockIAppSeriesCall { - c.Call = c.Call.Return(items, err) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockIAppSeriesCall) Do(f func(context.Context, string, storage.Timeframe, string, storage.SeriesRequest) ([]storage.SeriesItem, error)) *MockIAppSeriesCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockIAppSeriesCall) DoAndReturn(f func(context.Context, string, storage.Timeframe, string, storage.SeriesRequest) ([]storage.SeriesItem, error)) *MockIAppSeriesCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // Update mocks base method. func (m_2 *MockIApp) Update(ctx context.Context, m *storage.App) error { m_2.ctrl.T.Helper() diff --git a/internal/storage/mock/generic.go b/internal/storage/mock/generic.go index 95dfffa..ff26d21 100644 --- a/internal/storage/mock/generic.go +++ b/internal/storage/mock/generic.go @@ -237,82 +237,6 @@ func (c *MockTransactionDeleteAppCall) DoAndReturn(f func(context.Context, uint6 return c } -// DeleteAppBridges mocks base method. -func (m *MockTransaction) DeleteAppBridges(ctx context.Context, appId uint64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAppBridges", ctx, appId) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteAppBridges indicates an expected call of DeleteAppBridges. -func (mr *MockTransactionMockRecorder) DeleteAppBridges(ctx, appId any) *MockTransactionDeleteAppBridgesCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAppBridges", reflect.TypeOf((*MockTransaction)(nil).DeleteAppBridges), ctx, appId) - return &MockTransactionDeleteAppBridgesCall{Call: call} -} - -// MockTransactionDeleteAppBridgesCall wrap *gomock.Call -type MockTransactionDeleteAppBridgesCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockTransactionDeleteAppBridgesCall) Return(arg0 error) *MockTransactionDeleteAppBridgesCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockTransactionDeleteAppBridgesCall) Do(f func(context.Context, uint64) error) *MockTransactionDeleteAppBridgesCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockTransactionDeleteAppBridgesCall) DoAndReturn(f func(context.Context, uint64) error) *MockTransactionDeleteAppBridgesCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// DeleteAppId mocks base method. -func (m *MockTransaction) DeleteAppId(ctx context.Context, appId uint64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAppId", ctx, appId) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteAppId indicates an expected call of DeleteAppId. -func (mr *MockTransactionMockRecorder) DeleteAppId(ctx, appId any) *MockTransactionDeleteAppIdCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAppId", reflect.TypeOf((*MockTransaction)(nil).DeleteAppId), ctx, appId) - return &MockTransactionDeleteAppIdCall{Call: call} -} - -// MockTransactionDeleteAppIdCall wrap *gomock.Call -type MockTransactionDeleteAppIdCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockTransactionDeleteAppIdCall) Return(arg0 error) *MockTransactionDeleteAppIdCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockTransactionDeleteAppIdCall) Do(f func(context.Context, uint64) error) *MockTransactionDeleteAppIdCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockTransactionDeleteAppIdCall) DoAndReturn(f func(context.Context, uint64) error) *MockTransactionDeleteAppIdCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // Exec mocks base method. func (m *MockTransaction) Exec(ctx context.Context, query string, params ...any) (int64, error) { m.ctrl.T.Helper() @@ -1604,92 +1528,6 @@ func (c *MockTransactionSaveAppCall) DoAndReturn(f func(context.Context, *storag return c } -// SaveAppBridges mocks base method. -func (m *MockTransaction) SaveAppBridges(ctx context.Context, bridges ...storage.AppBridge) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range bridges { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SaveAppBridges", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveAppBridges indicates an expected call of SaveAppBridges. -func (mr *MockTransactionMockRecorder) SaveAppBridges(ctx any, bridges ...any) *MockTransactionSaveAppBridgesCall { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, bridges...) - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAppBridges", reflect.TypeOf((*MockTransaction)(nil).SaveAppBridges), varargs...) - return &MockTransactionSaveAppBridgesCall{Call: call} -} - -// MockTransactionSaveAppBridgesCall wrap *gomock.Call -type MockTransactionSaveAppBridgesCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockTransactionSaveAppBridgesCall) Return(arg0 error) *MockTransactionSaveAppBridgesCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockTransactionSaveAppBridgesCall) Do(f func(context.Context, ...storage.AppBridge) error) *MockTransactionSaveAppBridgesCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockTransactionSaveAppBridgesCall) DoAndReturn(f func(context.Context, ...storage.AppBridge) error) *MockTransactionSaveAppBridgesCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// SaveAppId mocks base method. -func (m *MockTransaction) SaveAppId(ctx context.Context, ids ...storage.AppId) error { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range ids { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SaveAppId", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveAppId indicates an expected call of SaveAppId. -func (mr *MockTransactionMockRecorder) SaveAppId(ctx any, ids ...any) *MockTransactionSaveAppIdCall { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, ids...) - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAppId", reflect.TypeOf((*MockTransaction)(nil).SaveAppId), varargs...) - return &MockTransactionSaveAppIdCall{Call: call} -} - -// MockTransactionSaveAppIdCall wrap *gomock.Call -type MockTransactionSaveAppIdCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockTransactionSaveAppIdCall) Return(arg0 error) *MockTransactionSaveAppIdCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockTransactionSaveAppIdCall) Do(f func(context.Context, ...storage.AppId) error) *MockTransactionSaveAppIdCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockTransactionSaveAppIdCall) DoAndReturn(f func(context.Context, ...storage.AppId) error) *MockTransactionSaveAppIdCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // SaveBalanceUpdates mocks base method. func (m *MockTransaction) SaveBalanceUpdates(ctx context.Context, updates ...storage.BalanceUpdate) error { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/app.go b/internal/storage/postgres/app.go index 968718f..2fa5906 100644 --- a/internal/storage/postgres/app.go +++ b/internal/storage/postgres/app.go @@ -5,10 +5,10 @@ package postgres import ( "context" + "fmt" "github.com/celenium-io/astria-indexer/internal/storage" "github.com/dipdup-net/go-lib/database" - sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -39,15 +39,21 @@ func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilter query := app.DB().NewSelect(). Table(storage.ViewLeaderboard). - ColumnExpr("*"). Offset(fltrs.Offset) if len(fltrs.Category) > 0 { query = query.Where("category IN (?)", bun.In(fltrs.Category)) } - query = sortScope(query, fltrs.SortField, fltrs.Sort) + query = sortScope(query, fmt.Sprintf("%s.%s", storage.ViewLeaderboard, fltrs.SortField), fltrs.Sort) query = limitScope(query, fltrs.Limit) + + query = query. + ColumnExpr("leaderboard.*"). + ColumnExpr("address.hash as bridge__hash"). + ColumnExpr("rollup.astria_id as rollup__astria_id"). + Join("left join address on native_bridge_id = address.id"). + Join("left join rollup on rollup.id = rollup_id") err = query.Scan(ctx, &rollups) return } @@ -55,131 +61,13 @@ func (app *App) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilter func (app *App) BySlug(ctx context.Context, slug string) (result storage.AppWithStats, err error) { err = app.DB().NewSelect(). Table(storage.ViewLeaderboard). + ColumnExpr("leaderboard.*"). + ColumnExpr("address.hash as bridge__hash"). + ColumnExpr("rollup.astria_id as rollup__astria_id"). + Join("left join address on native_bridge_id = address.id"). + Join("left join rollup on rollup.id = rollup_id"). Where("slug = ?", slug). Limit(1). Scan(ctx, &result) return } - -func (app *App) Actions(ctx context.Context, slug string, limit, offset int, sort sdk.SortOrder) (result []storage.RollupAction, err error) { - var id uint64 - if err = app.DB().NewSelect(). - Column("id"). - Model((*storage.App)(nil)). - Where("slug = ?", slug). - Limit(1). - Scan(ctx, &id); err != nil { - return - } - - var appIds []storage.AppId - if err = app.DB().NewSelect(). - Model(&appIds). - Where("app_id = ?", id). - Scan(ctx); err != nil { - return - } - - if len(appIds) == 0 { - return - } - - subQuery := app.DB().NewSelect(). - Model((*storage.RollupAction)(nil)) - - subQuery.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { - for i := range appIds { - sq = sq.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { - return q.Where("rollup_id = ?", appIds[i].RolllupId).Where("sender_id = ?", appIds[i].AddressId) - }) - } - - return sq - }) - - subQuery = limitScope(subQuery, limit) - subQuery = offsetScope(subQuery, offset) - subQuery = sortScope(subQuery, "action_id", sort) - - err = app.DB().NewSelect(). - TableExpr("(?) as rollup_action", subQuery). - ColumnExpr("rollup_action.*"). - ColumnExpr("action.data as action__data, action.position as action__position"). - ColumnExpr("tx.hash as tx__hash"). - ColumnExpr("fee.asset as action__fee__asset, fee.amount as action__fee__amount"). - Join("left join fee on fee.action_id = rollup_action.action_id"). - Join("left join action on action.id = rollup_action.action_id"). - Join("left join tx on tx.id = rollup_action.tx_id"). - Scan(ctx, &result) - - return -} - -func (app *App) Series(ctx context.Context, slug string, timeframe storage.Timeframe, column string, req storage.SeriesRequest) (items []storage.SeriesItem, err error) { - var id uint64 - if err = app.DB().NewSelect(). - Column("id"). - Model((*storage.App)(nil)). - Where("slug = ?", slug). - Limit(1). - Scan(ctx, &id); err != nil { - return - } - - var appIds []storage.AppId - if err = app.DB().NewSelect(). - Model(&appIds). - Where("app_id = ?", id). - Scan(ctx); err != nil { - return - } - - if len(appIds) == 0 { - return - } - - query := app.DB().NewSelect().Order("time desc").Limit(100).Group("time") - - switch timeframe { - case storage.TimeframeHour: - query = query.Table("app_stats_by_hour") - case storage.TimeframeDay: - query = query.Table("app_stats_by_day") - case storage.TimeframeMonth: - query = query.Table("app_stats_by_month") - default: - return nil, errors.Errorf("invalid timeframe: %s", timeframe) - } - - switch column { - case "actions_count": - query = query.ColumnExpr("sum(actions_count) as value, time as ts") - case "size": - query = query.ColumnExpr("sum(size) as value, time as ts") - case "size_per_action": - query = query.ColumnExpr("(sum(size) / sum(actions_count)) as value, time as ts") - default: - return nil, errors.Errorf("invalid column: %s", column) - } - - if !req.From.IsZero() { - query = query.Where("time >= ?", req.From) - } - if !req.To.IsZero() { - query = query.Where("time < ?", req.To) - } - - query.WhereGroup(" AND ", func(sq *bun.SelectQuery) *bun.SelectQuery { - for i := range appIds { - sq = sq.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { - return q.Where("rollup_id = ?", appIds[i].RolllupId).Where("sender_id = ?", appIds[i].AddressId) - }) - } - - return sq - }) - - err = query.Scan(ctx, &items) - - return -} diff --git a/internal/storage/postgres/app_test.go b/internal/storage/postgres/app_test.go index 4a5cd00..a6c71a1 100644 --- a/internal/storage/postgres/app_test.go +++ b/internal/storage/postgres/app_test.go @@ -39,8 +39,10 @@ func (s *StorageTestSuite) TestLeaderboard() { s.Require().EqualValues(1, app.ActionsCount, column) s.Require().False(app.LastActionTime.IsZero()) s.Require().False(app.FirstActionTime.IsZero()) - s.Require().EqualValues(1, app.ActionsCountPct) - s.Require().EqualValues(1, app.SizePct) + s.Require().NotNil(app.Bridge) + s.Require().EqualValues("astria1lm45urgugesyhaymn68xww0m6g49zreqa32w7p", app.Bridge.Hash) + s.Require().NotNil(app.Rollup) + s.Require().EqualValues("19ba8abb3e4b56a309df6756c47b97e298e3a72d88449d36a0fadb1ca7366539", hex.EncodeToString(app.Rollup.AstriaId)) } } @@ -71,8 +73,10 @@ func (s *StorageTestSuite) TestLeaderboardWithCategory() { s.Require().EqualValues(1, app.ActionsCount, column) s.Require().False(app.LastActionTime.IsZero()) s.Require().False(app.FirstActionTime.IsZero()) - s.Require().EqualValues(1, app.ActionsCountPct) - s.Require().EqualValues(1, app.SizePct) + s.Require().NotNil(app.Bridge) + s.Require().EqualValues("astria1lm45urgugesyhaymn68xww0m6g49zreqa32w7p", app.Bridge.Hash) + s.Require().NotNil(app.Rollup) + s.Require().EqualValues("19ba8abb3e4b56a309df6756c47b97e298e3a72d88449d36a0fadb1ca7366539", hex.EncodeToString(app.Rollup.AstriaId)) } } @@ -91,47 +95,8 @@ func (s *StorageTestSuite) TestAppBySlug() { s.Require().EqualValues(1, app.ActionsCount) s.Require().False(app.LastActionTime.IsZero()) s.Require().False(app.FirstActionTime.IsZero()) - s.Require().EqualValues(1, app.ActionsCountPct) - s.Require().EqualValues(1, app.SizePct) -} - -func (s *StorageTestSuite) TestAppActions() { - ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer ctxCancel() - - actions, err := s.storage.App.Actions(ctx, "app-1", 10, 0, sdk.SortOrderAsc) - s.Require().NoError(err) - s.Require().Len(actions, 1) - - action := actions[0] - s.Require().EqualValues(1, action.RollupId) - s.Require().EqualValues(1, action.ActionId) - s.Require().EqualValues(1, action.TxId) - s.Require().EqualValues(1, action.SenderId) - s.Require().EqualValues(34, action.Size) - s.Require().EqualValues(7316, action.Height) - s.Require().EqualValues("20b0e6310801e7b2a16c69aace7b1a1d550e5c49c80f546941bb1ac747487fe5", hex.EncodeToString(action.Tx.Hash)) - s.Require().EqualValues(types.ActionTypeRollupDataSubmission, action.ActionType) - s.Require().NotNil(action.Action.Data) - s.Require().NotNil(action.Action.Fee) -} - -func (s *StorageTestSuite) TestAppSeries() { - ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer ctxCancel() - - for _, tf := range []storage.Timeframe{ - "day", "hour", "month", - } { - for _, column := range []string{ - "size", "actions_count", "size_per_action", - } { - series, err := s.storage.App.Series(ctx, "app-1", tf, column, storage.SeriesRequest{ - From: time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC), - }) - s.Require().NoError(err, column, tf) - s.Require().Len(series, 1, column, tf) - - } - } + s.Require().NotNil(app.Bridge) + s.Require().EqualValues("astria1lm45urgugesyhaymn68xww0m6g49zreqa32w7p", app.Bridge.Hash) + s.Require().NotNil(app.Rollup) + s.Require().EqualValues("19ba8abb3e4b56a309df6756c47b97e298e3a72d88449d36a0fadb1ca7366539", hex.EncodeToString(app.Rollup.AstriaId)) } diff --git a/internal/storage/postgres/core.go b/internal/storage/postgres/core.go index 2abeb2f..13c011d 100644 --- a/internal/storage/postgres/core.go +++ b/internal/storage/postgres/core.go @@ -7,14 +7,12 @@ import ( "context" models "github.com/celenium-io/astria-indexer/internal/storage" - "github.com/celenium-io/astria-indexer/internal/storage/postgres/migrations" "github.com/dipdup-net/go-lib/config" "github.com/dipdup-net/go-lib/database" "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" "github.com/pkg/errors" "github.com/uptrace/bun" - "github.com/uptrace/bun/migrate" "go.opentelemetry.io/otel/trace" ) @@ -104,8 +102,6 @@ func initDatabase(ctx context.Context, conn *database.Bun) error { (*models.RollupAction)(nil), (*models.RollupAddress)(nil), (*models.AddressAction)(nil), - (*models.AppId)(nil), - (*models.AppBridge)(nil), ) if err := database.CreateTables(ctx, conn, models.Models...); err != nil { @@ -139,18 +135,19 @@ func initDatabaseWithMigrations(ctx context.Context, conn *database.Bun) error { return migrateDatabase(ctx, conn) } -func migrateDatabase(ctx context.Context, db *database.Bun) error { - migrator := migrate.NewMigrator(db.DB(), migrations.Migrations) - if err := migrator.Init(ctx); err != nil { - return err - } - if err := migrator.Lock(ctx); err != nil { - return err - } - defer migrator.Unlock(ctx) //nolint:errcheck - - _, err := migrator.Migrate(ctx) - return err +func migrateDatabase(_ context.Context, _ *database.Bun) error { + // migrator := migrate.NewMigrator(db.DB(), migrations.Migrations) + // if err := migrator.Init(ctx); err != nil { + // return err + // } + // if err := migrator.Lock(ctx); err != nil { + // return err + // } + // defer migrator.Unlock(ctx) //nolint:errcheck + + // _, err := migrator.Migrate(ctx) + // return err + return nil } func createHypertables(ctx context.Context, conn *database.Bun) error { diff --git a/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql b/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql deleted file mode 100644 index 6626485..0000000 --- a/internal/storage/postgres/migrations/20241106_rollup_action_sender_id.up.sql +++ /dev/null @@ -1,16 +0,0 @@ -ALTER TABLE IF EXISTS public.rollup_action ADD COLUMN IF NOT EXISTS sender_id int8 NOT NULL DEFAULT 0; - ---bun:split - -COMMENT ON COLUMN public.rollup_action.sender_id IS 'Internal id of sender address'; - ---bun:split - -with actions as ( - select signer_id, rollup_action.tx_id from rollup_action - left join tx on tx_id = tx.id -) -update rollup_action as ra -set sender_id = actions.signer_id -from actions -where ra.tx_id = actions.tx_id; diff --git a/internal/storage/postgres/migrations/migrations.go b/internal/storage/postgres/migrations/migrations.go deleted file mode 100644 index cb6412b..0000000 --- a/internal/storage/postgres/migrations/migrations.go +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2024 PK Lab AG -// SPDX-License-Identifier: MIT - -package migrations - -import ( - "embed" - - "github.com/uptrace/bun/migrate" -) - -var Migrations = migrate.NewMigrations() - -//go:embed *.sql -var sqlMigrations embed.FS - -func init() { - if err := Migrations.Discover(sqlMigrations); err != nil { - panic(err) - } -} diff --git a/internal/storage/postgres/transaction.go b/internal/storage/postgres/transaction.go index 3c8f286..dd16dbd 100644 --- a/internal/storage/postgres/transaction.go +++ b/internal/storage/postgres/transaction.go @@ -575,46 +575,11 @@ func (tx Transaction) UpdateApp(ctx context.Context, app *models.App) error { if app.VM != "" { query = query.Set("vm = ?", app.VM) } - - _, err := query.Exec(ctx) - return err -} - -func (tx Transaction) SaveAppId(ctx context.Context, ids ...models.AppId) error { - if len(ids) == 0 { - return nil + if app.RollupId > 0 { + query = query.Set("rollup_id = ?", app.RollupId) } - _, err := tx.Tx().NewInsert().Model(&ids).Exec(ctx) - return err -} -func (tx Transaction) DeleteAppId(ctx context.Context, appId uint64) error { - if appId == 0 { - return nil - } - _, err := tx.Tx().NewDelete(). - Model((*models.AppId)(nil)). - Where("app_id = ?", appId). - Exec(ctx) - return err -} - -func (tx Transaction) SaveAppBridges(ctx context.Context, bridges ...models.AppBridge) error { - if len(bridges) == 0 { - return nil - } - _, err := tx.Tx().NewInsert().Model(&bridges).Exec(ctx) - return err -} - -func (tx Transaction) DeleteAppBridges(ctx context.Context, appId uint64) error { - if appId == 0 { - return nil - } - _, err := tx.Tx().NewDelete(). - Model((*models.AppBridge)(nil)). - Where("app_id = ?", appId). - Exec(ctx) + _, err := query.Exec(ctx) return err } diff --git a/internal/storage/rollup_action.go b/internal/storage/rollup_action.go index c9a4ea1..56f60ac 100644 --- a/internal/storage/rollup_action.go +++ b/internal/storage/rollup_action.go @@ -17,16 +17,14 @@ type RollupAction struct { RollupId uint64 `bun:"rollup_id,pk" comment:"Rollup internal id"` ActionId uint64 `bun:"action_id,pk" comment:"Action internal id"` Time time.Time `bun:"time,notnull,pk" comment:"Action time"` - SenderId uint64 `bun:"sender_id,notnull" comment:"Internal id of sender address"` ActionType types.ActionType `bun:"action_type,type:action_type" comment:"Action type"` Height pkgTypes.Level `bun:"height" comment:"Action block height"` TxId uint64 `bun:"tx_id" comment:"Transaction internal id"` Size int64 `bun:"size" comment:"Count bytes which was pushed to the rollup"` - Action *Action `bun:"rel:belongs-to,join:action_id=id"` - Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` - Tx *Tx `bun:"rel:belongs-to,join:tx_id=id"` - Sender *Address `bun:"rel:belongs-to,join:sender_id=id"` + Action *Action `bun:"rel:belongs-to,join:action_id=id"` + Rollup *Rollup `bun:"rel:belongs-to,join:rollup_id=id"` + Tx *Tx `bun:"rel:belongs-to,join:tx_id=id"` } func (RollupAction) TableName() string { diff --git a/pkg/indexer/storage/storage.go b/pkg/indexer/storage/storage.go index 9ecc51b..4273cee 100644 --- a/pkg/indexer/storage/storage.go +++ b/pkg/indexer/storage/storage.go @@ -195,9 +195,6 @@ func (module *Module) processBlockInTransaction(ctx context.Context, tx storage. for i := range block.Txs { for j := range block.Txs[i].Actions { block.Txs[i].Actions[j].TxId = block.Txs[i].Id - if block.Txs[i].Actions[j].RollupAction != nil { - block.Txs[i].Actions[j].RollupAction.SenderId = block.Txs[i].SignerId - } actions = append(actions, &block.Txs[i].Actions[j]) } } diff --git a/test/data/app.yml b/test/data/app.yml index ccfc615..3005327 100644 --- a/test/data/app.yml +++ b/test/data/app.yml @@ -13,4 +13,6 @@ category: social provider: best vm: SVM - slug: app-1 \ No newline at end of file + slug: app-1 + rollup_id: 1 + native_bridge_id: 1 \ No newline at end of file diff --git a/test/data/app_bridge.yml b/test/data/app_bridge.yml deleted file mode 100644 index e798b45..0000000 --- a/test/data/app_bridge.yml +++ /dev/null @@ -1,3 +0,0 @@ -- app_id: 1 - bridge_id: 1 - native: true \ No newline at end of file diff --git a/test/data/app_id.yaml b/test/data/app_id.yaml deleted file mode 100644 index 535e540..0000000 --- a/test/data/app_id.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- app_id: 1 - rollup_id: 1 - address_id: 1 \ No newline at end of file diff --git a/test/data/rollup_action.yml b/test/data/rollup_action.yml index 8e300d4..45fc10c 100644 --- a/test/data/rollup_action.yml +++ b/test/data/rollup_action.yml @@ -3,6 +3,5 @@ time: '2023-11-30T23:52:23.265Z' height: 7316 tx_id: 1 - sender_id: 1 size: 34 action_type: rollup_data_submission