diff --git a/.gitignore b/.gitignore index b7e15fbb..d70d41a2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ go.work # IDE and other 3rd party tools .idea +*.private.* diff --git a/.run/requests/address.http b/.run/requests/address.http new file mode 100644 index 00000000..664b5c33 --- /dev/null +++ b/.run/requests/address.http @@ -0,0 +1,16 @@ +################################################ Local + +### GET All indexed addresses +http://{{host}}/v1/address + +### Get count of all indexed addresses +http://{{host}}/v1/address/count + +### Get specific address data +http://{{host}}/v1/address/celestia1z4909eqzfzngegw43tr2vle7d69fhww4hmmusw + +### Get specific address transactions +http://{{host}}/v1/address/celestia1z4909eqzfzngegw43tr2vle7d69fhww4hmmusw/txs + +### Get specific address messages +http://{{host}}/v1/address/celestia1z4909eqzfzngegw43tr2vle7d69fhww4hmmusw/messages diff --git a/.run/requests/namespace.http b/.run/requests/namespace.http new file mode 100644 index 00000000..f549b2fb --- /dev/null +++ b/.run/requests/namespace.http @@ -0,0 +1,6 @@ +### GET namespace messages +http://{{host}}/v1/namespace/00000000000000000000000000000000000042690c204d39600fddd3/0/messages?limit=2 + +### GET namespace messages +http://{{host}}/v1/namespace/00000000000000000000000000000000000042690c204d39600fddd3/0/messages?limit=2 + diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 226b7492..dc7166b8 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -1,6 +1,3 @@ -// SPDX-FileCopyrightText: 2023 PK Lab AG -// SPDX-License-Identifier: MIT - // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs @@ -153,6 +150,78 @@ const docTemplate = `{ } } }, + "/v1/address/{hash}/messages": { + "get": { + "description": "Get address messages", + "produces": [ + "application/json" + ], + "tags": [ + "address" + ], + "summary": "Get address messages", + "operationId": "address-messages", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.Message" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address/{hash}/txs": { "get": { "description": "Get address transactions", @@ -165,14 +234,25 @@ const docTemplate = `{ "summary": "Get address transactions", "operationId": "address-transactions", "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + }, { "maximum": 100, + "minimum": 1, "type": "integer", "description": "Count of requested entities", "name": "limit", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Offset", "name": "offset", @@ -237,18 +317,21 @@ const docTemplate = `{ "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Time from in unix timestamp", "name": "from", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Time to in unix timestamp", "name": "to", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Block number", "name": "height", @@ -1720,7 +1803,7 @@ const docTemplate = `{ } } }, - "github_com_dipdup-io_celestia-indexer_internal_storage_types.Status": { + "github_com_celenium-io_celestia-indexer_internal_storage_types.Status": { "type": "string", "enum": [ "success", @@ -2357,7 +2440,7 @@ const docTemplate = `{ "status": { "allOf": [ { - "$ref": "#/definitions/github_com_dipdup-io_celestia-indexer_internal_storage_types.Status" + "$ref": "#/definitions/github_com_celenium-io_celestia-indexer_internal_storage_types.Status" } ], "example": "success" diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 6d4652d9..cdc2f883 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -144,6 +144,78 @@ } } }, + "/v1/address/{hash}/messages": { + "get": { + "description": "Get address messages", + "produces": [ + "application/json" + ], + "tags": [ + "address" + ], + "summary": "Get address messages", + "operationId": "address-messages", + "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.Message" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + } + }, "/v1/address/{hash}/txs": { "get": { "description": "Get address transactions", @@ -156,14 +228,25 @@ "summary": "Get address transactions", "operationId": "address-transactions", "parameters": [ + { + "maxLength": 48, + "minLength": 48, + "type": "string", + "description": "Hash", + "name": "hash", + "in": "path", + "required": true + }, { "maximum": 100, + "minimum": 1, "type": "integer", "description": "Count of requested entities", "name": "limit", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Offset", "name": "offset", @@ -228,18 +311,21 @@ "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Time from in unix timestamp", "name": "from", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Time to in unix timestamp", "name": "to", "in": "query" }, { + "minimum": 1, "type": "integer", "description": "Block number", "name": "height", @@ -1711,7 +1797,7 @@ } } }, - "github_com_dipdup-io_celestia-indexer_internal_storage_types.Status": { + "github_com_celenium-io_celestia-indexer_internal_storage_types.Status": { "type": "string", "enum": [ "success", @@ -2348,7 +2434,7 @@ "status": { "allOf": [ { - "$ref": "#/definitions/github_com_dipdup-io_celestia-indexer_internal_storage_types.Status" + "$ref": "#/definitions/github_com_celenium-io_celestia-indexer_internal_storage_types.Status" } ], "example": "success" diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 4a43f04b..9ccc3255 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -11,7 +11,7 @@ definitions: 'namespace', 'tx' type: string type: object - github_com_dipdup-io_celestia-indexer_internal_storage_types.Status: + github_com_celenium-io_celestia-indexer_internal_storage_types.Status: enum: - success - failed @@ -485,7 +485,7 @@ definitions: type: integer status: allOf: - - $ref: '#/definitions/github_com_dipdup-io_celestia-indexer_internal_storage_types.Status' + - $ref: '#/definitions/github_com_celenium-io_celestia-indexer_internal_storage_types.Status' example: success time: example: "2023-07-04T03:10:57+00:00" @@ -712,18 +712,77 @@ paths: summary: Get address info tags: - address + /v1/address/{hash}/messages: + get: + description: Get address messages + operationId: address-messages + parameters: + - description: Hash + in: path + maxLength: 48 + minLength: 48 + name: hash + required: true + type: string + - description: Count of requested entities + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - description: Offset + in: query + minimum: 1 + name: offset + type: integer + - description: Sort order + enum: + - asc + - desc + in: query + name: sort + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/responses.Message' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.Error' + summary: Get address messages + tags: + - address /v1/address/{hash}/txs: get: description: Get address transactions operationId: address-transactions parameters: + - description: Hash + in: path + maxLength: 48 + minLength: 48 + name: hash + required: true + type: string - description: Count of requested entities in: query maximum: 100 + minimum: 1 name: limit type: integer - description: Offset in: query + minimum: 1 name: offset type: integer - description: Sort order @@ -777,14 +836,17 @@ paths: type: string - description: Time from in unix timestamp in: query + minimum: 1 name: from type: integer - description: Time to in unix timestamp in: query + minimum: 1 name: to type: integer - description: Block number in: query + minimum: 1 name: height type: integer produces: diff --git a/cmd/api/handler/address.go b/cmd/api/handler/address.go index dcfa28d2..1bfc7faa 100644 --- a/cmd/api/handler/address.go +++ b/cmd/api/handler/address.go @@ -117,14 +117,15 @@ func (handler *AddressHandler) List(c echo.Context) error { // @Description Get address transactions // @Tags address // @ID address-transactions -// @Param limit query integer false "Count of requested entities" mininum(1) maximum(100) -// @Param offset query integer false "Offset" mininum(1) +// @Param hash path string true "Hash" minlength(48) maxlength(48) +// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) +// @Param offset query integer false "Offset" minimum(1) // @Param sort query string false "Sort order" Enums(asc, desc) // @Param status query types.Status false "Comma-separated status list" // @Param msg_type query types.MsgType false "Comma-separated message types list" -// @Param from query integer false "Time from in unix timestamp" mininum(1) -// @Param to query integer false "Time to in unix timestamp" mininum(1) -// @Param height query integer false "Block number" mininum(1) +// @Param from query integer false "Time from in unix timestamp" minimum(1) +// @Param to query integer false "Time to in unix timestamp" minimum(1) +// @Param height query integer false "Block number" minimum(1) // @Produce json // @Success 200 {array} responses.Tx // @Failure 400 {object} Error @@ -172,6 +173,77 @@ func (handler *AddressHandler) Transactions(c echo.Context) error { return returnArray(c, response) } +type getAddressMessages struct { + Hash string `param:"hash" validate:"required,address"` + 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"` +} + +func (p *getAddressMessages) SetDefault() { + if p.Limit == 0 { + p.Limit = 10 + } + if p.Sort == "" { + p.Sort = asc + } +} + +func (p *getAddressMessages) ToFilters() storage.AddressMsgsFilter { + return storage.AddressMsgsFilter{ + Limit: int(p.Limit), + Offset: int(p.Offset), + Sort: pgSort(p.Sort), + } +} + +// Messages godoc +// +// @Summary Get address messages +// @Description Get address messages +// @Tags address +// @ID address-messages +// @Param hash path string true "Hash" minlength(48) maxlength(48) +// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) +// @Param offset query integer false "Offset" minimum(1) +// @Param sort query string false "Sort order" Enums(asc, desc) +// @Produce json +// @Success 200 {array} responses.Message +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /v1/address/{hash}/messages [get] +func (handler *AddressHandler) Messages(c echo.Context) error { + req, err := bindAndValidate[getAddressMessages](c) + if err != nil { + return badRequestError(c, err) + } + + req.SetDefault() + + _, hash, err := types.Address(req.Hash).Decode() + if err != nil { + return badRequestError(c, err) + } + + address, err := handler.address.ByHash(c.Request().Context(), hash) + if err := handleError(c, err, handler.address); err != nil { + return err + } + + filters := req.ToFilters() + msgs, err := handler.address.Messages(c.Request().Context(), address.Id, filters) + if err := handleError(c, err, handler.txs); err != nil { + return err + } + + response := make([]responses.Message, len(msgs)) + for i := range msgs { + response[i] = responses.NewMessageForAddress(msgs[i]) + } + + return returnArray(c, response) +} + // Count godoc // // @Summary Get count of addresses in network diff --git a/cmd/api/handler/address_test.go b/cmd/api/handler/address_test.go index f67121da..16200fde 100644 --- a/cmd/api/handler/address_test.go +++ b/cmd/api/handler/address_test.go @@ -230,6 +230,60 @@ func (s *AddressTestSuite) TestListHeight() { s.Require().Equal(types.StatusSuccess, tx.Status) } +func (s *AddressTestSuite) TestMessages() { + q := make(url.Values) + q.Set("limit", "10") + q.Set("offset", "0") + q.Set("sort", "desc") + + req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/address/:hash/messages") + c.SetParamNames("hash") + c.SetParamValues(testAddress) + + s.address.EXPECT(). + ByHash(gomock.Any(), testHashAddress). + Return(storage.Address{ + Id: 1, + Hash: testHashAddress, + Address: testAddress, + }, nil) + + s.address.EXPECT(). + Messages(gomock.Any(), uint64(1), gomock.Any()). + Return([]storage.MsgAddress{ + { + AddressId: 1, + MsgId: 1, + Type: types.MsgAddressTypeDelegator, + Msg: &storage.Message{ + Id: 1, + Height: 1000, + Position: 0, + Type: types.MsgWithdrawDelegatorReward, + TxId: 1, + Data: nil, + }, + }, + }, nil) + + s.Require().NoError(s.handler.Messages(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var msgs []responses.Message + err := json.NewDecoder(rec.Body).Decode(&msgs) + s.Require().NoError(err) + s.Require().Len(msgs, 1) + + msg := msgs[0] + s.Require().EqualValues(1, msg.Id) + s.Require().EqualValues(1000, msg.Height) + s.Require().Equal(int64(0), msg.Position) + s.Require().EqualValues(types.MsgWithdrawDelegatorReward, msg.Type) +} + func (s *AddressTestSuite) TestCount() { req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() diff --git a/cmd/api/handler/responses/message.go b/cmd/api/handler/responses/message.go index 8d115183..779f3dee 100644 --- a/cmd/api/handler/responses/message.go +++ b/cmd/api/handler/responses/message.go @@ -34,3 +34,15 @@ func NewMessage(msg storage.Message) Message { Data: msg.Data, } } + +func NewMessageForAddress(msg storage.MsgAddress) Message { + return Message{ + Id: msg.MsgId, + Height: msg.Msg.Height, + Time: msg.Msg.Time, + Position: msg.Msg.Position, + TxId: msg.Msg.TxId, + Type: msg.Msg.Type, + Data: msg.Msg.Data, + } +} diff --git a/cmd/api/init.go b/cmd/api/init.go index 5b83d29f..78e1f299 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -219,6 +219,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto addressGroup.GET("/count", addressHandlers.Count) addressGroup.GET("/:hash", addressHandlers.Get) addressGroup.GET("/:hash/txs", addressHandlers.Transactions) + addressGroup.GET("/:hash/messages", addressHandlers.Messages) } blockHandlers := handler.NewBlockHandler(db.Blocks, db.BlockStats, db.Event, db.Namespace, db.State, cfg.Indexer.Name) diff --git a/internal/storage/address.go b/internal/storage/address.go index 3d2bd562..a7a45686 100644 --- a/internal/storage/address.go +++ b/internal/storage/address.go @@ -18,12 +18,19 @@ type AddressListFilter struct { Sort storage.SortOrder } +type AddressMsgsFilter struct { + Limit int + Offset int + Sort storage.SortOrder +} + //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed type IAddress interface { storage.Table[*Address] ByHash(ctx context.Context, hash []byte) (Address, error) - ListWithBalance(ctx context.Context, fltrs AddressListFilter) ([]Address, error) + ListWithBalance(ctx context.Context, filters AddressListFilter) ([]Address, error) + Messages(ctx context.Context, id uint64, filters AddressMsgsFilter) ([]MsgAddress, error) } // Address - diff --git a/internal/storage/mock/address.go b/internal/storage/mock/address.go index 2d1cb4ce..7d380590 100644 --- a/internal/storage/mock/address.go +++ b/internal/storage/mock/address.go @@ -274,18 +274,18 @@ func (c *IAddressListCall) DoAndReturn(f func(context.Context, uint64, uint64, s } // ListWithBalance mocks base method. -func (m *MockIAddress) ListWithBalance(ctx context.Context, fltrs storage.AddressListFilter) ([]storage.Address, error) { +func (m *MockIAddress) ListWithBalance(ctx context.Context, filters storage.AddressListFilter) ([]storage.Address, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListWithBalance", ctx, fltrs) + ret := m.ctrl.Call(m, "ListWithBalance", ctx, filters) ret0, _ := ret[0].([]storage.Address) ret1, _ := ret[1].(error) return ret0, ret1 } // ListWithBalance indicates an expected call of ListWithBalance. -func (mr *MockIAddressMockRecorder) ListWithBalance(ctx, fltrs any) *IAddressListWithBalanceCall { +func (mr *MockIAddressMockRecorder) ListWithBalance(ctx, filters any) *IAddressListWithBalanceCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWithBalance", reflect.TypeOf((*MockIAddress)(nil).ListWithBalance), ctx, fltrs) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWithBalance", reflect.TypeOf((*MockIAddress)(nil).ListWithBalance), ctx, filters) return &IAddressListWithBalanceCall{Call: call} } @@ -312,6 +312,45 @@ func (c *IAddressListWithBalanceCall) DoAndReturn(f func(context.Context, storag return c } +// Messages mocks base method. +func (m *MockIAddress) Messages(ctx context.Context, id uint64, filters storage.AddressMsgsFilter) ([]storage.MsgAddress, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Messages", ctx, id, filters) + ret0, _ := ret[0].([]storage.MsgAddress) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Messages indicates an expected call of Messages. +func (mr *MockIAddressMockRecorder) Messages(ctx, id, filters any) *IAddressMessagesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Messages", reflect.TypeOf((*MockIAddress)(nil).Messages), ctx, id, filters) + return &IAddressMessagesCall{Call: call} +} + +// IAddressMessagesCall wrap *gomock.Call +type IAddressMessagesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *IAddressMessagesCall) Return(arg0 []storage.MsgAddress, arg1 error) *IAddressMessagesCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *IAddressMessagesCall) Do(f func(context.Context, uint64, storage.AddressMsgsFilter) ([]storage.MsgAddress, error)) *IAddressMessagesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *IAddressMessagesCall) DoAndReturn(f func(context.Context, uint64, storage.AddressMsgsFilter) ([]storage.MsgAddress, error)) *IAddressMessagesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Save mocks base method. func (m_2 *MockIAddress) Save(ctx context.Context, m *storage.Address) error { m_2.ctrl.T.Helper() diff --git a/internal/storage/postgres/address.go b/internal/storage/postgres/address.go index 119c5592..f59f90f6 100644 --- a/internal/storage/postgres/address.go +++ b/internal/storage/postgres/address.go @@ -5,6 +5,7 @@ package postgres import ( "context" + "github.com/uptrace/bun" "github.com/celenium-io/celestia-indexer/internal/storage" "github.com/dipdup-net/go-lib/database" @@ -32,13 +33,31 @@ func (a *Address) ByHash(ctx context.Context, hash []byte) (address storage.Addr return } -func (a *Address) ListWithBalance(ctx context.Context, fltrs storage.AddressListFilter) (result []storage.Address, err error) { +func (a *Address) ListWithBalance(ctx context.Context, filters storage.AddressListFilter) (result []storage.Address, err error) { query := a.DB().NewSelect().Model(&result). - Offset(fltrs.Offset). + Offset(filters.Offset). Relation("Balance") - query = addressListFilter(query, fltrs) + query = addressListFilter(query, filters) err = query.Scan(ctx) return } + +func (a *Address) Messages(ctx context.Context, id uint64, filters storage.AddressMsgsFilter) (msgs []storage.MsgAddress, err error) { + query := a.DB().NewSelect().Model(&msgs). + Where("address_id = ?", id). + Offset(filters.Offset). + Relation("Msg") + + query = addressMsgsFilter(query, filters) + + err = query.Scan(ctx) + return +} + +func addressMsgsFilter(query *bun.SelectQuery, filters storage.AddressMsgsFilter) *bun.SelectQuery { + query = limitScope(query, filters.Limit) + query = sortScope(query, "msg_id", filters.Sort) + return query +} diff --git a/internal/storage/postgres/storage_test.go b/internal/storage/postgres/storage_test.go index 01e789a2..bd4aaa6b 100644 --- a/internal/storage/postgres/storage_test.go +++ b/internal/storage/postgres/storage_test.go @@ -292,6 +292,27 @@ func (s *StorageTestSuite) TestAddressList() { s.Require().Equal("utia", addresses[1].Balance.Currency) } +func (s *StorageTestSuite) TestAddressMessages() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + messages, err := s.storage.Address.Messages(ctx, 1, storage.AddressMsgsFilter{ + Limit: 10, + Offset: 0, + Sort: sdk.SortOrderAsc, + }) + s.Require().NoError(err) + s.Require().Len(messages, 1) + + s.Require().EqualValues(types.MsgAddressTypeFromAddress, messages[0].Type) + + msg := messages[0].Msg + s.Require().EqualValues(1, msg.Id) + s.Require().EqualValues(1000, msg.Height) + s.Require().EqualValues(0, msg.Position) + s.Require().Equal(types.MsgWithdrawDelegatorReward, msg.Type) +} + func (s *StorageTestSuite) TestEventByTxId() { ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) defer ctxCancel()