From 11ec292207490bc129fa22e54004479b9619e160 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 10 Dec 2024 01:47:44 +0300 Subject: [PATCH] Feature: add assets request --- cmd/api/docs/docs.go | 81 ++++++++++++++++++++++++ cmd/api/docs/swagger.json | 81 ++++++++++++++++++++++++ cmd/api/docs/swagger.yaml | 57 +++++++++++++++++ cmd/api/handler/asset.go | 72 +++++++++++++++++++++ cmd/api/handler/asset_test.go | 84 +++++++++++++++++++++++++ cmd/api/handler/responses/asset.go | 24 +++++++ cmd/api/init.go | 6 ++ internal/storage/asset.go | 26 ++++++++ internal/storage/mock/asset.go | 83 ++++++++++++++++++++++++ internal/storage/postgres/asset.go | 50 +++++++++++++++ internal/storage/postgres/asset_test.go | 56 +++++++++++++++++ internal/storage/postgres/core.go | 2 + internal/storage/postgres/index.go | 8 +++ internal/storage/postgres/stats_test.go | 4 +- test/data/fee.yml | 10 ++- test/data/transfer.yml | 9 ++- 16 files changed, 649 insertions(+), 4 deletions(-) create mode 100644 cmd/api/handler/asset.go create mode 100644 cmd/api/handler/asset_test.go create mode 100644 cmd/api/handler/responses/asset.go create mode 100644 internal/storage/asset.go create mode 100644 internal/storage/mock/asset.go create mode 100644 internal/storage/postgres/asset.go create mode 100644 internal/storage/postgres/asset_test.go diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index bb591a6..431b706 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -763,6 +763,57 @@ const docTemplate = `{ } } }, + "/v1/asset": { + "get": { + "description": "Get assets info", + "produces": [ + "application/json" + ], + "tags": [ + "assets" + ], + "summary": "Get assets info", + "operationId": "get-asset", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.Asset" + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/block": { "get": { "description": "List blocks info", @@ -3067,6 +3118,36 @@ const docTemplate = `{ } } }, + "responses.Asset": { + "type": "object", + "properties": { + "asset": { + "type": "string", + "format": "string", + "example": "nria" + }, + "fee": { + "type": "string", + "format": "string", + "example": "1000" + }, + "fee_count": { + "type": "integer", + "format": "number", + "example": 100 + }, + "transfer_count": { + "type": "integer", + "format": "number", + "example": 100 + }, + "transferred": { + "type": "string", + "format": "string", + "example": "1000" + } + } + }, "responses.Balance": { "description": "Balance of address information", "type": "object", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index e3c9037..ecad338 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -753,6 +753,57 @@ } } }, + "/v1/asset": { + "get": { + "description": "Get assets info", + "produces": [ + "application/json" + ], + "tags": [ + "assets" + ], + "summary": "Get assets info", + "operationId": "get-asset", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/responses.Asset" + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/block": { "get": { "description": "List blocks info", @@ -3057,6 +3108,36 @@ } } }, + "responses.Asset": { + "type": "object", + "properties": { + "asset": { + "type": "string", + "format": "string", + "example": "nria" + }, + "fee": { + "type": "string", + "format": "string", + "example": "1000" + }, + "fee_count": { + "type": "integer", + "format": "number", + "example": 100 + }, + "transfer_count": { + "type": "integer", + "format": "number", + "example": 100 + }, + "transferred": { + "type": "string", + "format": "string", + "example": "1000" + } + } + }, "responses.Balance": { "description": "Balance of address information", "type": "object", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 58c6379..0310b9d 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -179,6 +179,29 @@ definitions: format: string type: string type: object + responses.Asset: + properties: + asset: + example: nria + format: string + type: string + fee: + example: "1000" + format: string + type: string + fee_count: + example: 100 + format: number + type: integer + transfer_count: + example: 100 + format: number + type: integer + transferred: + example: "1000" + format: string + type: string + type: object responses.Balance: description: Balance of address information properties: @@ -1320,6 +1343,40 @@ paths: summary: Get count of addresses in network tags: - address + /v1/asset: + get: + description: Get assets info + operationId: get-asset + parameters: + - description: Count of requested entities + in: query + maximum: 100 + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/responses.Asset' + "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 assets info + tags: + - assets /v1/block: get: description: List blocks info diff --git a/cmd/api/handler/asset.go b/cmd/api/handler/asset.go new file mode 100644 index 0000000..414f9b1 --- /dev/null +++ b/cmd/api/handler/asset.go @@ -0,0 +1,72 @@ +// 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/labstack/echo/v4" +) + +type AssetHandler struct { + asset storage.IAsset + blocks storage.IBlock +} + +func NewAssetHandler( + asset storage.IAsset, + blocks storage.IBlock, +) *AssetHandler { + return &AssetHandler{ + asset: asset, + blocks: blocks, + } +} + +type assetListRequest struct { + Limit uint64 `query:"limit" validate:"omitempty,min=1,max=100"` + Offset uint64 `query:"offset" validate:"omitempty,min=0"` +} + +func (p *assetListRequest) SetDefault() { + if p.Limit == 0 { + p.Limit = 10 + } +} + +// List godoc +// +// @Summary Get assets info +// @Description Get assets info +// @Tags assets +// @ID get-asset +// @Param limit query integer false "Count of requested entities" mininum(1) maximum(100) +// @Param offset query integer false "Offset" mininum(1) +// @Produce json +// @Success 200 {object} responses.Asset +// @Success 204 +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /v1/asset [get] +func (handler *AssetHandler) List(c echo.Context) error { + req, err := bindAndValidate[assetListRequest](c) + if err != nil { + return badRequestError(c, err) + } + req.SetDefault() + + assets, err := handler.asset.List(c.Request().Context(), int(req.Limit), int(req.Offset)) + if err != nil { + return handleError(c, err, handler.blocks) + } + + response := make([]responses.Asset, len(assets)) + for i := range assets { + response[i] = responses.NewAsset(assets[i]) + } + + return c.JSON(http.StatusOK, response) +} diff --git a/cmd/api/handler/asset_test.go b/cmd/api/handler/asset_test.go new file mode 100644 index 0000000..2931f7f --- /dev/null +++ b/cmd/api/handler/asset_test.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "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/labstack/echo/v4" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +// AssetTestSuite - +type AssetTestSuite struct { + suite.Suite + asset *mock.MockIAsset + block *mock.MockIBlock + echo *echo.Echo + handler *AssetHandler + ctrl *gomock.Controller +} + +// SetupSuite - +func (s *AssetTestSuite) SetupSuite() { + s.echo = echo.New() + s.echo.Validator = NewApiValidator() + s.ctrl = gomock.NewController(s.T()) + s.asset = mock.NewMockIAsset(s.ctrl) + s.block = mock.NewMockIBlock(s.ctrl) + s.handler = NewAssetHandler(s.asset, s.block) +} + +// TearDownSuite - +func (s *AssetTestSuite) TearDownSuite() { + s.ctrl.Finish() + s.Require().NoError(s.echo.Shutdown(context.Background())) +} + +func TestSuiteAsset_Run(t *testing.T) { + suite.Run(t, new(AssetTestSuite)) +} + +func (s *AssetTestSuite) TestList() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/asset") + + s.asset.EXPECT(). + List(gomock.Any(), 10, 0). + Return([]storage.Asset{ + { + Asset: "asset", + Transferred: decimal.NewFromInt(10), + Fee: decimal.NewFromInt(20), + TransferCount: 2, + FeeCount: 3, + }, + }, nil). + Times(1) + + s.Require().NoError(s.handler.List(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var assets []responses.Asset + err := json.NewDecoder(rec.Body).Decode(&assets) + s.Require().NoError(err) + s.Require().Len(assets, 1) + + s.Require().EqualValues("asset", assets[0].Asset) + s.Require().EqualValues("10", assets[0].Transferred) + s.Require().EqualValues("20", assets[0].Fee) + s.Require().EqualValues(2, assets[0].TransferCount) + s.Require().EqualValues(3, assets[0].FeeCount) +} diff --git a/cmd/api/handler/responses/asset.go b/cmd/api/handler/responses/asset.go new file mode 100644 index 0000000..f24cd89 --- /dev/null +++ b/cmd/api/handler/responses/asset.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package responses + +import "github.com/celenium-io/astria-indexer/internal/storage" + +type Asset struct { + Fee string `example:"1000" format:"string" json:"fee" swaggertype:"string"` + FeeCount int `example:"100" format:"number" json:"fee_count" swaggertype:"integer"` + Transferred string `example:"1000" format:"string" json:"transferred" swaggertype:"string"` + TransferCount int `example:"100" format:"number" json:"transfer_count" swaggertype:"integer"` + Asset string `example:"nria" format:"string" json:"asset" swaggertype:"string"` +} + +func NewAsset(asset storage.Asset) Asset { + return Asset{ + Asset: asset.Asset, + Fee: asset.Fee.String(), + FeeCount: asset.FeeCount, + Transferred: asset.Transferred.String(), + TransferCount: asset.TransferCount, + } +} diff --git a/cmd/api/init.go b/cmd/api/init.go index db14d6d..1e119be 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -372,6 +372,12 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto } } + assetHandler := handler.NewAssetHandler(db.Asset, db.Blocks) + assets := v1.Group("/asset") + { + assets.GET("", assetHandler.List) + } + appHandler := handler.NewAppHandler(db.App) apps := v1.Group("/app") { diff --git a/internal/storage/asset.go b/internal/storage/asset.go new file mode 100644 index 0000000..0e911da --- /dev/null +++ b/internal/storage/asset.go @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import ( + "context" + + "github.com/shopspring/decimal" + "github.com/uptrace/bun" +) + +type IAsset interface { + List(ctx context.Context, limit int, offset int) ([]Asset, error) +} + +//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed +type Asset struct { + bun.BaseModel `bun:"asset"` + + Asset string `bun:"asset"` + Fee decimal.Decimal `bun:"fee"` + FeeCount int `bun:"fee_count"` + Transferred decimal.Decimal `bun:"transferred"` + TransferCount int `bun:"transfer_count"` +} diff --git a/internal/storage/mock/asset.go b/internal/storage/mock/asset.go new file mode 100644 index 0000000..16eefb7 --- /dev/null +++ b/internal/storage/mock/asset.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: asset.go +// +// Generated by this command: +// +// mockgen -source=asset.go -destination=mock/asset.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" + gomock "go.uber.org/mock/gomock" +) + +// MockIAsset is a mock of IAsset interface. +type MockIAsset struct { + ctrl *gomock.Controller + recorder *MockIAssetMockRecorder +} + +// MockIAssetMockRecorder is the mock recorder for MockIAsset. +type MockIAssetMockRecorder struct { + mock *MockIAsset +} + +// NewMockIAsset creates a new mock instance. +func NewMockIAsset(ctrl *gomock.Controller) *MockIAsset { + mock := &MockIAsset{ctrl: ctrl} + mock.recorder = &MockIAssetMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIAsset) EXPECT() *MockIAssetMockRecorder { + return m.recorder +} + +// List mocks base method. +func (m *MockIAsset) List(ctx context.Context, limit, offset int) ([]storage.Asset, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, limit, offset) + ret0, _ := ret[0].([]storage.Asset) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockIAssetMockRecorder) List(ctx, limit, offset any) *MockIAssetListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIAsset)(nil).List), ctx, limit, offset) + return &MockIAssetListCall{Call: call} +} + +// MockIAssetListCall wrap *gomock.Call +type MockIAssetListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIAssetListCall) Return(arg0 []storage.Asset, arg1 error) *MockIAssetListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIAssetListCall) Do(f func(context.Context, int, int) ([]storage.Asset, error)) *MockIAssetListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIAssetListCall) DoAndReturn(f func(context.Context, int, int) ([]storage.Asset, error)) *MockIAssetListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/storage/postgres/asset.go b/internal/storage/postgres/asset.go new file mode 100644 index 0000000..6a8aed9 --- /dev/null +++ b/internal/storage/postgres/asset.go @@ -0,0 +1,50 @@ +// 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" +) + +type Asset struct { + db *database.Bun +} + +// NewAsset - +func NewAsset(db *database.Bun) *Asset { + return &Asset{ + db: db, + } +} + +func (a *Asset) List(ctx context.Context, limit int, offset int) (assets []storage.Asset, err error) { + transferredQuery := a.db.DB().NewSelect(). + Model((*storage.Transfer)(nil)). + ColumnExpr("asset, count(*) as c, sum(amount) as amount"). + Group("asset") + + feesQuery := a.db.DB().NewSelect(). + Model((*storage.Fee)(nil)). + ColumnExpr("asset, count(*) as c, sum(amount) as amount"). + Group("asset") + + query := a.db.DB().NewSelect(). + With("fees", feesQuery). + With("transferred", transferredQuery). + Table("fees"). + ColumnExpr("(case when fees.asset is NULL then transferred.asset else fees.asset end) as asset"). + ColumnExpr("(case when fees.amount is NULL then 0 else fees.amount end) as fee"). + ColumnExpr("(case when transferred.amount is NULL then 0 else transferred.amount end) as transferred"). + ColumnExpr("fees.c as fee_count, transferred.c as transfer_count"). + Join("full outer join transferred on transferred.asset = fees.asset") + + query = limitScope(query, limit) + query = offsetScope(query, offset) + + err = query.Scan(ctx, &assets) + return +} diff --git a/internal/storage/postgres/asset_test.go b/internal/storage/postgres/asset_test.go new file mode 100644 index 0000000..f25939e --- /dev/null +++ b/internal/storage/postgres/asset_test.go @@ -0,0 +1,56 @@ +// 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/shopspring/decimal" +) + +func (s *StorageTestSuite) TestAssetList() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + assets, err := s.storage.Asset.List(ctx, 10, 0) + s.Require().NoError(err) + s.Require().Len(assets, 3) + + m := map[string]storage.Asset{ + "asset-1": { + Asset: "asset-1", + FeeCount: 0, + Fee: decimal.Zero, + Transferred: decimal.NewFromInt(1), + TransferCount: 1, + }, + "asset-2": { + Asset: "asset-2", + FeeCount: 1, + Fee: decimal.NewFromInt(100), + Transferred: decimal.Zero, + TransferCount: 0, + }, + "nria": { + Asset: "nria", + FeeCount: 1, + Fee: decimal.NewFromInt(100), + Transferred: decimal.NewFromInt(1), + TransferCount: 1, + }, + } + + for i := range assets { + s.Require().Contains(m, assets[i].Asset) + + a := m[assets[i].Asset] + s.Require().Equal(a.Asset, assets[i].Asset) + s.Require().Equal(a.TransferCount, assets[i].TransferCount) + s.Require().Equal(a.FeeCount, assets[i].FeeCount) + s.Require().Equal(a.Transferred.String(), assets[i].Transferred.String()) + s.Require().Equal(a.Fee.String(), assets[i].Fee.String()) + } +} diff --git a/internal/storage/postgres/core.go b/internal/storage/postgres/core.go index 8c404b5..129a1d4 100644 --- a/internal/storage/postgres/core.go +++ b/internal/storage/postgres/core.go @@ -42,6 +42,7 @@ type Storage struct { Search models.ISearch Stats models.IStats App models.IApp + Asset models.IAsset Notificator *Notificator } @@ -77,6 +78,7 @@ func Create(ctx context.Context, cfg config.Database, scriptsDir string, withMig Search: NewSearch(strg.Connection()), App: NewApp(strg.Connection()), Stats: NewStats(strg.Connection()), + Asset: NewAsset(strg.Connection()), Notificator: NewNotificator(cfg, strg.Connection().DB()), } diff --git a/internal/storage/postgres/index.go b/internal/storage/postgres/index.go index 20244ec..6e3be37 100644 --- a/internal/storage/postgres/index.go +++ b/internal/storage/postgres/index.go @@ -334,6 +334,14 @@ func createIndices(ctx context.Context, conn *database.Bun) error { Exec(ctx); err != nil { return err } + if _, err := tx.NewCreateIndex(). + IfNotExists(). + Model((*storage.Transfer)(nil)). + Index("transfer_asset_idx"). + Column("asset"). + Exec(ctx); err != nil { + return err + } // Deposit if _, err := tx.NewCreateIndex(). diff --git a/internal/storage/postgres/stats_test.go b/internal/storage/postgres/stats_test.go index 3ef3032..2edb309 100644 --- a/internal/storage/postgres/stats_test.go +++ b/internal/storage/postgres/stats_test.go @@ -146,7 +146,7 @@ func (s *StatsTestSuite) TestFeeSummary() { summary, err := s.storage.Stats.FeeSummary(ctx) s.Require().NoError(err) - s.Require().Len(summary, 1) + s.Require().Len(summary, 2) } func (s *StatsTestSuite) TestTokenTransferDistribution() { @@ -155,7 +155,7 @@ func (s *StatsTestSuite) TestTokenTransferDistribution() { summary, err := s.storage.Stats.TokenTransferDistribution(ctx, 10) s.Require().NoError(err) - s.Require().Len(summary, 1) + s.Require().Len(summary, 2) } func (s *StatsTestSuite) TestActiveAddressCount() { diff --git a/test/data/fee.yml b/test/data/fee.yml index be4dad6..712bbd6 100644 --- a/test/data/fee.yml +++ b/test/data/fee.yml @@ -5,4 +5,12 @@ tx_id: 1 asset: nria amount: 100 - payer_id: 1 \ No newline at end of file + payer_id: 1 +- id: 2 + height: 7316 + time: '2023-11-30T23:52:23.265Z' + action_id: 2 + tx_id: 2 + asset: asset-2 + amount: 100 + payer_id: 2 \ No newline at end of file diff --git a/test/data/transfer.yml b/test/data/transfer.yml index 927e97c..8e54cb5 100644 --- a/test/data/transfer.yml +++ b/test/data/transfer.yml @@ -4,4 +4,11 @@ amount: 1 asset: nria src_id: 1 - dest_id: 3 \ No newline at end of file + dest_id: 3 +- id: 2 + height: 7965 + time: '2023-12-01T00:18:07.575Z' + amount: 1 + asset: asset-1 + src_id: 2 + dest_id: 4 \ No newline at end of file