From b09c91d36e306cc4b003c5dd0eb55beaa9a5785c Mon Sep 17 00:00:00 2001 From: Artem Poltorzhitskiy Date: Fri, 4 Oct 2024 16:49:38 +0200 Subject: [PATCH] Feature: add active addresses count (#45) --- cmd/api/docs/docs.go | 27 +++++++++++++++++ cmd/api/docs/swagger.json | 27 +++++++++++++++++ cmd/api/docs/swagger.yaml | 18 ++++++++++++ cmd/api/handler/stats.go | 18 ++++++++++++ cmd/api/handler/stats_test.go | 19 ++++++++++++ cmd/api/init.go | 1 + internal/storage/mock/stats.go | 39 +++++++++++++++++++++++++ internal/storage/postgres/stats.go | 9 ++++++ internal/storage/postgres/stats_test.go | 23 +++++++++++++++ internal/storage/stats.go | 1 + 10 files changed, 182 insertions(+) diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index f54e13b..f9d2440 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -1782,6 +1782,33 @@ const docTemplate = `{ } } }, + "/v1/stats/summary/active_addresses_count": { + "get": { + "description": "Active adddresses count", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Active adddresses count", + "operationId": "stats-active-addresses-count", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "integer" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/stats/summary/{timeframe}": { "get": { "description": "Get network summary for the last period", diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 51227c9..6789ec5 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -1772,6 +1772,33 @@ } } }, + "/v1/stats/summary/active_addresses_count": { + "get": { + "description": "Active adddresses count", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Active adddresses count", + "operationId": "stats-active-addresses-count", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "integer" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/stats/summary/{timeframe}": { "get": { "description": "Get network summary for the last period", diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index a9c26f4..fd14b0b 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -1892,6 +1892,24 @@ paths: summary: Get network summary for the last period tags: - stats + /v1/stats/summary/active_addresses_count: + get: + description: Active adddresses count + operationId: stats-active-addresses-count + produces: + - application/json + responses: + "200": + description: OK + schema: + type: integer + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Active adddresses count + tags: + - stats /v1/stats/token/transfer_distribution: get: description: Token transfer distribution diff --git a/cmd/api/handler/stats.go b/cmd/api/handler/stats.go index 8f69f49..9487623 100644 --- a/cmd/api/handler/stats.go +++ b/cmd/api/handler/stats.go @@ -234,3 +234,21 @@ func (sh StatsHandler) TokenTransferDistribution(c echo.Context) error { } return c.JSON(http.StatusOK, response) } + +// ActiveAddressesCount godoc +// +// @Summary Active adddresses count +// @Description Active adddresses count +// @Tags stats +// @ID stats-active-addresses-count +// @Produce json +// @Success 200 {integer} int64 +// @Failure 500 {object} Error +// @Router /v1/stats/summary/active_addresses_count [get] +func (sh StatsHandler) ActiveAddressesCount(c echo.Context) error { + count, err := sh.repo.ActiveAddressesCount(c.Request().Context()) + if err != nil { + return handleError(c, err, sh.rollups) + } + return c.JSON(http.StatusOK, count) +} diff --git a/cmd/api/handler/stats_test.go b/cmd/api/handler/stats_test.go index 60262c4..889ed3f 100644 --- a/cmd/api/handler/stats_test.go +++ b/cmd/api/handler/stats_test.go @@ -304,3 +304,22 @@ func (s *StatsTestSuite) TestTokenTransferDistribution() { s.Require().EqualValues(100, summary.TransfersCount) s.Require().EqualValues(currency.DefaultCurrency, summary.Asset) } + +func (s *StatsTestSuite) TestActiveAddressesCount() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/v1/stats/summary/active_addresses_count") + + s.stats.EXPECT(). + ActiveAddressesCount(gomock.Any()). + Return(100, nil) + + s.Require().NoError(s.handler.ActiveAddressesCount(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var result int64 + err := json.NewDecoder(rec.Body).Decode(&result) + s.Require().NoError(err) + s.Require().EqualValues(100, result) +} diff --git a/cmd/api/init.go b/cmd/api/init.go index b533a31..897540e 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -343,6 +343,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto { stats.GET("/summary", statsHandler.Summary) stats.GET("/summary/:timeframe", statsHandler.SummaryTimeframe) + stats.GET("/summary/active_addresses_count", statsHandler.ActiveAddressesCount) stats.GET("/series/:name/:timeframe", statsHandler.Series) rollup := stats.Group("/rollup") diff --git a/internal/storage/mock/stats.go b/internal/storage/mock/stats.go index 3edb0b8..2c82fca 100644 --- a/internal/storage/mock/stats.go +++ b/internal/storage/mock/stats.go @@ -43,6 +43,45 @@ func (m *MockIStats) EXPECT() *MockIStatsMockRecorder { return m.recorder } +// ActiveAddressesCount mocks base method. +func (m *MockIStats) ActiveAddressesCount(ctx context.Context) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ActiveAddressesCount", ctx) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ActiveAddressesCount indicates an expected call of ActiveAddressesCount. +func (mr *MockIStatsMockRecorder) ActiveAddressesCount(ctx any) *MockIStatsActiveAddressesCountCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveAddressesCount", reflect.TypeOf((*MockIStats)(nil).ActiveAddressesCount), ctx) + return &MockIStatsActiveAddressesCountCall{Call: call} +} + +// MockIStatsActiveAddressesCountCall wrap *gomock.Call +type MockIStatsActiveAddressesCountCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIStatsActiveAddressesCountCall) Return(arg0 int64, arg1 error) *MockIStatsActiveAddressesCountCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIStatsActiveAddressesCountCall) Do(f func(context.Context) (int64, error)) *MockIStatsActiveAddressesCountCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIStatsActiveAddressesCountCall) DoAndReturn(f func(context.Context) (int64, error)) *MockIStatsActiveAddressesCountCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // FeeSummary mocks base method. func (m *MockIStats) FeeSummary(ctx context.Context) ([]storage.FeeSummary, error) { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/stats.go b/internal/storage/postgres/stats.go index c637bca..ee03236 100644 --- a/internal/storage/postgres/stats.go +++ b/internal/storage/postgres/stats.go @@ -217,3 +217,12 @@ func (s Stats) TokenTransferDistribution(ctx context.Context, limit int) (items err = query.Scan(ctx, &items) return } + +func (s Stats) ActiveAddressesCount(ctx context.Context) (val int64, err error) { + err = s.db.DB().NewSelect(). + Model((*storage.Tx)(nil)). + ColumnExpr("count(distinct signer_id)"). + Where("time > now() - '1 month'::interval"). + Scan(ctx, &val) + return +} diff --git a/internal/storage/postgres/stats_test.go b/internal/storage/postgres/stats_test.go index 0816fcf..6e9b6f3 100644 --- a/internal/storage/postgres/stats_test.go +++ b/internal/storage/postgres/stats_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/celenium-io/astria-indexer/internal/storage/types" "github.com/dipdup-net/go-lib/config" "github.com/dipdup-net/go-lib/database" "github.com/go-testfixtures/testfixtures/v3" @@ -157,6 +158,28 @@ func (s *StatsTestSuite) TestTokenTransferDistribution() { s.Require().Len(summary, 1) } +func (s *StatsTestSuite) TestActiveAddressCount() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + tx, err := BeginTransaction(ctx, s.storage.Transactable) + s.Require().NoError(err) + + err = tx.SaveTransactions(ctx, &storage.Tx{ + Time: time.Now().UTC(), + SignerId: 1, + Status: types.StatusSuccess, + }) + s.Require().NoError(err) + + s.Require().NoError(tx.Flush(ctx)) + s.Require().NoError(tx.Close(ctx)) + + value, err := s.storage.Stats.ActiveAddressesCount(ctx) + s.Require().NoError(err) + s.Require().EqualValues(1, value) +} + func TestSuiteStats_Run(t *testing.T) { suite.Run(t, new(StatsTestSuite)) } diff --git a/internal/storage/stats.go b/internal/storage/stats.go index 5ae0b4f..16aefba 100644 --- a/internal/storage/stats.go +++ b/internal/storage/stats.go @@ -135,4 +135,5 @@ type IStats interface { RollupSeries(ctx context.Context, rollupId uint64, timeframe Timeframe, name string, req SeriesRequest) ([]SeriesItem, error) FeeSummary(ctx context.Context) ([]FeeSummary, error) TokenTransferDistribution(ctx context.Context, limit int) ([]TokenTransferDistributionItem, error) + ActiveAddressesCount(ctx context.Context) (int64, error) }