From 5cf899099ce01632c62d883d9dc616edf3c13e1c Mon Sep 17 00:00:00 2001 From: Artem Poltorzhitskiy Date: Tue, 10 Dec 2024 11:35:15 +0100 Subject: [PATCH] Feature: add supply and sort to assets list (#77) --- cmd/api/docs/docs.go | 28 +++++++++++++++++++ cmd/api/docs/swagger.json | 28 +++++++++++++++++++ cmd/api/docs/swagger.yaml | 21 ++++++++++++++ cmd/api/handler/asset.go | 17 ++++++++---- cmd/api/handler/asset_test.go | 3 +- cmd/api/handler/responses/asset.go | 2 ++ internal/storage/asset.go | 4 ++- internal/storage/mock/asset.go | 13 +++++---- internal/storage/postgres/address_test.go | 4 +-- internal/storage/postgres/asset.go | 34 +++++++++++++++++++---- internal/storage/postgres/asset_test.go | 23 +++++++++------ test/data/balance.yml | 5 +++- 12 files changed, 152 insertions(+), 30 deletions(-) diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 431b706..40e20e8 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -787,6 +787,29 @@ const docTemplate = `{ "description": "Offset", "name": "offset", "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + }, + { + "enum": [ + "fee", + "fee_count", + "transferred", + "transfer_count", + "supply" + ], + "type": "string", + "description": "Field using for sorting. Default: fee", + "name": "sort_by", + "in": "query" } ], "responses": { @@ -3136,6 +3159,11 @@ const docTemplate = `{ "format": "number", "example": 100 }, + "supply": { + "type": "string", + "format": "string", + "example": "1000" + }, "transfer_count": { "type": "integer", "format": "number", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index ecad338..7ec52f6 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -777,6 +777,29 @@ "description": "Offset", "name": "offset", "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + }, + { + "enum": [ + "fee", + "fee_count", + "transferred", + "transfer_count", + "supply" + ], + "type": "string", + "description": "Field using for sorting. Default: fee", + "name": "sort_by", + "in": "query" } ], "responses": { @@ -3126,6 +3149,11 @@ "format": "number", "example": 100 }, + "supply": { + "type": "string", + "format": "string", + "example": "1000" + }, "transfer_count": { "type": "integer", "format": "number", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 0310b9d..fc8a186 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -193,6 +193,10 @@ definitions: example: 100 format: number type: integer + supply: + example: "1000" + format: string + type: string transfer_count: example: 100 format: number @@ -1357,6 +1361,23 @@ paths: in: query name: offset type: integer + - description: Sort order + enum: + - asc + - desc + in: query + name: sort + type: string + - description: 'Field using for sorting. Default: fee' + enum: + - fee + - fee_count + - transferred + - transfer_count + - supply + in: query + name: sort_by + type: string produces: - application/json responses: diff --git a/cmd/api/handler/asset.go b/cmd/api/handler/asset.go index 414f9b1..ef81594 100644 --- a/cmd/api/handler/asset.go +++ b/cmd/api/handler/asset.go @@ -27,14 +27,19 @@ func NewAssetHandler( } type assetListRequest struct { - Limit uint64 `query:"limit" validate:"omitempty,min=1,max=100"` - Offset uint64 `query:"offset" validate:"omitempty,min=0"` + Limit uint64 `query:"limit" validate:"omitempty,min=1,max=100"` + Offset uint64 `query:"offset" validate:"omitempty,min=0"` + Sort string `query:"sort" validate:"omitempty,oneof=asc desc"` + SortField string `query:"sort_by" validate:"omitempty,oneof=fee fee_count transferred transfer_count supply"` } func (p *assetListRequest) SetDefault() { if p.Limit == 0 { p.Limit = 10 } + if p.Sort == "" { + p.Sort = desc + } } // List godoc @@ -43,8 +48,10 @@ func (p *assetListRequest) SetDefault() { // @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) +// @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" Enums(asc, desc) +// @Param sort_by query string false "Field using for sorting. Default: fee" Enums(fee, fee_count, transferred, transfer_count, supply) // @Produce json // @Success 200 {object} responses.Asset // @Success 204 @@ -58,7 +65,7 @@ func (handler *AssetHandler) List(c echo.Context) error { } req.SetDefault() - assets, err := handler.asset.List(c.Request().Context(), int(req.Limit), int(req.Offset)) + assets, err := handler.asset.List(c.Request().Context(), int(req.Limit), int(req.Offset), req.SortField, pgSort(req.Sort)) if err != nil { return handleError(c, err, handler.blocks) } diff --git a/cmd/api/handler/asset_test.go b/cmd/api/handler/asset_test.go index 2931f7f..122f8db 100644 --- a/cmd/api/handler/asset_test.go +++ b/cmd/api/handler/asset_test.go @@ -13,6 +13,7 @@ import ( "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" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/labstack/echo/v4" "github.com/shopspring/decimal" "github.com/stretchr/testify/suite" @@ -56,7 +57,7 @@ func (s *AssetTestSuite) TestList() { c.SetPath("/asset") s.asset.EXPECT(). - List(gomock.Any(), 10, 0). + List(gomock.Any(), 10, 0, "", sdk.SortOrderDesc). Return([]storage.Asset{ { Asset: "asset", diff --git a/cmd/api/handler/responses/asset.go b/cmd/api/handler/responses/asset.go index f24cd89..625119e 100644 --- a/cmd/api/handler/responses/asset.go +++ b/cmd/api/handler/responses/asset.go @@ -11,6 +11,7 @@ type Asset struct { 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"` + Supply string `example:"1000" format:"string" json:"supply" swaggertype:"string"` } func NewAsset(asset storage.Asset) Asset { @@ -20,5 +21,6 @@ func NewAsset(asset storage.Asset) Asset { FeeCount: asset.FeeCount, Transferred: asset.Transferred.String(), TransferCount: asset.TransferCount, + Supply: asset.Supply.String(), } } diff --git a/internal/storage/asset.go b/internal/storage/asset.go index 0e911da..317b6b5 100644 --- a/internal/storage/asset.go +++ b/internal/storage/asset.go @@ -6,12 +6,13 @@ package storage import ( "context" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/shopspring/decimal" "github.com/uptrace/bun" ) type IAsset interface { - List(ctx context.Context, limit int, offset int) ([]Asset, error) + List(ctx context.Context, limit int, offset int, sortBy string, order sdk.SortOrder) ([]Asset, error) } //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed @@ -23,4 +24,5 @@ type Asset struct { FeeCount int `bun:"fee_count"` Transferred decimal.Decimal `bun:"transferred"` TransferCount int `bun:"transfer_count"` + Supply decimal.Decimal `bun:"supply"` } diff --git a/internal/storage/mock/asset.go b/internal/storage/mock/asset.go index 16eefb7..7247cd8 100644 --- a/internal/storage/mock/asset.go +++ b/internal/storage/mock/asset.go @@ -17,6 +17,7 @@ import ( 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" ) @@ -44,18 +45,18 @@ func (m *MockIAsset) EXPECT() *MockIAssetMockRecorder { } // List mocks base method. -func (m *MockIAsset) List(ctx context.Context, limit, offset int) ([]storage.Asset, error) { +func (m *MockIAsset) List(ctx context.Context, limit, offset int, sortBy string, order storage0.SortOrder) ([]storage.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List", ctx, limit, offset) + ret := m.ctrl.Call(m, "List", ctx, limit, offset, sortBy, order) 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 { +func (mr *MockIAssetMockRecorder) List(ctx, limit, offset, sortBy, order any) *MockIAssetListCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIAsset)(nil).List), ctx, limit, offset) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIAsset)(nil).List), ctx, limit, offset, sortBy, order) return &MockIAssetListCall{Call: call} } @@ -71,13 +72,13 @@ func (c *MockIAssetListCall) Return(arg0 []storage.Asset, arg1 error) *MockIAsse } // Do rewrite *gomock.Call.Do -func (c *MockIAssetListCall) Do(f func(context.Context, int, int) ([]storage.Asset, error)) *MockIAssetListCall { +func (c *MockIAssetListCall) Do(f func(context.Context, int, int, string, storage0.SortOrder) ([]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 { +func (c *MockIAssetListCall) DoAndReturn(f func(context.Context, int, int, string, storage0.SortOrder) ([]storage.Asset, error)) *MockIAssetListCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/storage/postgres/address_test.go b/internal/storage/postgres/address_test.go index 41adae0..a33db08 100644 --- a/internal/storage/postgres/address_test.go +++ b/internal/storage/postgres/address_test.go @@ -58,7 +58,7 @@ func (s *StorageTestSuite) TestAddressListWithBalancesWithAsset() { addresses, err := s.storage.Address.ListWithBalance(ctx, storage.AddressListFilter{ Sort: sdk.SortOrderAsc, Limit: 1, - Asset: "test", + Asset: "asset-1", }) s.Require().NoError(err) s.Require().Len(addresses, 1) @@ -73,7 +73,7 @@ func (s *StorageTestSuite) TestAddressListWithBalancesWithAsset() { balance := address.Balance[0] s.Require().EqualValues("10", balance.Total.String()) - s.Require().EqualValues("test", balance.Currency) + s.Require().EqualValues("asset-1", balance.Currency) s.Require().EqualValues("astria1lm45urgugesyhaymn68xww0m6g49zreqa32w7p", address.Hash) } diff --git a/internal/storage/postgres/asset.go b/internal/storage/postgres/asset.go index 6a8aed9..b633223 100644 --- a/internal/storage/postgres/asset.go +++ b/internal/storage/postgres/asset.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" ) type Asset struct { @@ -21,7 +22,15 @@ func NewAsset(db *database.Bun) *Asset { } } -func (a *Asset) List(ctx context.Context, limit int, offset int) (assets []storage.Asset, err error) { +var validSortFieldsForAssetList = map[string]struct{}{ + "fee": {}, + "transferred": {}, + "transfer_count": {}, + "fee_count": {}, + "supply": {}, +} + +func (a *Asset) List(ctx context.Context, limit int, offset int, sortBy string, order sdk.SortOrder) (assets []storage.Asset, err error) { transferredQuery := a.db.DB().NewSelect(). Model((*storage.Transfer)(nil)). ColumnExpr("asset, count(*) as c, sum(amount) as amount"). @@ -32,19 +41,34 @@ func (a *Asset) List(ctx context.Context, limit int, offset int) (assets []stora ColumnExpr("asset, count(*) as c, sum(amount) as amount"). Group("asset") + supplyQuery := a.db.DB().NewSelect(). + Model((*storage.Balance)(nil)). + ColumnExpr("currency, sum(total) as amount"). + Group("currency") + 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"). + With("supply", supplyQuery). + Table("supply"). + ColumnExpr("(case when fees.asset is not NULL then fees.asset when supply.currency is not NULL then supply.currency else transferred.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") + ColumnExpr("(case when supply.amount is NULL then 0 else supply.amount end) as supply"). + ColumnExpr("(case when fees.c is NULL then 0 else fees.c end) as fee_count"). + ColumnExpr("(case when transferred.c is NULL then 0 else transferred.c end) as transfer_count"). + Join("left join transferred on supply.currency = transferred.asset"). + Join("left join fees on supply.currency = fees.asset") query = limitScope(query, limit) query = offsetScope(query, offset) + if _, ok := validSortFieldsForAssetList[sortBy]; ok { + query = sortScope(query, sortBy, order) + } else { + query = sortScope(query, "supply", order) + } + err = query.Scan(ctx, &assets) return } diff --git a/internal/storage/postgres/asset_test.go b/internal/storage/postgres/asset_test.go index f25939e..0f7f954 100644 --- a/internal/storage/postgres/asset_test.go +++ b/internal/storage/postgres/asset_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/celenium-io/astria-indexer/internal/storage" + sdk "github.com/dipdup-net/indexer-sdk/pkg/storage" "github.com/shopspring/decimal" ) @@ -15,10 +16,6 @@ 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", @@ -26,6 +23,7 @@ func (s *StorageTestSuite) TestAssetList() { Fee: decimal.Zero, Transferred: decimal.NewFromInt(1), TransferCount: 1, + Supply: decimal.NewFromInt(10), }, "asset-2": { Asset: "asset-2", @@ -33,6 +31,7 @@ func (s *StorageTestSuite) TestAssetList() { Fee: decimal.NewFromInt(100), Transferred: decimal.Zero, TransferCount: 0, + Supply: decimal.NewFromInt(100), }, "nria": { Asset: "nria", @@ -40,17 +39,23 @@ func (s *StorageTestSuite) TestAssetList() { Fee: decimal.NewFromInt(100), Transferred: decimal.NewFromInt(1), TransferCount: 1, + Supply: decimal.RequireFromString("1000000000000000000001"), }, } + assets, err := s.storage.Asset.List(ctx, 10, 0, "fee", sdk.SortOrderAsc) + s.Require().NoError(err) + s.Require().Len(assets, len(m)) + 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()) + s.Require().Equal(a.Asset, assets[i].Asset, a.Asset) + s.Require().Equal(a.TransferCount, assets[i].TransferCount, a.Asset) + s.Require().Equal(a.FeeCount, assets[i].FeeCount, a.Asset) + s.Require().Equal(a.Transferred.String(), assets[i].Transferred.String(), a.Asset) + s.Require().Equal(a.Fee.String(), assets[i].Fee.String(), a.Asset) + s.Require().Equal(a.Supply.String(), assets[i].Supply.String(), a.Asset) } } diff --git a/test/data/balance.yml b/test/data/balance.yml index 0712856..d955883 100644 --- a/test/data/balance.yml +++ b/test/data/balance.yml @@ -17,5 +17,8 @@ currency: nria total: 1 - id: 1 - currency: test + currency: asset-1 total: 10 +- id: 9 + currency: asset-2 + total: 100