diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 178e476e..609fa92e 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -2727,6 +2727,12 @@ "description": "Comma-separated rollup category list", "name": "category", "in": "query" + }, + { + "type": "string", + "description": "Comma-separated rollup tags list", + "name": "tags", + "in": "query" } ], "responses": { @@ -2837,6 +2843,12 @@ "description": "Comma-separated rollup category list", "name": "category", "in": "query" + }, + { + "type": "string", + "description": "Comma-separated rollup tags list", + "name": "tags", + "in": "query" } ], "responses": { @@ -5689,6 +5701,12 @@ "responses.Enums": { "type": "object", "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, "event_type": { "type": "array", "items": { @@ -5712,6 +5730,12 @@ "items": { "type": "string" } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -6284,6 +6308,11 @@ "format": "string", "example": "https://github.com/account" }, + "category": { + "type": "string", + "format": "string", + "example": "nft" + }, "compression": { "type": "string", "format": "string", @@ -6496,6 +6525,11 @@ "format": "string", "example": "https://github.com/account" }, + "category": { + "type": "string", + "format": "string", + "example": "nft" + }, "compression": { "type": "string", "format": "string", diff --git a/cmd/api/handler/constant.go b/cmd/api/handler/constant.go index 20255e4a..79d3ef99 100644 --- a/cmd/api/handler/constant.go +++ b/cmd/api/handler/constant.go @@ -14,18 +14,18 @@ import ( type ConstantHandler struct { constants storage.IConstant denomMetadata storage.IDenomMetadata - address storage.IAddress + rollup storage.IRollup } func NewConstantHandler( constants storage.IConstant, denomMetadata storage.IDenomMetadata, - address storage.IAddress, + rollup storage.IRollup, ) *ConstantHandler { return &ConstantHandler{ constants: constants, denomMetadata: denomMetadata, - address: address, + rollup: rollup, } } @@ -43,11 +43,11 @@ func NewConstantHandler( func (handler *ConstantHandler) Get(c echo.Context) error { consts, err := handler.constants.All(c.Request().Context()) if err != nil { - return handleError(c, err, handler.address) + return handleError(c, err, handler.rollup) } dm, err := handler.denomMetadata.All(c.Request().Context()) if err != nil { - return handleError(c, err, handler.address) + return handleError(c, err, handler.rollup) } return c.JSON(http.StatusOK, responses.NewConstants(consts, dm)) } @@ -62,5 +62,9 @@ func (handler *ConstantHandler) Get(c echo.Context) error { // @Success 200 {object} responses.Enums // @Router /enums [get] func (handler *ConstantHandler) Enums(c echo.Context) error { - return c.JSON(http.StatusOK, responses.NewEnums()) + tags, err := handler.rollup.Tags(c.Request().Context()) + if err != nil { + return handleError(c, err, handler.rollup) + } + return c.JSON(http.StatusOK, responses.NewEnums(tags)) } diff --git a/cmd/api/handler/constant_test.go b/cmd/api/handler/constant_test.go index 7184f5e7..cd9ba00c 100644 --- a/cmd/api/handler/constant_test.go +++ b/cmd/api/handler/constant_test.go @@ -22,7 +22,7 @@ type ConstantTestSuite struct { suite.Suite constants *mock.MockIConstant denomMetadata *mock.MockIDenomMetadata - address *mock.MockIAddress + rollup *mock.MockIRollup echo *echo.Echo handler *ConstantHandler ctrl *gomock.Controller @@ -35,7 +35,8 @@ func (s *ConstantTestSuite) SetupSuite() { s.ctrl = gomock.NewController(s.T()) s.constants = mock.NewMockIConstant(s.ctrl) s.denomMetadata = mock.NewMockIDenomMetadata(s.ctrl) - s.address = mock.NewMockIAddress(s.ctrl) + s.rollup = mock.NewMockIRollup(s.ctrl) + s.handler = NewConstantHandler(s.constants, s.denomMetadata, s.rollup) } // TearDownSuite - @@ -54,6 +55,11 @@ func (s *ConstantTestSuite) TestEnums() { c := s.echo.NewContext(req, rec) c.SetPath("/enums") + s.rollup.EXPECT(). + Tags(gomock.Any()). + Return([]string{"ai", "zk"}, nil). + Times(1) + s.Require().NoError(s.handler.Enums(c)) s.Require().Equal(http.StatusOK, rec.Code) @@ -63,4 +69,6 @@ func (s *ConstantTestSuite) TestEnums() { s.Require().Len(enums.EventType, 59) s.Require().Len(enums.MessageType, 76) s.Require().Len(enums.Status, 2) + s.Require().Len(enums.Categories, 5) + s.Require().Len(enums.Tags, 2) } diff --git a/cmd/api/handler/responses/constants.go b/cmd/api/handler/responses/constants.go index ea1a4f67..4a80483c 100644 --- a/cmd/api/handler/responses/constants.go +++ b/cmd/api/handler/responses/constants.go @@ -69,14 +69,18 @@ type Enums struct { Status []string `json:"status"` MessageType []string `json:"message_type"` EventType []string `json:"event_type"` + Categories []string `json:"categories"` RollupTypes []string `json:"rollup_type"` + Tags []string `json:"tags"` } -func NewEnums() Enums { +func NewEnums(tags []string) Enums { return Enums{ Status: types.StatusNames(), MessageType: types.MsgTypeNames(), EventType: types.EventTypeNames(), + Categories: types.RollupCategoryNames(), RollupTypes: types.RollupTypeNames(), + Tags: tags, } } diff --git a/cmd/api/handler/responses/rollup.go b/cmd/api/handler/responses/rollup.go index 2431c736..5a332cf2 100644 --- a/cmd/api/handler/responses/rollup.go +++ b/cmd/api/handler/responses/rollup.go @@ -67,6 +67,7 @@ func NewRollupWithStats(r storage.RollupWithStats) RollupWithStats { LastAction: r.LastActionTime, FirstAction: r.FirstActionTime, Compression: r.Compression, + Category: r.Category.String(), Type: r.Type.String(), Provider: r.Provider, VM: r.VM, @@ -90,6 +91,7 @@ type Rollup struct { BridgeContract string `example:"https://github.com/account" format:"string" json:"bridge,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"` Compression string `example:"zip" format:"string" json:"compression,omitempty" swaggertype:"string"` VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` @@ -115,6 +117,7 @@ func NewRollup(r *storage.Rollup) Rollup { Explorer: r.Explorer, Links: r.Links, Compression: r.Compression, + Category: r.Category.String(), Type: r.Type.String(), Provider: r.Provider, VM: r.VM, @@ -156,6 +159,7 @@ type RollupWithDayStats struct { BridgeContract string `example:"https://github.com/account" format:"string" json:"bridge,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"` Compression string `example:"zip" format:"string" json:"compression,omitempty" swaggertype:"string"` VM string `example:"evm" format:"string" json:"vm,omitempty" swaggertype:"string"` @@ -187,6 +191,7 @@ func NewRollupWithDayStats(r storage.RollupWithDayStats) RollupWithDayStats { BridgeContract: r.BridgeContract, Stack: r.Stack, Compression: r.Compression, + Category: r.Category.String(), Type: r.Type.String(), Provider: r.Provider, VM: r.VM, diff --git a/cmd/api/handler/rollup.go b/cmd/api/handler/rollup.go index 05d97a34..0865ec24 100644 --- a/cmd/api/handler/rollup.go +++ b/cmd/api/handler/rollup.go @@ -33,12 +33,13 @@ func NewRollupHandler( } type rollupList 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 blobs_count size fee"` - Tags StringArray `query:"tags" validate:"omitempty"` - Type StringArray `query:"type" validate:"omitempty,dive,type"` + 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 blobs_count size fee"` + Tags StringArray `query:"tags" validate:"omitempty"` + Category StringArray `query:"category" validate:"omitempty,dive,category"` + Type StringArray `query:"type" validate:"omitempty,dive,type"` } func (p *rollupList) SetDefault() { @@ -64,6 +65,7 @@ func (p *rollupList) SetDefault() { // @Param sort query string false "Sort order. Default: desc" Enums(asc, desc) // @Param sort_by query string false "Sort field. Default: size" Enums(time, blobs_count, size, fee) // @Param category query string false "Comma-separated rollup category list" +// @Param tags query string false "Comma-separated rollup tags list" // @Produce json // @Success 200 {array} responses.RollupWithStats // @Failure 400 {object} Error @@ -81,11 +83,17 @@ func (handler RollupHandler) Leaderboard(c echo.Context) error { rollupTypes[i] = types.RollupType(req.Type[i]) } + categories := make([]types.RollupCategory, len(req.Category)) + for i := range categories { + categories[i] = types.RollupCategory(req.Category[i]) + } + rollups, err := handler.rollups.Leaderboard(c.Request().Context(), storage.LeaderboardFilters{ SortField: req.SortBy, Sort: pgSort(req.Sort), Limit: req.Limit, Offset: req.Offset, + Category: categories, Tags: req.Tags, Type: rollupTypes, }) @@ -100,12 +108,13 @@ func (handler RollupHandler) Leaderboard(c echo.Context) error { } type rollupDayList 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=avg_size blobs_count total_size total_fee throughput namespace_count pfb_count mb_price"` - Tags StringArray `query:"tags" validate:"omitempty"` - Type StringArray `query:"type" validate:"omitempty,dive,type"` + 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=avg_size blobs_count total_size total_fee throughput namespace_count pfb_count mb_price"` + Category StringArray `query:"category" validate:"omitempty,dive,category"` + Tags StringArray `query:"tags" validate:"omitempty"` + Type StringArray `query:"type" validate:"omitempty,dive,type"` } func (p *rollupDayList) SetDefault() { @@ -131,6 +140,7 @@ func (p *rollupDayList) SetDefault() { // @Param sort query string false "Sort order. Default: desc" Enums(asc, desc) // @Param sort_by query string false "Sort field. Default: mb_price" Enums(avg_size, blobs_count, total_size, total_fee, throughput, namespace_count, pfb_count, mb_price) // @Param category query string false "Comma-separated rollup category list" +// @Param tags query string false "Comma-separated rollup tags list" // @Produce json // @Success 200 {array} responses.RollupWithDayStats // @Failure 400 {object} Error @@ -148,12 +158,18 @@ func (handler RollupHandler) LeaderboardDay(c echo.Context) error { rollupTypes[i] = types.RollupType(req.Type[i]) } + categories := make([]types.RollupCategory, len(req.Category)) + for i := range categories { + categories[i] = types.RollupCategory(req.Category[i]) + } + rollups, err := handler.rollups.LeaderboardDay(c.Request().Context(), storage.LeaderboardFilters{ SortField: req.SortBy, Sort: pgSort(req.Sort), Limit: req.Limit, Offset: req.Offset, Tags: req.Tags, + Category: categories, Type: rollupTypes, }) if err != nil { diff --git a/cmd/api/handler/rollup_auth.go b/cmd/api/handler/rollup_auth.go index 7632af60..4a04fa72 100644 --- a/cmd/api/handler/rollup_auth.go +++ b/cmd/api/handler/rollup_auth.go @@ -50,6 +50,7 @@ type createRollupRequest struct { 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,category"` Tags []string `json:"tags" validate:"omitempty"` Type string `json:"type" validate:"omitempty,oneof=settled sovereign"` Compression string `json:"compression" validate:"omitempty"` @@ -99,6 +100,7 @@ func (handler RollupAuthHandler) createRollup(ctx context.Context, req *createRo Provider: req.Provider, VM: req.VM, Type: enums.RollupType(req.Type), + Category: enums.RollupCategory(req.Category), Slug: slug.Make(req.Name), Tags: req.Tags, } @@ -165,6 +167,7 @@ type updateRollupRequest struct { Bridge string `json:"bridge" validate:"omitempty,eth_addr"` Explorer string `json:"explorer" validate:"omitempty,url"` Stack string `json:"stack" validate:"omitempty"` + Category string `json:"category" validate:"omitempty,category"` Type string `json:"type" validate:"omitempty,oneof=settled sovereign"` Compression string `json:"compression" validate:"omitempty"` Provider string `json:"provider" validate:"omitempty"` @@ -215,6 +218,7 @@ func (handler RollupAuthHandler) updateRollup(ctx context.Context, req *updateRo Provider: req.Provider, VM: req.VM, Type: enums.RollupType(req.Type), + Category: enums.RollupCategory(req.Category), Links: req.Links, Tags: req.Tags, } diff --git a/cmd/api/handler/rollup_test.go b/cmd/api/handler/rollup_test.go index d6631868..11df6c85 100644 --- a/cmd/api/handler/rollup_test.go +++ b/cmd/api/handler/rollup_test.go @@ -32,7 +32,8 @@ var ( Twitter: "https://x.com", Logo: "image.png", Slug: "test-rollup", - Tags: []string{"nft"}, + Tags: []string{"ai"}, + Category: types.RollupCategoryNft, Type: types.RollupTypeSettled, } testRollupWithStats = storage.RollupWithStats{ @@ -96,8 +97,9 @@ func (s *RollupTestSuite) TestLeaderboard() { } { q := make(url.Values) q.Add("sort_by", sort) - q.Add("tags", "nft,gaming") q.Add("type", "sovereign") + q.Add("category", "nft,gaming") + q.Add("tags", "ai") req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) rec := httptest.NewRecorder() @@ -110,13 +112,14 @@ func (s *RollupTestSuite) TestLeaderboard() { Sort: sdk.SortOrderDesc, Limit: 10, Offset: 0, - Tags: []string{ - "nft", - "gaming", + Category: []types.RollupCategory{ + types.RollupCategoryNft, + types.RollupCategoryGaming, }, Type: []types.RollupType{ types.RollupTypeSovereign, }, + Tags: []string{"ai"}, }). Return([]storage.RollupWithStats{testRollupWithStats}, nil). Times(1) @@ -150,8 +153,9 @@ func (s *RollupTestSuite) TestLeaderboardDay() { } { q := make(url.Values) q.Add("sort_by", sort) - q.Add("tags", "nft,gaming") + q.Add("category", "nft,gaming") q.Add("type", "sovereign") + q.Add("tags", "ai") req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) rec := httptest.NewRecorder() @@ -164,13 +168,14 @@ func (s *RollupTestSuite) TestLeaderboardDay() { Sort: sdk.SortOrderDesc, Limit: 10, Offset: 0, - Tags: []string{ - "nft", - "gaming", + Category: []types.RollupCategory{ + types.RollupCategoryNft, + types.RollupCategoryGaming, }, Type: []types.RollupType{ types.RollupTypeSovereign, }, + Tags: []string{"ai"}, }). Return([]storage.RollupWithDayStats{ { diff --git a/cmd/api/handler/validators.go b/cmd/api/handler/validators.go index 1fab1a77..5c592292 100644 --- a/cmd/api/handler/validators.go +++ b/cmd/api/handler/validators.go @@ -32,6 +32,9 @@ func NewCelestiaApiValidator() *CelestiaApiValidator { if err := v.RegisterValidation("namespace", namespaceValidator()); err != nil { panic(err) } + if err := v.RegisterValidation("category", categoryValidator()); err != nil { + panic(err) + } if err := v.RegisterValidation("type", typeValidator()); err != nil { panic(err) } @@ -103,6 +106,13 @@ func namespaceValidator() validator.Func { } } +func categoryValidator() validator.Func { + return func(fl validator.FieldLevel) bool { + _, err := types.ParseRollupCategory(fl.Field().String()) + return err == nil + } +} + func typeValidator() validator.Func { return func(fl validator.FieldLevel) bool { _, err := types.ParseRollupType(fl.Field().String()) diff --git a/cmd/api/init.go b/cmd/api/init.go index 73742ad9..ef28c0b5 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -273,7 +273,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto stateHandlers := handler.NewStateHandler(db.State, db.Validator, cfg.Indexer.Name) v1.GET("/head", stateHandlers.Head) - constantsHandler := handler.NewConstantHandler(db.Constants, db.DenomMetadata, db.Address) + constantsHandler := handler.NewConstantHandler(db.Constants, db.DenomMetadata, db.Rollup) v1.GET("/constants", constantsHandler.Get) v1.GET("/enums", constantsHandler.Enums) diff --git a/internal/storage/mock/rollup.go b/internal/storage/mock/rollup.go index 7ee5f838..f2db2567 100644 --- a/internal/storage/mock/rollup.go +++ b/internal/storage/mock/rollup.go @@ -744,6 +744,45 @@ func (c *MockIRollupSeriesCall) DoAndReturn(f func(context.Context, uint64, stri return c } +// Tags mocks base method. +func (m *MockIRollup) Tags(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tags", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Tags indicates an expected call of Tags. +func (mr *MockIRollupMockRecorder) Tags(ctx any) *MockIRollupTagsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tags", reflect.TypeOf((*MockIRollup)(nil).Tags), ctx) + return &MockIRollupTagsCall{Call: call} +} + +// MockIRollupTagsCall wrap *gomock.Call +type MockIRollupTagsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIRollupTagsCall) Return(arg0 []string, arg1 error) *MockIRollupTagsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIRollupTagsCall) Do(f func(context.Context) ([]string, error)) *MockIRollupTagsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIRollupTagsCall) DoAndReturn(f func(context.Context) ([]string, error)) *MockIRollupTagsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Update mocks base method. func (m_2 *MockIRollup) Update(ctx context.Context, m *storage.Rollup) error { m_2.ctrl.T.Helper() diff --git a/internal/storage/postgres/custom_types.go b/internal/storage/postgres/custom_types.go index c62bfecc..215bbb0e 100644 --- a/internal/storage/postgres/custom_types.go +++ b/internal/storage/postgres/custom_types.go @@ -104,6 +104,16 @@ func createTypes(ctx context.Context, conn *database.Bun) error { ); err != nil { return err } + + if _, err := tx.ExecContext( + ctx, + createTypeQuery, + "rollup_category", + bun.Safe("rollup_category"), + bun.In(types.RollupCategoryValues()), + ); err != nil { + return err + } return nil }) } diff --git a/internal/storage/postgres/migrations/20250102_rollup_tags.up.sql b/internal/storage/postgres/migrations/20250102_rollup_tags.up.sql index 3095f18d..c6a22121 100644 --- a/internal/storage/postgres/migrations/20250102_rollup_tags.up.sql +++ b/internal/storage/postgres/migrations/20250102_rollup_tags.up.sql @@ -6,10 +6,6 @@ COMMENT ON COLUMN public."rollup".tags IS 'Rollup tags'; --bun:split -UPDATE rollup SET tags = ARRAY[category] WHERE category is not NULL; - ---bun:split - REFRESH MATERIALIZED VIEW leaderboard; --bun:split diff --git a/internal/storage/postgres/rollup.go b/internal/storage/postgres/rollup.go index 03f2bc48..b446abb8 100644 --- a/internal/storage/postgres/rollup.go +++ b/internal/storage/postgres/rollup.go @@ -42,6 +42,10 @@ func (r *Rollup) Leaderboard(ctx context.Context, fltrs storage.LeaderboardFilte ColumnExpr("*"). Offset(fltrs.Offset) + if len(fltrs.Category) > 0 { + query = query.Where("category IN (?)", bun.In(fltrs.Category)) + } + if len(fltrs.Tags) > 0 { query = query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { for i := range fltrs.Tags { @@ -79,6 +83,10 @@ func (r *Rollup) LeaderboardDay(ctx context.Context, fltrs storage.LeaderboardFi Offset(fltrs.Offset). Join("left join rollup on rollup.id = rollup_id") + if len(fltrs.Category) > 0 { + query = query.Where("category IN (?)", bun.In(fltrs.Category)) + } + if len(fltrs.Tags) > 0 { query = query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { for i := range fltrs.Tags { @@ -339,3 +347,12 @@ func (r *Rollup) RollupStatsGrouping(ctx context.Context, fltrs storage.RollupGr err = query.Scan(ctx, &results) return } + +func (r *Rollup) Tags(ctx context.Context) (arr []string, err error) { + err = r.DB().NewSelect(). + Model((*storage.Rollup)(nil)). + Distinct(). + ColumnExpr("unnest(tags)"). + Scan(ctx, &arr) + return +} diff --git a/internal/storage/postgres/rollup_test.go b/internal/storage/postgres/rollup_test.go index 8022be23..add46a0b 100644 --- a/internal/storage/postgres/rollup_test.go +++ b/internal/storage/postgres/rollup_test.go @@ -46,7 +46,7 @@ func (s *StorageTestSuite) TestRollupLeaderboard() { } } -func (s *StorageTestSuite) TestRollupLeaderboardWithTags() { +func (s *StorageTestSuite) TestRollupLeaderboardWithCategory() { ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) defer ctxCancel() @@ -62,7 +62,7 @@ func (s *StorageTestSuite) TestRollupLeaderboardWithTags() { Sort: sdk.SortOrderDesc, Limit: 10, Offset: 0, - Tags: []string{"nft"}, + Category: []types.RollupCategory{types.RollupCategoryNft}, }) s.Require().NoError(err, column) s.Require().Len(rollups, 1, column) @@ -72,14 +72,53 @@ func (s *StorageTestSuite) TestRollupLeaderboardWithTags() { s.Require().EqualValues("The third", rollup.Description, column) s.Require().EqualValues(34, rollup.Size, column) s.Require().EqualValues(3, rollup.BlobsCount, column) - s.Require().False(rollup.LastActionTime.IsZero()) - s.Require().False(rollup.FirstActionTime.IsZero()) - s.Require().Equal("7000", rollup.Fee.String()) - s.Require().EqualValues(0.6363636363636364, rollup.FeePct) - s.Require().EqualValues(0.42857142857142855, rollup.BlobsCountPct) - s.Require().EqualValues(0.3953488372093023, rollup.SizePct) - s.Require().Len(rollup.Tags, 1) - s.Require().EqualValues("nft", rollup.Tags[0]) + s.Require().False(rollup.LastActionTime.IsZero(), column) + s.Require().False(rollup.FirstActionTime.IsZero(), column) + s.Require().Equal("7000", rollup.Fee.String(), column) + s.Require().EqualValues(0.6363636363636364, rollup.FeePct, column) + s.Require().EqualValues(0.42857142857142855, rollup.BlobsCountPct, column) + s.Require().EqualValues(0.3953488372093023, rollup.SizePct, column) + s.Require().Len(rollup.Tags, 2, column) + s.Require().Contains(rollup.Tags, "zk", column) + s.Require().Contains(rollup.Tags, "ai", column) + } +} + +func (s *StorageTestSuite) TestRollupLeaderboardWithTags() { + 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{ + sizeColumn, blobsCountColumn, timeColumn, feeColumn, "", + } { + + rollups, err := s.storage.Rollup.Leaderboard(ctx, storage.LeaderboardFilters{ + SortField: column, + Sort: sdk.SortOrderDesc, + Limit: 10, + Offset: 0, + Tags: []string{"zk"}, + }) + s.Require().NoError(err, column) + s.Require().Len(rollups, 2, column) + + rollup := rollups[0] + s.Require().EqualValues("Rollup 3", rollup.Name, column) + s.Require().EqualValues("The third", rollup.Description, column) + s.Require().EqualValues(34, rollup.Size, column) + s.Require().EqualValues(3, rollup.BlobsCount, column) + s.Require().False(rollup.LastActionTime.IsZero(), column) + s.Require().False(rollup.FirstActionTime.IsZero(), column) + s.Require().Equal("7000", rollup.Fee.String(), column) + s.Require().EqualValues(0.6363636363636364, rollup.FeePct, column) + s.Require().EqualValues(0.42857142857142855, rollup.BlobsCountPct, column) + s.Require().EqualValues(0.3953488372093023, rollup.SizePct, column) + s.Require().Len(rollup.Tags, 2, column) + s.Require().Contains(rollup.Tags, "zk", column) + s.Require().Contains(rollup.Tags, "ai", column) } } @@ -284,3 +323,14 @@ func (s *StorageTestSuite) TestRollupStatsGrouping() { s.Require().EqualValues(4, rollup.BlobsCount, column) s.Require().EqualValues("OP Stack", rollup.Group, column) } + +func (s *StorageTestSuite) TestRollupTags() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + tags, err := s.storage.Rollup.Tags(ctx) + s.Require().NoError(err) + s.Require().Len(tags, 2) + s.Require().Contains(tags, "zk") + s.Require().Contains(tags, "ai") +} diff --git a/internal/storage/postgres/transaction.go b/internal/storage/postgres/transaction.go index 25a615fd..7f1fd79e 100644 --- a/internal/storage/postgres/transaction.go +++ b/internal/storage/postgres/transaction.go @@ -647,6 +647,9 @@ func (tx Transaction) UpdateRollup(ctx context.Context, rollup *models.Rollup) e if rollup.Type != "" { query = query.Set("type = ?", rollup.Type) } + if rollup.Category != "" { + query = query.Set("category = ?", rollup.Category) + } if rollup.Tags != nil { query = query.Set("tags = ?", pq.Array(rollup.Tags)) } diff --git a/internal/storage/postgres/transaction_test.go b/internal/storage/postgres/transaction_test.go index a1dc293f..dc2e2bad 100644 --- a/internal/storage/postgres/transaction_test.go +++ b/internal/storage/postgres/transaction_test.go @@ -749,6 +749,7 @@ func (s *TransactionTestSuite) TestSaveUpdateAndDeleteRollup() { Explorer: testLink, Stack: "stack", Type: types.RollupTypeSettled, + Category: types.RollupCategoryFinance, Tags: []string{"tag"}, Links: []string{testLink}, } @@ -777,6 +778,7 @@ func (s *TransactionTestSuite) TestSaveUpdateAndDeleteRollup() { s.Require().EqualValues(testLink, newRollup.BridgeContract) s.Require().EqualValues("test", newRollup.DeFiLama) s.Require().EqualValues("stack", newRollup.Stack) + s.Require().EqualValues("finance", newRollup.Category) s.Require().Len(newRollup.Links, 1) s.Require().Len(newRollup.Tags, 1) diff --git a/internal/storage/rollup.go b/internal/storage/rollup.go index 9385c1fe..8e70e9c2 100644 --- a/internal/storage/rollup.go +++ b/internal/storage/rollup.go @@ -18,6 +18,7 @@ type LeaderboardFilters struct { Sort sdk.SortOrder Limit int Offset int + Category []types.RollupCategory Tags []string Type []types.RollupType } @@ -43,31 +44,33 @@ type IRollup interface { Distribution(ctx context.Context, rollupId uint64, series, groupBy string) (items []DistributionItem, err error) BySlug(ctx context.Context, slug string) (RollupWithStats, error) RollupStatsGrouping(ctx context.Context, fltrs RollupGroupStatsFilters) ([]RollupGroupedStats, error) + Tags(ctx context.Context) ([]string, error) } // Rollup - type Rollup struct { bun.BaseModel `bun:"rollup" comment:"Table with rollups."` - Id uint64 `bun:"id,pk,autoincrement" comment:"Unique internal identity"` - Name string `bun:"name" comment:"Rollup's name"` - Description string `bun:"description" comment:"Rollup's description"` - Website string `bun:"website" comment:"Website"` - GitHub string `bun:"github" comment:"Github repository"` - Twitter string `bun:"twitter" comment:"Twitter account"` - Logo string `bun:"logo" comment:"Link to rollup logo"` - Slug string `bun:"slug,unique:rollup_slug" comment:"Rollup slug"` - BridgeContract string `bun:"bridge_contract" comment:"Link to bridge contract"` - L2Beat string `bun:"l2_beat" comment:"Link to L2 Beat"` - DeFiLama string `bun:"defi_lama" comment:"DeFi Lama chain name"` - Explorer string `bun:"explorer" comment:"Link to chain explorer"` - Stack string `bun:"stack" comment:"Underlaying stack"` - Compression string `bun:"compression" comment:"Compression"` - Provider string `bun:"provider" comment:"RaaS provider"` - Type types.RollupType `bun:"type,type:rollup_type" comment:"Type of rollup: settled or sovereign"` - Tags []string `bun:"tags,array"` - VM string `bun:"vm" comment:"Virtual machine"` - Links []string `bun:"links,array" comment:"Other links to rollup related sites"` + Id uint64 `bun:"id,pk,autoincrement" comment:"Unique internal identity"` + Name string `bun:"name" comment:"Rollup's name"` + Description string `bun:"description" comment:"Rollup's description"` + Website string `bun:"website" comment:"Website"` + GitHub string `bun:"github" comment:"Github repository"` + Twitter string `bun:"twitter" comment:"Twitter account"` + Logo string `bun:"logo" comment:"Link to rollup logo"` + Slug string `bun:"slug,unique:rollup_slug" comment:"Rollup slug"` + BridgeContract string `bun:"bridge_contract" comment:"Link to bridge contract"` + L2Beat string `bun:"l2_beat" comment:"Link to L2 Beat"` + DeFiLama string `bun:"defi_lama" comment:"DeFi Lama chain name"` + Explorer string `bun:"explorer" comment:"Link to chain explorer"` + Stack string `bun:"stack" comment:"Underlaying stack"` + Compression string `bun:"compression" comment:"Compression"` + Provider string `bun:"provider" comment:"RaaS provider"` + Type types.RollupType `bun:"type,type:rollup_type" comment:"Type of rollup: settled or sovereign"` + Category types.RollupCategory `bun:"category,type:rollup_category" comment:"Category of rollup"` + Tags []string `bun:"tags,array"` + VM string `bun:"vm" comment:"Virtual machine"` + Links []string `bun:"links,array" comment:"Other links to rollup related sites"` Providers []*RollupProvider `bun:"rel:has-many,join:id=rollup_id"` } @@ -89,6 +92,7 @@ func (r Rollup) IsEmpty() bool { r.Explorer == "" && r.Stack == "" && r.Links == nil && + r.Category == "" && r.Tags == nil && r.Compression == "" && r.Provider == "" && diff --git a/internal/storage/types/rolllup.go b/internal/storage/types/rolllup.go index 0059eabf..c45409e1 100644 --- a/internal/storage/types/rolllup.go +++ b/internal/storage/types/rolllup.go @@ -3,6 +3,18 @@ package types +/* + ENUM( + uncategorized, + finance, + gaming, + nft, + social + ) +*/ +//go:generate go-enum --marshal --sql --values --names +type RollupCategory string + // swagger:enum RollupType /* ENUM( diff --git a/internal/storage/types/rolllup_enum.go b/internal/storage/types/rolllup_enum.go index ec7bc44b..cd98cabf 100644 --- a/internal/storage/types/rolllup_enum.go +++ b/internal/storage/types/rolllup_enum.go @@ -16,6 +16,130 @@ import ( "strings" ) +const ( + // RollupCategoryUncategorized is a RollupCategory of type uncategorized. + RollupCategoryUncategorized RollupCategory = "uncategorized" + // RollupCategoryFinance is a RollupCategory of type finance. + RollupCategoryFinance RollupCategory = "finance" + // RollupCategoryGaming is a RollupCategory of type gaming. + RollupCategoryGaming RollupCategory = "gaming" + // RollupCategoryNft is a RollupCategory of type nft. + RollupCategoryNft RollupCategory = "nft" + // RollupCategorySocial is a RollupCategory of type social. + RollupCategorySocial RollupCategory = "social" +) + +var ErrInvalidRollupCategory = fmt.Errorf("not a valid RollupCategory, try [%s]", strings.Join(_RollupCategoryNames, ", ")) + +var _RollupCategoryNames = []string{ + string(RollupCategoryUncategorized), + string(RollupCategoryFinance), + string(RollupCategoryGaming), + string(RollupCategoryNft), + string(RollupCategorySocial), +} + +// RollupCategoryNames returns a list of possible string values of RollupCategory. +func RollupCategoryNames() []string { + tmp := make([]string, len(_RollupCategoryNames)) + copy(tmp, _RollupCategoryNames) + return tmp +} + +// RollupCategoryValues returns a list of the values for RollupCategory +func RollupCategoryValues() []RollupCategory { + return []RollupCategory{ + RollupCategoryUncategorized, + RollupCategoryFinance, + RollupCategoryGaming, + RollupCategoryNft, + RollupCategorySocial, + } +} + +// String implements the Stringer interface. +func (x RollupCategory) 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 RollupCategory) IsValid() bool { + _, err := ParseRollupCategory(string(x)) + return err == nil +} + +var _RollupCategoryValue = map[string]RollupCategory{ + "uncategorized": RollupCategoryUncategorized, + "finance": RollupCategoryFinance, + "gaming": RollupCategoryGaming, + "nft": RollupCategoryNft, + "social": RollupCategorySocial, +} + +// ParseRollupCategory attempts to convert a string to a RollupCategory. +func ParseRollupCategory(name string) (RollupCategory, error) { + if x, ok := _RollupCategoryValue[name]; ok { + return x, nil + } + return RollupCategory(""), fmt.Errorf("%s is %w", name, ErrInvalidRollupCategory) +} + +// MarshalText implements the text marshaller method. +func (x RollupCategory) MarshalText() ([]byte, error) { + return []byte(string(x)), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *RollupCategory) UnmarshalText(text []byte) error { + tmp, err := ParseRollupCategory(string(text)) + if err != nil { + return err + } + *x = tmp + return nil +} + +var errRollupCategoryNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *RollupCategory) Scan(value interface{}) (err error) { + if value == nil { + *x = RollupCategory("") + 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 = ParseRollupCategory(v) + case []byte: + *x, err = ParseRollupCategory(string(v)) + case RollupCategory: + *x = v + case *RollupCategory: + if v == nil { + return errRollupCategoryNilPtr + } + *x = *v + case *string: + if v == nil { + return errRollupCategoryNilPtr + } + *x, err = ParseRollupCategory(*v) + default: + return errors.New("invalid type for RollupCategory") + } + + return +} + +// Value implements the driver Valuer interface. +func (x RollupCategory) Value() (driver.Value, error) { + return x.String(), nil +} + const ( // RollupTypeSovereign is a RollupType of type sovereign. RollupTypeSovereign RollupType = "sovereign" diff --git a/test/data/rollup.yml b/test/data/rollup.yml index bedf62cb..926423be 100644 --- a/test/data/rollup.yml +++ b/test/data/rollup.yml @@ -6,11 +6,12 @@ github: https://github.com/rollup1 logo: https://rollup1.com/image.png slug: rollup_1 - tags: RAW='{finance}' + tags: RAW='{zk}' stack: OP Stack type: settled defi_lama: defi links: RAW='{https://rollup1.com}' + category: finance - id: 2 name: Rollup 2 description: The second @@ -19,10 +20,11 @@ github: https://github.com/rollup2 logo: https://rollup2.com/image.png slug: rollup_2 - tags: RAW='{gaming}' + tags: RAW='{ai}' stack: OP Stack type: settled defi_lama: lama + category: gaming - id: 3 name: Rollup 3 description: The third @@ -31,7 +33,8 @@ github: https://github.com/rollup3 logo: https://rollup3.com/image.png slug: rollup_3 - tags: RAW='{nft}' + tags: RAW='{ai,zk}' stack: Custom Stack type: sovereign defi_lama: name + category: nft