diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index b668152..f54e13b 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -1832,6 +1832,45 @@ const docTemplate = `{ } } }, + "/v1/stats/token/transfer_distribution": { + "get": { + "description": "Token transfer distribution", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Token transfer distribution", + "operationId": "stats-token-transfer-distribution", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.TokenTransferDistributionItem" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/tx": { "get": { "description": "List transactions info", @@ -3197,6 +3236,26 @@ const docTemplate = `{ } } }, + "responses.TokenTransferDistributionItem": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "format": "integer", + "example": "1000000" + }, + "asset": { + "type": "string", + "format": "string", + "example": "nria" + }, + "transfers_count": { + "type": "integer", + "format": "integer", + "example": 1000000 + } + } + }, "responses.Tx": { "type": "object", "properties": { diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 7447fa1..51227c9 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -1822,6 +1822,45 @@ } } }, + "/v1/stats/token/transfer_distribution": { + "get": { + "description": "Token transfer distribution", + "produces": [ + "application/json" + ], + "tags": [ + "stats" + ], + "summary": "Token transfer distribution", + "operationId": "stats-token-transfer-distribution", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.TokenTransferDistributionItem" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/tx": { "get": { "description": "List transactions info", @@ -3187,6 +3226,26 @@ } } }, + "responses.TokenTransferDistributionItem": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "format": "integer", + "example": "1000000" + }, + "asset": { + "type": "string", + "format": "string", + "example": "nria" + }, + "transfers_count": { + "type": "integer", + "format": "integer", + "example": 1000000 + } + } + }, "responses.Tx": { "type": "object", "properties": { diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 83f9466..a9c26f4 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -524,6 +524,21 @@ definitions: format: int64 type: integer type: object + responses.TokenTransferDistributionItem: + properties: + amount: + example: "1000000" + format: integer + type: string + asset: + example: nria + format: string + type: string + transfers_count: + example: 1000000 + format: integer + type: integer + type: object responses.Tx: properties: action_types: @@ -1877,6 +1892,32 @@ paths: summary: Get network summary for the last period tags: - stats + /v1/stats/token/transfer_distribution: + get: + description: Token transfer distribution + operationId: stats-token-transfer-distribution + parameters: + - description: Count of requested entities + in: query + maximum: 100 + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/responses.TokenTransferDistributionItem' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Token transfer distribution + tags: + - stats /v1/tx: get: description: List transactions info diff --git a/cmd/api/handler/responses/stats.go b/cmd/api/handler/responses/stats.go index e895462..8e18456 100644 --- a/cmd/api/handler/responses/stats.go +++ b/cmd/api/handler/responses/stats.go @@ -116,3 +116,17 @@ func NewFeeSummary(summary storage.FeeSummary) FeeSummary { FeeCount: summary.FeeCount, } } + +type TokenTransferDistributionItem struct { + Asset string `example:"nria" format:"string" json:"asset"` + Amount string `example:"1000000" format:"integer" json:"amount"` + TransfersCount int64 `example:"1000000" format:"integer" json:"transfers_count"` +} + +func NewTokenTransferDistributionItem(summary storage.TokenTransferDistributionItem) TokenTransferDistributionItem { + return TokenTransferDistributionItem{ + Asset: summary.Asset, + Amount: summary.Amount, + TransfersCount: summary.TransfersCount, + } +} diff --git a/cmd/api/handler/stats.go b/cmd/api/handler/stats.go index ce33797..8f69f49 100644 --- a/cmd/api/handler/stats.go +++ b/cmd/api/handler/stats.go @@ -195,3 +195,42 @@ func (sh StatsHandler) FeeSummary(c echo.Context) error { } return c.JSON(http.StatusOK, response) } + +type tokenTransferDistributionRequest struct { + Limit uint64 `query:"limit" validate:"omitempty,min=1,max=100"` +} + +func (p *tokenTransferDistributionRequest) SetDefault() { + if p.Limit == 0 { + p.Limit = 10 + } +} + +// TokenTransferDistribution godoc +// +// @Summary Token transfer distribution +// @Description Token transfer distribution +// @Tags stats +// @ID stats-token-transfer-distribution +// @Param limit query integer false "Count of requested entities" mininum(1) maximum(100) +// @Produce json +// @Success 200 {array} responses.TokenTransferDistributionItem +// @Failure 500 {object} Error +// @Router /v1/stats/token/transfer_distribution [get] +func (sh StatsHandler) TokenTransferDistribution(c echo.Context) error { + req, err := bindAndValidate[tokenTransferDistributionRequest](c) + if err != nil { + return badRequestError(c, err) + } + req.SetDefault() + + items, err := sh.repo.TokenTransferDistribution(c.Request().Context(), int(req.Limit)) + if err != nil { + return handleError(c, err, sh.rollups) + } + response := make([]responses.TokenTransferDistributionItem, len(items)) + for i := range items { + response[i] = responses.NewTokenTransferDistributionItem(items[i]) + } + return c.JSON(http.StatusOK, response) +} diff --git a/cmd/api/handler/stats_test.go b/cmd/api/handler/stats_test.go index 7350a56..60262c4 100644 --- a/cmd/api/handler/stats_test.go +++ b/cmd/api/handler/stats_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/celenium-io/astria-indexer/cmd/api/handler/responses" + "github.com/celenium-io/astria-indexer/internal/currency" "github.com/celenium-io/astria-indexer/internal/storage" "github.com/celenium-io/astria-indexer/internal/storage/mock" "github.com/labstack/echo/v4" @@ -243,3 +244,63 @@ func (s *StatsTestSuite) TestSummaryTimeframe() { s.Require().EqualValues(7, summary.BytesInBlockPct) } } + +func (s *StatsTestSuite) TestFeeSummary() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/v1/stats/fee/summary") + + s.stats.EXPECT(). + FeeSummary(gomock.Any()). + Return([]storage.FeeSummary{ + { + Asset: currency.DefaultCurrency, + Amount: "1000", + FeeCount: 100, + }, + }, nil) + + s.Require().NoError(s.handler.FeeSummary(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var result []responses.FeeSummary + err := json.NewDecoder(rec.Body).Decode(&result) + s.Require().NoError(err) + s.Require().Len(result, 1) + + summary := result[0] + s.Require().EqualValues("1000", summary.Amount) + s.Require().EqualValues(100, summary.FeeCount) + s.Require().EqualValues(currency.DefaultCurrency, summary.Asset) +} + +func (s *StatsTestSuite) TestTokenTransferDistribution() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/v1/stats/token/transfer_distribution") + + s.stats.EXPECT(). + TokenTransferDistribution(gomock.Any(), 10). + Return([]storage.TokenTransferDistributionItem{ + { + Asset: currency.DefaultCurrency, + Amount: "1000", + TransfersCount: 100, + }, + }, nil) + + s.Require().NoError(s.handler.TokenTransferDistribution(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var result []responses.TokenTransferDistributionItem + err := json.NewDecoder(rec.Body).Decode(&result) + s.Require().NoError(err) + s.Require().Len(result, 1) + + summary := result[0] + s.Require().EqualValues("1000", summary.Amount) + s.Require().EqualValues(100, summary.TransfersCount) + s.Require().EqualValues(currency.DefaultCurrency, summary.Asset) +} diff --git a/cmd/api/init.go b/cmd/api/init.go index a425d76..b533a31 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -354,6 +354,11 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto { fee.GET("/summary", statsHandler.FeeSummary) } + + token := stats.Group("/token") + { + token.GET("/transfer_distribution", statsHandler.TokenTransferDistribution) + } } if cfg.ApiConfig.Prometheus { diff --git a/database/views/09_transfer_stats_by_hour.sql b/database/views/09_transfer_stats_by_hour.sql new file mode 100644 index 0000000..10a0887 --- /dev/null +++ b/database/views/09_transfer_stats_by_hour.sql @@ -0,0 +1,12 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS transfer_stats_by_hour +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 hour'::interval, time) AS ts, + transfer.asset as asset, + count(*) as transfers_count, + sum(amount) as amount + from transfer + group by 1, 2 + order by 1 desc; + +CALL add_view_refresh_job('transfer_stats_by_hour', INTERVAL '1 minute', INTERVAL '1 minute'); diff --git a/database/views/10_transfer_stats_by_day.sql b/database/views/10_transfer_stats_by_day.sql new file mode 100644 index 0000000..5d0ca23 --- /dev/null +++ b/database/views/10_transfer_stats_by_day.sql @@ -0,0 +1,12 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS transfer_stats_by_day +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 day'::interval, transfer_stats_by_hour.ts) AS ts, + transfer_stats_by_hour.asset as asset, + sum(transfers_count) as transfers_count, + sum(amount) as amount + from transfer_stats_by_hour + group by 1, 2 + order by 1 desc; + +CALL add_view_refresh_job('transfer_stats_by_day', INTERVAL '1 minute', INTERVAL '1 minute'); diff --git a/database/views/11_transfer_stats_by_month.sql b/database/views/11_transfer_stats_by_month.sql new file mode 100644 index 0000000..4f5ba43 --- /dev/null +++ b/database/views/11_transfer_stats_by_month.sql @@ -0,0 +1,12 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS transfer_stats_by_month +WITH (timescaledb.continuous, timescaledb.materialized_only=false) AS + select + time_bucket('1 month'::interval, transfer_stats_by_day.ts) AS ts, + transfer_stats_by_day.asset as asset, + sum(transfers_count) as transfers_count, + sum(amount) as amount + from transfer_stats_by_day + group by 1, 2 + order by 1 desc; + +CALL add_view_refresh_job('transfer_stats_by_month', INTERVAL '1 minute', INTERVAL '1 hour'); diff --git a/internal/storage/block.go b/internal/storage/block.go index d18fb9f..7de856f 100644 --- a/internal/storage/block.go +++ b/internal/storage/block.go @@ -58,6 +58,7 @@ type Block struct { BlockSignatures []BlockSignature `bun:"-"` // internal field for saving block signatures Constants []*Constant `bun:"-"` // internal field for updating constants Bridges []*Bridge `bun:"-"` // internal field for saving bridges + Transfers []*Transfer `bun:"-"` // internal field for saving transfers Txs []*Tx `bun:"rel:has-many"` Stats *BlockStats `bun:"rel:has-one,join:height=height"` diff --git a/internal/storage/generic.go b/internal/storage/generic.go index 185c03f..0895bf6 100644 --- a/internal/storage/generic.go +++ b/internal/storage/generic.go @@ -36,6 +36,7 @@ var Models = []any{ &BlockSignature{}, &Bridge{}, &Fee{}, + &Transfer{}, } //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed @@ -56,6 +57,7 @@ type Transaction interface { SaveTransactions(ctx context.Context, txs ...*Tx) error SaveValidators(ctx context.Context, validators ...*Validator) error SaveFees(ctx context.Context, fees ...*Fee) error + SaveTransfers(ctx context.Context, transfers ...*Transfer) error RetentionBlockSignatures(ctx context.Context, height types.Level) error RollbackActions(ctx context.Context, height types.Level) (actions []Action, err error) @@ -73,6 +75,7 @@ type Transaction interface { RollbackTxs(ctx context.Context, height types.Level) (txs []Tx, err error) RollbackValidators(ctx context.Context, height types.Level) (err error) RollbackFees(ctx context.Context, height types.Level) (err error) + RollbackTransfers(ctx context.Context, height types.Level) (err error) UpdateAddresses(ctx context.Context, address ...*Address) error UpdateConstants(ctx context.Context, constants ...*Constant) error UpdateRollups(ctx context.Context, rollups ...*Rollup) error diff --git a/internal/storage/mock/generic.go b/internal/storage/mock/generic.go index 9761330..77f9ab3 100644 --- a/internal/storage/mock/generic.go +++ b/internal/storage/mock/generic.go @@ -1052,6 +1052,44 @@ func (c *MockTransactionRollbackRollupsCall) DoAndReturn(f func(context.Context, return c } +// RollbackTransfers mocks base method. +func (m *MockTransaction) RollbackTransfers(ctx context.Context, height types.Level) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RollbackTransfers", ctx, height) + ret0, _ := ret[0].(error) + return ret0 +} + +// RollbackTransfers indicates an expected call of RollbackTransfers. +func (mr *MockTransactionMockRecorder) RollbackTransfers(ctx, height any) *MockTransactionRollbackTransfersCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackTransfers", reflect.TypeOf((*MockTransaction)(nil).RollbackTransfers), ctx, height) + return &MockTransactionRollbackTransfersCall{Call: call} +} + +// MockTransactionRollbackTransfersCall wrap *gomock.Call +type MockTransactionRollbackTransfersCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionRollbackTransfersCall) Return(err error) *MockTransactionRollbackTransfersCall { + c.Call = c.Call.Return(err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionRollbackTransfersCall) Do(f func(context.Context, types.Level) error) *MockTransactionRollbackTransfersCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionRollbackTransfersCall) DoAndReturn(f func(context.Context, types.Level) error) *MockTransactionRollbackTransfersCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // RollbackTxs mocks base method. func (m *MockTransaction) RollbackTxs(ctx context.Context, height types.Level) ([]storage.Tx, error) { m.ctrl.T.Helper() @@ -1690,6 +1728,49 @@ func (c *MockTransactionSaveTransactionsCall) DoAndReturn(f func(context.Context return c } +// SaveTransfers mocks base method. +func (m *MockTransaction) SaveTransfers(ctx context.Context, transfers ...*storage.Transfer) error { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range transfers { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SaveTransfers", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveTransfers indicates an expected call of SaveTransfers. +func (mr *MockTransactionMockRecorder) SaveTransfers(ctx any, transfers ...any) *MockTransactionSaveTransfersCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, transfers...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransfers", reflect.TypeOf((*MockTransaction)(nil).SaveTransfers), varargs...) + return &MockTransactionSaveTransfersCall{Call: call} +} + +// MockTransactionSaveTransfersCall wrap *gomock.Call +type MockTransactionSaveTransfersCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockTransactionSaveTransfersCall) Return(arg0 error) *MockTransactionSaveTransfersCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockTransactionSaveTransfersCall) Do(f func(context.Context, ...*storage.Transfer) error) *MockTransactionSaveTransfersCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockTransactionSaveTransfersCall) DoAndReturn(f func(context.Context, ...*storage.Transfer) error) *MockTransactionSaveTransfersCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SaveValidators mocks base method. func (m *MockTransaction) SaveValidators(ctx context.Context, validators ...*storage.Validator) error { m.ctrl.T.Helper() diff --git a/internal/storage/mock/stats.go b/internal/storage/mock/stats.go index 17b767b..3edb0b8 100644 --- a/internal/storage/mock/stats.go +++ b/internal/storage/mock/stats.go @@ -237,3 +237,42 @@ func (c *MockIStatsSummaryTimeframeCall) DoAndReturn(f func(context.Context, sto c.Call = c.Call.DoAndReturn(f) return c } + +// TokenTransferDistribution mocks base method. +func (m *MockIStats) TokenTransferDistribution(ctx context.Context, limit int) ([]storage.TokenTransferDistributionItem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TokenTransferDistribution", ctx, limit) + ret0, _ := ret[0].([]storage.TokenTransferDistributionItem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TokenTransferDistribution indicates an expected call of TokenTransferDistribution. +func (mr *MockIStatsMockRecorder) TokenTransferDistribution(ctx, limit any) *MockIStatsTokenTransferDistributionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenTransferDistribution", reflect.TypeOf((*MockIStats)(nil).TokenTransferDistribution), ctx, limit) + return &MockIStatsTokenTransferDistributionCall{Call: call} +} + +// MockIStatsTokenTransferDistributionCall wrap *gomock.Call +type MockIStatsTokenTransferDistributionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIStatsTokenTransferDistributionCall) Return(arg0 []storage.TokenTransferDistributionItem, arg1 error) *MockIStatsTokenTransferDistributionCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIStatsTokenTransferDistributionCall) Do(f func(context.Context, int) ([]storage.TokenTransferDistributionItem, error)) *MockIStatsTokenTransferDistributionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIStatsTokenTransferDistributionCall) DoAndReturn(f func(context.Context, int) ([]storage.TokenTransferDistributionItem, error)) *MockIStatsTokenTransferDistributionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/storage/mock/transfer.go b/internal/storage/mock/transfer.go new file mode 100644 index 0000000..831d29e --- /dev/null +++ b/internal/storage/mock/transfer.go @@ -0,0 +1,315 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +// Code generated by MockGen. DO NOT EDIT. +// Source: transfer.go +// +// Generated by this command: +// +// mockgen -source=transfer.go -destination=mock/transfer.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" +) + +// MockITransfer is a mock of ITransfer interface. +type MockITransfer struct { + ctrl *gomock.Controller + recorder *MockITransferMockRecorder +} + +// MockITransferMockRecorder is the mock recorder for MockITransfer. +type MockITransferMockRecorder struct { + mock *MockITransfer +} + +// NewMockITransfer creates a new mock instance. +func NewMockITransfer(ctrl *gomock.Controller) *MockITransfer { + mock := &MockITransfer{ctrl: ctrl} + mock.recorder = &MockITransferMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockITransfer) EXPECT() *MockITransferMockRecorder { + return m.recorder +} + +// CursorList mocks base method. +func (m *MockITransfer) CursorList(ctx context.Context, id, limit uint64, order storage0.SortOrder, cmp storage0.Comparator) ([]*storage.Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CursorList", ctx, id, limit, order, cmp) + ret0, _ := ret[0].([]*storage.Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CursorList indicates an expected call of CursorList. +func (mr *MockITransferMockRecorder) CursorList(ctx, id, limit, order, cmp any) *MockITransferCursorListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CursorList", reflect.TypeOf((*MockITransfer)(nil).CursorList), ctx, id, limit, order, cmp) + return &MockITransferCursorListCall{Call: call} +} + +// MockITransferCursorListCall wrap *gomock.Call +type MockITransferCursorListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferCursorListCall) Return(arg0 []*storage.Transfer, arg1 error) *MockITransferCursorListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferCursorListCall) Do(f func(context.Context, uint64, uint64, storage0.SortOrder, storage0.Comparator) ([]*storage.Transfer, error)) *MockITransferCursorListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferCursorListCall) DoAndReturn(f func(context.Context, uint64, uint64, storage0.SortOrder, storage0.Comparator) ([]*storage.Transfer, error)) *MockITransferCursorListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetByID mocks base method. +func (m *MockITransfer) GetByID(ctx context.Context, id uint64) (*storage.Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", ctx, id) + ret0, _ := ret[0].(*storage.Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockITransferMockRecorder) GetByID(ctx, id any) *MockITransferGetByIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockITransfer)(nil).GetByID), ctx, id) + return &MockITransferGetByIDCall{Call: call} +} + +// MockITransferGetByIDCall wrap *gomock.Call +type MockITransferGetByIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferGetByIDCall) Return(arg0 *storage.Transfer, arg1 error) *MockITransferGetByIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferGetByIDCall) Do(f func(context.Context, uint64) (*storage.Transfer, error)) *MockITransferGetByIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferGetByIDCall) DoAndReturn(f func(context.Context, uint64) (*storage.Transfer, error)) *MockITransferGetByIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IsNoRows mocks base method. +func (m *MockITransfer) 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 *MockITransferMockRecorder) IsNoRows(err any) *MockITransferIsNoRowsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNoRows", reflect.TypeOf((*MockITransfer)(nil).IsNoRows), err) + return &MockITransferIsNoRowsCall{Call: call} +} + +// MockITransferIsNoRowsCall wrap *gomock.Call +type MockITransferIsNoRowsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferIsNoRowsCall) Return(arg0 bool) *MockITransferIsNoRowsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferIsNoRowsCall) Do(f func(error) bool) *MockITransferIsNoRowsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferIsNoRowsCall) DoAndReturn(f func(error) bool) *MockITransferIsNoRowsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// LastID mocks base method. +func (m *MockITransfer) 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 *MockITransferMockRecorder) LastID(ctx any) *MockITransferLastIDCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastID", reflect.TypeOf((*MockITransfer)(nil).LastID), ctx) + return &MockITransferLastIDCall{Call: call} +} + +// MockITransferLastIDCall wrap *gomock.Call +type MockITransferLastIDCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferLastIDCall) Return(arg0 uint64, arg1 error) *MockITransferLastIDCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferLastIDCall) Do(f func(context.Context) (uint64, error)) *MockITransferLastIDCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferLastIDCall) DoAndReturn(f func(context.Context) (uint64, error)) *MockITransferLastIDCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// List mocks base method. +func (m *MockITransfer) List(ctx context.Context, limit, offset uint64, order storage0.SortOrder) ([]*storage.Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, limit, offset, order) + ret0, _ := ret[0].([]*storage.Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockITransferMockRecorder) List(ctx, limit, offset, order any) *MockITransferListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockITransfer)(nil).List), ctx, limit, offset, order) + return &MockITransferListCall{Call: call} +} + +// MockITransferListCall wrap *gomock.Call +type MockITransferListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferListCall) Return(arg0 []*storage.Transfer, arg1 error) *MockITransferListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferListCall) Do(f func(context.Context, uint64, uint64, storage0.SortOrder) ([]*storage.Transfer, error)) *MockITransferListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferListCall) DoAndReturn(f func(context.Context, uint64, uint64, storage0.SortOrder) ([]*storage.Transfer, error)) *MockITransferListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Save mocks base method. +func (m_2 *MockITransfer) Save(ctx context.Context, m *storage.Transfer) 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 *MockITransferMockRecorder) Save(ctx, m any) *MockITransferSaveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockITransfer)(nil).Save), ctx, m) + return &MockITransferSaveCall{Call: call} +} + +// MockITransferSaveCall wrap *gomock.Call +type MockITransferSaveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferSaveCall) Return(arg0 error) *MockITransferSaveCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferSaveCall) Do(f func(context.Context, *storage.Transfer) error) *MockITransferSaveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferSaveCall) DoAndReturn(f func(context.Context, *storage.Transfer) error) *MockITransferSaveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Update mocks base method. +func (m_2 *MockITransfer) Update(ctx context.Context, m *storage.Transfer) 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 *MockITransferMockRecorder) Update(ctx, m any) *MockITransferUpdateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockITransfer)(nil).Update), ctx, m) + return &MockITransferUpdateCall{Call: call} +} + +// MockITransferUpdateCall wrap *gomock.Call +type MockITransferUpdateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockITransferUpdateCall) Return(arg0 error) *MockITransferUpdateCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockITransferUpdateCall) Do(f func(context.Context, *storage.Transfer) error) *MockITransferUpdateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockITransferUpdateCall) DoAndReturn(f func(context.Context, *storage.Transfer) error) *MockITransferUpdateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/storage/postgres/core.go b/internal/storage/postgres/core.go index ce11cb1..b133919 100644 --- a/internal/storage/postgres/core.go +++ b/internal/storage/postgres/core.go @@ -28,6 +28,7 @@ type Storage struct { Bridges models.IBridge Constants models.IConstant Tx models.ITx + Transfers models.ITransfer Fee models.IFee Action models.IAction Address models.IAddress @@ -61,6 +62,7 @@ func Create(ctx context.Context, cfg config.Database, scriptsDir string) (Storag BlockSignatures: NewBlockSignature(strg.Connection()), Rollup: NewRollup(strg.Connection()), Tx: NewTx(strg.Connection()), + Transfers: NewTransfer(strg.Connection()), Validator: NewValidator(strg.Connection()), State: NewState(strg.Connection()), Search: NewSearch(strg.Connection()), @@ -128,6 +130,7 @@ func createHypertables(ctx context.Context, conn *database.Bun) error { &models.BlockSignature{}, &models.RollupAction{}, &models.Fee{}, + &models.Transfer{}, } { if _, err := tx.ExecContext(ctx, `SELECT create_hypertable(?, 'time', chunk_time_interval => INTERVAL '1 month', if_not_exists => TRUE);`, diff --git a/internal/storage/postgres/index.go b/internal/storage/postgres/index.go index 785d0c4..c543fba 100644 --- a/internal/storage/postgres/index.go +++ b/internal/storage/postgres/index.go @@ -299,6 +299,33 @@ func createIndices(ctx context.Context, conn *database.Bun) error { return err } + // Transfer + if _, err := tx.NewCreateIndex(). + IfNotExists(). + Model((*storage.Transfer)(nil)). + Index("transfer_height_idx"). + Column("height"). + Using("BRIN"). + Exec(ctx); err != nil { + return err + } + if _, err := tx.NewCreateIndex(). + IfNotExists(). + Model((*storage.Transfer)(nil)). + Index("transfer_src_id_idx"). + Column("src_id"). + Exec(ctx); err != nil { + return err + } + if _, err := tx.NewCreateIndex(). + IfNotExists(). + Model((*storage.Transfer)(nil)). + Index("transfer_dest_id_idx"). + Column("dest_id"). + Exec(ctx); err != nil { + return err + } + return nil }) } diff --git a/internal/storage/postgres/stats.go b/internal/storage/postgres/stats.go index 814960f..c637bca 100644 --- a/internal/storage/postgres/stats.go +++ b/internal/storage/postgres/stats.go @@ -203,3 +203,17 @@ func (s Stats) FeeSummary(ctx context.Context) (response []storage.FeeSummary, e Scan(ctx, &response) return } + +func (s Stats) TokenTransferDistribution(ctx context.Context, limit int) (items []storage.TokenTransferDistributionItem, err error) { + query := s.db.DB().NewSelect(). + Table(storage.ViewTransferStatsByMonth). + ColumnExpr("sum(transfers_count) as transfers_count"). + ColumnExpr("sum(amount) as amount"). + Column("asset"). + Group("asset"). + Order("amount desc") + + query = limitScope(query, limit) + err = query.Scan(ctx, &items) + return +} diff --git a/internal/storage/postgres/stats_test.go b/internal/storage/postgres/stats_test.go index 5eab260..0816fcf 100644 --- a/internal/storage/postgres/stats_test.go +++ b/internal/storage/postgres/stats_test.go @@ -145,7 +145,16 @@ func (s *StatsTestSuite) TestFeeSummary() { summary, err := s.storage.Stats.FeeSummary(ctx) s.Require().NoError(err) - s.Require().Len(summary, len(summary)) + s.Require().Len(summary, 1) +} + +func (s *StatsTestSuite) TestTokenTransferDistribution() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + summary, err := s.storage.Stats.TokenTransferDistribution(ctx, 10) + s.Require().NoError(err) + s.Require().Len(summary, 1) } func TestSuiteStats_Run(t *testing.T) { diff --git a/internal/storage/postgres/transaction.go b/internal/storage/postgres/transaction.go index d64a5c2..f36e96e 100644 --- a/internal/storage/postgres/transaction.go +++ b/internal/storage/postgres/transaction.go @@ -111,6 +111,15 @@ func (tx Transaction) SaveFees(ctx context.Context, fees ...*models.Fee) error { return err } +func (tx Transaction) SaveTransfers(ctx context.Context, transfers ...*models.Transfer) error { + if len(transfers) == 0 { + return nil + } + + _, err := tx.Tx().NewInsert().Model(&transfers).Returning("id").Exec(ctx) + return err +} + func (tx Transaction) SaveValidators(ctx context.Context, validators ...*models.Validator) error { if len(validators) == 0 { return nil @@ -331,6 +340,14 @@ func (tx Transaction) RollbackFees(ctx context.Context, height types.Level) (err return } +func (tx Transaction) RollbackTransfers(ctx context.Context, height types.Level) (err error) { + _, err = tx.Tx().NewDelete(). + Model((*models.Transfer)(nil)). + Where("height = ?", height). + Exec(ctx) + return +} + func (tx Transaction) RollbackBalances(ctx context.Context, ids []uint64) error { if len(ids) == 0 { return nil diff --git a/internal/storage/postgres/transaction_test.go b/internal/storage/postgres/transaction_test.go index 4095e5d..6cdc5a7 100644 --- a/internal/storage/postgres/transaction_test.go +++ b/internal/storage/postgres/transaction_test.go @@ -419,6 +419,29 @@ func (s *TransactionTestSuite) TestSaveBridges() { s.Require().NoError(tx.Close(ctx)) } +func (s *TransactionTestSuite) TestSaveTransfers() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + tx, err := BeginTransaction(ctx, s.storage.Transactable) + s.Require().NoError(err) + + transfers := make([]*storage.Transfer, 5) + for i := 0; i < 5; i++ { + transfers[i] = new(storage.Transfer) + transfers[i].SourceId = uint64(i + 1000) + transfers[i].DestinationId = uint64(i + 100) + transfers[i].Amount = decimal.NewFromInt(int64(i)) + transfers[i].Asset = string(currency.Nria) + } + + err = tx.SaveTransfers(ctx, transfers...) + s.Require().NoError(err) + + s.Require().NoError(tx.Flush(ctx)) + s.Require().NoError(tx.Close(ctx)) +} + func (s *TransactionTestSuite) TestGetProposerId() { ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) defer ctxCancel() @@ -659,6 +682,20 @@ func (s *TransactionTestSuite) TestRollbackRollupAddresses() { s.Require().NoError(tx.Close(ctx)) } +func (s *TransactionTestSuite) TestRollbackTransfers() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + tx, err := BeginTransaction(ctx, s.storage.Transactable) + s.Require().NoError(err) + + err = tx.RollbackTransfers(ctx, 7965) + s.Require().NoError(err) + + s.Require().NoError(tx.Flush(ctx)) + s.Require().NoError(tx.Close(ctx)) +} + func (s *TransactionTestSuite) TestRollbackRollups() { ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) defer ctxCancel() diff --git a/internal/storage/postgres/transfer.go b/internal/storage/postgres/transfer.go new file mode 100644 index 0000000..3d8ef04 --- /dev/null +++ b/internal/storage/postgres/transfer.go @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package postgres + +import ( + "github.com/celenium-io/astria-indexer/internal/storage" + "github.com/dipdup-net/go-lib/database" + "github.com/dipdup-net/indexer-sdk/pkg/storage/postgres" +) + +// Transfer - +type Transfer struct { + *postgres.Table[*storage.Transfer] +} + +// NewTransfer - +func NewTransfer(db *database.Bun) *Transfer { + return &Transfer{ + Table: postgres.NewTable[*storage.Transfer](db), + } +} diff --git a/internal/storage/stats.go b/internal/storage/stats.go index 441b9f1..5ae0b4f 100644 --- a/internal/storage/stats.go +++ b/internal/storage/stats.go @@ -121,6 +121,12 @@ type FeeSummary struct { FeeCount int64 `bun:"fee_count"` } +type TokenTransferDistributionItem struct { + Asset string `bun:"asset"` + Amount string `bun:"amount"` + TransfersCount int64 `bun:"transfers_count"` +} + //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed type IStats interface { Summary(ctx context.Context) (NetworkSummary, error) @@ -128,4 +134,5 @@ type IStats interface { Series(ctx context.Context, timeframe Timeframe, name string, req SeriesRequest) ([]SeriesItem, error) 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) } diff --git a/internal/storage/transfer.go b/internal/storage/transfer.go new file mode 100644 index 0000000..f121bc1 --- /dev/null +++ b/internal/storage/transfer.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import ( + "time" + + pkgTypes "github.com/celenium-io/astria-indexer/pkg/types" + "github.com/dipdup-net/indexer-sdk/pkg/storage" + "github.com/shopspring/decimal" + "github.com/uptrace/bun" +) + +//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed +type ITransfer interface { + storage.Table[*Transfer] +} + +type Transfer struct { + bun.BaseModel `bun:"transfer" comment:"Table with asset transfers"` + + Id uint64 `bun:"id,pk,notnull,autoincrement" comment:"Unique internal identity"` + Height pkgTypes.Level `bun:"height,notnull" comment:"The number (height) of this block"` + Time time.Time `bun:"time,pk,notnull" comment:"The time of block"` + Asset string `bun:"asset" comment:"Transfer asset"` + Amount decimal.Decimal `bun:"amount,type:numeric" comment:"Transfer amount"` + SourceId uint64 `bun:"src_id" comment:"Who made transfer"` + DestinationId uint64 `bun:"dest_id" comment:"Who receive transfer"` + + Source *Address `bun:"rel:belongs-to"` + Destination *Address `bun:"rel:belongs-to"` +} + +func (Transfer) TableName() string { + return "transfer" +} diff --git a/internal/storage/views.go b/internal/storage/views.go index f823b91..8074757 100644 --- a/internal/storage/views.go +++ b/internal/storage/views.go @@ -4,13 +4,16 @@ package storage const ( - ViewBlockStatsByHour = "block_stats_by_hour" - ViewBlockStatsByDay = "block_stats_by_day" - ViewBlockStatsByMonth = "block_stats_by_month" - ViewRollupStatsByHour = "rollup_stats_by_hour" - ViewRollupStatsByDay = "rollup_stats_by_day" - ViewRollupStatsByMonth = "rollup_stats_by_month" - ViewFeeStatsByHour = "fee_stats_by_hour" - ViewFeeStatsByDay = "fee_stats_by_day" - ViewFeeStatsByMonth = "fee_stats_by_month" + ViewBlockStatsByHour = "block_stats_by_hour" + ViewBlockStatsByDay = "block_stats_by_day" + ViewBlockStatsByMonth = "block_stats_by_month" + ViewRollupStatsByHour = "rollup_stats_by_hour" + ViewRollupStatsByDay = "rollup_stats_by_day" + ViewRollupStatsByMonth = "rollup_stats_by_month" + ViewFeeStatsByHour = "fee_stats_by_hour" + ViewFeeStatsByDay = "fee_stats_by_day" + ViewFeeStatsByMonth = "fee_stats_by_month" + ViewTransferStatsByHour = "transfer_stats_by_hour" + ViewTransferStatsByDay = "transfer_stats_by_day" + ViewTransferStatsByMonth = "transfer_stats_by_month" ) diff --git a/pkg/indexer/decode/actions.go b/pkg/indexer/decode/actions.go index aa422f2..f85480d 100644 --- a/pkg/indexer/decode/actions.go +++ b/pkg/indexer/decode/actions.go @@ -342,6 +342,13 @@ func parseTransferAction(body *astria.Action_TransferAction, from string, height decAmount := decimal.RequireFromString(amount) + transfer := storage.Transfer{ + Height: height, + Time: action.Time, + Asset: asset, + Amount: decAmount, + } + if from == to { addr := ctx.Addresses.Set(from, height, decimal.Zero, "", 1, 0) action.Addresses = append(action.Addresses, &storage.AddressAction{ @@ -351,6 +358,9 @@ func parseTransferAction(body *astria.Action_TransferAction, from string, height Height: action.Height, ActionType: action.Type, }) + + transfer.Source = addr + transfer.Destination = addr } else { toAddr := ctx.Addresses.Set(to, height, decAmount, asset, 1, 0) fromAddr := ctx.Addresses.Set(from, height, decAmount.Neg(), asset, 1, 0) @@ -383,7 +393,12 @@ func parseTransferAction(body *astria.Action_TransferAction, from string, height Currency: asset, Update: decAmount.Copy().Neg(), }) + + transfer.Source = fromAddr + transfer.Destination = toAddr } + + ctx.AddTransfer(&transfer) } return nil } diff --git a/pkg/indexer/decode/context.go b/pkg/indexer/decode/context.go index 323e904..12e8331 100644 --- a/pkg/indexer/decode/context.go +++ b/pkg/indexer/decode/context.go @@ -26,6 +26,7 @@ type Context struct { Constants map[string]*storage.Constant Bridges map[string]*storage.Bridge Fees []*storage.Fee + Transfers []*storage.Transfer Proposer string bridgeAssets map[string]string @@ -41,6 +42,7 @@ func NewContext(bridgeAssets map[string]string) Context { Constants: make(map[string]*storage.Constant), Bridges: make(map[string]*storage.Bridge), Fees: make([]*storage.Fee, 0), + Transfers: make([]*storage.Transfer, 0), bridgeAssets: bridgeAssets, } @@ -82,3 +84,7 @@ func (ctx *Context) AddFee(fee *storage.Fee) { func (ctx *Context) AddBridgeAsset(bridge, asset string) { ctx.bridgeAssets[bridge] = asset } + +func (ctx *Context) AddTransfer(transfer *storage.Transfer) { + ctx.Transfers = append(ctx.Transfers, transfer) +} diff --git a/pkg/indexer/parser/parse.go b/pkg/indexer/parser/parse.go index e0bf678..20a0812 100644 --- a/pkg/indexer/parser/parse.go +++ b/pkg/indexer/parser/parse.go @@ -62,6 +62,7 @@ func (p *Module) parse(ctx context.Context, b types.BlockData) error { ActionTypes: decodeCtx.ActionTypes, Constants: decodeCtx.ConstantsArray(), Bridges: decodeCtx.BridgesArray(), + Transfers: decodeCtx.Transfers, Txs: txs, Stats: &storage.BlockStats{ diff --git a/pkg/indexer/parser/parser_test.go b/pkg/indexer/parser/parser_test.go index ff694a2..94dcbc8 100644 --- a/pkg/indexer/parser/parser_test.go +++ b/pkg/indexer/parser/parser_test.go @@ -69,6 +69,7 @@ func getExpectedBlock() storage.Block { BlockSignatures: []storage.BlockSignature{}, Constants: make([]*storage.Constant, 0), Bridges: make([]*storage.Bridge, 0), + Transfers: make([]*storage.Transfer, 0), } } diff --git a/pkg/indexer/rollback/rollback.go b/pkg/indexer/rollback/rollback.go index 6a14ff8..c397bfb 100644 --- a/pkg/indexer/rollback/rollback.go +++ b/pkg/indexer/rollback/rollback.go @@ -210,6 +210,10 @@ func rollbackBlock(ctx context.Context, tx storage.Transaction, height types.Lev return err } + if err := tx.RollbackTransfers(ctx, height); err != nil { + return err + } + if err := tx.RollbackBlockSignatures(ctx, height); err != nil { return err } diff --git a/pkg/indexer/rollback/rollback_test.go b/pkg/indexer/rollback/rollback_test.go index 7556fe4..3327d5a 100644 --- a/pkg/indexer/rollback/rollback_test.go +++ b/pkg/indexer/rollback/rollback_test.go @@ -255,8 +255,12 @@ func Test_rollbackBlock(t *testing.T) { tx.EXPECT(). RollbackFees(ctx, height). Return(nil). - MaxTimes(1). - MinTimes(1) + Times(1) + + tx.EXPECT(). + RollbackTransfers(ctx, height). + Return(nil). + Times(1) lastBlock := storage.Block{ Height: height - 1, diff --git a/pkg/indexer/storage/storage.go b/pkg/indexer/storage/storage.go index 95d4750..e829d92 100644 --- a/pkg/indexer/storage/storage.go +++ b/pkg/indexer/storage/storage.go @@ -185,6 +185,10 @@ func (module *Module) processBlockInTransaction(ctx context.Context, tx storage. return state, err } + if err := saveTransfers(ctx, tx, block.Transfers, addrToId); err != nil { + return state, err + } + var actions = make([]*storage.Action, 0) for i := range block.Txs { diff --git a/pkg/indexer/storage/transfer.go b/pkg/indexer/storage/transfer.go new file mode 100644 index 0000000..3de28e4 --- /dev/null +++ b/pkg/indexer/storage/transfer.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 PK Lab AG +// SPDX-License-Identifier: MIT + +package storage + +import ( + "context" + + "github.com/celenium-io/astria-indexer/internal/storage" +) + +func saveTransfers( + ctx context.Context, + tx storage.Transaction, + transfers []*storage.Transfer, + addrToId map[string]uint64, +) error { + for i := range transfers { + if transfers[i].Source != nil { + if id, ok := addrToId[transfers[i].Source.Hash]; ok { + transfers[i].SourceId = id + } + } + + if transfers[i].Destination != nil { + if id, ok := addrToId[transfers[i].Destination.Hash]; ok { + transfers[i].DestinationId = id + } + } + } + + return tx.SaveTransfers(ctx, transfers...) +} diff --git a/test/data/action.yml b/test/data/action.yml index 603d98f..e9ae3b3 100644 --- a/test/data/action.yml +++ b/test/data/action.yml @@ -14,6 +14,6 @@ type: transfer tx_id: 2 data: - to: "b385e68e3a3a2d250c7c4024972576d698b9e748" + to: astria16rgmx2s86kk2r69rhjnvs9y44ujfhadc7yav9a amount: "1" - asset_id: "cEAxyGj9PTyEoc+oy0Xeuk6nRrRGl/f0pu0bj2wjm4I=" \ No newline at end of file + asset: nria \ No newline at end of file diff --git a/test/data/transfer.yml b/test/data/transfer.yml new file mode 100644 index 0000000..927e97c --- /dev/null +++ b/test/data/transfer.yml @@ -0,0 +1,7 @@ +- id: 1 + height: 7965 + time: '2023-12-01T00:18:07.575Z' + amount: 1 + asset: nria + src_id: 1 + dest_id: 3 \ No newline at end of file