diff --git a/api/docs/docs.go b/api/docs/docs.go index fc140ac07..f24bf64d9 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -1188,6 +1188,32 @@ const docTemplate = `{ } } }, + "/api/v1/vaas/parse": { + "post": { + "description": "Parse a VAA.", + "tags": [ + "Wormscan" + ], + "operationId": "parse-vaa", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response-array_vaa_VaaStats" + } + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/vaa-counts": { "get": { "description": "Returns the total number of VAAs emitted for each blockchain.", diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 263b2cd56..0ec4e2881 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1181,6 +1181,32 @@ } } }, + "/api/v1/vaas/parse": { + "post": { + "description": "Parse a VAA.", + "tags": [ + "Wormscan" + ], + "operationId": "parse-vaa", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response-array_vaa_VaaStats" + } + }, + "400": { + "description": "Bad Request" + }, + "404": { + "description": "Not Found" + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/vaas/vaa-counts": { "get": { "description": "Returns the total number of VAAs emitted for each blockchain.", diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 4b6922583..1eeb814df 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -1672,6 +1672,23 @@ paths: description: Internal Server Error tags: - Wormscan + /api/v1/vaas/parse: + post: + description: Parse a VAA. + operationId: parse-vaa + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response-array_vaa_VaaStats' + "400": + description: Bad Request + "404": + description: Not Found + "500": + description: Internal Server Error + tags: + - Wormscan /api/v1/vaas/vaa-counts: get: description: Returns the total number of VAAs emitted for each blockchain. diff --git a/api/handlers/vaa/service.go b/api/handlers/vaa/service.go index 3db434d7b..e07d87a4f 100644 --- a/api/handlers/vaa/service.go +++ b/api/handlers/vaa/service.go @@ -11,6 +11,7 @@ import ( "github.com/wormhole-foundation/wormhole-explorer/api/response" "github.com/wormhole-foundation/wormhole-explorer/api/types" "github.com/wormhole-foundation/wormhole-explorer/common/client/cache" + vaaPayloadParser "github.com/wormhole-foundation/wormhole-explorer/common/client/parser" "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" ) @@ -19,15 +20,17 @@ import ( type Service struct { repo *Repository getCacheFunc cache.CacheGetFunc + parseVaaFunc vaaPayloadParser.ParseVaaFunc logger *zap.Logger } // NewService creates a new VAA Service. -func NewService(r *Repository, getCacheFunc cache.CacheGetFunc, logger *zap.Logger) *Service { +func NewService(r *Repository, getCacheFunc cache.CacheGetFunc, parseVaaFunc vaaPayloadParser.ParseVaaFunc, logger *zap.Logger) *Service { s := Service{ repo: r, getCacheFunc: getCacheFunc, + parseVaaFunc: parseVaaFunc, logger: logger.With(zap.String("module", "VaaService")), } @@ -228,3 +231,26 @@ func (s *Service) discardVaaNotIndexed(ctx context.Context, chain vaa.ChainID, e } return true } + +// ParseVaa parse a vaa payload. +func (s *Service) ParseVaa(ctx context.Context, vaaByte []byte) (any, error) { + // unmarshal vaa + vaa, err := vaa.Unmarshal(vaaByte) + if err != nil { + requestID := fmt.Sprintf("%v", ctx.Value("requestid")) + s.logger.Error("error unmarshal vaa to parse", zap.Error(err), zap.String("requestID", requestID)) + return nil, errs.ErrInternalError + } + + // call vaa payload parser api + parsedVaa, err := s.parseVaaFunc(vaa) + if err != nil { + if errors.Is(err, vaaPayloadParser.ErrNotFound) { + return nil, errs.ErrNotFound + } + requestID := fmt.Sprintf("%v", ctx.Value("requestid")) + s.logger.Error("error parse vaa", zap.Error(err), zap.String("requestID", requestID)) + return nil, errs.ErrInternalError + } + return parsedVaa, nil +} diff --git a/api/internal/config/config.go b/api/internal/config/config.go index 89bbb7e2a..f8cc9db1e 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -53,6 +53,11 @@ type AppConfig struct { Bucket30Days string BucketInfinite string } + VaaPayloadParser struct { + Enabled bool + URL string + Timeout int64 + } RateLimit struct { Enabled bool // Max number of requests per minute diff --git a/api/main.go b/api/main.go index dc44cdbde..f69004f26 100644 --- a/api/main.go +++ b/api/main.go @@ -40,8 +40,10 @@ import ( "github.com/wormhole-foundation/wormhole-explorer/api/routes/wormscan" rpcApi "github.com/wormhole-foundation/wormhole-explorer/api/rpc" wormscanCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache" + vaaPayloadParser "github.com/wormhole-foundation/wormhole-explorer/common/client/parser" xlogger "github.com/wormhole-foundation/wormhole-explorer/common/logger" "github.com/wormhole-foundation/wormhole-explorer/common/utils" + sdk "github.com/wormhole-foundation/wormhole/sdk/vaa" "go.uber.org/zap" ) @@ -124,6 +126,12 @@ func main() { rootLogger.Info("initializing InfluxDB client") influxCli := newInfluxClient(cfg.Influx.URL, cfg.Influx.Token) + //VaaPayloadParser client + vaaParserFunc, err := NewVaaParserFunc(cfg, rootLogger) + if err != nil { + rootLogger.Fatal("failed to initialize VAA parser", zap.Error(err)) + } + // Set up repositories rootLogger.Info("initializing repositories") addressRepo := address.NewRepository(db, rootLogger) @@ -146,7 +154,7 @@ func main() { // Set up services rootLogger.Info("initializing services") addressService := address.NewService(addressRepo, rootLogger) - vaaService := vaa.NewService(vaaRepo, cache.Get, rootLogger) + vaaService := vaa.NewService(vaaRepo, cache.Get, vaaParserFunc, rootLogger) obsService := observations.NewService(obsRepo, rootLogger) governorService := governor.NewService(governorRepo, rootLogger) infrastructureService := infrastructure.NewService(infrastructureRepo, rootLogger) @@ -301,3 +309,18 @@ func NewRateLimiter(ctx context.Context, cfg *config.AppConfig, logger *zap.Logg return router, nil } + +// NewVaaParserFunc returns a function to parse VAA payload. +func NewVaaParserFunc(cfg *config.AppConfig, logger *zap.Logger) (vaaPayloadParser.ParseVaaFunc, error) { + if cfg.RunMode == config.RunModeDevelopmernt && !cfg.VaaPayloadParser.Enabled { + return func(vaa *sdk.VAA) (*vaaPayloadParser.ParseVaaWithStandarizedPropertiesdResponse, error) { + return &vaaPayloadParser.ParseVaaWithStandarizedPropertiesdResponse{}, nil + }, nil + } + vaaPayloadParserClient, err := vaaPayloadParser.NewParserVAAAPIClient(cfg.VaaPayloadParser.Timeout, + cfg.VaaPayloadParser.URL, logger) + if err != nil { + return nil, fmt.Errorf("failed to initialize VAA parser client: %w", err) + } + return vaaPayloadParserClient.ParseVaaWithStandarizedProperties, nil +} diff --git a/api/response/error.go b/api/response/error.go index 42b8278d2..2e24f524d 100644 --- a/api/response/error.go +++ b/api/response/error.go @@ -145,3 +145,21 @@ func NewInvalidQueryParamError(ctx *fiber.Ctx, message string, err error) APIErr Details: []ErrorDetail{detail}, } } + +func NewRequestBodyError(ctx *fiber.Ctx, message string, err error) APIError { + if message == "" { + message = "INVALID BODY" + } + detail := ErrorDetail{ + RequestID: fmt.Sprintf("%v", ctx.Locals("requestid")), + } + if enableStackTrace && err != nil { + detail.StackTrace = fmt.Sprintf("%+v\n", err) + } + return APIError{ + StatusCode: fiber.StatusBadRequest, + Code: InvalidParam, + Message: message, + Details: []ErrorDetail{detail}, + } +} diff --git a/api/routes/wormscan/routes.go b/api/routes/wormscan/routes.go index 0f2b084cf..0a95d482d 100644 --- a/api/routes/wormscan/routes.go +++ b/api/routes/wormscan/routes.go @@ -81,6 +81,7 @@ func RegisterRoutes( vaas.Get("/:chain", vaaCtrl.FindByChain) vaas.Get("/:chain/:emitter", vaaCtrl.FindByEmitter) vaas.Get("/:chain/:emitter/:sequence", vaaCtrl.FindById) + vaas.Post("/parse", vaaCtrl.ParseVaa) // oservations resource observations := api.Group("/observations") diff --git a/api/routes/wormscan/vaa/controller.go b/api/routes/wormscan/vaa/controller.go index 56743f7fd..d6076ae8a 100644 --- a/api/routes/wormscan/vaa/controller.go +++ b/api/routes/wormscan/vaa/controller.go @@ -2,11 +2,14 @@ package vaa import ( + "encoding/base64" "strconv" "github.com/gofiber/fiber/v2" + "github.com/pkg/errors" "github.com/wormhole-foundation/wormhole-explorer/api/handlers/vaa" "github.com/wormhole-foundation/wormhole-explorer/api/middleware" + "github.com/wormhole-foundation/wormhole-explorer/api/response" _ "github.com/wormhole-foundation/wormhole-explorer/api/response" // required by swaggo "go.uber.org/zap" ) @@ -190,3 +193,47 @@ func (c *Controller) GetVaaCount(ctx *fiber.Ctx) error { return ctx.JSON(vaas) } + +// ParseVaa godoc +// @Description Parse a VAA. +// @Tags Wormscan +// @ID parse-vaa +// @Success 200 {object} response.Response[[]vaa.VaaStats] +// @Failure 400 +// @Failure 404 +// @Failure 500 +// @Router /api/v1/vaas/parse [post] +func (c *Controller) ParseVaa(ctx *fiber.Ctx) error { + + parseVaaBody := struct { + Vaa string `json:"vaa"` + }{} + + err := ctx.BodyParser(&parseVaaBody) + if err != nil { + return response.NewRequestBodyError(ctx, + "invalid vaa request, unable to parse", + errors.WithStack(err)) + } + + if len(parseVaaBody.Vaa) == 0 { + return response.NewRequestBodyError( + ctx, + "invalid vaa request, vaa is empty", + nil) + } + + vaa, err := base64.StdEncoding.DecodeString(parseVaaBody.Vaa) + if err != nil { + return response.NewRequestBodyError(ctx, + "invalid vaa request, vaa is not base64 encoded", + errors.WithStack(err)) + } + + parsedVaa, err := c.srv.ParseVaa(ctx.Context(), vaa) + if err != nil { + return err + } + + return ctx.JSON(parsedVaa) +} diff --git a/common/client/parser/parser.go b/common/client/parser/parser.go index 72625d39f..934bf6240 100644 --- a/common/client/parser/parser.go +++ b/common/client/parser/parser.go @@ -39,6 +39,9 @@ type ParserVAAAPIClient struct { Logger *zap.Logger } +// ParseVaaFunc represent a parse vaa function. +type ParseVaaFunc func(vaa *sdk.VAA) (*ParseVaaWithStandarizedPropertiesdResponse, error) + // NewParserVAAAPIClient create new instances of ParserVAAAPIClient. func NewParserVAAAPIClient(timeout int64, baseURL string, logger *zap.Logger) (ParserVAAAPIClient, error) { if timeout == 0 {