From ffd60cb60885bae5a92098f7f494e38a83100cce Mon Sep 17 00:00:00 2001 From: Artem Poltorzhitskiy Date: Sun, 8 Sep 2024 22:55:22 +0200 Subject: [PATCH] Feature: endpoint with all blobs (#277) --- cmd/api/docs/swagger.json | 152 +++++++++++++++++++ cmd/api/handler/address.go | 10 +- cmd/api/handler/namespace.go | 166 ++++++++++++++++++--- cmd/api/handler/namespace_test.go | 44 ++++++ cmd/api/handler/responses/blob.go | 36 +++++ cmd/api/handler/rollup.go | 42 +++--- cmd/api/handler/stats.go | 6 +- cmd/api/handler/validator.go | 4 +- cmd/api/init.go | 1 + cmd/api/main.go | 2 +- cmd/api/routes_test.go | 1 + internal/storage/blob_log.go | 14 ++ internal/storage/mock/blob_log.go | 39 +++++ internal/storage/postgres/blob_log.go | 25 +++- internal/storage/postgres/blob_log_test.go | 31 ++++ internal/storage/postgres/scopes.go | 43 +++++- 16 files changed, 553 insertions(+), 63 deletions(-) diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 3482d620..63203a42 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -1057,6 +1057,108 @@ } }, "/blob": { + "get": { + "description": "Returns blobs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "namespace" + ], + "summary": "List all blobs with filters", + "operationId": "get-blobs", + "parameters": [ + { + "maximum": 100, + "type": "integer", + "description": "Count of requested entities", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "description": "Sort order. Default: desc", + "name": "sort", + "in": "query" + }, + { + "enum": [ + "time", + "size" + ], + "type": "string", + "description": "Sort field. If it's empty internal id is used", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Commitment value in URLbase64 format", + "name": "commitment", + "in": "query" + }, + { + "type": "integer", + "description": "Time from in unix timestamp", + "name": "from", + "in": "query" + }, + { + "type": "integer", + "description": "Time to in unix timestamp", + "name": "to", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated celestia addresses", + "name": "signers", + "in": "query" + }, + { + "type": "string", + "description": "Comma-separated celestia namespaces", + "name": "namespaces", + "in": "query" + }, + { + "type": "integer", + "description": "Last entity id which is used for cursor pagination", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.LightBlobLog" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.Error" + } + } + } + }, "post": { "description": "Returns blob", "consumes": [ @@ -5634,6 +5736,56 @@ } } }, + "responses.LightBlobLog": { + "type": "object", + "properties": { + "commitment": { + "type": "string", + "format": "base64", + "example": "vbGakK59+Non81TE3ULg5Ve5ufT9SFm/bCyY+WLR3gg=" + }, + "content_type": { + "type": "string", + "format": "string", + "example": "image/png" + }, + "height": { + "type": "integer", + "format": "integer", + "example": 100 + }, + "id": { + "type": "integer", + "format": "integer", + "example": 200 + }, + "namespace": { + "type": "string", + "format": "base64", + "example": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAs2bWWU6FOB0=" + }, + "signer": { + "type": "string", + "format": "string", + "example": "celestia1jc92qdnty48pafummfr8ava2tjtuhfdw774w60" + }, + "size": { + "type": "integer", + "format": "integer", + "example": 10 + }, + "time": { + "type": "string", + "format": "date-time", + "example": "2023-07-04T03:10:57+00:00" + }, + "tx_hash": { + "type": "string", + "format": "binary", + "example": "652452A670018D629CC116E510BA88C1CABE061336661B1F3D206D248BD558AF" + } + } + }, "responses.Message": { "type": "object", "properties": { diff --git a/cmd/api/handler/address.go b/cmd/api/handler/address.go index 45d0ad52..3fc2ae28 100644 --- a/cmd/api/handler/address.go +++ b/cmd/api/handler/address.go @@ -256,10 +256,10 @@ func (p *getAddressMessages) ToFilters() storage.AddressMsgsFilter { // @Description Get address messages // @Tags address // @ID address-messages -// @Param hash path string true "Hash" minlength(47) maxlength(47) -// @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 hash path string true "Hash" minlength(47) maxlength(47) +// @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 msg_type query storageTypes.MsgType false "Comma-separated message types list" // @Produce json // @Success 200 {array} responses.MessageForAddress @@ -333,7 +333,7 @@ func (req *getBlobLogsForAddress) SetDefault() { // @Param offset query integer false "Offset" minimum(1) // @Param sort query string false "Sort order. Default: desc" Enums(asc, desc) // @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) -// @Param joins query boolean false "Flag indicating whether entities of transaction and namespace should be attached or not. Default: true" +// @Param joins query boolean false "Flag indicating whether entities of transaction and namespace should be attached or not. Default: true" // @Produce json // @Success 200 {array} responses.BlobLog // @Failure 400 {object} Error diff --git a/cmd/api/handler/namespace.go b/cmd/api/handler/namespace.go index 37d9f054..7aa4c05c 100644 --- a/cmd/api/handler/namespace.go +++ b/cmd/api/handler/namespace.go @@ -4,6 +4,7 @@ package handler import ( + "context" "encoding/base64" "encoding/hex" "net/http" @@ -347,6 +348,125 @@ func (handler *NamespaceHandler) Count(c echo.Context) error { return c.JSON(http.StatusOK, state.TotalNamespaces) } +type listBlobsRequest struct { + Limit int `query:"limit" validate:"omitempty,min=1,max=100"` + Offset int `query:"offset" validate:"omitempty,min=0"` + Sort string `query:"sort" validate:"omitempty,oneof=asc desc"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=time size"` + Commitment string `query:"commitment" validate:"omitempty,base64url"` + Signers StringArray `query:"signers" validate:"omitempty,dive,address"` + Namespaces StringArray `query:"namespaces" validate:"omitempty,dive,namespace"` + Cursor uint64 `query:"cursor" validate:"omitempty,min=0"` + + From int64 `example:"1692892095" query:"from" swaggertype:"integer" validate:"omitempty,min=1"` + To int64 `example:"1692892095" query:"to" swaggertype:"integer" validate:"omitempty,min=1"` +} + +func (req *listBlobsRequest) SetDefault() { + if req.Limit == 0 { + req.Limit = 0 + } + if req.Sort == "" { + req.Sort = desc + } +} + +func (req listBlobsRequest) toDbRequest(ctx context.Context, ns storage.INamespace, addrs storage.IAddress) (storage.ListBlobLogFilters, error) { + fltrs := storage.ListBlobLogFilters{ + Limit: req.Limit, + Offset: req.Offset, + Sort: pgSort(req.Sort), + SortBy: req.SortBy, + Commitment: req.Commitment, + Cursor: req.Cursor, + Namespaces: make([]uint64, len(req.Namespaces)), + } + if req.From > 0 { + fltrs.From = time.Unix(req.From, 0).UTC() + } + if req.To > 0 { + fltrs.To = time.Unix(req.To, 0).UTC() + } + + var err error + + if len(req.Namespaces) > 0 { + for i := range req.Namespaces { + hash, err := base64.StdEncoding.DecodeString(req.Namespaces[i]) + if err != nil { + return fltrs, err + } + + n, err := ns.ByNamespaceIdAndVersion(ctx, hash[1:], hash[0]) + if err != nil { + return fltrs, err + } + fltrs.Namespaces[i] = n.Id + } + } + + if len(req.Signers) > 0 { + hash := make([][]byte, len(req.Signers)) + for i := range req.Signers { + if _, hash[i], err = types.Address(req.Signers[i]).Decode(); err != nil { + return fltrs, err + } + } + + if fltrs.Signers, err = addrs.IdByHash(ctx, hash...); err != nil { + return fltrs, err + } + } + + return fltrs, nil +} + +// Blobs godoc +// +// @Summary List all blobs with filters +// @Description Returns blobs +// @Tags namespace +// @ID get-blobs +// @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. Default: desc" Enums(asc, desc) +// @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) +// @Param commitment query string false "Commitment value in URLbase64 format" +// @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 signers query string false "Comma-separated celestia addresses" +// @Param namespaces query string false "Comma-separated celestia namespaces" +// @Param cursor query integer false "Last entity id which is used for cursor pagination" mininum(1) +// @Accept json +// @Produce json +// @Success 200 {array} responses.LightBlobLog +// @Failure 400 {object} Error +// @Router /blob [get] +func (handler *NamespaceHandler) Blobs(c echo.Context) error { + req, err := bindAndValidate[listBlobsRequest](c) + if err != nil { + return badRequestError(c, err) + } + req.SetDefault() + + fltrs, err := req.toDbRequest(c.Request().Context(), handler.namespace, handler.address) + if err != nil { + return handleError(c, err, handler.blobLogs) + } + + blob, err := handler.blobLogs.ListBlobs(c.Request().Context(), fltrs) + if err != nil { + return badRequestError(c, err) + } + + response := make([]responses.LightBlobLog, len(blob)) + for i := range blob { + response[i] = responses.NewLightBlobLog(blob[i]) + } + + return returnArray(c, response) +} + type postBlobRequest struct { Hash string `json:"hash" validate:"required,namespace"` Height types.Level `json:"height" validate:"required,min=1"` @@ -464,27 +584,27 @@ func (req *getBlobLogsForNamespace) SetDefault() { // GetBlobLogs godoc // -// @Summary Get blob changes for namespace -// @Description Returns blob changes for namespace -// @Tags namespace -// @ID get-blob-logs -// @Param id path string true "Namespace id in hexadecimal" minlength(56) maxlength(56) -// @Param version path integer true "Version of namespace" -// @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. Default: desc" Enums(asc, desc) -// @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) -// @Param commitment query string false "Commitment value in URLbase64 format" -// @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 joins query boolean false "Flag indicating whether entities of rollup, transaction and signer should be attached or not. Default: true" -// @Param signers query string false "Comma-separated celestia addresses" -// @Param cursor query integer false "Last entity id which is used for cursor pagination" mininum(1) -// @Produce json -// @Success 200 {array} responses.BlobLog -// @Failure 400 {object} Error -// @Failure 500 {object} Error -// @Router /namespace/{id}/{version}/blobs [get] +// @Summary Get blob changes for namespace +// @Description Returns blob changes for namespace +// @Tags namespace +// @ID get-blob-logs +// @Param id path string true "Namespace id in hexadecimal" minlength(56) maxlength(56) +// @Param version path integer true "Version of namespace" +// @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. Default: desc" Enums(asc, desc) +// @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) +// @Param commitment query string false "Commitment value in URLbase64 format" +// @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 joins query boolean false "Flag indicating whether entities of rollup, transaction and signer should be attached or not. Default: true" +// @Param signers query string false "Comma-separated celestia addresses" +// @Param cursor query integer false "Last entity id which is used for cursor pagination" mininum(1) +// @Produce json +// @Success 200 {array} responses.BlobLog +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /namespace/{id}/{version}/blobs [get] func (handler *NamespaceHandler) GetBlobLogs(c echo.Context) error { req, err := bindAndValidate[getBlobLogsForNamespace](c) if err != nil { @@ -566,8 +686,8 @@ func (handler *NamespaceHandler) GetBlobLogs(c echo.Context) error { // @ID get-namespace-rollups // @Param id path string true "Namespace id in hexadecimal" minlength(56) maxlength(56) // @Param version path integer true "Version of namespace" -// @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) // @Produce json // @Success 200 {array} responses.Rollup // @Failure 400 {object} Error diff --git a/cmd/api/handler/namespace_test.go b/cmd/api/handler/namespace_test.go index 186c86c0..3aa1e497 100644 --- a/cmd/api/handler/namespace_test.go +++ b/cmd/api/handler/namespace_test.go @@ -746,3 +746,47 @@ func (s *NamespaceTestSuite) TestBlobMetadata() { s.Require().NotNil(blob.Tx) s.Require().NotNil(blob.Signer) } + +func (s *NamespaceTestSuite) TestBlobs() { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/blobs") + + s.blobLogs.EXPECT(). + ListBlobs(gomock.Any(), gomock.Any()). + Return([]storage.BlobLog{ + { + NamespaceId: testNamespace.Id, + MsgId: 1, + TxId: 1, + SignerId: 1, + Signer: &storage.Address{ + Address: testAddress, + }, + Commitment: "test_commitment", + Size: 1000, + Height: 1000, + Time: testTime, + Tx: &testTx, + Namespace: &testNamespace, + }, + }, nil). + Times(1) + + s.Require().NoError(s.handler.Blobs(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var blobs []responses.LightBlobLog + err := json.NewDecoder(rec.Body).Decode(&blobs) + s.Require().NoError(err) + s.Require().Len(blobs, 1) + + blob := blobs[0] + s.Require().EqualValues(1000, blob.Size) + s.Require().EqualValues(1000, blob.Height) + s.Require().EqualValues(testTime, blob.Time) + s.Require().EqualValues("test_commitment", blob.Commitment) + s.Require().EqualValues(testAddress, blob.Signer) + s.Require().EqualValues(testNamespace.Hash(), blob.Namespace) +} diff --git a/cmd/api/handler/responses/blob.go b/cmd/api/handler/responses/blob.go index a36fb88d..b5cb4d9d 100644 --- a/cmd/api/handler/responses/blob.go +++ b/cmd/api/handler/responses/blob.go @@ -5,6 +5,7 @@ package responses import ( "encoding/base64" + "encoding/hex" "net/http" "time" @@ -75,3 +76,38 @@ func NewBlobLog(blob storage.BlobLog) BlobLog { return b } + +type LightBlobLog struct { + Id uint64 `example:"200" format:"integer" json:"id" swaggertype:"integer"` + Commitment string `example:"vbGakK59+Non81TE3ULg5Ve5ufT9SFm/bCyY+WLR3gg=" format:"base64" json:"commitment" swaggertype:"string"` + Size int64 `example:"10" format:"integer" json:"size" swaggertype:"integer"` + Height pkgTypes.Level `example:"100" format:"integer" json:"height" swaggertype:"integer"` + Time time.Time `example:"2023-07-04T03:10:57+00:00" format:"date-time" json:"time" swaggertype:"string"` + Signer string `example:"celestia1jc92qdnty48pafummfr8ava2tjtuhfdw774w60" format:"string" json:"signer" swaggertype:"string"` + ContentType string `example:"image/png" format:"string" json:"content_type" swaggertype:"string"` + Namespace string `example:"AAAAAAAAAAAAAAAAAAAAAAAAAAAAs2bWWU6FOB0=" format:"base64" json:"namespace" swaggertype:"string"` + TxHash string `example:"652452A670018D629CC116E510BA88C1CABE061336661B1F3D206D248BD558AF" format:"binary" json:"tx_hash" swaggertype:"string"` +} + +func NewLightBlobLog(blob storage.BlobLog) LightBlobLog { + b := LightBlobLog{ + Id: blob.Id, + Commitment: blob.Commitment, + Size: blob.Size, + Height: blob.Height, + Time: blob.Time, + ContentType: blob.ContentType, + } + + if blob.Namespace != nil { + b.Namespace = blob.Namespace.Hash() + } + if blob.Signer != nil { + b.Signer = blob.Signer.Address + } + if blob.Tx != nil { + b.TxHash = hex.EncodeToString(blob.Tx.Hash) + } + + return b +} diff --git a/cmd/api/handler/rollup.go b/cmd/api/handler/rollup.go index 426a5e35..b4fa4caf 100644 --- a/cmd/api/handler/rollup.go +++ b/cmd/api/handler/rollup.go @@ -200,21 +200,21 @@ func (p *getRollupPagesWithSort) SetDefault() { // GetBlobs godoc // -// @Summary Get rollup blobs -// @Description Get rollup blobs -// @Tags rollup -// @ID get-rollup-blobs -// @Param id path integer true "Internal identity" 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. Default: desc" Enums(asc, desc) -// @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) -// @Param joins query boolean false "Flag indicating whether entities of transaction and signer should be attached or not. Default: true" -// @Produce json -// @Success 200 {array} responses.BlobLog -// @Failure 400 {object} Error -// @Failure 500 {object} Error -// @Router /rollup/{id}/blobs [get] +// @Summary Get rollup blobs +// @Description Get rollup blobs +// @Tags rollup +// @ID get-rollup-blobs +// @Param id path integer true "Internal identity" 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. Default: desc" Enums(asc, desc) +// @Param sort_by query string false "Sort field. If it's empty internal id is used" Enums(time, size) +// @Param joins query boolean false "Flag indicating whether entities of transaction and signer should be attached or not. Default: true" +// @Produce json +// @Success 200 {array} responses.BlobLog +// @Failure 400 {object} Error +// @Failure 500 {object} Error +// @Router /rollup/{id}/blobs [get] func (handler RollupHandler) GetBlobs(c echo.Context) error { req, err := bindAndValidate[getRollupPagesWithSort](c) if err != nil { @@ -382,9 +382,9 @@ type rollupDistributionRequest struct { // @Description Get rollup distribution // @Tags rollup // @ID get-rollup-distribution -// @Param id path integer true "Internal identity" mininum(1) -// @Param name path string true "Series name" Enums(blobs_count, size, size_per_blob, fee_per_blob) -// @Param timeframe path string true "Timeframe" Enums(hour, day) +// @Param id path integer true "Internal identity" mininum(1) +// @Param name path string true "Series name" Enums(blobs_count, size, size_per_blob, fee_per_blob) +// @Param timeframe path string true "Timeframe" Enums(hour, day) // @Produce json // @Success 200 {array} responses.DistributionItem // @Failure 400 {object} Error @@ -425,9 +425,9 @@ type exportBlobsRequest struct { // @Description Export rollup blobs // @Tags rollup // @ID rollup-export -// @Param id path integer true "Internal identity" mininum(1) -// @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 id path integer true "Internal identity" mininum(1) +// @Param from query integer false "Time from in unix timestamp" mininum(1) +// @Param to query integer false "Time to in unix timestamp" mininum(1) // @Success 200 // @Failure 400 {object} Error // @Failure 500 {object} Error diff --git a/cmd/api/handler/stats.go b/cmd/api/handler/stats.go index 17f380ca..ecac45f0 100644 --- a/cmd/api/handler/stats.go +++ b/cmd/api/handler/stats.go @@ -414,7 +414,7 @@ type stakingSeriesRequest struct { // @Description Get histogram for staking with precomputed stats by series name and timeframe // @Tags stats // @ID stats-staking-series -// @Param id path string true "Validator id" minlength(56) maxlength(56) +// @Param id path string true "Validator id" minlength(56) maxlength(56) // @Param timeframe path string true "Timeframe" Enums(hour, day, month) // @Param name path string true "Series name" Enums(rewards, commissions, flow) // @Param from query integer false "Time from in unix timestamp" mininum(1) @@ -459,8 +459,8 @@ type squareSizeRequest struct { // @Description Get histogram for square size distribution // @Tags stats // @ID stats-square-size -// @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 from query integer false "Time from in unix timestamp" mininum(1) +// @Param to query integer false "Time to in unix timestamp" mininum(1) // @Produce json // @Success 200 {array} responses.SquareSizeResponse // @Failure 400 {object} Error diff --git a/cmd/api/handler/validator.go b/cmd/api/handler/validator.go index d48d8133..0e6fb9ab 100644 --- a/cmd/api/handler/validator.go +++ b/cmd/api/handler/validator.go @@ -242,7 +242,7 @@ func (p *validatorDelegationsRequest) SetDefault() { // @Tags validator // @ID validator-delegators // @Param id path integer true "Internal validator id" -// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) +// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) // @Param offset query integer false "Offset" minimum(1) // @Param show_zero query boolean false "Show zero delegations" // @Produce json @@ -283,7 +283,7 @@ func (handler *ValidatorHandler) Delegators(c echo.Context) error { // @Tags validator // @ID validator-jails // @Param id path integer true "Internal validator id" -// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) +// @Param limit query integer false "Count of requested entities" minimum(1) maximum(100) // @Param offset query integer false "Offset" minimum(1) // @Produce json // @Success 200 {array} responses.Jail diff --git a/cmd/api/init.go b/cmd/api/init.go index 9e572ae9..9e2bad24 100644 --- a/cmd/api/init.go +++ b/cmd/api/init.go @@ -351,6 +351,7 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto blobGroup := v1.Group("/blob") { + blobGroup.GET("", namespaceHandlers.Blobs) blobGroup.POST("", namespaceHandlers.Blob) blobGroup.POST("/metadata", namespaceHandlers.BlobMetadata) } diff --git a/cmd/api/main.go b/cmd/api/main.go index 82a94dda..008b84de 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -24,7 +24,7 @@ var rootCmd = &cobra.Command{ // @description This is docs of Celenium API. // @host api-mainnet.celenium.io // @schemes https -// @BasePath /v1 +// @BasePath /v1 // // @x-servers [{"url": "api-mainnet.celenium.io", "description": "Celenium Mainnet API"},{"url": "api-mocha.celenium.io", "description": "Celenium Mocha API"},{"url": "api-arabica.celenium.io", "description": "Celenium Arabica API"}] // @query.collection.format multi diff --git a/cmd/api/routes_test.go b/cmd/api/routes_test.go index 33a3f9da..ed115dcc 100644 --- a/cmd/api/routes_test.go +++ b/cmd/api/routes_test.go @@ -73,6 +73,7 @@ func TestRoutes(t *testing.T) { "/v1/address/:hash/grants GET": {}, "/v1/address/:hash/granters GET": {}, "/v1/blob POST": {}, + "/v1/blob GET": {}, "/v1/stats/price/current GET": {}, "/v1/swagger/doc.json GET": {}, "/v1/auth/rollup/:id DELETE": {}, diff --git a/internal/storage/blob_log.go b/internal/storage/blob_log.go index 3f272d7c..cf20921f 100644 --- a/internal/storage/blob_log.go +++ b/internal/storage/blob_log.go @@ -27,6 +27,19 @@ type BlobLogFilters struct { Cursor uint64 } +type ListBlobLogFilters struct { + Limit int + Offset int + Sort sdk.SortOrder + SortBy string + From time.Time + To time.Time + Commitment string + Signers []uint64 + Namespaces []uint64 + Cursor uint64 +} + //go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed type IBlobLog interface { sdk.Table[*BlobLog] @@ -40,6 +53,7 @@ type IBlobLog interface { CountByHeight(ctx context.Context, height types.Level) (int, error) ExportByProviders(ctx context.Context, providers []RollupProvider, from, to time.Time, stream io.Writer) (err error) Blob(ctx context.Context, height types.Level, nsId uint64, commitment string) (BlobLog, error) + ListBlobs(ctx context.Context, fltrs ListBlobLogFilters) ([]BlobLog, error) } type BlobLog struct { diff --git a/internal/storage/mock/blob_log.go b/internal/storage/mock/blob_log.go index c20b372a..8ee3e67a 100644 --- a/internal/storage/mock/blob_log.go +++ b/internal/storage/mock/blob_log.go @@ -591,6 +591,45 @@ func (c *MockIBlobLogListCall) DoAndReturn(f func(context.Context, uint64, uint6 return c } +// ListBlobs mocks base method. +func (m *MockIBlobLog) ListBlobs(ctx context.Context, fltrs storage.ListBlobLogFilters) ([]storage.BlobLog, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListBlobs", ctx, fltrs) + ret0, _ := ret[0].([]storage.BlobLog) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListBlobs indicates an expected call of ListBlobs. +func (mr *MockIBlobLogMockRecorder) ListBlobs(ctx, fltrs any) *MockIBlobLogListBlobsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBlobs", reflect.TypeOf((*MockIBlobLog)(nil).ListBlobs), ctx, fltrs) + return &MockIBlobLogListBlobsCall{Call: call} +} + +// MockIBlobLogListBlobsCall wrap *gomock.Call +type MockIBlobLogListBlobsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIBlobLogListBlobsCall) Return(arg0 []storage.BlobLog, arg1 error) *MockIBlobLogListBlobsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIBlobLogListBlobsCall) Do(f func(context.Context, storage.ListBlobLogFilters) ([]storage.BlobLog, error)) *MockIBlobLogListBlobsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIBlobLogListBlobsCall) DoAndReturn(f func(context.Context, storage.ListBlobLogFilters) ([]storage.BlobLog, error)) *MockIBlobLogListBlobsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // Save mocks base method. func (m_2 *MockIBlobLog) Save(ctx context.Context, m *storage.BlobLog) error { m_2.ctrl.T.Helper() diff --git a/internal/storage/postgres/blob_log.go b/internal/storage/postgres/blob_log.go index 0e2c393e..5a9c769f 100644 --- a/internal/storage/postgres/blob_log.go +++ b/internal/storage/postgres/blob_log.go @@ -48,7 +48,7 @@ func (bl *BlobLog) ByNamespace(ctx context.Context, nsId uint64, fltrs storage.B Join("left join tx on tx.id = blob_log.tx_id"). Join("left join rollup_provider as p on blob_log.signer_id = p.address_id and blob_log.namespace_id = p.namespace_id"). Join("left join rollup on rollup.id = p.rollup_id") - query = blobLogSort(query, fltrs) + query = blobLogSort(query, fltrs.SortBy, fltrs.Sort) } else { query = blobsQuery } @@ -91,7 +91,7 @@ func (bl *BlobLog) ByProviders(ctx context.Context, providers []storage.RollupPr Join("left join tx on tx.id = blob_log.tx_id") } - query = blobLogSort(query, fltrs) + query = blobLogSort(query, fltrs.SortBy, fltrs.Sort) err = query.Scan(ctx, &logs) return } @@ -185,7 +185,7 @@ func (bl *BlobLog) BySigner(ctx context.Context, signerId uint64, fltrs storage. } - query = blobLogSort(query, fltrs) + query = blobLogSort(query, fltrs.SortBy, fltrs.Sort) err = query.Scan(ctx, &logs) return } @@ -270,3 +270,22 @@ func (bl *BlobLog) Blob(ctx context.Context, height types.Level, nsId uint64, co Scan(ctx, &l) return } + +func (bl *BlobLog) ListBlobs(ctx context.Context, fltrs storage.ListBlobLogFilters) (logs []storage.BlobLog, err error) { + blobLogQuery := bl.DB().NewSelect().Model((*storage.BlobLog)(nil)) + blobLogQuery = listBlobLogFilters(blobLogQuery, fltrs) + + query := bl.DB().NewSelect(). + ColumnExpr("blob_log.*"). + ColumnExpr("signer.address as signer__address"). + ColumnExpr("ns.version as namespace__version, ns.namespace_id as namespace__namespace_id"). + ColumnExpr("tx.hash as tx__hash, tx.id as tx__id"). + TableExpr("(?) as blob_log", blobLogQuery). + Join("left join address as signer on signer.id = blob_log.signer_id"). + Join("left join namespace as ns on ns.id = blob_log.namespace_id"). + Join("left join tx on tx.id = blob_log.tx_id") + + query = blobLogSort(query, fltrs.SortBy, fltrs.Sort) + err = query.Scan(ctx, &logs) + return +} diff --git a/internal/storage/postgres/blob_log_test.go b/internal/storage/postgres/blob_log_test.go index 79f771e1..e2d4faf2 100644 --- a/internal/storage/postgres/blob_log_test.go +++ b/internal/storage/postgres/blob_log_test.go @@ -400,3 +400,34 @@ func (s *StorageTestSuite) TestBlob() { s.Require().NotNil(log.Tx) s.Require().EqualValues(4, log.Tx.Id) } + +func (s *StorageTestSuite) TestListBlob() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + logs, err := s.storage.BlobLogs.ListBlobs(ctx, storage.ListBlobLogFilters{ + Namespaces: []uint64{2}, + Signers: []uint64{1}, + SortBy: "time", + Sort: sdk.SortOrderAsc, + Limit: 1, + }) + s.Require().NoError(err) + s.Require().Len(logs, 1) + + log := logs[0] + s.Require().EqualValues(1, log.Id) + s.Require().EqualValues(0, log.Height) + s.Require().EqualValues("RWW7eaKKXasSGK/DS8PlpErARbl5iFs1vQIycYEAlk0=", log.Commitment) + s.Require().EqualValues(10, log.Size) + s.Require().EqualValues(2, log.NamespaceId) + s.Require().EqualValues(1, log.SignerId) + s.Require().EqualValues(1, log.MsgId) + s.Require().EqualValues(4, log.TxId) + + s.Require().NotNil(log.Signer) + s.Require().EqualValues("celestia1mm8yykm46ec3t0dgwls70g0jvtm055wk9ayal8", log.Signer.Address) + + s.Require().NotNil(log.Tx) + s.Require().NotNil(log.Namespace) +} diff --git a/internal/storage/postgres/scopes.go b/internal/storage/postgres/scopes.go index 99867e8e..e3968ed7 100644 --- a/internal/storage/postgres/scopes.go +++ b/internal/storage/postgres/scopes.go @@ -128,15 +128,48 @@ func blobLogFilters(query *bun.SelectQuery, fltrs storage.BlobLogFilters) *bun.S } query = limitScope(query, fltrs.Limit) - return blobLogSort(query, fltrs) + return blobLogSort(query, fltrs.SortBy, fltrs.Sort) } -func blobLogSort(query *bun.SelectQuery, fltrs storage.BlobLogFilters) *bun.SelectQuery { - switch fltrs.SortBy { +func listBlobLogFilters(query *bun.SelectQuery, fltrs storage.ListBlobLogFilters) *bun.SelectQuery { + if fltrs.Offset > 0 { + query = query.Offset(fltrs.Offset) + } + + if !fltrs.From.IsZero() { + query = query.Where("time >= ?", fltrs.From) + } + if !fltrs.To.IsZero() { + query = query.Where("time < ?", fltrs.To) + } + if fltrs.Commitment != "" { + query = query.Where("commitment = ?", fltrs.Commitment) + } + if len(fltrs.Signers) > 0 { + query = query.Where("signer_id IN (?)", bun.In(fltrs.Signers)) + } + if len(fltrs.Namespaces) > 0 { + query = query.Where("namespace_id IN (?)", bun.In(fltrs.Namespaces)) + } + if fltrs.Cursor > 0 { + switch fltrs.Sort { + case sdk.SortOrderAsc: + query = query.Where("id > ?", fltrs.Cursor) + case sdk.SortOrderDesc: + query = query.Where("id < ?", fltrs.Cursor) + } + } + + query = limitScope(query, fltrs.Limit) + return blobLogSort(query, fltrs.SortBy, fltrs.Sort) +} + +func blobLogSort(query *bun.SelectQuery, sortBy string, sort sdk.SortOrder) *bun.SelectQuery { + switch sortBy { case sizeColumn, timeColumn: - query = sortScope(query, fmt.Sprintf("blob_log.%s", fltrs.SortBy), fltrs.Sort) + query = sortScope(query, fmt.Sprintf("blob_log.%s", sortBy), sort) case "": - query = sortScope(query, "blob_log.time", fltrs.Sort) + query = sortScope(query, "blob_log.time", sort) default: return query }